feedback5 W5S: i18n phase 2, ui — HUD, shop bar, detail card, bond strip and popup, 机变 overlay, enemy drawer, team panel, under-frame, guide, emotes, match chrome, connection banner, components, the loadout / roster models, the store, net and observe texts through t() / tParts() (whole sentences where a number or a name sat inside), msgid tables marked N_() and translated where shown, the shop's refusal reasons compared through t(); tools/i18n.mjs reads aliased imports (t as tr); English for the 59 new msgids; Chinese output unchanged (zh render proof; five rewrites checked by hand, listed in the notes)
This commit is contained in:
1 parent
166c4fbbfb
commit
647bd23ef2
33 files changed
+530
-427
No files matched your search
+60
-1
@@ -1010,5 +1010,64 @@
|
|||||||
"全部持有:没有由替补干员上场的棋子": "Everything is owned: no chess takes the field as a stand-in",
|
"全部持有:没有由替补干员上场的棋子": "Everything is owned: no chess takes the field as a stand-in",
|
||||||
"没有可下掉的干员": "No Operators can be marked",
|
"没有可下掉的干员": "No Operators can be marked",
|
||||||
"{name}({note})": "{name} ({note})",
|
"{name}({note})": "{name} ({note})",
|
||||||
"本局耗时 {n} 分钟": "Duration: {n} {mins|minute|minutes}"
|
"本局耗时 {n} 分钟": "Duration: {n} {mins|minute|minutes}",
|
||||||
|
"{tip} · {n} 名成员": "{tip} · {n} {n|member|members}",
|
||||||
|
"{names} 在场:核心盟约激活人数 +{harmony}": "{names} on the field: Core Alliance head count +{harmony}",
|
||||||
|
"{harmonyName}已激活:核心盟约激活人数 +{harmony}": "{harmonyName} active: Core Alliance head count +{harmony}",
|
||||||
|
"层数不显示": "Stacks not shown",
|
||||||
|
"已激活 · {tier} 阶": "Active · Tier {tier}",
|
||||||
|
"已激活": "Active",
|
||||||
|
"bond-threshold::名": "+",
|
||||||
|
"({note})": " ({note})",
|
||||||
|
"观战中,可点击左侧成员头像前往查看": "Spectating · tap a member's avatar on the left to view their field",
|
||||||
|
"基础 {v}": "Base {v}",
|
||||||
|
"{items} → 【{name}】": "{items} → [{name}]",
|
||||||
|
"list::、": ", ",
|
||||||
|
"与{morph}一同装备时,携带者视为【{bond}】成员": "Equipped together with {morph}, the wearer counts as a member of [{bond}]",
|
||||||
|
":在场 {count}/{cap}": ": {count}/{cap} on the field",
|
||||||
|
":在场 {count}": ": {count} on the field",
|
||||||
|
"拖拽至干员身上进行配发(每名干员最多 2 件,配发后无法取下)": "Drag onto an Operator to equip (up to 2 per Operator; equipped Equipment cannot be removed)",
|
||||||
|
"调度中心不出售 · 获取途径:{source}": "Not sold in the Dispatch Center · Source: {source}",
|
||||||
|
"免疫:{list}": "Immune to: {list}",
|
||||||
|
"确定要出售精锐干员「{name}」吗?出售后获得 {price} 资金。": "Sell the Elite Operator \"{name}\"? You get {price} Funds.",
|
||||||
|
"之后的每场作战": "every later battle",
|
||||||
|
"还剩 {left} 场作战": "{left} {left|battle|battles} left",
|
||||||
|
"第 {r} 回合 · 即将迎击 {n} 名敌人": "Round {r} · {n} incoming {count|enemy|enemies}",
|
||||||
|
"特训敌人·": "Tactical Training/",
|
||||||
|
"{bandOwner} 的策略": "{bandOwner}'s Strategy",
|
||||||
|
"确认替换": "Replace",
|
||||||
|
"「{name}」的装备栏已满(2/2)。选择一件装备进行替换,{warn}。": "\"{name}\" has no free Equipment slot (2/2). Choose a piece of Equipment to replace: {warn}.",
|
||||||
|
"将销毁「{old}」并配发「{item}」": "Destroys \"{old}\" and equips \"{item}\"",
|
||||||
|
"{name} 漏过的敌人还剩 {n} 个(联防中)": "{n} {n|enemy|enemies} that got past {name} still standing (Unite in progress)",
|
||||||
|
"你漏过的敌人还剩 {n} 个(队友正在迎战)": "{n} {n|enemy|enemies} that got past you still standing (your teammates are engaging)",
|
||||||
|
"{name} 漏过的敌人已全部被击倒": "Every enemy that got past {name} has been defeated",
|
||||||
|
"你漏过的敌人已全部被击倒": "Every enemy that got past you has been defeated",
|
||||||
|
"目标生命值 {lp}:本回合已有 {cap} 个以上敌人进入蓝门,结算时扣除 {pending} 点(每回合至多 {cap} 点)": "LP {lp}: {cap}+ enemies reached the blue gate this round; settlement costs {pending} LP (max {cap} per round)",
|
||||||
|
"目标生命值 {lp}:本回合已有 {pending} 个敌人进入蓝门,结算时扣除 {pending} 点(每回合至多 {cap} 点)": "LP {lp}: {pending} {pending|enemy|enemies} reached the blue gate this round; settlement costs {pending} LP (max {cap} per round)",
|
||||||
|
"临时整备区还有 {count} 个单位:{rule}": "{count} {count|unit is|units are} still on the Temporary Bench: {rule}",
|
||||||
|
"临时整备区 {n} 个单位待处理": "Temporary Bench: {n} {count|unit|units} pending",
|
||||||
|
"{n} 秒后全队生命值开始流失": "Team LP starts draining in {n} {count|second|seconds}",
|
||||||
|
"超时 · 生命值 {n}/秒": "Overtime · LP {n}/s",
|
||||||
|
"这份调配来自更新的版本(v{v}),请先更新游戏": "This loadout comes from a newer version (v{v}). Update the game first.",
|
||||||
|
"{n}秒": "{n}s",
|
||||||
|
"干员持有是局外设置,修改将在下一局生效": "The Operator Roster is set outside matches; changes apply from the next match",
|
||||||
|
"离开观战": "Stop Spectating",
|
||||||
|
"离开观战席并返回大厅,本局模拟不受影响;观战席空着时可以凭同盟密钥再次观战。": "Leave the spectator seat and return to the lobby; the simulation goes on. While a spectator seat is free you can watch again with the Alliance Key.",
|
||||||
|
"{away}:由 AI 托管你的席位(自动部署、准备与选择),随时可以返回。": "{away}: the AI takes over your seat (deploying, readying and picking); you can come back at any time.",
|
||||||
|
"{quit}:离开后无法返回本局,你的干员将回到共享卡池。": "{quit}: you cannot come back to this match, and your Operators return to the shared pool.",
|
||||||
|
"灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)": "Gray: some of the Alliance's Operators cannot appear (it can still be activated by Operators of other Alliances or by Equipment)",
|
||||||
|
"这不是干员持有的数据": "This is not Operator Roster data",
|
||||||
|
"里面没有干员持有的数据": "It holds no Operator Roster data",
|
||||||
|
"{name},价格 {price}": "{name}, price {price}",
|
||||||
|
"技能 {slot}:{name}": "Skill {slot}: {name}",
|
||||||
|
"技能:{name}": "Skill: {name}",
|
||||||
|
"目标生命值 {lp},联防中:漏过的敌人还剩 {left} 个,按现在结算扣除 {pending} 点(每回合至多 {cap} 点)": "LP {lp}, Unite in progress: {left} {left|enemy|enemies} that got through still standing; settling now would cost {pending} LP (max {cap} per round)",
|
||||||
|
"目标生命值 {lp},联防中,结算时扣除至多 {pending} 点": "LP {lp}, Unite in progress: settlement costs up to {pending} LP",
|
||||||
|
"目标生命值 {lp},结算时扣除 {pending} 点": "LP {lp}: settlement costs {pending} LP",
|
||||||
|
"{name} 操作": "{name} actions",
|
||||||
|
"撤退至整备区(Q)": "Retreat to Bench (Q)",
|
||||||
|
"撤退[Q]": "Retreat [Q]",
|
||||||
|
"出售(+{sell} 资金,X)": "Sell (+{sell} Funds, X)",
|
||||||
|
"出售[X]": "Sell [X]",
|
||||||
|
"临时整备区 {n} 件道具待处理": "Temporary Bench: {n} {count|item|items} pending"
|
||||||
}
|
}
|
||||||
+11
-10
@@ -10,6 +10,7 @@
|
|||||||
|
|
||||||
import { PHASE } from '../../../shared/constants.js';
|
import { PHASE } from '../../../shared/constants.js';
|
||||||
import { data } from '../data.js';
|
import { data } from '../data.js';
|
||||||
|
import { t } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const isObj = (v) => !!v && typeof v === 'object';
|
const isObj = (v) => !!v && typeof v === 'object';
|
||||||
const COMBAT = new Set([PHASE.COMBAT, PHASE.UNITE, PHASE.FINAL_ASSAULT, PHASE.HIDDEN_CORE]);
|
const COMBAT = new Set([PHASE.COMBAT, PHASE.UNITE, PHASE.FINAL_ASSAULT, PHASE.HIDDEN_CORE]);
|
||||||
@@ -27,7 +28,7 @@ export function fieldOf(pub, playerId) {
|
|||||||
|
|
||||||
/** Display name of a player id ('队友' when unknown). */
|
/** Display name of a player id ('队友' when unknown). */
|
||||||
export function nameOf(pub, playerId) {
|
export function nameOf(pub, playerId) {
|
||||||
return players(pub).find((p) => p.playerId === playerId)?.name || '队友';
|
return players(pub).find((p) => p.playerId === playerId)?.name || t('队友');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,20 +40,20 @@ export function nameOf(pub, playerId) {
|
|||||||
* (its result is on the way to the server)
|
* (its result is on the way to the server)
|
||||||
*/
|
*/
|
||||||
export function observeTarget(p, pub, myId, { observing = false, ownDone = false } = {}) {
|
export function observeTarget(p, pub, myId, { observing = false, ownDone = false } = {}) {
|
||||||
if (!isObj(p)) return { reason: '无效的目标' };
|
if (!isObj(p)) return { reason: t('无效的目标') };
|
||||||
if (p.playerId === myId) return observing ? { back: true } : { reason: null };
|
if (p.playerId === myId) return observing ? { back: true } : { reason: null };
|
||||||
if (p.alive === false || p.status === 'left') return { reason: '该队友已被淘汰,无法查看' };
|
if (p.alive === false || p.status === 'left') return { reason: t('该队友已被淘汰,无法查看') };
|
||||||
const phase = pub?.phase;
|
const phase = pub?.phase;
|
||||||
const me = players(pub).find((x) => x.playerId === myId) || null;
|
const me = players(pub).find((x) => x.playerId === myId) || null;
|
||||||
const meAlive = me ? me.alive !== false : true;
|
const meAlive = me ? me.alive !== false : true;
|
||||||
if (!COMBAT.has(phase)) return { fieldId: `n:${p.playerId}` };
|
if (!COMBAT.has(phase)) return { fieldId: `n:${p.playerId}` };
|
||||||
const target = fieldOf(pub, p.playerId);
|
const target = fieldOf(pub, p.playerId);
|
||||||
if (!target) return { reason: '该队友当前没有战场' };
|
if (!target) return { reason: t('该队友当前没有战场') };
|
||||||
const own = fieldOf(pub, myId);
|
const own = fieldOf(pub, myId);
|
||||||
if (!meAlive || !own) return { fieldId: target.fieldId };
|
if (!meAlive || !own) return { fieldId: target.fieldId };
|
||||||
if (own.fieldId === target.fieldId) return { reason: '队友与你在同一战场,使用 ‹ › 切换视角' };
|
if (own.fieldId === target.fieldId) return { reason: t('队友与你在同一战场,使用 ‹ › 切换视角') };
|
||||||
if (target.kind === 'boss' || target.kind === 'hidden') return { reason: '无法查看另一组队友的战场' };
|
if (target.kind === 'boss' || target.kind === 'hidden') return { reason: t('无法查看另一组队友的战场') };
|
||||||
if (own.kind === 'normal' && own.live !== false && !ownDone) return { reason: '作战中无法查看队友,作战结束后可前往查看' };
|
if (own.kind === 'normal' && own.live !== false && !ownDone) return { reason: t('作战中无法查看队友,作战结束后可前往查看') };
|
||||||
return { fieldId: target.fieldId };
|
return { fieldId: target.fieldId };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ export function teammateProgress(pub, myId) {
|
|||||||
const p = players(pub).find((x) => x.playerId === pid);
|
const p = players(pub).find((x) => x.playerId === pid);
|
||||||
const pr = isObj(f.progress) ? f.progress : null;
|
const pr = isObj(f.progress) ? f.progress : null;
|
||||||
out.push({
|
out.push({
|
||||||
playerId: pid, name: p?.name || '队友', isBot: !!p?.isBot,
|
playerId: pid, name: p?.name || t('队友'), isBot: !!p?.isBot,
|
||||||
killed: Number.isFinite(pr?.killed) ? pr.killed : null, total: Number.isFinite(pr?.total) ? pr.total : null,
|
killed: Number.isFinite(pr?.killed) ? pr.killed : null, total: Number.isFinite(pr?.total) ? pr.total : null,
|
||||||
done: f.live === false || !!pr?.done,
|
done: f.live === false || !!pr?.done,
|
||||||
});
|
});
|
||||||
@@ -114,13 +115,13 @@ export function cameraLayers(field, pub, myId) {
|
|||||||
if (!isObj(field) || (field.kind !== 'unite' && field.kind !== 'boss' && field.kind !== 'hidden')) return [];
|
if (!isObj(field) || (field.kind !== 'unite' && field.kind !== 'boss' && field.kind !== 'hidden')) return [];
|
||||||
const sides = sidesOf(field);
|
const sides = sidesOf(field);
|
||||||
const at = (side) => Object.keys(sides).find((pid) => sides[pid] === side) || null;
|
const at = (side) => Object.keys(sides).find((pid) => sides[pid] === side) || null;
|
||||||
const label = (pid) => (!pid ? '无人在家' : pid === myId ? '你自己' : nameOf(pub, pid));
|
const label = (pid) => (!pid ? t('无人在家') : pid === myId ? t('你自己') : nameOf(pub, pid));
|
||||||
const left = at('L');
|
const left = at('L');
|
||||||
const right = at('R');
|
const right = at('R');
|
||||||
if ((field.kind === 'boss' || field.kind === 'hidden') && (!left || !right)) return [];
|
if ((field.kind === 'boss' || field.kind === 'hidden') && (!left || !right)) return [];
|
||||||
return [
|
return [
|
||||||
{ key: 'L', label: label(left), self: left === myId, watch: !!left && left !== myId },
|
{ key: 'L', label: label(left), self: left === myId, watch: !!left && left !== myId },
|
||||||
{ key: 'ALL', label: '全景', self: false, watch: false },
|
{ key: 'ALL', label: t('全景'), self: false, watch: false },
|
||||||
{ key: 'R', label: label(right), self: right === myId, watch: !!right && right !== myId },
|
{ key: 'R', label: label(right), self: right === myId, watch: !!right && right !== myId },
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-7
@@ -31,6 +31,7 @@
|
|||||||
|
|
||||||
import { PROTOCOL_VERSION, ERR_TEXT } from '../../shared/constants.js';
|
import { PROTOCOL_VERSION, ERR_TEXT } from '../../shared/constants.js';
|
||||||
import { validateC2S } from '../../shared/protocol.js';
|
import { validateC2S } from '../../shared/protocol.js';
|
||||||
|
import { N_ } from '../../shared/i18n.js';
|
||||||
|
|
||||||
export const REQUEST_TIMEOUT_MS = 8000;
|
export const REQUEST_TIMEOUT_MS = 8000;
|
||||||
export const HELLO_TIMEOUT_MS = 8000;
|
export const HELLO_TIMEOUT_MS = 8000;
|
||||||
@@ -40,12 +41,12 @@ export const BACKOFF = Object.freeze({ base: 500, factor: 2, max: 10000, jitter:
|
|||||||
|
|
||||||
/** Client-side error codes (in addition to shared ERR codes). */
|
/** Client-side error codes (in addition to shared ERR codes). */
|
||||||
export const CLIENT_ERR_TEXT = Object.freeze({
|
export const CLIENT_ERR_TEXT = Object.freeze({
|
||||||
TIMEOUT: '请求超时,请重试',
|
TIMEOUT: N_('请求超时,请重试'),
|
||||||
OFFLINE: '未连接到服务器',
|
OFFLINE: N_('未连接到服务器'),
|
||||||
DISCONNECTED: '连接已断开,请重试',
|
DISCONNECTED: N_('连接已断开,请重试'),
|
||||||
CLOSED: '连接已关闭',
|
CLOSED: N_('连接已关闭'),
|
||||||
REPLACED: '该身份已在其他页面登录',
|
REPLACED: N_('该身份已在其他页面登录'),
|
||||||
VERSION: '客户端版本与服务器不一致,请刷新页面',
|
VERSION: N_('客户端版本与服务器不一致,请刷新页面'),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Server close code: the session was taken over by another socket (server/net.js CLOSE.REPLACED). */
|
/** Server close code: the session was taken over by another socket (server/net.js CLOSE.REPLACED). */
|
||||||
@@ -62,7 +63,7 @@ const QUIET_SWAP_MIN_AGE_MS = 5000;
|
|||||||
* @returns {string}
|
* @returns {string}
|
||||||
*/
|
*/
|
||||||
export function errorText(code, msg) {
|
export function errorText(code, msg) {
|
||||||
return ERR_TEXT[code] || CLIENT_ERR_TEXT[code] || (typeof msg === 'string' && msg) || String(code || '未知错误');
|
return ERR_TEXT[code] || CLIENT_ERR_TEXT[code] || (typeof msg === 'string' && msg) || String(code || N_('未知错误'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Error thrown/rejected by requests. `code` is an ERR code or a CLIENT_ERR_TEXT key. */
|
/** Error thrown/rejected by requests. `code` is an ERR code or a CLIENT_ERR_TEXT key. */
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ function Detail({ m, chess, golden, entries, onChange, onReset, locked, notOwned
|
|||||||
<h2 class="lo-dhead__name">${chess.name}</h2>
|
<h2 class="lo-dhead__name">${chess.name}</h2>
|
||||||
<span class="lo-dhead__en">${chess.appellation || ''}</span>
|
<span class="lo-dhead__en">${chess.appellation || ''}</span>
|
||||||
<span class="lo-dhead__class">
|
<span class="lo-dhead__class">
|
||||||
<${Img} src=${profGlyphUrl(m, chess.profession)} class="lo-dhead__prof lo-profglyph" />${PROF_NAME[chess.profession] || ''}
|
<${Img} src=${profGlyphUrl(m, chess.profession)} class="lo-dhead__prof lo-profglyph" />${t(PROF_NAME[chess.profession] || '')}
|
||||||
<i class="lo-sep"></i><${Img} src=${subProfIconUrl(m, chess)} class="lo-dhead__prof" />${chess.subProfessionName || ''}
|
<i class="lo-sep"></i><${Img} src=${subProfIconUrl(m, chess)} class="lo-dhead__prof" />${chess.subProfessionName || ''}
|
||||||
</span>
|
</span>
|
||||||
<span class="lo-dhead__bonds">${(chess.bonds || []).map((b) => html`<span key=${b} class="lo-bond">
|
<span class="lo-dhead__bonds">${(chess.bonds || []).map((b) => html`<span key=${b} class="lo-bond">
|
||||||
@@ -365,8 +365,8 @@ function Filters({ m, filters, onFilters, bonds }) {
|
|||||||
<div class="lo-frow">
|
<div class="lo-frow">
|
||||||
<div class="lo-chips lo-chips--prof" role="group" aria-label=${t('职业')}>
|
<div class="lo-chips lo-chips--prof" role="group" aria-label=${t('职业')}>
|
||||||
${PROF_ORDER.map((p) => html`<button key=${p} type="button" class=${cx('lo-chip', 'lo-chip--prof', filters.prof === p && 'is-on')}
|
${PROF_ORDER.map((p) => html`<button key=${p} type="button" class=${cx('lo-chip', 'lo-chip--prof', filters.prof === p && 'is-on')}
|
||||||
aria-pressed=${filters.prof === p ? 'true' : 'false'} title=${PROF_NAME[p]} onClick=${() => set({ prof: filters.prof === p ? null : p })}>
|
aria-pressed=${filters.prof === p ? 'true' : 'false'} title=${t(PROF_NAME[p])} onClick=${() => set({ prof: filters.prof === p ? null : p })}>
|
||||||
<${Img} src=${profGlyphUrl(m, p)} class="lo-chip__icon lo-profglyph" fallback=${html`<span>${PROF_NAME[p][0]}</span>`} /><span class="lo-chip__lbl">${PROF_NAME[p]}</span></button>`)}
|
<${Img} src=${profGlyphUrl(m, p)} class="lo-chip__icon lo-profglyph" fallback=${html`<span>${t(PROF_NAME[p])[0]}</span>`} /><span class="lo-chip__lbl">${t(PROF_NAME[p])}</span></button>`)}
|
||||||
</div>
|
</div>
|
||||||
<label class="lo-select">
|
<label class="lo-select">
|
||||||
<span class="lo-select__k">${t('盟约')}</span>
|
<span class="lo-select__k">${t('盟约')}</span>
|
||||||
|
|||||||
+3
-2
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
import { useLayoutEffect, useReducer, useRef } from '../vendor/hooks.module.js';
|
import { useLayoutEffect, useReducer, useRef } from '../vendor/hooks.module.js';
|
||||||
import { PHASE } from '../../shared/constants.js';
|
import { PHASE } from '../../shared/constants.js';
|
||||||
|
import { t } from '../../shared/i18n.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create an observable store.
|
* Create an observable store.
|
||||||
@@ -125,8 +126,8 @@ export function isSpectating(room, playerId) {
|
|||||||
export function sessionResetNotice(prev, playerId) {
|
export function sessionResetNotice(prev, playerId) {
|
||||||
const prevId = prev?.me?.playerId;
|
const prevId = prev?.me?.playerId;
|
||||||
if (prevId == null || prevId === playerId) return null;
|
if (prevId == null || prevId === playerId) return null;
|
||||||
if (prev?.match?.public || prev?.room?.inMatch) return '服务器会话已重置,上一局模拟已结束';
|
if (prev?.match?.public || prev?.room?.inMatch) return t('服务器会话已重置,上一局模拟已结束');
|
||||||
if (prev?.room) return '服务器会话已重置,已返回大厅';
|
if (prev?.room) return t('服务器会话已重置,已返回大厅');
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+25
-24
@@ -28,13 +28,13 @@ import { sortBonds, bondMembers, nextThreshold, bondTier, harmonyMembers, HARMON
|
|||||||
import { formatBondEffect } from './richText.js';
|
import { formatBondEffect } from './richText.js';
|
||||||
import { bondIconUrl } from './assetUrls.js';
|
import { bondIconUrl } from './assetUrls.js';
|
||||||
import { data } from '../data.js';
|
import { data } from '../data.js';
|
||||||
import { t } from '../../../shared/i18n.js';
|
import { t, tc, tParts, tName } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
|
|
||||||
/** The "👁 name" tag of a teammate's strip (DESIGN §20.15). */
|
/** The "👁 name" tag of a teammate's strip (DESIGN §20.15). */
|
||||||
function OwnerTag({ owner }) {
|
function OwnerTag({ owner }) {
|
||||||
return html`<span class="bstrip__owner" title=${`正在查看 ${owner} 的盟约`} data-owner=${owner}>
|
return html`<span class="bstrip__owner" title=${t('正在查看 {owner} 的盟约', { owner })} data-owner=${owner}>
|
||||||
<${GIcon} name="eye" class="bstrip__eye" /><${MicroLabel}>${owner}</${MicroLabel}>
|
<${GIcon} name="eye" class="bstrip__eye" /><${MicroLabel}>${owner}</${MicroLabel}>
|
||||||
</span>`;
|
</span>`;
|
||||||
}
|
}
|
||||||
@@ -47,12 +47,12 @@ export function BondStrip({ bonds, layersDisabled = false, onOpen, openId = null
|
|||||||
const sorted = sortBonds(bonds, (id) => data.lookup('bonds', id));
|
const sorted = sortBonds(bonds, (id) => data.lookup('bonds', id));
|
||||||
if (!sorted.length) {
|
if (!sorted.length) {
|
||||||
return html`<div class=${cx('bstrip', 'bstrip--empty', owner && 'is-other')} data-owner=${owner || null}>
|
return html`<div class=${cx('bstrip', 'bstrip--empty', owner && 'is-other')} data-owner=${owner || null}>
|
||||||
${owner ? html`<${OwnerTag} owner=${owner} />` : html`<${MicroLabel}>BONDS</${MicroLabel}>`}<span>${owner ? `${owner} 尚未激活盟约` : '部署干员以激活盟约'}</span></div>`;
|
${owner ? html`<${OwnerTag} owner=${owner} />` : html`<${MicroLabel}>BONDS</${MicroLabel}>`}<span>${owner ? t('{owner} 尚未激活盟约', { owner }) : t('部署干员以激活盟约')}</span></div>`;
|
||||||
}
|
}
|
||||||
const m = data.get('assets');
|
const m = data.get('assets');
|
||||||
const shown = sorted.slice(0, max);
|
const shown = sorted.slice(0, max);
|
||||||
const strip = html`<div class=${cx('bstrip', layersDisabled && 'is-frozen', owner && 'is-other')} role="list"
|
const strip = html`<div class=${cx('bstrip', layersDisabled && 'is-frozen', owner && 'is-other')} role="list"
|
||||||
aria-label=${owner ? `${owner} 的盟约` : '我的盟约'} data-owner=${owner || null}>
|
aria-label=${owner ? t('{owner} 的盟约', { owner }) : t('我的盟约')} data-owner=${owner || null}>
|
||||||
${owner ? html`<${OwnerTag} owner=${owner} />` : null}
|
${owner ? html`<${OwnerTag} owner=${owner} />` : null}
|
||||||
${shown.map((b) => {
|
${shown.map((b) => {
|
||||||
const rec = data.lookup('bonds', b.bondId);
|
const rec = data.lookup('bonds', b.bondId);
|
||||||
@@ -64,8 +64,8 @@ export function BondStrip({ bonds, layersDisabled = false, onOpen, openId = null
|
|||||||
class=${cx('bslot', 'is-off', openId === b.bondId && 'is-open')}>
|
class=${cx('bslot', 'is-off', openId === b.bondId && 'is-open')}>
|
||||||
<${BondDisc} name=${name} icon=${bondIconUrl(m, b.bondId)} tier=${0} maxTier=${Math.max(1, th.length)} active=${false}
|
<${BondDisc} name=${name} icon=${bondIconUrl(m, b.bondId)} tier=${0} maxTier=${Math.max(1, th.length)} active=${false}
|
||||||
disabled=${true} size="sm" showName=${true} layersDisabled=${layersDisabled} onClick=${() => onOpen(b.bondId)}
|
disabled=${true} size="sm" showName=${true} layersDisabled=${layersDisabled} onClick=${() => onOpen(b.bondId)}
|
||||||
title=${`${briefingBondTip(name, 'off')} · ${b.count ?? 0} 名成员`} />
|
title=${t('{tip} · {n} 名成员', { tip: briefingBondTip(name, 'off'), n: b.count ?? 0 })} />
|
||||||
<span class="bslot__count bslot__off">本局禁用</span>
|
<span class="bslot__count bslot__off">${t('本局禁用')}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
const next = nextThreshold(b.count ?? 0, th);
|
const next = nextThreshold(b.count ?? 0, th);
|
||||||
@@ -74,13 +74,13 @@ export function BondStrip({ bonds, layersDisabled = false, onOpen, openId = null
|
|||||||
<${BondDisc} name=${rec?.name || b.bondId} icon=${bondIconUrl(m, b.bondId)} layers=${rec?.noStack ? undefined : b.layers ?? 0}
|
<${BondDisc} name=${rec?.name || b.bondId} icon=${bondIconUrl(m, b.bondId)} layers=${rec?.noStack ? undefined : b.layers ?? 0}
|
||||||
tier=${b.tier ?? 0} maxTier=${Math.max(1, th.length)} active=${!!b.active} size="sm" showName=${true}
|
tier=${b.tier ?? 0} maxTier=${Math.max(1, th.length)} active=${!!b.active} size="sm" showName=${true}
|
||||||
layersDisabled=${layersDisabled} onClick=${() => onOpen(b.bondId)}
|
layersDisabled=${layersDisabled} onClick=${() => onOpen(b.bondId)}
|
||||||
title=${`${rec?.name || b.bondId} ${b.count ?? 0}/${next ?? th[th.length - 1] ?? '-'}${b.harmony > 0 ? `(含调和 +${b.harmony})` : ''}`} />
|
title=${`${rec?.name || b.bondId} ${b.count ?? 0}/${next ?? th[th.length - 1] ?? '-'}${b.harmony > 0 ? t('(含调和 +{harmony})', { harmony: b.harmony }) : ''}`} />
|
||||||
<span class=${cx('bslot__count', 'num', next == null && 'is-max')}>${b.count ?? 0}<small>/${next ?? th[th.length - 1] ?? '-'}</small></span>
|
<span class=${cx('bslot__count', 'num', next == null && 'is-max')}>${b.count ?? 0}<small>/${next ?? th[th.length - 1] ?? '-'}</small></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
})}
|
})}
|
||||||
${sorted.length > shown.length ? html`<span class="bstrip__more num">+${sorted.length - shown.length}</span>` : null}
|
${sorted.length > shown.length ? html`<span class="bstrip__more num">+${sorted.length - shown.length}</span>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
return layersDisabled ? html`<${Tooltip} text="层数叠加已禁用" placement="bottom">${strip}<//>` : strip;
|
return layersDisabled ? html`<${Tooltip} text=${t('层数叠加已禁用')} placement="bottom">${strip}<//>` : strip;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,38 +122,39 @@ export function BondPopup({ bondId, entry, priv, banned = [], onClose, onMember,
|
|||||||
const si = memberStandIn(priv, rawChess(x.id), data.get('backups'), rawChess);
|
const si = memberStandIn(priv, rawChess(x.id), data.get('backups'), rawChess);
|
||||||
return si ? { ...x, name: si.name } : x;
|
return si ? { ...x, name: si.name } : x;
|
||||||
}) : [];
|
}) : [];
|
||||||
const harmonyName = data.lookup('bonds', HARMONY_BOND)?.name || '调和';
|
const harmonyName = data.lookup('bonds', HARMONY_BOND)?.name || tName('调和');
|
||||||
const harmonyText = `${harmonyBy.length ? `${harmonyBy.map((x) => x.name).join('、')} 在场` : `${harmonyName}已激活`}:核心盟约激活人数 +${harmony}`;
|
const harmonyText = harmonyBy.length ? t('{names} 在场:核心盟约激活人数 +{harmony}', { names: harmonyBy.map((x) => x.name), harmony })
|
||||||
|
: t('{harmonyName}已激活:核心盟约激活人数 +{harmony}', { harmonyName, harmony });
|
||||||
return html`<div class=${cx('bpop', 'brackets', `bpop--${at}`, over && 'is-over', owner && 'is-other')} data-place=${at} data-owner=${owner || null}
|
return html`<div class=${cx('bpop', 'brackets', `bpop--${at}`, over && 'is-over', owner && 'is-other')} data-place=${at} data-owner=${owner || null}
|
||||||
role="dialog" aria-label=${owner ? `${owner} 的盟约:${b.name}` : `盟约:${b.name}`}>
|
role="dialog" aria-label=${owner ? t('{owner} 的盟约:{name}', { owner, name: b.name }) : t('盟约:{name}', { name: b.name })}>
|
||||||
<button type="button" class="bpop__close" aria-label="关闭" onClick=${onClose}><${Icon} name="close" /></button>
|
<button type="button" class="bpop__close" aria-label=${t('关闭')} onClick=${onClose}><${Icon} name="close" /></button>
|
||||||
<header class="bpop__head">
|
<header class="bpop__head">
|
||||||
<div class=${cx('bpop__disc', active && 'is-active')}><${BondGlyph} bondId=${bondId} /></div>
|
<div class=${cx('bpop__disc', active && 'is-active')}><${BondGlyph} bondId=${bondId} /></div>
|
||||||
<div class="bpop__titles">
|
<div class="bpop__titles">
|
||||||
<${MicroLabel} tone="mint">${b.isCore ? 'CORE BOND // 核心盟约' : 'ADD-ON BOND // 附加盟约'}</${MicroLabel}>
|
<${MicroLabel} tone="mint">${b.isCore ? t('CORE BOND // 核心盟约') : t('ADD-ON BOND // 附加盟约')}</${MicroLabel}>
|
||||||
${owner ? html`<span class="bpop__owner"><${GIcon} name="eye" /><b>${owner}</b> 的盟约</span>` : null}
|
${owner ? html`<span class="bpop__owner"><${GIcon} name="eye" />${tParts('{owner} 的盟约', { owner: html`<b>${owner}</b>` })}</span>` : null}
|
||||||
<h3 class="bpop__name">${b.name}</h3>
|
<h3 class="bpop__name">${b.name}</h3>
|
||||||
<div class="bpop__facts">
|
<div class="bpop__facts">
|
||||||
${off ? null : html`<span>在场 <b class="num">${count}</b>${next != null ? html`<small class="num">/${next}</small>` : null}${countsHand ? html`<small>(含整备区)</small>` : null}${harmony ? html`<small class="bpop__hnote" data-harmony=${harmony}>(含${harmonyName} +${harmony})</small>` : null}</span>`}
|
${off ? null : html`<span>${t('在场')} <b class="num">${count}</b>${next != null ? html`<small class="num">/${next}</small>` : null}${countsHand ? html`<small>${t('(含整备区)')}</small>` : null}${harmony ? html`<small class="bpop__hnote" data-harmony=${harmony}>${t('(含{harmonyName} +{harmony})', { harmonyName, harmony })}</small>` : null}</span>`}
|
||||||
${b.noStack ? html`<span>层数不显示</span>` : html`<span>层数 <b class="num t-mint">${layers}</b></span>`}
|
${b.noStack ? html`<span>${t('层数不显示')}</span>` : html`<span>${t('层数')} <b class="num t-mint">${layers}</b></span>`}
|
||||||
<span class=${active ? 't-mint' : 't-lo'}>${active ? `已激活${th.length > 1 ? ` · ${tier} 阶` : ''}` : off ? '本局禁用' : '未激活'}</span>
|
<span class=${active ? 't-mint' : 't-lo'}>${active ? (th.length > 1 ? t('已激活 · {tier} 阶', { tier }) : t('已激活')) : off ? t('本局禁用') : t('未激活')}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
${off ? html`<p class="bpop__off" role="note">本模式下该盟约不会激活(其干员仍可能因所属的其他盟约出现)</p>` : null}
|
${off ? html`<p class="bpop__off" role="note">${t('本模式下该盟约不会激活(其干员仍可能因所属的其他盟约出现)')}</p>` : null}
|
||||||
<div class="bpop__tiers">
|
<div class="bpop__tiers">
|
||||||
${th.map((n, i) => html`<span key=${i} class=${cx('bpop__tier', i < tier && 'is-on')}><b class="num">${n}</b><small>${b.maxCount != null ? '名及以下' : '名'}</small></span>`)}
|
${th.map((n, i) => html`<span key=${i} class=${cx('bpop__tier', i < tier && 'is-on')}><b class="num">${n}</b><small>${b.maxCount != null ? t('名及以下') : tc('bond-threshold', '名')}</small></span>`)}
|
||||||
</div>
|
</div>
|
||||||
${hasNow ? html`<section class="bpop__sec bpop__sec--now">
|
${hasNow ? html`<section class="bpop__sec bpop__sec--now">
|
||||||
<h4>当前效果 ${b.noStack ? null : html`<small class="num">(${layers} 层)</small>`}</h4>
|
<h4>${t('当前效果')} ${b.noStack ? null : html`<small class="num">${t('({layers} 层)', { layers })}</small>`}</h4>
|
||||||
<${RichText} as="p" text=${formatBondEffect(b, layers)} class="bpop__desc" />
|
<${RichText} as="p" text=${formatBondEffect(b, layers)} class="bpop__desc" />
|
||||||
</section>` : null}
|
</section>` : null}
|
||||||
<section class="bpop__sec">
|
<section class="bpop__sec">
|
||||||
<h4>盟约效果</h4>
|
<h4>${t('盟约效果')}</h4>
|
||||||
<${RichText} as="p" text=${b.descRaw || b.desc} class="bpop__desc" />
|
<${RichText} as="p" text=${b.descRaw || b.desc} class="bpop__desc" />
|
||||||
</section>
|
</section>
|
||||||
<section class="bpop__sec">
|
<section class="bpop__sec">
|
||||||
<h4>成员 <small>${memberHeadCount(members, countsHand)}/${members.length}</small></h4>
|
<h4>${t('成员')} <small>${memberHeadCount(members, countsHand)}/${members.length}</small></h4>
|
||||||
${harmony ? (harmonyBy.length
|
${harmony ? (harmonyBy.length
|
||||||
? html`<button type="button" class="bpop__harmony" data-harmony=${harmony} title=${harmonyText} onClick=${() => onMember?.(harmonyBy[0].id, null)}>
|
? html`<button type="button" class="bpop__harmony" data-harmony=${harmony} title=${harmonyText} onClick=${() => onMember?.(harmonyBy[0].id, null)}>
|
||||||
<${BondGlyph} bondId=${HARMONY_BOND} /><b>${harmonyName} +${harmony}</b><span>${harmonyText}</span></button>`
|
<${BondGlyph} bondId=${HARMONY_BOND} /><b>${harmonyName} +${harmony}</b><span>${harmonyText}</span></button>`
|
||||||
@@ -167,11 +168,11 @@ export function BondPopup({ bondId, entry, priv, banned = [], onClose, onMember,
|
|||||||
return html`<button key=${mb.id} type="button" class=${cx('bpop__member', mb.onBoard && 'is-on', mb.owned && !mb.onBoard && 'is-owned', mb.banned && 'is-banned', mb.granted && 'is-granted', si && 'is-standin')}
|
return html`<button key=${mb.id} type="button" class=${cx('bpop__member', mb.onBoard && 'is-on', mb.owned && !mb.onBoard && 'is-owned', mb.banned && 'is-banned', mb.granted && 'is-granted', si && 'is-standin')}
|
||||||
onClick=${() => (si ? onMember?.(mb.id, items, si.standInFor) : onMember?.(mb.id, items))}
|
onClick=${() => (si ? onMember?.(mb.id, items, si.standInFor) : onMember?.(mb.id, items))}
|
||||||
data-granted=${mb.granted ? '1' : null} data-standin=${si ? si.charId : null}
|
data-granted=${mb.granted ? '1' : null} data-standin=${si ? si.charId : null}
|
||||||
title=${`${name}${si ? `(${standInForText(mb.name)})` : ''}${mb.granted ? '(变形同构体:视为本盟约成员)' : ''}${mb.banned ? '(本局禁用)' : mb.onBoard ? '(在场)' : mb.owned ? '(整备区)' : ''}`}>
|
title=${`${name}${si ? t('({note})', { note: standInForText(mb.name) }) : ''}${mb.granted ? t('(变形同构体:视为本盟约成员)') : ''}${mb.banned ? t('(本局禁用)') : mb.onBoard ? t('(在场)') : mb.owned ? t('(整备区)') : ''}`}>
|
||||||
<${UnitThumb} kind="chess" id=${mb.id} size="sm" dim=${!mb.owned || mb.banned} rec=${si || (mb.diy ? mb.rec : null)} title=${name} />
|
<${UnitThumb} kind="chess" id=${mb.id} size="sm" dim=${!mb.owned || mb.banned} rec=${si || (mb.diy ? mb.rec : null)} title=${name} />
|
||||||
<span class="bpop__mname">${name}</span>
|
<span class="bpop__mname">${name}</span>
|
||||||
${mb.banned ? html`<span class="bpop__ban"><${Icon} name="close" /></span>` : null}
|
${mb.banned ? html`<span class="bpop__ban"><${Icon} name="close" /></span>` : null}
|
||||||
${mb.granted ? html`<span class="bpop__iso" aria-hidden="true">同构</span>` : null}
|
${mb.granted ? html`<span class="bpop__iso" aria-hidden="true">${t('同构')}</span>` : null}
|
||||||
${mb.diy ? html`<span class="bpop__iso bpop__diy" data-diy="1" aria-hidden="true">${t('自选')}</span>` : null}
|
${mb.diy ? html`<span class="bpop__iso bpop__diy" data-diy="1" aria-hidden="true">${t('自选')}</span>` : null}
|
||||||
</button>`;
|
</button>`;
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { itemIconUrl, enemyIconUrl, uiUrl } from './assetUrls.js';
|
|||||||
import { richTextPlain } from './richText.js';
|
import { richTextPlain } from './richText.js';
|
||||||
import { sortedPlayers } from './gameLogic.js';
|
import { sortedPlayers } from './gameLogic.js';
|
||||||
import { data } from '../data.js';
|
import { data } from '../data.js';
|
||||||
|
import { t } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ export function resolveSpCard(card, family) {
|
|||||||
else icon = uiUrl(m, `buffIcon/${team ? 'icon_team_buff' : 'icon_player_buff'}`);
|
else icon = uiUrl(m, `buffIcon/${team ? 'icon_team_buff' : 'icon_player_buff'}`);
|
||||||
return {
|
return {
|
||||||
kind,
|
kind,
|
||||||
name: card.name || item?.name || eff?.name || bounty?.name || tactic?.name || '机变',
|
name: card.name || item?.name || eff?.name || bounty?.name || tactic?.name || t('机变'),
|
||||||
desc: cardText(card, item?.descRaw || eff?.descRaw || '') || item?.desc || eff?.desc || bounty?.desc || tactic?.desc || '',
|
desc: cardText(card, item?.descRaw || eff?.descRaw || '') || item?.desc || eff?.desc || bounty?.desc || tactic?.desc || '',
|
||||||
tier: Number.isFinite(card.tier) ? card.tier : item?.tier ?? bounty?.tier ?? null,
|
tier: Number.isFinite(card.tier) ? card.tier : item?.tier ?? bounty?.tier ?? null,
|
||||||
icon,
|
icon,
|
||||||
@@ -158,7 +159,7 @@ export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null,
|
|||||||
const players = new Map(sortedPlayers(pub).map((p) => [p.playerId, p]));
|
const players = new Map(sortedPlayers(pub).map((p) => [p.playerId, p]));
|
||||||
const myTurn = solo || sp.turnPid === myId;
|
const myTurn = solo || sp.turnPid === myId;
|
||||||
const mine = sp.pickOf.get(myId);
|
const mine = sp.pickOf.get(myId);
|
||||||
const turnName = players.get(sp.turnPid)?.name || '队友';
|
const turnName = players.get(sp.turnPid)?.name || t('队友');
|
||||||
const special = /_s$/.test(String(sp.family || ''));
|
const special = /_s$/.test(String(sp.family || ''));
|
||||||
const order = solo ? [] : sp.order;
|
const order = solo ? [] : sp.order;
|
||||||
const timed = !solo && !sp.untimed;
|
const timed = !solo && !sp.untimed;
|
||||||
@@ -171,25 +172,25 @@ export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null,
|
|||||||
if (t && typeof t.closest === 'function' && t.closest('.spcard, .spov__confirm')) return;
|
if (t && typeof t.closest === 'function' && t.closest('.spcard, .spov__confirm')) return;
|
||||||
onDisarm();
|
onDisarm();
|
||||||
};
|
};
|
||||||
return html`<div class=${cx('spov', armed != null && 'has-armed')} role="dialog" aria-label="机变阶段" onPointerDown=${onDown}>
|
return html`<div class=${cx('spov', armed != null && 'has-armed')} role="dialog" aria-label=${t('机变阶段')} onPointerDown=${onDown}>
|
||||||
<div class="spov__veil" aria-hidden="true"></div>
|
<div class="spov__veil" aria-hidden="true"></div>
|
||||||
<div class="spov__inner">
|
<div class="spov__inner">
|
||||||
<header class="spov__head">
|
<header class="spov__head">
|
||||||
<div class="spov__titles">
|
<div class="spov__titles">
|
||||||
<${MicroLabel} tone="mint">CONTINGENCY // 机变阶段</${MicroLabel}>
|
<${MicroLabel} tone="mint">${t('CONTINGENCY // 机变阶段')}</${MicroLabel}>
|
||||||
<h2 class=${cx('spov__title', special && 'is-special')}>${sp.name || fam?.name || '机变'}<span class="spov__bar">|</span><span class="spov__desc"><${RichText} text=${sp.desc || fam?.desc || '选择一项'} /></span></h2>
|
<h2 class=${cx('spov__title', special && 'is-special')}>${sp.name || fam?.name || t('机变')}<span class="spov__bar">|</span><span class="spov__desc"><${RichText} text=${sp.desc || fam?.desc || t('选择一项')} /></span></h2>
|
||||||
<p class="spov__sub">${timed ? '倒计时结束后仍未选定将自动分配' : '选择一项(无时间限制)'}${mine == null && myTurn ? ' · 点击卡牌选中,再次点击确认' : ''}</p>
|
<p class="spov__sub">${timed ? t('倒计时结束后仍未选定将自动分配') : t('选择一项(无时间限制)')}${mine == null && myTurn ? t(' · 点击卡牌选中,再次点击确认') : ''}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="spov__turn">
|
<div class="spov__turn">
|
||||||
${mine != null ? html`<span class="spov__turntxt is-done"><${Icon} name="check" />已完成选择</span>`
|
${mine != null ? html`<span class="spov__turntxt is-done"><${Icon} name="check" />${t('已完成选择')}</span>`
|
||||||
: myTurn ? html`<span class="spov__turntxt is-mine">当前轮到你决策</span>`
|
: myTurn ? html`<span class="spov__turntxt is-mine">${t('当前轮到你决策')}</span>`
|
||||||
: html`<span class="spov__turntxt">${turnName} 正在决策…<${Icon} name="hourglass" /></span>`}
|
: html`<span class="spov__turntxt">${t('{turnName} 正在决策…', { turnName })}<${Icon} name="hourglass" /></span>`}
|
||||||
${armed != null ? html`<${Button} variant="primary" size="lg" icon="check" class="spov__confirm" data-testid="sp-confirm"
|
${armed != null ? html`<${Button} variant="primary" size="lg" icon="check" class="spov__confirm" data-testid="sp-confirm"
|
||||||
title=${armedName ? `确认选择「${armedName}」(再次点击卡牌亦可)` : '确认选择'} onClick=${onConfirm}>确认选择<//>` : null}
|
title=${armedName ? t('确认选择「{armedName}」(再次点击卡牌亦可)', { armedName }) : t('确认选择')} onClick=${onConfirm}>${t('确认选择')}<//>` : null}
|
||||||
${timed ? html`<${Countdown} deadline=${pub?.deadline} total=${total ?? undefined} size="sm" />` : null}
|
${timed ? html`<${Countdown} deadline=${pub?.deadline} total=${total ?? undefined} size="sm" />` : null}
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
${order.length ? html`<div class="spov__order" aria-label="决策顺序">
|
${order.length ? html`<div class="spov__order" aria-label=${t('决策顺序')}>
|
||||||
${order.map((pid, i) => {
|
${order.map((pid, i) => {
|
||||||
const p = players.get(pid);
|
const p = players.get(pid);
|
||||||
const picked = sp.pickOf.has(pid);
|
const picked = sp.pickOf.has(pid);
|
||||||
@@ -198,7 +199,7 @@ export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null,
|
|||||||
return html`<div key=${pid} class=${cx('spov__who', cur && 'is-cur', picked && 'is-done', pid === myId && 'is-self')}>
|
return html`<div key=${pid} class=${cx('spov__who', cur && 'is-cur', picked && 'is-done', pid === myId && 'is-self')}>
|
||||||
<span class="spov__idx num">${i + 1}</span>
|
<span class="spov__idx num">${i + 1}</span>
|
||||||
<${PlayerAvatar} player=${p || { name: '?' }} size="sm" self=${pid === myId} />
|
<${PlayerAvatar} player=${p || { name: '?' }} size="sm" self=${pid === myId} />
|
||||||
<span class="spov__wname">${p?.name || '博士'}</span>
|
<span class="spov__wname">${p?.name || t('博士')}</span>
|
||||||
<span class="spov__wstate">${left ? html`<${Icon} name="exit" />` : picked ? html`<${Icon} name="check" />` : cur ? html`<${Icon} name="hourglass" />` : html`<${Icon} name="dots" />`}</span>
|
<span class="spov__wstate">${left ? html`<${Icon} name="exit" />` : picked ? html`<${Icon} name="check" />` : cur ? html`<${Icon} name="hourglass" />` : html`<${Icon} name="dots" />`}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
})}
|
})}
|
||||||
@@ -210,26 +211,26 @@ export function ChoiceView({ pub, sp, myId, solo, busyIdx = null, total = null,
|
|||||||
const can = cardPickable(sp, card, { myId, solo, busyIdx });
|
const can = cardPickable(sp, card, { myId, solo, busyIdx });
|
||||||
const busy = pickBusy(busyIdx, card, mine);
|
const busy = pickBusy(busyIdx, card, mine);
|
||||||
const isArmed = can && armed === card.idx;
|
const isArmed = can && armed === card.idx;
|
||||||
const takerName = taker ? (card.takenBy === myId ? '你' : taker.name) : null;
|
const takerName = taker ? (card.takenBy === myId ? t('你') : taker.name) : null;
|
||||||
return html`<button key=${card.idx} type="button" class=${cx('spcard', `spcard--${r.kind}`, card.takenBy && 'is-taken', card.takenBy === myId && 'is-mine', can && 'is-pickable', isArmed && 'is-armed', busy && 'is-busy')}
|
return html`<button key=${card.idx} type="button" class=${cx('spcard', `spcard--${r.kind}`, card.takenBy && 'is-taken', card.takenBy === myId && 'is-mine', can && 'is-pickable', isArmed && 'is-armed', busy && 'is-busy')}
|
||||||
aria-busy=${busy ? 'true' : undefined} aria-pressed=${can ? String(isArmed) : undefined} disabled=${!can} onClick=${() => can && onTap(card.idx)}
|
aria-busy=${busy ? 'true' : undefined} aria-pressed=${can ? String(isArmed) : undefined} disabled=${!can} onClick=${() => can && onTap(card.idx)}
|
||||||
aria-label=${isArmed ? `${r.name},已选中,再次点击确认` : takerName ? `${r.name},${takerName}已选择` : r.name} title=${`${r.name}\n${richTextPlain(r.desc)}`}>
|
aria-label=${isArmed ? t('{name},已选中,再次点击确认', { name: r.name }) : takerName ? t('{name},{takerName}已选择', { name: r.name, takerName }) : r.name} title=${`${r.name}\n${richTextPlain(r.desc)}`}>
|
||||||
<span class="spcard__glow" aria-hidden="true"></span>
|
<span class="spcard__glow" aria-hidden="true"></span>
|
||||||
<span class="spcard__head">
|
<span class="spcard__head">
|
||||||
<span class="spcard__icon"><${Img} src=${r.icon} fallback=${html`<${GIcon} name=${r.kind === 'bounty' ? 'target' : 'bolt'} />`} /></span>
|
<span class="spcard__icon"><${Img} src=${r.icon} fallback=${html`<${GIcon} name=${r.kind === 'bounty' ? 'target' : 'bolt'} />`} /></span>
|
||||||
<span class="spcard__title">
|
<span class="spcard__title">
|
||||||
<b class="spcard__name">${r.name}</b>
|
<b class="spcard__name">${r.name}</b>
|
||||||
${r.team || (r.kind === 'bounty' && r.coin) ? html`<span class="spcard__tags">
|
${r.team || (r.kind === 'bounty' && r.coin) ? html`<span class="spcard__tags">
|
||||||
${r.team ? html`<span class="spcard__tag spcard__tag--team">全队获得</span>` : null}
|
${r.team ? html`<span class="spcard__tag spcard__tag--team">${t('全队获得')}</span>` : null}
|
||||||
${r.kind === 'bounty' && r.coin ? html`<span class="spcard__tag spcard__tag--coin">赏金 ${r.coin}</span>` : null}
|
${r.kind === 'bounty' && r.coin ? html`<span class="spcard__tag spcard__tag--coin">${t('赏金 {coin}', { coin: r.coin })}</span>` : null}
|
||||||
</span>` : null}
|
</span>` : null}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<${RichText} text=${r.desc} class="spcard__desc" />
|
<${RichText} text=${r.desc} class="spcard__desc" />
|
||||||
${r.tier ? html`<${TierChip} tier=${r.tier} size="md" class="spcard__tier" />` : null}
|
${r.tier ? html`<${TierChip} tier=${r.tier} size="md" class="spcard__tier" />` : null}
|
||||||
${taker ? html`<span class="spcard__taker" title=${`${taker.name} 已选择`}><${PlayerAvatar} player=${taker} size="sm" /></span>` : null}
|
${taker ? html`<span class="spcard__taker" title=${t('{name} 已选择', { name: taker.name })}><${PlayerAvatar} player=${taker} size="sm" /></span>` : null}
|
||||||
${isArmed ? html`<span class="spcard__confirm" role="status"><b>确认选择</b><small>再次点击</small></span>` : null}
|
${isArmed ? html`<span class="spcard__confirm" role="status"><b>${t('确认选择')}</b><small>${t('再次点击')}</small></span>` : null}
|
||||||
${busy ? html`<span class="spcard__busy" role="status">选择中</span>` : null}
|
${busy ? html`<span class="spcard__busy" role="status">${t('选择中')}</span>` : null}
|
||||||
</button>`;
|
</button>`;
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
+16
-15
@@ -13,17 +13,18 @@ import { html, Icon } from './components.js';
|
|||||||
import { DpCounter } from './hud.js';
|
import { DpCounter } from './hud.js';
|
||||||
import { GIcon } from './gameComponents.js';
|
import { GIcon } from './gameComponents.js';
|
||||||
import { switcherLabel, cycleField } from './gameLogic.js';
|
import { switcherLabel, cycleField } from './gameLogic.js';
|
||||||
|
import { t } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
|
|
||||||
/** Teammates' progress under the waiting pill (css/screens/game.css .chud__progress / .chud__prog). */
|
/** Teammates' progress under the waiting pill (css/screens/game.css .chud__progress / .chud__prog). */
|
||||||
function ProgressList({ list }) {
|
function ProgressList({ list }) {
|
||||||
if (!Array.isArray(list) || !list.length) return null;
|
if (!Array.isArray(list) || !list.length) return null;
|
||||||
return html`<div class="chud__progress" role="list" aria-label="队友作战进度">
|
return html`<div class="chud__progress" role="list" aria-label=${t('队友作战进度')}>
|
||||||
${list.map((p) => html`<span key=${p.playerId} role="listitem" class=${cx('chud__prog', p.done && 'is-done')}>
|
${list.map((p) => html`<span key=${p.playerId} role="listitem" class=${cx('chud__prog', p.done && 'is-done')}>
|
||||||
<b>${p.name}</b>
|
<b>${p.name}</b>
|
||||||
${p.done
|
${p.done
|
||||||
? html`<span class="chud__prog__ok" aria-label="作战结束">✓</span>`
|
? html`<span class="chud__prog__ok" aria-label=${t('作战结束')}>✓</span>`
|
||||||
: html`<span class="num">${p.killed != null && p.total != null ? `${p.killed}/${p.total}` : '•••'}</span>`}
|
: html`<span class="num">${p.killed != null && p.total != null ? `${p.killed}/${p.total}` : '•••'}</span>`}
|
||||||
</span>`)}
|
</span>`)}
|
||||||
</div>`;
|
</div>`;
|
||||||
@@ -44,14 +45,14 @@ export function CombatHud({ pub, myId, watching, hud, myDone, onWatch, spectatin
|
|||||||
return html`<div class="chud">
|
return html`<div class="chud">
|
||||||
<${DpCounter} dp=${hud?.dp} />
|
<${DpCounter} dp=${hud?.dp} />
|
||||||
<div class="chud__bottom">
|
<div class="chud__bottom">
|
||||||
${myDone ? html`<div class="chud__msg" role="status"><${Icon} name="hourglass" /><span>作战结束,等待队友完成作战</span></div>` : null}
|
${myDone ? html`<div class="chud__msg" role="status"><${Icon} name="hourglass" /><span>${t('作战结束,等待队友完成作战')}</span></div>` : null}
|
||||||
${spectating ? (spectator ? html`<div class="chud__msg" role="status"><${GIcon} name="eye" /><span>观战中</span></div>`
|
${spectating ? (spectator ? html`<div class="chud__msg" role="status"><${GIcon} name="eye" /><span>${t('观战中')}</span></div>`
|
||||||
: html`<div class="chud__msg chud__msg--dead" role="status"><${Icon} name="close" /><span>你已被淘汰,正在观战</span></div>`) : null}
|
: html`<div class="chud__msg chud__msg--dead" role="status"><${Icon} name="close" /><span>${t('你已被淘汰,正在观战')}</span></div>`) : null}
|
||||||
${fields.length ? html`<div class=${cx('vswitch', !canCycle && 'is-single')}>
|
${fields.length ? html`<div class=${cx('vswitch', !canCycle && 'is-single')}>
|
||||||
<button type="button" class="vswitch__arrow" disabled=${!canCycle} aria-label="上一个战场"
|
<button type="button" class="vswitch__arrow" disabled=${!canCycle} aria-label=${t('上一个战场')}
|
||||||
onClick=${() => { const n = cycleField(fields, watching, -1); if (n && n !== watching) onWatch(n); }}><${Icon} name="chevronLeft" /></button>
|
onClick=${() => { const n = cycleField(fields, watching, -1); if (n && n !== watching) onWatch(n); }}><${Icon} name="chevronLeft" /></button>
|
||||||
<span class="vswitch__label">${label}</span>
|
<span class="vswitch__label">${label}</span>
|
||||||
<button type="button" class="vswitch__arrow" disabled=${!canCycle} aria-label="下一个战场"
|
<button type="button" class="vswitch__arrow" disabled=${!canCycle} aria-label=${t('下一个战场')}
|
||||||
onClick=${() => { const n = cycleField(fields, watching, 1); if (n && n !== watching) onWatch(n); }}><${Icon} name="chevronRight" /></button>
|
onClick=${() => { const n = cycleField(fields, watching, 1); if (n && n !== watching) onWatch(n); }}><${Icon} name="chevronRight" /></button>
|
||||||
</div>` : null}
|
</div>` : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -72,22 +73,22 @@ function ClientHud({ hud, myDone, spectating, spectator = false, client }) {
|
|||||||
return html`<div class="chud">
|
return html`<div class="chud">
|
||||||
<${DpCounter} dp=${hud?.dp} />
|
<${DpCounter} dp=${hud?.dp} />
|
||||||
<div class="chud__bottom">
|
<div class="chud__bottom">
|
||||||
${myDone && !observing ? html`<div class="chud__msg chud__wait" role="status"><${Icon} name="hourglass" /><span>作战结束,等待队友完成作战</span></div>
|
${myDone && !observing ? html`<div class="chud__msg chud__wait" role="status"><${Icon} name="hourglass" /><span>${t('作战结束,等待队友完成作战')}</span></div>
|
||||||
<${ProgressList} list=${client.progress} />` : null}
|
<${ProgressList} list=${client.progress} />` : null}
|
||||||
${spectating && !observing ? (spectator ? html`<div class="chud__msg" role="status"><${GIcon} name="eye" /><span>观战中,可点击左侧成员头像前往查看</span></div>`
|
${spectating && !observing ? (spectator ? html`<div class="chud__msg" role="status"><${GIcon} name="eye" /><span>${t('观战中,可点击左侧成员头像前往查看')}</span></div>`
|
||||||
: html`<div class="chud__msg chud__msg--dead" role="status"><${Icon} name="close" /><span>你已被淘汰,可点击队友头像前往查看</span></div>`) : null}
|
: html`<div class="chud__msg chud__msg--dead" role="status"><${Icon} name="close" /><span>${t('你已被淘汰,可点击队友头像前往查看')}</span></div>`) : null}
|
||||||
${observing && !halves ? html`<div class="vswitch chud__observe is-single" role="status">
|
${observing && !halves ? html`<div class="vswitch chud__observe is-single" role="status">
|
||||||
<span class="vswitch__label"><${GIcon} name="eye" /><span>${observing.name}</span></span>
|
<span class="vswitch__label"><${GIcon} name="eye" /><span>${observing.name}</span></span>
|
||||||
${client.onBack ? html`<button type="button" class="btn btn--secondary btn--sm chud__back" onClick=${() => client.onBack()}>
|
${client.onBack ? html`<button type="button" class="btn btn--secondary btn--sm chud__back" onClick=${() => client.onBack()}>
|
||||||
<span class="btn__label">返回战场</span></button>` : null}
|
<span class="btn__label">${t('返回战场')}</span></button>` : null}
|
||||||
</div>` : null}
|
</div>` : null}
|
||||||
${halves ? html`<div class=${cx('vswitch', 'chud__layers', observing && 'is-observing')}>
|
${halves ? html`<div class=${cx('vswitch', 'chud__layers', observing && 'is-observing')}>
|
||||||
<button type="button" class="vswitch__arrow" disabled=${idx <= 0} aria-label="左侧战场" onClick=${() => step(-1)}><${Icon} name="chevronLeft" /></button>
|
<button type="button" class="vswitch__arrow" disabled=${idx <= 0} aria-label=${t('左侧战场')} onClick=${() => step(-1)}><${Icon} name="chevronLeft" /></button>
|
||||||
<span class="vswitch__label">
|
<span class="vswitch__label">
|
||||||
${cur && cur.watch ? html`<${GIcon} name="eye" />` : null}<span>${cur ? cur.label : '全景'}</span></span>
|
${cur && cur.watch ? html`<${GIcon} name="eye" />` : null}<span>${cur ? cur.label : t('全景')}</span></span>
|
||||||
<button type="button" class="vswitch__arrow" disabled=${idx >= layers.length - 1} aria-label="右侧战场" onClick=${() => step(1)}><${Icon} name="chevronRight" /></button>
|
<button type="button" class="vswitch__arrow" disabled=${idx >= layers.length - 1} aria-label=${t('右侧战场')} onClick=${() => step(1)}><${Icon} name="chevronRight" /></button>
|
||||||
${observing && client.onBack ? html`<button type="button" class="btn btn--secondary btn--sm chud__back" onClick=${() => client.onBack()}>
|
${observing && client.onBack ? html`<button type="button" class="btn btn--secondary btn--sm chud__back" onClick=${() => client.onBack()}>
|
||||||
<span class="btn__label">返回战场</span></button>` : null}
|
<span class="btn__label">${t('返回战场')}</span></button>` : null}
|
||||||
</div>` : null}
|
</div>` : null}
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
+11
-11
@@ -194,12 +194,12 @@ export function tierChipUrl(t) {
|
|||||||
* @param {{ tier: number, golden?: boolean, size?: 'sm'|'md'|'lg', class?: string }} props
|
* @param {{ tier: number, golden?: boolean, size?: 'sm'|'md'|'lg', class?: string }} props
|
||||||
*/
|
*/
|
||||||
export function TierChip({ tier, golden = false, size = 'md', class: cls }) {
|
export function TierChip({ tier, golden = false, size = 'md', class: cls }) {
|
||||||
const t = Math.max(1, Math.min(6, Number(tier) | 0 || 1));
|
const n = Math.max(1, Math.min(6, Number(tier) | 0 || 1));
|
||||||
const src = tierChipUrl(t);
|
const src = tierChipUrl(n);
|
||||||
const [bad, setBad] = useState(null);
|
const [bad, setBad] = useState(null);
|
||||||
const img = !!src && bad !== src;
|
const img = !!src && bad !== src;
|
||||||
return html`<span class=${cx('tier', `tier--${t}`, `tier--${size}`, golden && 'tier--golden', img && 'tier--img', cls)} aria-label=${`${t}阶`}>
|
return html`<span class=${cx('tier', `tier--${n}`, `tier--${size}`, golden && 'tier--golden', img && 'tier--img', cls)} aria-label=${t('{tier}阶', { tier: n })}>
|
||||||
${img ? html`<img src=${src} alt="" draggable=${false} onError=${() => setBad(src)} />` : roman(t)}
|
${img ? html`<img src=${src} alt="" draggable=${false} onError=${() => setBad(src)} />` : roman(n)}
|
||||||
</span>`;
|
</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ export function BondDisc({
|
|||||||
: html`<span class="bond__glyph">${glyph}</span>`}
|
: html`<span class="bond__glyph">${glyph}</span>`}
|
||||||
</span>
|
</span>
|
||||||
${stack != null ? html`<span class="bond__count">${stack}</span>` : null}
|
${stack != null ? html`<span class="bond__count">${stack}</span>` : null}
|
||||||
${disabled ? html`<span class="bond__ban" aria-label="禁用"><${Icon} name="close" /></span>` : null}
|
${disabled ? html`<span class="bond__ban" aria-label=${t('禁用')}><${Icon} name="close" /></span>` : null}
|
||||||
</span>
|
</span>
|
||||||
${showName && name ? html`<span class="bond__name">${name}</span>` : null}
|
${showName && name ? html`<span class="bond__name">${name}</span>` : null}
|
||||||
<//>`;
|
<//>`;
|
||||||
@@ -373,7 +373,7 @@ export function Countdown({ deadline, seconds, total, warnAt = 10, label = 'COUN
|
|||||||
|
|
||||||
// untimed phase: no timer on screen (hooks above run unconditionally, so the component may toggle freely)
|
// untimed phase: no timer on screen (hooks above run unconditionally, so the component may toggle freely)
|
||||||
if (remain == null) return null;
|
if (remain == null) return null;
|
||||||
return html`<div class=${cx('countdown', `countdown--${size}`, warn && 'is-warn', cls)} role="timer" aria-label=${remain == null ? '无倒计时' : `剩余${remain}秒`}>
|
return html`<div class=${cx('countdown', `countdown--${size}`, warn && 'is-warn', cls)} role="timer" aria-label=${remain == null ? t('无倒计时') : t('剩余{remain}秒', { remain })}>
|
||||||
<div class="countdown__main">
|
<div class="countdown__main">
|
||||||
<${SevenSeg} text=${text} tone=${warn ? 'orange' : 'mint'} flicker=${warn && remain > 0} />
|
<${SevenSeg} text=${text} tone=${warn ? 'orange' : 'mint'} flicker=${warn && remain > 0} />
|
||||||
${label ? html`<span class="countdown__label">${label}</span>` : null}
|
${label ? html`<span class="countdown__label">${label}</span>` : null}
|
||||||
@@ -489,7 +489,7 @@ export function DialogHost() {
|
|||||||
}, []);
|
}, []);
|
||||||
const d = dialogs[0];
|
const d = dialogs[0];
|
||||||
if (!d) return null;
|
if (!d) return null;
|
||||||
const { title = '确认', text, okText = '确认', cancelText = '取消', tone, danger, micro = 'CONFIRMATION' } = d.opts;
|
const { title = t('确认'), text, okText = t('确认'), cancelText = t('取消'), tone, danger, micro = 'CONFIRMATION' } = d.opts;
|
||||||
const isConfirm = d.kind === 'confirm';
|
const isConfirm = d.kind === 'confirm';
|
||||||
// Enter confirms unless a specific button has focus (then the native click decides).
|
// Enter confirms unless a specific button has focus (then the native click decides).
|
||||||
const onKey = (e) => {
|
const onKey = (e) => {
|
||||||
@@ -670,7 +670,7 @@ export function Spinner({ size = 'md', label, tone = 'mint', class: cls }) {
|
|||||||
<polygon class="spinner__track" points="25,3 44,14 44,36 25,47 6,36 6,14" />
|
<polygon class="spinner__track" points="25,3 44,14 44,36 25,47 6,36 6,14" />
|
||||||
<polygon class="spinner__arc" points="25,3 44,14 44,36 25,47 6,36 6,14" />
|
<polygon class="spinner__arc" points="25,3 44,14 44,36 25,47 6,36 6,14" />
|
||||||
</svg>
|
</svg>
|
||||||
${label ? html`<span class="spinner__label">${label}</span>` : html`<span class="sr-only">加载中</span>`}
|
${label ? html`<span class="spinner__label">${label}</span>` : html`<span class="sr-only">${t('加载中')}</span>`}
|
||||||
</span>`;
|
</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -695,8 +695,8 @@ export function AvatarFrame({ name = '', src, size = 'md', seat = 0, host, bot,
|
|||||||
: bot ? html`<${Icon} name="robot" class="avatar__bot" />`
|
: bot ? html`<${Icon} name="robot" class="avatar__bot" />`
|
||||||
: html`<span class="avatar__glyph">${glyph}</span>`}
|
: html`<span class="avatar__glyph">${glyph}</span>`}
|
||||||
</div>
|
</div>
|
||||||
${host ? html`<span class="avatar__badge avatar__badge--host" title="创建者"><${Icon} name="crown" /></span>` : null}
|
${host ? html`<span class="avatar__badge avatar__badge--host" title=${t('创建者')}><${Icon} name="crown" /></span>` : null}
|
||||||
${self ? html`<span class="avatar__badge avatar__badge--self" title="你"><${Icon} name="user" /></span>` : null}
|
${self ? html`<span class="avatar__badge avatar__badge--self" title=${t('你')}><${Icon} name="user" /></span>` : null}
|
||||||
${bot && !empty ? html`<span class="avatar__tag">AI</span>` : null}
|
${bot && !empty ? html`<span class="avatar__tag">AI</span>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -755,7 +755,7 @@ export function doctorNo(id) {
|
|||||||
export function PingPill({ ms, online = true, class: cls }) {
|
export function PingPill({ ms, online = true, class: cls }) {
|
||||||
const ok = online && Number.isFinite(ms);
|
const ok = online && Number.isFinite(ms);
|
||||||
const tier = !ok ? 'off' : ms < 60 ? 'low' : ms < 200 ? 'medium' : 'high';
|
const tier = !ok ? 'off' : ms < 60 ? 'low' : ms < 200 ? 'medium' : 'high';
|
||||||
return html`<span class=${cx('ping', `ping--${tier}`, cls)} title=${ok ? `当前延迟 ${ms}ms` : '未连接'}>
|
return html`<span class=${cx('ping', `ping--${tier}`, cls)} title=${ok ? t('当前延迟 {ms}ms', { ms }) : t('未连接')}>
|
||||||
<${Icon} name=${ok ? 'signal' : 'wifiOff'} class="ping__icon" />
|
<${Icon} name=${ok ? 'signal' : 'wifiOff'} class="ping__icon" />
|
||||||
<span class="ping__value">${ok ? Math.min(9999, Math.round(ms)) : '--'}</span><span class="ping__unit">ms</span>
|
<span class="ping__value">${ok ? Math.min(9999, Math.round(ms)) : '--'}</span><span class="ping__unit">ms</span>
|
||||||
</span>`;
|
</span>`;
|
||||||
|
|||||||
+13
-12
@@ -9,6 +9,7 @@ import { html, Button, Icon, useTicker } from './components.js';
|
|||||||
import { net, CLIENT_ERR_TEXT } from '../net.js';
|
import { net, CLIENT_ERR_TEXT } from '../net.js';
|
||||||
import { useStore, shallowEqual } from '../store.js';
|
import { useStore, shallowEqual } from '../store.js';
|
||||||
import { useDocClass } from './device.js';
|
import { useDocClass } from './device.js';
|
||||||
|
import { t } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
/** Whether the banner shows for this connection state (mirrors the early returns below). */
|
/** Whether the banner shows for this connection state (mirrors the early returns below). */
|
||||||
export function bannerVisible(conn, entered, restoring, buildStale = false) {
|
export function bannerVisible(conn, entered, restoring, buildStale = false) {
|
||||||
@@ -28,15 +29,15 @@ export function ConnectionBanner() {
|
|||||||
if (!entered) return null;
|
if (!entered) return null;
|
||||||
if (conn.status === 'online' && !restoring && !buildStale) return null;
|
if (conn.status === 'online' && !restoring && !buildStale) return null;
|
||||||
if (conn.status === 'online' && restoring) {
|
if (conn.status === 'online' && restoring) {
|
||||||
return html`<div class="conn-banner" role="status"><${Icon} name="refresh" /><span>正在同步同盟状态…</span></div>`;
|
return html`<div class="conn-banner" role="status"><${Icon} name="refresh" /><span>${t('正在同步同盟状态…')}</span></div>`;
|
||||||
}
|
}
|
||||||
if (conn.status === 'online' && buildStale) {
|
if (conn.status === 'online' && buildStale) {
|
||||||
// the server has a newer build than this page: the guard reloads by itself once the match is over, the button is
|
// the server has a newer build than this page: the guard reloads by itself once the match is over, the button is
|
||||||
// for a player who would rather do it now
|
// for a player who would rather do it now
|
||||||
return html`<div class="conn-banner" role="alert">
|
return html`<div class="conn-banner" role="alert">
|
||||||
<${Icon} name="refresh" />
|
<${Icon} name="refresh" />
|
||||||
<span>服务器已更新,本局结束后刷新</span>
|
<span>${t('服务器已更新,本局结束后刷新')}</span>
|
||||||
<${Button} size="sm" variant="secondary" icon="refresh" onClick=${() => location.reload()}>刷新页面<//>
|
<${Button} size="sm" variant="secondary" icon="refresh" onClick=${() => location.reload()}>${t('刷新页面')}<//>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
if (!conn.everOnline && (conn.status === 'connecting' || conn.status === 'handshaking' || conn.status === 'idle')) return null;
|
if (!conn.everOnline && (conn.status === 'connecting' || conn.status === 'handshaking' || conn.status === 'idle')) return null;
|
||||||
@@ -47,18 +48,18 @@ export function ConnectionBanner() {
|
|||||||
// Short transitional states (a rename re-sends hello on the live socket) only show if they linger.
|
// Short transitional states (a rename re-sends hello on the live socket) only show if they linger.
|
||||||
const transient = conn.status === 'connecting' || conn.status === 'handshaking' || (conn.status === 'connected' && !rejected);
|
const transient = conn.status === 'connecting' || conn.status === 'handshaking' || (conn.status === 'connected' && !rejected);
|
||||||
const text = conn.status === 'reconnecting'
|
const text = conn.status === 'reconnecting'
|
||||||
? '与服务器的连接已中断,正在重连'
|
? t('与服务器的连接已中断,正在重连')
|
||||||
: replaced ? '该身份已在其他页面登录'
|
: replaced ? t('该身份已在其他页面登录')
|
||||||
: conn.status === 'closed' ? '连接已关闭'
|
: conn.status === 'closed' ? t('连接已关闭')
|
||||||
: rejected ? conn.lastError.text : '正在连接服务器';
|
: rejected ? t(conn.lastError.text) : t('正在连接服务器');
|
||||||
const action = conn.status === 'reconnecting' ? { label: '立即重连', run: () => net.retryNow() }
|
const action = conn.status === 'reconnecting' ? { label: t('立即重连'), run: () => net.retryNow() }
|
||||||
: conn.status === 'closed' ? { label: replaced ? '在此页面继续' : '重新连接', run: () => net.connect() }
|
: conn.status === 'closed' ? { label: replaced ? t('在此页面继续') : t('重新连接'), run: () => net.connect() }
|
||||||
: versionMismatch ? { label: '刷新页面', run: () => location.reload() }
|
: versionMismatch ? { label: t('刷新页面'), run: () => location.reload() }
|
||||||
: rejected ? { label: '重试', run: () => net.reconnectNow() } : null;
|
: rejected ? { label: t('重试'), run: () => net.reconnectNow() } : null;
|
||||||
return html`<div class=${`conn-banner${transient ? ' conn-banner--soft' : ''}`} role="alert">
|
return html`<div class=${`conn-banner${transient ? ' conn-banner--soft' : ''}`} role="alert">
|
||||||
<${Icon} name="wifiOff" />
|
<${Icon} name="wifiOff" />
|
||||||
<span>${text}</span>
|
<span>${text}</span>
|
||||||
${conn.status === 'reconnecting' ? html`<span class="conn-banner__sub">第 ${conn.attempt} 次 · ${secs}s</span>` : null}
|
${conn.status === 'reconnecting' ? html`<span class="conn-banner__sub">${t('第 {attempt} 次 · {secs}s', { attempt: conn.attempt, secs })}</span>` : null}
|
||||||
${action ? html`<${Button} size="sm" variant="secondary" icon="refresh" onClick=${action.run}>${action.label}<//>` : null}
|
${action ? html`<${Button} size="sm" variant="secondary" icon="refresh" onClick=${action.run}>${action.label}<//>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
+92
-92
@@ -41,18 +41,18 @@ import { data } from '../data.js';
|
|||||||
import { attackRangeGrid } from '../../../shared/loadoutRecord.js';
|
import { attackRangeGrid } from '../../../shared/loadoutRecord.js';
|
||||||
import { SKILL_SUMMON_START_DEPLOY } from '../../../shared/constants.js';
|
import { SKILL_SUMMON_START_DEPLOY } from '../../../shared/constants.js';
|
||||||
import { moduleBadge } from './loadoutModel.js';
|
import { moduleBadge } from './loadoutModel.js';
|
||||||
import { t } from '../../../shared/i18n.js';
|
import { t, tc, N_ } from '../../../shared/i18n.js';
|
||||||
import { audio } from '../audio.js';
|
import { audio } from '../audio.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
|
|
||||||
const PROF_NAME = { PIONEER: '先锋', WARRIOR: '近卫', TANK: '重装', SNIPER: '狙击', CASTER: '术师', MEDIC: '医疗', SUPPORT: '辅助', SPECIAL: '特种', TOKEN: '召唤物' };
|
const PROF_NAME = { PIONEER: N_('先锋'), WARRIOR: N_('近卫'), TANK: N_('重装'), SNIPER: N_('狙击'), CASTER: N_('术师'), MEDIC: N_('医疗'), SUPPORT: N_('辅助'), SPECIAL: N_('特种'), TOKEN: N_('召唤物') };
|
||||||
const SP_TYPE = { INCREASE_WITH_TIME: '自动回复', INCREASE_WHEN_ATTACK: '攻击回复', INCREASE_WHEN_TAKEN_DAMAGE: '受击回复', ON_DEPLOY: '被动' };
|
const SP_TYPE = { INCREASE_WITH_TIME: N_('自动回复'), INCREASE_WHEN_ATTACK: N_('攻击回复'), INCREASE_WHEN_TAKEN_DAMAGE: N_('受击回复'), ON_DEPLOY: N_('被动') };
|
||||||
const SKILL_TYPE = { MANUAL: '自动触发', AUTO: '自动触发', PASSIVE: '被动' };
|
const SKILL_TYPE = { MANUAL: N_('自动触发'), AUTO: N_('自动触发'), PASSIVE: N_('被动') };
|
||||||
/** Fallback type icon by trigger, for a garrison record without its official `eventTypeIcon` (garrisonTypeIconKey). */
|
/** Fallback type icon by trigger, for a garrison record without its official `eventTypeIcon` (garrisonTypeIconKey). */
|
||||||
const EVENT_ICON = { IN_BATTLE: 's_icon_battle', SERVER_GAIN: 's_icon_bond', SERVER_PREP_START: 's_icon_bond', SERVER_PREP_FIN: 's_icon_bond', SERVER_CHESS_SOLD: 's_icon_gold', SERVER_PRICE: 's_icon_gold', SERVER_REFRESH_SHOP: 's_icon_gold' };
|
const EVENT_ICON = { IN_BATTLE: 's_icon_battle', SERVER_GAIN: 's_icon_bond', SERVER_PREP_START: 's_icon_bond', SERVER_PREP_FIN: 's_icon_bond', SERVER_CHESS_SOLD: 's_icon_gold', SERVER_PRICE: 's_icon_gold', SERVER_REFRESH_SHOP: 's_icon_gold' };
|
||||||
const RANK = { NORMAL: '普通', ELITE: '精英', BOSS: '领袖' };
|
const RANK = { NORMAL: N_('普通'), ELITE: N_('精英'), BOSS: N_('领袖') };
|
||||||
const DMG = { phys: '物理', arts: '法术', heal: '治疗', true: '真实', none: '无' };
|
const DMG = { phys: N_('物理'), arts: N_('法术'), heal: N_('治疗'), true: N_('真实'), none: N_('无') };
|
||||||
|
|
||||||
/** A range grid of at least this many tiles covers the field (纯烬艾雅法拉 S3 "攻击范围扩大至整个战场"): named, not drawn. */
|
/** A range grid of at least this many tiles covers the field (纯烬艾雅法拉 S3 "攻击范围扩大至整个战场"): named, not drawn. */
|
||||||
export const FIELD_WIDE_CELLS = 400;
|
export const FIELD_WIDE_CELLS = 400;
|
||||||
@@ -87,7 +87,7 @@ export function cardRangeGrid(live, rec, chess) {
|
|||||||
|
|
||||||
/** Mini range map. */
|
/** Mini range map. */
|
||||||
export function RangeGrid({ grid, class: cls }) {
|
export function RangeGrid({ grid, class: cls }) {
|
||||||
if (Array.isArray(grid) && grid.length >= FIELD_WIDE_CELLS) return html`<span class=${cx('rgrid-all', cls)} aria-label="攻击范围">全场</span>`;
|
if (Array.isArray(grid) && grid.length >= FIELD_WIDE_CELLS) return html`<span class=${cx('rgrid-all', cls)} aria-label=${t('攻击范围')}>${t('全场')}</span>`;
|
||||||
const box = rangeGridBox(grid);
|
const box = rangeGridBox(grid);
|
||||||
if (!box.cells.size) return html`<span class="t-dim">—</span>`;
|
if (!box.cells.size) return html`<span class="t-dim">—</span>`;
|
||||||
const cells = [];
|
const cells = [];
|
||||||
@@ -97,7 +97,7 @@ export function RangeGrid({ grid, class: cls }) {
|
|||||||
cells.push(html`<i key=${`${r},${c}`} class=${cx(box.cells.has(tileKey(r, c)) && 'on', self && 'self')}></i>`);
|
cells.push(html`<i key=${`${r},${c}`} class=${cx(box.cells.has(tileKey(r, c)) && 'on', self && 'self')}></i>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return html`<div class=${cx('rgrid', cls)} style=${rangeGridStyle(box)} aria-label="攻击范围">${cells}</div>`;
|
return html`<div class=${cx('rgrid', cls)} style=${rangeGridStyle(box)} aria-label=${t('攻击范围')}>${cells}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Stat({ k, v, sub, tone = null, title }) {
|
function Stat({ k, v, sub, tone = null, title }) {
|
||||||
@@ -140,15 +140,15 @@ export function liveStat(live, key, fallback, fmt = fmtNum) {
|
|||||||
const d = cur - base;
|
const d = cur - base;
|
||||||
sub = key === 'interval' ? `${d > 0 ? '+' : '−'}${Math.abs(d).toFixed(2)}` : `${d > 0 ? '+' : '−'}${key === 'res' || key === 'moveSpeed' ? Math.round(Math.abs(d) * 10) / 10 : fmtNum(Math.abs(d))}`;
|
sub = key === 'interval' ? `${d > 0 ? '+' : '−'}${Math.abs(d).toFixed(2)}` : `${d > 0 ? '+' : '−'}${key === 'res' || key === 'moveSpeed' ? Math.round(Math.abs(d) * 10) / 10 : fmtNum(Math.abs(d))}`;
|
||||||
}
|
}
|
||||||
return { v: fmt(cur), tone, sub, title: base != null ? `基础 ${fmt(base)}` : undefined };
|
return { v: fmt(cur), tone, sub, title: base != null ? t('基础 {v}', { v: fmt(base) }) : undefined };
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The tag of a live stats block: 实时 (battle) / 开战时 (the prep preview). */
|
/** The tag of a live stats block: 实时 (battle) / 开战时 (the prep preview). */
|
||||||
function LiveTag({ live }) {
|
function LiveTag({ live }) {
|
||||||
if (!live) return null;
|
if (!live) return null;
|
||||||
const battle = live.src === 'battle';
|
const battle = live.src === 'battle';
|
||||||
return html`<span class=${cx('dstats__tag', battle && 'is-battle')} title=${battle ? '当前作战中的实时数值(绿色为增益,红色为减益)'
|
return html`<span class=${cx('dstats__tag', battle && 'is-battle')} title=${battle ? t('当前作战中的实时数值(绿色为增益,红色为减益)')
|
||||||
: '下一场作战开始时的数值:已计入装备、盟约层数、特质、策略与机变效果(不含技能与作战中的临时效果)'}>${battle ? '实时' : '开战时'}</span>`;
|
: t('下一场作战开始时的数值:已计入装备、盟约层数、特质、策略与机变效果(不含技能与作战中的临时效果)')}>${battle ? t('实时') : t('开战时')}</span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fmtInterval = (v) => (Number.isFinite(v) && v > 0 ? `${v.toFixed(2)}s` : '—');
|
const fmtInterval = (v) => (Number.isFinite(v) && v > 0 ? `${v.toFixed(2)}s` : '—');
|
||||||
@@ -181,16 +181,16 @@ export function MorphPairings({ off = null, carried = null }) {
|
|||||||
if (!rows.length) return null;
|
if (!rows.length) return null;
|
||||||
const wearer = Array.isArray(carried);
|
const wearer = Array.isArray(carried);
|
||||||
return html`<div class=${cx('dmorph', wearer && 'is-wearer')}>
|
return html`<div class=${cx('dmorph', wearer && 'is-wearer')}>
|
||||||
<p class="dmorph__lead">搭配以下装备时,携带者视为对应盟约的成员:</p>
|
<p class="dmorph__lead">${t('搭配以下装备时,携带者视为对应盟约的成员:')}</p>
|
||||||
<ul class="dmorph__list" aria-label="变形同构体对应关系">
|
<ul class="dmorph__list" aria-label=${t('变形同构体对应关系')}>
|
||||||
${rows.map((r) => html`<li key=${r.bondId} class=${cx('dmorph__row', r.off && 'is-off', r.worn && 'is-worn')} data-bond=${r.bondId}
|
${rows.map((r) => html`<li key=${r.bondId} class=${cx('dmorph__row', r.off && 'is-off', r.worn && 'is-worn')} data-bond=${r.bondId}
|
||||||
title=${`${r.items.map((it) => it.name).join('、')} → 【${r.name}】${r.off ? '(本局禁用)' : ''}`}>
|
title=${`${t('{items} → 【{name}】', { items: r.items.map((it) => it.name), name: r.name })}${r.off ? t('(本局禁用)') : ''}`}>
|
||||||
<span class="dmorph__bond">【${r.name}】</span>
|
<span class="dmorph__bond">${t('【{name}】', { name: r.name })}</span>
|
||||||
<span class="dmorph__items">${r.items.map((it, i) => html`<span key=${it.id} class=${cx('dmorph__item', it.worn && 'is-worn')}>${i ? '、' : ''}${it.name}</span>`)}${r.worn
|
<span class="dmorph__items">${r.items.map((it, i) => html`<span key=${it.id} class=${cx('dmorph__item', it.worn && 'is-worn')}>${i ? tc('list', '、') : ''}${it.name}</span>`)}${r.worn
|
||||||
? html`<span class="dmorph__tag is-on">${r.off ? '已搭配' : '生效中'}</span>` : null}${r.off ? html`<span class="dmorph__tag is-off">本局禁用</span>` : null}</span>
|
? html`<span class="dmorph__tag is-on">${r.off ? t('已搭配') : t('生效中')}</span>` : null}${r.off ? html`<span class="dmorph__tag is-off">${t('本局禁用')}</span>` : null}</span>
|
||||||
</li>`)}
|
</li>`)}
|
||||||
</ul>
|
</ul>
|
||||||
${wearer && !rows.some((r) => r.worn) ? html`<p class="dmorph__none">暂未生效:需与上表中的一件装备一同携带</p>` : null}
|
${wearer && !rows.some((r) => r.worn) ? html`<p class="dmorph__none">${t('暂未生效:需与上表中的一件装备一同携带')}</p>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -207,8 +207,8 @@ export function MorphGrantLine({ item, off = null, carried = null }) {
|
|||||||
const isOff = isOffIn(off, item.giveBondId);
|
const isOff = isOffIn(off, item.giveBondId);
|
||||||
const worn = grantedBonds(carried, (id) => data.lookup('items', id)).includes(item.giveBondId);
|
const worn = grantedBonds(carried, (id) => data.lookup('items', id)).includes(item.giveBondId);
|
||||||
return html`<p class=${cx('dhint', 'dhint--morph', worn && 'is-worn', isOff && 'is-off')} data-bond=${item.giveBondId}>
|
return html`<p class=${cx('dhint', 'dhint--morph', worn && 'is-worn', isOff && 'is-off')} data-bond=${item.giveBondId}>
|
||||||
<${Icon} name="info" /><span>与${morph.name}一同装备时,携带者视为【${bond.name}】成员${isOff ? html`<span class="dmorph__tag is-off">本局禁用</span>` : null}${worn
|
<${Icon} name="info" /><span>${t('与{morph}一同装备时,携带者视为【{bond}】成员', { morph: morph.name, bond: bond.name })}${isOff ? html`<span class="dmorph__tag is-off">${t('本局禁用')}</span>` : null}${worn
|
||||||
? html`<span class="dmorph__tag is-on">${isOff ? '已搭配' : '生效中'}</span>` : null}</span>
|
? html`<span class="dmorph__tag is-on">${isOff ? t('已搭配') : t('生效中')}</span>` : null}</span>
|
||||||
</p>`;
|
</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,7 +233,7 @@ export function BondChips({ bondIds, bonds = [], onBond = null, off = null, gran
|
|||||||
if (!ids.length) return null;
|
if (!ids.length) return null;
|
||||||
const iso = new Set(Array.isArray(granted) ? granted : []);
|
const iso = new Set(Array.isArray(granted) ? granted : []);
|
||||||
const mine = new Map((Array.isArray(bonds) ? bonds : []).filter((b) => b && typeof b.bondId === 'string').map((b) => [b.bondId, b]));
|
const mine = new Map((Array.isArray(bonds) ? bonds : []).filter((b) => b && typeof b.bondId === 'string').map((b) => [b.bondId, b]));
|
||||||
return html`<div class="dbonds dbonds--top" role="list" aria-label="所属盟约">
|
return html`<div class="dbonds dbonds--top" role="list" aria-label=${t('所属盟约')}>
|
||||||
${ids.map((id) => {
|
${ids.map((id) => {
|
||||||
const rec = data.lookup('bonds', id);
|
const rec = data.lookup('bonds', id);
|
||||||
// a bond this mode never activates (gameLogic modeOffBonds): 本局禁用, no count or tier pips
|
// a bond this mode never activates (gameLogic modeOffBonds): 本局禁用, no count or tier pips
|
||||||
@@ -245,21 +245,21 @@ export function BondChips({ bondIds, bonds = [], onBond = null, off = null, gran
|
|||||||
const active = !isOff && (e ? !!e.active : tier > 0);
|
const active = !isOff && (e ? !!e.active : tier > 0);
|
||||||
const next = nextThreshold(count, th);
|
const next = nextThreshold(count, th);
|
||||||
const cap = next ?? th[th.length - 1] ?? null;
|
const cap = next ?? th[th.length - 1] ?? null;
|
||||||
const isoTag = iso.has(id) ? '(变形同构体)' : '';
|
const isoTag = iso.has(id) ? t('(变形同构体)') : '';
|
||||||
// the count holds 调和's +1 (the server's bond entry says so, DESIGN §21.26)
|
// the count holds 调和's +1 (the server's bond entry says so, DESIGN §21.26)
|
||||||
const harmonyTag = !isOff && Number.isInteger(e?.harmony) && e.harmony > 0 ? `(含调和 +${e.harmony})` : '';
|
const harmonyTag = !isOff && Number.isInteger(e?.harmony) && e.harmony > 0 ? t('(含调和 +{harmony})', { harmony: e.harmony }) : '';
|
||||||
const label = isOff ? briefingBondTip(rec?.name || id, 'off')
|
const label = isOff ? briefingBondTip(rec?.name || id, 'off')
|
||||||
: `${rec?.name || id}${isoTag}:在场 ${count}${cap != null ? `/${cap}` : ''}${harmonyTag}${active ? `,已激活 ${tier} 阶` : ',未激活'}`;
|
: `${rec?.name || id}${isoTag}${cap != null ? t(':在场 {count}/{cap}', { count, cap }) : t(':在场 {count}', { count })}${harmonyTag}${active ? t(',已激活 {tier} 阶', { tier }) : t(',未激活')}`;
|
||||||
const body = isOff
|
const body = isOff
|
||||||
? html`
|
? html`
|
||||||
<${BondGlyph} bondId=${id} class="dbond__icon" />
|
<${BondGlyph} bondId=${id} class="dbond__icon" />
|
||||||
<span class="dbond__name">${rec?.name || id}</span>
|
<span class="dbond__name">${rec?.name || id}</span>
|
||||||
${iso.has(id) ? html`<span class="dbond__iso">同构</span>` : null}
|
${iso.has(id) ? html`<span class="dbond__iso">${t('同构')}</span>` : null}
|
||||||
<span class="dbond__off">本局禁用</span>`
|
<span class="dbond__off">${t('本局禁用')}</span>`
|
||||||
: html`
|
: html`
|
||||||
<${BondGlyph} bondId=${id} class="dbond__icon" />
|
<${BondGlyph} bondId=${id} class="dbond__icon" />
|
||||||
<span class="dbond__name">${rec?.name || id}</span>
|
<span class="dbond__name">${rec?.name || id}</span>
|
||||||
${iso.has(id) ? html`<span class="dbond__iso">同构</span>` : null}
|
${iso.has(id) ? html`<span class="dbond__iso">${t('同构')}</span>` : null}
|
||||||
<span class=${cx('dbond__count', 'num', next == null && count > 0 && 'is-max')}>${count}${cap != null ? html`<small>/${cap}</small>` : null}</span>
|
<span class=${cx('dbond__count', 'num', next == null && count > 0 && 'is-max')}>${count}${cap != null ? html`<small>/${cap}</small>` : null}</span>
|
||||||
${th.length ? html`<span class="dbond__tiers" aria-hidden="true">${th.map((_, i) => html`<i key=${i} class=${i < tier ? 'on' : ''}></i>`)}</span>` : null}`;
|
${th.length ? html`<span class="dbond__tiers" aria-hidden="true">${th.map((_, i) => html`<i key=${i} class=${i < tier ? 'on' : ''}></i>`)}</span>` : null}`;
|
||||||
const cls = cx('dbond', active && 'is-active', isOff && 'is-off', rec?.isCore && 'is-core', iso.has(id) && 'is-granted');
|
const cls = cx('dbond', active && 'is-active', isOff && 'is-off', rec?.isCore && 'is-core', iso.has(id) && 'is-granted');
|
||||||
@@ -306,9 +306,9 @@ export function garrisonTypeIconKey(garrison) {
|
|||||||
|
|
||||||
/** The operator's own effect (特质, garrisons.json): trigger chip + description, compact. */
|
/** The operator's own effect (特质, garrisons.json): trigger chip + description, compact. */
|
||||||
function GarrisonBlock({ garrison, m }) {
|
function GarrisonBlock({ garrison, m }) {
|
||||||
return html`<section class="dgarrison" aria-label="特质" data-garrison=${garrison.garrisonId || ''}>
|
return html`<section class="dgarrison" aria-label=${t('特质')} data-garrison=${garrison.garrisonId || ''}>
|
||||||
<div class="dgarrison__head">
|
<div class="dgarrison__head">
|
||||||
<span class="dgarrison__k">特质</span>
|
<span class="dgarrison__k">${t('特质')}</span>
|
||||||
<span class="dgarrison__type">
|
<span class="dgarrison__type">
|
||||||
<${Img} src=${uiUrl(m, `garrisonTypeIcon/${garrisonTypeIconKey(garrison)}`)} class="dgarrison__icon" />
|
<${Img} src=${uiUrl(m, `garrisonTypeIcon/${garrisonTypeIconKey(garrison)}`)} class="dgarrison__icon" />
|
||||||
${garrison.eventTypeDesc || ''}
|
${garrison.eventTypeDesc || ''}
|
||||||
@@ -344,16 +344,16 @@ export function chessStatsBlock({ rec, chess, live = null }) {
|
|||||||
<div key="stats" class=${cx('dstats-wrap', live && 'is-live')} data-live=${live ? live.src || 'prep' : undefined}>
|
<div key="stats" class=${cx('dstats-wrap', live && 'is-live')} data-live=${live ? live.src || 'prep' : undefined}>
|
||||||
<div class="dstats">
|
<div class="dstats">
|
||||||
<${LiveTag} live=${live} />
|
<${LiveTag} live=${live} />
|
||||||
<${Stat} k="生命上限" ...${st.maxHp} />
|
<${Stat} k=${t('生命上限')} ...${st.maxHp} />
|
||||||
<${Stat} k="攻击" ...${st.atk} />
|
<${Stat} k=${t('攻击')} ...${st.atk} />
|
||||||
<${Stat} k="防御" ...${st.def} />
|
<${Stat} k=${t('防御')} ...${st.def} />
|
||||||
<${Stat} k="法术抗性" ...${st.res} />
|
<${Stat} k=${t('法术抗性')} ...${st.res} />
|
||||||
<${Stat} k="攻击间隔" ...${st.interval} />
|
<${Stat} k=${t('攻击间隔')} ...${st.interval} />
|
||||||
<${Stat} k="阻挡数" ...${st.blockCnt} />
|
<${Stat} k=${t('阻挡数')} ...${st.blockCnt} />
|
||||||
<${Stat} k="部署费用" v=${s.cost ?? '—'} />
|
<${Stat} k=${t('部署费用')} v=${s.cost ?? '—'} />
|
||||||
<${Stat} k="再部署" v=${s.respawnTime != null ? `${s.respawnTime}s` : '—'} />
|
<${Stat} k=${t('再部署')} v=${s.respawnTime != null ? `${s.respawnTime}s` : '—'} />
|
||||||
</div>
|
</div>
|
||||||
<div class="drange"><span class="dstat__k">攻击范围</span><${RangeGrid} grid=${cardRangeGrid(live, rec, chess)} /></div>
|
<div class="drange"><span class="dstat__k">${t('攻击范围')}</span><${RangeGrid} grid=${cardRangeGrid(live, rec, chess)} /></div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -396,8 +396,8 @@ export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bond
|
|||||||
<div class="dhead__info">
|
<div class="dhead__info">
|
||||||
<div class="dhead__chips">
|
<div class="dhead__chips">
|
||||||
<${TierChip} tier=${c.tier} golden=${golden} size="lg" />
|
<${TierChip} tier=${c.tier} golden=${golden} size="lg" />
|
||||||
${golden ? html`<span class="dtag-elite">精锐</span>` : null}
|
${golden ? html`<span class="dtag-elite">${t('精锐')}</span>` : null}
|
||||||
${piece?.kind === 'token' ? html`<span class="dtag-token">召唤物</span>` : null}
|
${piece?.kind === 'token' ? html`<span class="dtag-token">${t('召唤物')}</span>` : null}
|
||||||
${si ? html`<span class="dtag-standin" data-standin=${si.charId} title=${standInTip(si, c.name)}>${standInLabel(si)}</span>` : null}
|
${si ? html`<span class="dtag-standin" data-standin=${si.charId} title=${standInTip(si, c.name)}>${standInLabel(si)}</span>` : null}
|
||||||
${diy ? html`<span class="dtag-diy" data-diy=${c.charId || ''} title=${t('自选编队:所选技能与模组,没有特质,盟约按所属阵营分配')}>${t('自选')}</span>` : null}
|
${diy ? html`<span class="dtag-diy" data-diy=${c.charId || ''} title=${t('自选编队:所选技能与模组,没有特质,盟约按所属阵营分配')}>${t('自选')}</span>` : null}
|
||||||
</div>
|
</div>
|
||||||
@@ -405,11 +405,11 @@ export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bond
|
|||||||
${si ? html`<span class="dhead__for" data-for=${c.chessId}>${standInForText(c.name)}</span>` : html`<span class="dhead__en">${c.appellation || ''}</span>`}
|
${si ? html`<span class="dhead__for" data-for=${c.chessId}>${standInForText(c.name)}</span>` : html`<span class="dhead__en">${c.appellation || ''}</span>`}
|
||||||
<div class="dhead__class">
|
<div class="dhead__class">
|
||||||
<${Img} src=${profIconUrl(m, body.profession)} class="dhead__prof" />
|
<${Img} src=${profIconUrl(m, body.profession)} class="dhead__prof" />
|
||||||
<span>${PROF_NAME[body.profession] || body.profession || ''}</span>
|
<span>${t(PROF_NAME[body.profession]) || body.profession || ''}</span>
|
||||||
<i class="sep"></i>
|
<i class="sep"></i>
|
||||||
<${Img} src=${subProfIconUrl(m, body)} class="dhead__sub" />
|
<${Img} src=${subProfIconUrl(m, body)} class="dhead__sub" />
|
||||||
<span>${body.subProfessionName || ''}</span>
|
<span>${body.subProfessionName || ''}</span>
|
||||||
<span class="dhead__pos">${body.position === 'MELEE' ? '近战位' : '远程位'}</span>
|
<span class="dhead__pos">${body.position === 'MELEE' ? t('近战位') : t('远程位')}</span>
|
||||||
</div>
|
</div>
|
||||||
${hp ? html`<div class="dhp"><i style=${`width:${Math.max(0, Math.min(100, (hp.hp / Math.max(1, hp.max)) * 100))}%`}></i><span class="num">${fmtNum(hp.hp)} / ${fmtNum(hp.max)}</span></div>` : null}
|
${hp ? html`<div class="dhp"><i style=${`width:${Math.max(0, Math.min(100, (hp.hp / Math.max(1, hp.max)) * 100))}%`}></i><span class="num">${fmtNum(hp.hp)} / ${fmtNum(hp.max)}</span></div>` : null}
|
||||||
<${BondChips} bondIds=${bondIds} bonds=${bonds} off=${offBonds} onBond=${onBond} granted=${grantedIds} />
|
<${BondChips} bondIds=${bondIds} bonds=${bonds} off=${offBonds} onBond=${onBond} granted=${grantedIds} />
|
||||||
@@ -419,49 +419,49 @@ export function ChessDetail({ chess, piece, unit, snapHp, editable, onSell, bond
|
|||||||
blocks.trait = body.trait?.desc ? html`<p key="trait" class="dtrait"><${Icon} name="info" /><${RichText} text=${traitText(body, golden, lo)} /></p>` : null;
|
blocks.trait = body.trait?.desc ? html`<p key="trait" class="dtrait"><${Icon} name="info" /><${RichText} text=${traitText(body, golden, lo)} /></p>` : null;
|
||||||
// (`fr` is the body the unit fights with — a stand-in's record for a 补位 chess; `chess` is only the range's fallback)
|
// (`fr` is the body the unit fights with — a stand-in's record for a 补位 chess; `chess` is only the range's fallback)
|
||||||
blocks.stats = chessStatsBlock({ rec: fr, chess: c, live });
|
blocks.stats = chessStatsBlock({ rec: fr, chess: c, live });
|
||||||
blocks.skill = sk ? html`<${Section} key="skill" title="技能" micro="SKILL" class="dsec--skill">
|
blocks.skill = sk ? html`<${Section} key="skill" title=${t('技能')} micro="SKILL" class="dsec--skill">
|
||||||
<div class="dskill" data-skill=${sk.skillId || ''}>
|
<div class="dskill" data-skill=${sk.skillId || ''}>
|
||||||
<${Img} src=${skIcon} class="dskill__icon" fallback=${html`<span class="dskill__icon dskill__icon--empty">${skSlot ? html`<b class="num">${skSlot}</b>` : null}</span>`} />
|
<${Img} src=${skIcon} class="dskill__icon" fallback=${html`<span class="dskill__icon dskill__icon--empty">${skSlot ? html`<b class="num">${skSlot}</b>` : null}</span>`} />
|
||||||
<div class="dskill__meta">
|
<div class="dskill__meta">
|
||||||
<b class="dskill__name">${skSlot && (lo?.choices || 0) > 1 ? html`<span class="dskill__slot num" title=${`技能 ${skSlot}`}>${skSlot}</span>` : null}${sk.name}${lo && !lo.defaultSkill && !si ? html`<span class="dtag-loadout" title="干员调配中选择的技能">已调配</span>` : null}</b>
|
<b class="dskill__name">${skSlot && (lo?.choices || 0) > 1 ? html`<span class="dskill__slot num" title=${t('技能 {skSlot}', { skSlot })}>${skSlot}</span>` : null}${sk.name}${lo && !lo.defaultSkill && !si ? html`<span class="dtag-loadout" title=${t('干员调配中选择的技能')}>${t('已调配')}</span>` : null}</b>
|
||||||
<div class="dskill__tags">
|
<div class="dskill__tags">
|
||||||
<span class="dsp dsp--${sk.spType === 'INCREASE_WHEN_ATTACK' ? 'atk' : sk.spType === 'INCREASE_WHEN_TAKEN_DAMAGE' ? 'def' : 'time'}">${SP_TYPE[sk.spType] || '技力'}</span>
|
<span class="dsp dsp--${sk.spType === 'INCREASE_WHEN_ATTACK' ? 'atk' : sk.spType === 'INCREASE_WHEN_TAKEN_DAMAGE' ? 'def' : 'time'}">${t(SP_TYPE[sk.spType]) || t('技力')}</span>
|
||||||
<span class="dsp dsp--trig">${SKILL_TYPE[sk.skillType] || '自动触发'}</span>
|
<span class="dsp dsp--trig">${t(SKILL_TYPE[sk.skillType]) || t('自动触发')}</span>
|
||||||
${sk.spType !== 'ON_DEPLOY' && sk.skillType !== 'PASSIVE' ? html`<span class="dsp__num"><${GIcon} name="bolt" />初始 <b class="num">${sk.initSp ?? 0}</b> · 消耗 <b class="num">${sk.spCost ?? 0}</b></span>` : null}
|
${sk.spType !== 'ON_DEPLOY' && sk.skillType !== 'PASSIVE' ? html`<span class="dsp__num"><${GIcon} name="bolt" />${t('初始')} <b class="num">${sk.initSp ?? 0}</b> ${t('· 消耗')} <b class="num">${sk.spCost ?? 0}</b></span>` : null}
|
||||||
${sk.duration > 0 ? html`<span class="dsp__num">持续 <b class="num">${sk.duration}</b>s</span>` : null}
|
${sk.duration > 0 ? html`<span class="dsp__num">${t('持续')} <b class="num">${sk.duration}</b>s</span>` : null}
|
||||||
${sk.maxChargeTime > 1 ? html`<span class="dsp__num">充能 <b class="num">${sk.maxChargeTime}</b></span>` : null}
|
${sk.maxChargeTime > 1 ? html`<span class="dsp__num">${t('充能')} <b class="num">${sk.maxChargeTime}</b></span>` : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<${RichText} as="p" text=${sk.descRaw || sk.desc} class="dtext" />
|
<${RichText} as="p" text=${sk.descRaw || sk.desc} class="dtext" />
|
||||||
<//>` : null;
|
<//>` : null;
|
||||||
blocks.module = golden && lo?.module ? html`<${Section} key="module" title="模组" micro="MODULE" class="dsec--module">
|
blocks.module = golden && lo?.module ? html`<${Section} key="module" title=${t('模组')} micro="MODULE" class="dsec--module">
|
||||||
<div class=${cx('dmodule', lo.module.none && 'is-none')} data-module=${lo.module.id}>
|
<div class=${cx('dmodule', lo.module.none && 'is-none')} data-module=${lo.module.id}>
|
||||||
${!lo.module.none && lo.module.typeName ? html`<span class="dmodule__icon" data-type=${lo.module.typeName}>
|
${!lo.module.none && lo.module.typeName ? html`<span class="dmodule__icon" data-type=${lo.module.typeName}>
|
||||||
<${Img} src=${moduleTypeIconUrl(data.get('local'), lo.module.typeName)} fallback=${html`<b class="num">${moduleBadge(lo.module)}</b>`} /></span>` : null}
|
<${Img} src=${moduleTypeIconUrl(data.get('local'), lo.module.typeName)} fallback=${html`<b class="num">${moduleBadge(lo.module)}</b>`} /></span>` : null}
|
||||||
<b class="dmodule__name">${lo.module.name}</b>
|
<b class="dmodule__name">${lo.module.name}</b>
|
||||||
${lo.module.typeName ? html`<span class="dmodule__type">${lo.module.typeName}</span>` : null}
|
${lo.module.typeName ? html`<span class="dmodule__type">${lo.module.typeName}</span>` : null}
|
||||||
${!lo.defaultModule && !si ? html`<span class="dtag-loadout" title="干员调配中选择的模组">已调配</span>` : null}
|
${!lo.defaultModule && !si ? html`<span class="dtag-loadout" title=${t('干员调配中选择的模组')}>${t('已调配')}</span>` : null}
|
||||||
</div>
|
</div>
|
||||||
<//>` : null;
|
<//>` : null;
|
||||||
// (a 变形同构体 / bond item row shows its pairing against what this operator carries: ItemRow `carried`)
|
// (a 变形同构体 / bond item row shows its pairing against what this operator carries: ItemRow `carried`)
|
||||||
blocks.equip = piece?.kind === 'chess' ? html`<${Section} key="equip" title="装备" micro=${`EQUIP ${items.length}/2`} class="dsec--equip">
|
blocks.equip = piece?.kind === 'chess' ? html`<${Section} key="equip" title=${t('装备')} micro=${`EQUIP ${items.length}/2`} class="dsec--equip">
|
||||||
${items.length ? items.map((it) => html`<${ItemRow} key=${it.uid} itemId=${it.id} carried=${items} off=${offBonds} />`) : html`<p class="t-dim dempty">拖拽装备至该干员以配发(最多 2 件)</p>`}
|
${items.length ? items.map((it) => html`<${ItemRow} key=${it.uid} itemId=${it.id} carried=${items} off=${offBonds} />`) : html`<p class="t-dim dempty">${t('拖拽装备至该干员以配发(最多 2 件)')}</p>`}
|
||||||
<//>`
|
<//>`
|
||||||
// no own piece (a teammate's unit, a bond popup's 变形同构体 row): what it carries, read-only
|
// no own piece (a teammate's unit, a bond popup's 变形同构体 row): what it carries, read-only
|
||||||
: !piece && carried.length ? html`<${Section} key="equip" title="装备" micro=${`EQUIP ${carried.length}/2`} class="dsec--equip">
|
: !piece && carried.length ? html`<${Section} key="equip" title=${t('装备')} micro=${`EQUIP ${carried.length}/2`} class="dsec--equip">
|
||||||
${carried.map((id, i) => html`<${ItemRow} key=${`${i}:${id}`} itemId=${id} carried=${carried} off=${offBonds} />`)}
|
${carried.map((id, i) => html`<${ItemRow} key=${`${i}:${id}`} itemId=${id} carried=${carried} off=${offBonds} />`)}
|
||||||
<//>` : null;
|
<//>` : null;
|
||||||
const talents = chessTalents(fr);
|
const talents = chessTalents(fr);
|
||||||
blocks.talents = talents.length ? html`<${Section} key="talents" title="天赋" micro="TALENT" class="dsec--talent">
|
blocks.talents = talents.length ? html`<${Section} key="talents" title=${t('天赋')} micro="TALENT" class="dsec--talent">
|
||||||
${talents.map((t, i) => html`<div key=${i} class="dtalent"><b>${t.name}</b><${RichText} text=${t.descRaw || t.desc} class="dtext" /></div>`)}
|
${talents.map((t, i) => html`<div key=${i} class="dtalent"><b>${t.name}</b><${RichText} text=${t.descRaw || t.desc} class="dtext" /></div>`)}
|
||||||
<//>` : null;
|
<//>` : null;
|
||||||
blocks.actions = piece && editable && piece.kind !== 'item' ? html`<div key="actions" class="dactions">
|
blocks.actions = piece && editable && piece.kind !== 'item' ? html`<div key="actions" class="dactions">
|
||||||
<${Button} variant="amber" icon="close" class="dpanel__sell" onClick=${() => onSell(piece, si || c)}>出售<span class="dsell num">+${sell}</span><//>
|
<${Button} variant="amber" icon="close" class="dpanel__sell" onClick=${() => onSell(piece, si || c)}>${t('出售')}<span class="dsell num">+${sell}</span><//>
|
||||||
</div>` : null;
|
</div>` : null;
|
||||||
const out = CHESS_SECTIONS.map((k) => blocks[k]).filter(Boolean);
|
const out = CHESS_SECTIONS.map((k) => blocks[k]).filter(Boolean);
|
||||||
// a merge-completing shop / reward card: where the elite goes (shopBar mergeHint), right under the header
|
// a merge-completing shop / reward card: where the elite goes (shopBar mergeHint), right under the header
|
||||||
if (hint) out.splice(1, 0, html`<p key="merge" class="dhint dhint--merge"><${Icon} name="info" />可晋升:${hint}</p>`);
|
if (hint) out.splice(1, 0, html`<p key="merge" class="dhint dhint--merge"><${Icon} name="info" />${t('可晋升:{hint}', { hint })}</p>`);
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,21 +478,21 @@ export function ItemDetail({ item, piece, editable, onDestroy, offBonds = null }
|
|||||||
<div class="dhead dhead--item">
|
<div class="dhead dhead--item">
|
||||||
<div class=${cx('dhead__icon', item.isGolden && 'is-golden')}><${Img} src=${itemIconUrl(m, item)} fallback=${html`<${GIcon} name="bolt" />`} /></div>
|
<div class=${cx('dhead__icon', item.isGolden && 'is-golden')}><${Img} src=${itemIconUrl(m, item)} fallback=${html`<${GIcon} name="bolt" />`} /></div>
|
||||||
<div class="dhead__info">
|
<div class="dhead__info">
|
||||||
<div class="dhead__chips"><${TierChip} tier=${item.tier} golden=${item.isGolden} size="lg" />${item.isGolden ? html`<span class="dtag-elite">进阶</span>` : null}
|
<div class="dhead__chips"><${TierChip} tier=${item.tier} golden=${item.isGolden} size="lg" />${item.isGolden ? html`<span class="dtag-elite">${t('进阶')}</span>` : null}
|
||||||
<span class="dtag-kind">${item.itemType === 'MAGIC' ? '奇术' : '装备'}</span></div>
|
<span class="dtag-kind">${item.itemType === 'MAGIC' ? t('奇术') : t('装备')}</span></div>
|
||||||
<h3 class="dhead__name">${item.name}</h3>
|
<h3 class="dhead__name">${item.name}</h3>
|
||||||
${item.flavor ? html`<span class="dhead__flavor">${item.flavor}</span>` : null}
|
${item.flavor ? html`<span class="dhead__flavor">${item.flavor}</span>` : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<${Section} title="效果" micro="EFFECT"><${RichText} as="p" text=${item.descRaw || item.desc} class="dtext" /><//>
|
<${Section} title=${t('效果')} micro="EFFECT"><${RichText} as="p" text=${item.descRaw || item.desc} class="dtext" /><//>
|
||||||
${item.canGiveBond ? html`<${Section} title="天赋" micro="TALENT" class="dsec--morph"><${MorphPairings} off=${offBonds} /><//>` : null}
|
${item.canGiveBond ? html`<${Section} title=${t('天赋')} micro="TALENT" class="dsec--morph"><${MorphPairings} off=${offBonds} /><//>` : null}
|
||||||
${!item.canGiveBond && item.giveBondId ? html`<${MorphGrantLine} item=${item} off=${offBonds} />` : null}
|
${!item.canGiveBond && item.giveBondId ? html`<${MorphGrantLine} item=${item} off=${offBonds} />` : null}
|
||||||
${item.note ? html`<p class="dhint dhint--rule"><${Icon} name="info" />${item.note}</p>` : null}
|
${item.note ? html`<p class="dhint dhint--rule"><${Icon} name="info" />${item.note}</p>` : null}
|
||||||
${item.itemType === 'MAGIC'
|
${item.itemType === 'MAGIC'
|
||||||
? html`<p class="dhint"><${Icon} name="info" />将其拖拽至战场上的格子使用</p>`
|
? html`<p class="dhint"><${Icon} name="info" />${t('将其拖拽至战场上的格子使用')}</p>`
|
||||||
: html`<p class="dhint"><${Icon} name="info" />拖拽至干员身上进行配发(每名干员最多 2 件,配发后无法取下)${item.mergeable ? ';2 件相同装备自动合成进阶装备' : ''}</p>`}
|
: html`<p class="dhint"><${Icon} name="info" />${t('拖拽至干员身上进行配发(每名干员最多 2 件,配发后无法取下)')}${item.mergeable ? t(';2 件相同装备自动合成进阶装备') : ''}</p>`}
|
||||||
${item.shopExcluded ? html`<p class="dhint dhint--source"><${Icon} name="info" />调度中心不出售 · 获取途径:${item.shopExcludedBy || '效果获得'}</p>` : null}
|
${item.shopExcluded ? html`<p class="dhint dhint--source"><${Icon} name="info" />${t('调度中心不出售 · 获取途径:{source}', { source: item.shopExcludedBy || t('效果获得') })}</p>` : null}
|
||||||
${piece && editable ? html`<div class="dactions"><${Button} variant="danger" onClick=${() => onDestroy(piece, item)}>销毁道具<//></div>` : null}`;
|
${piece && editable ? html`<div class="dactions"><${Button} variant="danger" onClick=${() => onDestroy(piece, item)}>${t('销毁道具')}<//></div>` : null}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function EnemyDetail({ enemy, snapHp, count, live = null }) {
|
function EnemyDetail({ enemy, snapHp, count, live = null }) {
|
||||||
@@ -500,7 +500,7 @@ function EnemyDetail({ enemy, snapHp, count, live = null }) {
|
|||||||
const s = enemy.stats || {};
|
const s = enemy.stats || {};
|
||||||
const types = Array.isArray(enemy.acTypes) ? enemy.acTypes : enemy.acType ? [enemy.acType] : [];
|
const types = Array.isArray(enemy.acTypes) ? enemy.acTypes : enemy.acType ? [enemy.acType] : [];
|
||||||
const factions = data.get('factions')?.types || {};
|
const factions = data.get('factions')?.types || {};
|
||||||
const imm = Object.entries(s.immunities || {}).filter(([, v]) => v).map(([k]) => ({ stun: '晕眩', silence: '沉默', sleep: '沉睡', frozen: '冻结', levitate: '浮空' }[k] || k));
|
const imm = Object.entries(s.immunities || {}).filter(([, v]) => v).map(([k]) => ({ stun: t('晕眩'), silence: t('沉默'), sleep: t('沉睡'), frozen: t('冻结'), levitate: t('浮空') }[k] || k));
|
||||||
const interval = attackInterval(s.bat, s.aspd);
|
const interval = attackInterval(s.bat, s.aspd);
|
||||||
const hp = hpOf(live, snapHp);
|
const hp = hpOf(live, snapHp);
|
||||||
// a battle enemy: its live stats against its spawned ones (the round's multipliers included — unitStatsEntry base)
|
// a battle enemy: its live stats against its spawned ones (the round's multipliers included — unitStatsEntry base)
|
||||||
@@ -516,8 +516,8 @@ function EnemyDetail({ enemy, snapHp, count, live = null }) {
|
|||||||
</div>
|
</div>
|
||||||
<div class="dhead__info">
|
<div class="dhead__info">
|
||||||
<div class="dhead__chips">
|
<div class="dhead__chips">
|
||||||
<span class=${cx('drank', `drank--${(enemy.rank || 'NORMAL').toLowerCase()}`)}>${RANK[enemy.rank] || '普通'}</span>
|
<span class=${cx('drank', `drank--${(enemy.rank || 'NORMAL').toLowerCase()}`)}>${t(RANK[enemy.rank]) || t('普通')}</span>
|
||||||
<span class="dtag-kind">${s.motion === 'FLY' ? '空中' : '地面'}</span>
|
<span class="dtag-kind">${s.motion === 'FLY' ? t('空中') : t('地面')}</span>
|
||||||
${count ? html`<span class="dtag-kind num">×${count}</span>` : null}
|
${count ? html`<span class="dtag-kind num">×${count}</span>` : null}
|
||||||
</div>
|
</div>
|
||||||
<h3 class="dhead__name">${enemy.name}</h3>
|
<h3 class="dhead__name">${enemy.name}</h3>
|
||||||
@@ -527,19 +527,19 @@ function EnemyDetail({ enemy, snapHp, count, live = null }) {
|
|||||||
</div>
|
</div>
|
||||||
<div class=${cx('dstats', live && 'is-live')} data-live=${live ? live.src || 'battle' : undefined}>
|
<div class=${cx('dstats', live && 'is-live')} data-live=${live ? live.src || 'battle' : undefined}>
|
||||||
<${LiveTag} live=${live} />
|
<${LiveTag} live=${live} />
|
||||||
<${Stat} k="生命上限" ...${st.maxHp} />
|
<${Stat} k=${t('生命上限')} ...${st.maxHp} />
|
||||||
<${Stat} k="攻击" ...${st.atk} sub=${st.atk.sub || DMG[s.dmgType] || ''} />
|
<${Stat} k=${t('攻击')} ...${st.atk} sub=${st.atk.sub || t(DMG[s.dmgType]) || ''} />
|
||||||
<${Stat} k="防御" ...${st.def} />
|
<${Stat} k=${t('防御')} ...${st.def} />
|
||||||
<${Stat} k="法术抗性" ...${st.res} />
|
<${Stat} k=${t('法术抗性')} ...${st.res} />
|
||||||
<${Stat} k="移动速度" ...${st.moveSpeed} />
|
<${Stat} k=${t('移动速度')} ...${st.moveSpeed} />
|
||||||
<${Stat} k="攻击间隔" ...${st.interval} />
|
<${Stat} k=${t('攻击间隔')} ...${st.interval} />
|
||||||
<${Stat} k="攻击范围" v=${s.rangeRadius > 0 ? s.rangeRadius : '近战'} />
|
<${Stat} k=${t('攻击范围')} v=${s.rangeRadius > 0 ? s.rangeRadius : t('近战')} />
|
||||||
<${Stat} k="目标价值" v=${s.lpr ?? 1} />
|
<${Stat} k=${t('目标价值')} v=${s.lpr ?? 1} />
|
||||||
</div>
|
</div>
|
||||||
${imm.length ? html`<p class="dhint"><${Icon} name="shield" />免疫:${imm.join('、')}</p>` : null}
|
${imm.length ? html`<p class="dhint"><${Icon} name="shield" />${t('免疫:{list}', { list: imm })}</p>` : null}
|
||||||
${Array.isArray(enemy.abilities) && enemy.abilities.length ? html`<${Section} title="能力" micro="ABILITIES">
|
${Array.isArray(enemy.abilities) && enemy.abilities.length ? html`<${Section} title=${t('能力')} micro="ABILITIES">
|
||||||
<ul class="dabil">${abilityRows(enemy.abilities, !!live?.silenced).map((a, i) => html`<li key=${i} class=${a.off ? 'is-off' : null}><${RichText} text=${a.text} /></li>`)}</ul>
|
<ul class="dabil">${abilityRows(enemy.abilities, !!live?.silenced).map((a, i) => html`<li key=${i} class=${a.off ? 'is-off' : null}><${RichText} text=${a.text} /></li>`)}</ul>
|
||||||
<//>` : enemy.descRaw || enemy.desc ? html`<${Section} title="说明"><${RichText} as="p" text=${enemy.descRaw || enemy.desc} class="dtext" /><//>` : null}`;
|
<//>` : enemy.descRaw || enemy.desc ? html`<${Section} title=${t('说明')}><${RichText} as="p" text=${enemy.descRaw || enemy.desc} class="dtext" /><//>` : null}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -553,10 +553,10 @@ function EnemyDetail({ enemy, snapHp, count, live = null }) {
|
|||||||
export function summonDeployHint(token, startDeploy = SKILL_SUMMON_START_DEPLOY) {
|
export function summonDeployHint(token, startDeploy = SKILL_SUMMON_START_DEPLOY) {
|
||||||
if (!token || token.kind !== 'summon' || token.placeable !== true) return null;
|
if (!token || token.kind !== 'summon' || token.placeable !== true) return null;
|
||||||
const talent = Object.values(token.variants || {}).some((v) => (v?.sources || []).includes('talent'));
|
const talent = Object.values(token.variants || {}).some((v) => (v?.sources || []).includes('talent'));
|
||||||
if (talent) return '作战开始时在摆放的位置部署';
|
if (talent) return t('作战开始时在摆放的位置部署');
|
||||||
return startDeploy
|
return startDeploy
|
||||||
? '作战开始时在摆放的位置部署一次,之后所属干员每次发动技能时再次出现(未摆放则不会出现)'
|
? t('作战开始时在摆放的位置部署一次,之后所属干员每次发动技能时再次出现(未摆放则不会出现)')
|
||||||
: '所属干员发动技能时才在摆放的位置出现(未摆放则不会出现)';
|
: t('所属干员发动技能时才在摆放的位置出现(未摆放则不会出现)');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -599,20 +599,20 @@ export function TokenDetail({ token, piece, ownerId = null, snapHp = null, live
|
|||||||
<div class="dhead dhead--item">
|
<div class="dhead dhead--item">
|
||||||
<div class="dhead__icon"><${Img} src=${tokenAvatarUrl(m, token.tokenId)} fallback=${html`<${GIcon} name="target" />`} /></div>
|
<div class="dhead__icon"><${Img} src=${tokenAvatarUrl(m, token.tokenId)} fallback=${html`<${GIcon} name="target" />`} /></div>
|
||||||
<div class="dhead__info">
|
<div class="dhead__info">
|
||||||
<div class="dhead__chips"><span class="dtag-token">召唤物</span>${piece?.count > 1 ? html`<span class="dtag-kind num">×${piece.count}</span>` : null}</div>
|
<div class="dhead__chips"><span class="dtag-token">${t('召唤物')}</span>${piece?.count > 1 ? html`<span class="dtag-kind num">×${piece.count}</span>` : null}</div>
|
||||||
<h3 class="dhead__name">${token.name}</h3>
|
<h3 class="dhead__name">${token.name}</h3>
|
||||||
${hp ? html`<div class="dhp"><i style=${`width:${Math.max(0, Math.min(100, (hp.hp / Math.max(1, hp.max)) * 100))}%`}></i><span class="num">${fmtNum(hp.hp)} / ${fmtNum(hp.max)}</span></div>` : null}
|
${hp ? html`<div class="dhp"><i style=${`width:${Math.max(0, Math.min(100, (hp.hp / Math.max(1, hp.max)) * 100))}%`}></i><span class="num">${fmtNum(hp.hp)} / ${fmtNum(hp.max)}</span></div>` : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class=${cx('dstats', live && 'is-live')} data-live=${live ? live.src || 'prep' : undefined}>
|
<div class=${cx('dstats', live && 'is-live')} data-live=${live ? live.src || 'prep' : undefined}>
|
||||||
<${LiveTag} live=${live} />
|
<${LiveTag} live=${live} />
|
||||||
<${Stat} k="生命上限" ...${st.maxHp} /><${Stat} k="攻击" ...${st.atk} />
|
<${Stat} k=${t('生命上限')} ...${st.maxHp} /><${Stat} k=${t('攻击')} ...${st.atk} />
|
||||||
<${Stat} k="防御" ...${st.def} /><${Stat} k="阻挡数" ...${st.blockCnt} />
|
<${Stat} k=${t('防御')} ...${st.def} /><${Stat} k=${t('阻挡数')} ...${st.blockCnt} />
|
||||||
</div>
|
</div>
|
||||||
${hint ? html`<p class="dhint"><${Icon} name="info" />${hint}</p>` : null}
|
${hint ? html`<p class="dhint"><${Icon} name="info" />${hint}</p>` : null}
|
||||||
${token.descRaw || token.desc ? html`<${Section} title="说明"><${RichText} as="p" text=${token.descRaw || token.desc} class="dtext" /><//>` : null}
|
${token.descRaw || token.desc ? html`<${Section} title=${t('说明')}><${RichText} as="p" text=${token.descRaw || token.desc} class="dtext" /><//>` : null}
|
||||||
${skill ? html`<${Section} title="技能"><p class="dtext"><b>${skill.name}</b> ${skill.desc}</p><//>` : null}
|
${skill ? html`<${Section} title=${t('技能')}><p class="dtext"><b>${skill.name}</b> ${skill.desc}</p><//>` : null}
|
||||||
${talents.length ? html`<${Section} title="天赋">${talents.map((t, i) => html`<p class="dtext" key=${i}><b>${t.name}</b> ${t.desc}</p>`)}<//>` : null}`;
|
${talents.length ? html`<${Section} title=${t('天赋')}>${talents.map((t, i) => html`<p class="dtext" key=${i}><b>${t.name}</b> ${t.desc}</p>`)}<//>` : null}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -741,18 +741,18 @@ export function DetailPanel({ detail, editable, snapHp, onClose, onSell, onDestr
|
|||||||
const sellIt = async (piece, chess) => {
|
const sellIt = async (piece, chess) => {
|
||||||
const golden = piece.golden || chess?.isGolden;
|
const golden = piece.golden || chess?.isGolden;
|
||||||
if (golden) {
|
if (golden) {
|
||||||
const ok = await confirmDialog({ title: '出售精锐干员', text: `确定要出售精锐干员「${chess?.name || ''}」吗?出售后获得 ${chess?.sellPrice ?? 1} 资金。`, okText: '出售', danger: true });
|
const ok = await confirmDialog({ title: t('出售精锐干员'), text: t('确定要出售精锐干员「{name}」吗?出售后获得 {price} 资金。', { name: chess?.name || '', price: chess?.sellPrice ?? 1 }), okText: t('出售'), danger: true });
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
onSell(piece);
|
onSell(piece);
|
||||||
};
|
};
|
||||||
const destroyIt = async (piece, item) => {
|
const destroyIt = async (piece, item) => {
|
||||||
const ok = await confirmDialog({ title: '销毁道具', text: `道具无法出售。确定要销毁「${item?.name || ''}」吗?`, okText: '销毁', danger: true });
|
const ok = await confirmDialog({ title: t('销毁道具'), text: t('道具无法出售。确定要销毁「{name}」吗?', { name: item?.name || '' }), okText: t('销毁'), danger: true });
|
||||||
if (ok) onDestroy(piece);
|
if (ok) onDestroy(piece);
|
||||||
};
|
};
|
||||||
return html`<aside class=${cx('dpanel', 'brackets', `dpanel--${detail.type}`, side === 'right' && 'dpanel--right', side === 'right' && shopOpen && 'is-shop')} role="dialog" aria-label="详情"
|
return html`<aside class=${cx('dpanel', 'brackets', `dpanel--${detail.type}`, side === 'right' && 'dpanel--right', side === 'right' && shopOpen && 'is-shop')} role="dialog" aria-label=${t('详情')}
|
||||||
data-side=${side === 'right' ? 'right' : 'left'}>
|
data-side=${side === 'right' ? 'right' : 'left'}>
|
||||||
<button type="button" class="dpanel__close" aria-label="关闭" onClick=${onClose}><${Icon} name="close" /></button>
|
<button type="button" class="dpanel__close" aria-label=${t('关闭')} onClick=${onClose}><${Icon} name="close" /></button>
|
||||||
<div class="dpanel__scroll">
|
<div class="dpanel__scroll">
|
||||||
${detail.type === 'chess' ? html`<${ChessDetail} chess=${detail.chess} piece=${detail.piece} snapHp=${snapHp} editable=${editable} onSell=${sellIt}
|
${detail.type === 'chess' ? html`<${ChessDetail} chess=${detail.chess} piece=${detail.piece} snapHp=${snapHp} editable=${editable} onSell=${sellIt}
|
||||||
bonds=${bonds} offBonds=${offBonds} loadout=${loadout} onBond=${onBond} live=${liveNow} hint=${detail.hint || null} unitItems=${detail.unitItems || null}
|
bonds=${bonds} offBonds=${offBonds} loadout=${loadout} onBond=${onBond} live=${liveNow} hint=${detail.hint || null} unitItems=${detail.unitItems || null}
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
|
|
||||||
import { useEffect, useState } from '../../vendor/hooks.module.js';
|
import { useEffect, useState } from '../../vendor/hooks.module.js';
|
||||||
import { html, Icon } from './components.js';
|
import { html, Icon } from './components.js';
|
||||||
|
import { t } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
/** A touch held this long without moving opens the detail (contextmenu) on DOM controls. */
|
/** A touch held this long without moving opens the detail (contextmenu) on DOM controls. */
|
||||||
export const LONG_PRESS_MS = 520;
|
export const LONG_PRESS_MS = 520;
|
||||||
@@ -154,7 +155,7 @@ export function FullscreenButton({ class: cls = '' }) {
|
|||||||
return () => { d.removeEventListener('fullscreenchange', upd); d.removeEventListener('webkitfullscreenchange', upd); };
|
return () => { d.removeEventListener('fullscreenchange', upd); d.removeEventListener('webkitfullscreenchange', upd); };
|
||||||
}, []);
|
}, []);
|
||||||
if (!ok) return null;
|
if (!ok) return null;
|
||||||
const label = on ? '退出全屏' : '全屏';
|
const label = on ? t('退出全屏') : t('全屏');
|
||||||
return html`<button type="button" class=${`fsbtn tapx ${cls}`} aria-label=${label} title=${label} aria-pressed=${on ? 'true' : 'false'}
|
return html`<button type="button" class=${`fsbtn tapx ${cls}`} aria-label=${label} title=${label} aria-pressed=${on ? 'true' : 'false'}
|
||||||
onClick=${() => fullscreen.toggle()}>
|
onClick=${() => fullscreen.toggle()}>
|
||||||
<${Icon} name=${on ? 'collapse' : 'expand'} />
|
<${Icon} name=${on ? 'collapse' : 'expand'} />
|
||||||
|
|||||||
@@ -16,6 +16,7 @@
|
|||||||
import { DIY_LIMITS, checkDiyPicks } from '../../../shared/protocol.js';
|
import { DIY_LIMITS, checkDiyPicks } from '../../../shared/protocol.js';
|
||||||
import { diySlotIds, diySlot, diyPool, isPrototypePick, lockedSelection, isDiyModule, diyRecord } from '../../../shared/diy.js';
|
import { diySlotIds, diySlot, diyPool, isPrototypePick, lockedSelection, isDiyModule, diyRecord } from '../../../shared/diy.js';
|
||||||
import { unitForm } from '../../../shared/standIn.js';
|
import { unitForm } from '../../../shared/standIn.js';
|
||||||
|
import { N_ } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
/** localStorage key (store.js loadPref/savePref prefix `sp.pref.`) and format version. */
|
/** localStorage key (store.js loadPref/savePref prefix `sp.pref.`) and format version. */
|
||||||
export const DIY_PREF = 'diy';
|
export const DIY_PREF = 'diy';
|
||||||
@@ -186,16 +187,16 @@ export const serializeDiy = (picks, opts) => JSON.stringify(exportDiy(picks, opt
|
|||||||
export function parseDiyImport(input) {
|
export function parseDiyImport(input) {
|
||||||
let raw = input;
|
let raw = input;
|
||||||
if (typeof raw === 'string') {
|
if (typeof raw === 'string') {
|
||||||
if (raw.length > DIY_IMPORT_MAX_BYTES) return { ok: false, error: '内容过长,无法导入' };
|
if (raw.length > DIY_IMPORT_MAX_BYTES) return { ok: false, error: N_('内容过长,无法导入') };
|
||||||
const text = raw.trim();
|
const text = raw.trim();
|
||||||
if (!text) return { ok: false, error: '没有可导入的内容' };
|
if (!text) return { ok: false, error: N_('没有可导入的内容') };
|
||||||
try { raw = JSON.parse(text); } catch { return { ok: false, error: '无法识别的内容' }; }
|
try { raw = JSON.parse(text); } catch { return { ok: false, error: N_('无法识别的内容') }; }
|
||||||
}
|
}
|
||||||
if (!isObj(raw)) return { ok: false, error: '无法识别的格式' };
|
if (!isObj(raw)) return { ok: false, error: N_('无法识别的格式') };
|
||||||
const v = Number.isInteger(raw.v) ? raw.v : null;
|
const v = Number.isInteger(raw.v) ? raw.v : null;
|
||||||
if (v != null && v > DIY_VERSION) return { ok: false, error: '这份数据来自更新的版本(v{v}),请先更新游戏', params: { v } };
|
if (v != null && v > DIY_VERSION) return { ok: false, error: N_('这份数据来自更新的版本(v{v}),请先更新游戏'), params: { v } };
|
||||||
const kind = typeof raw.kind === 'string' ? raw.kind : null;
|
const kind = typeof raw.kind === 'string' ? raw.kind : null;
|
||||||
if (kind && kind !== DIY_EXPORT_KIND) return { ok: false, error: '这不是自选编队的数据' };
|
if (kind && kind !== DIY_EXPORT_KIND) return { ok: false, error: N_('这不是自选编队的数据') };
|
||||||
if (!isObj(raw.picks)) return { ok: false, error: '里面没有自选编队的数据' };
|
if (!isObj(raw.picks)) return { ok: false, error: N_('里面没有自选编队的数据') };
|
||||||
return { ok: true, picks: cleanPicks(raw.picks) };
|
return { ok: true, picks: cleanPicks(raw.picks) };
|
||||||
}
|
}
|
||||||
@@ -6,18 +6,26 @@ import { html, Tooltip, MicroLabel } from './components.js';
|
|||||||
import { Img, RichText, GIcon } from './gameComponents.js';
|
import { Img, RichText, GIcon } from './gameComponents.js';
|
||||||
import { effectIconUrl } from './assetUrls.js';
|
import { effectIconUrl } from './assetUrls.js';
|
||||||
import { data } from '../data.js';
|
import { data } from '../data.js';
|
||||||
|
import { t, N_ } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const KIND = { band: '策略', choice: '机变', team: '团队增益', item: '道具', garrison: '特质' };
|
const KIND = { band: N_('策略'), choice: N_('机变'), team: N_('团队增益'), item: N_('道具'), garrison: N_('特质') };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A 悬赏's counter line in the current language: the battles its enemies still come for (server/match/player/views.js
|
||||||
|
* sends the same Chinese text as `counterText` beside the count; a multi-round card has no count).
|
||||||
|
* @param {{ counter?: number|null }} e
|
||||||
|
*/
|
||||||
|
export const bountyCounterText = (e) => (e.counter == null ? t('之后的每场作战') : t('还剩 {left} 场作战', { left: e.counter }));
|
||||||
|
|
||||||
/** @param {{ effects: any[] }} props */
|
/** @param {{ effects: any[] }} props */
|
||||||
export function EffectsList({ effects }) {
|
export function EffectsList({ effects }) {
|
||||||
const list = (Array.isArray(effects) ? effects : []).filter((e) => e && (e.name || e.desc));
|
const list = (Array.isArray(effects) ? effects : []).filter((e) => e && (e.name || e.desc));
|
||||||
if (!list.length) return null;
|
if (!list.length) return null;
|
||||||
const m = data.get('assets');
|
const m = data.get('assets');
|
||||||
return html`<div class="effects" aria-label="生效中的效果">
|
return html`<div class="effects" aria-label=${t('生效中的效果')}>
|
||||||
<${MicroLabel}>EFFECTS</${MicroLabel}>
|
<${MicroLabel}>EFFECTS</${MicroLabel}>
|
||||||
${list.slice(0, 10).map((e, i) => html`<${Tooltip} key=${e.id ?? i} placement="bottom" text=${html`<div class="efftip">
|
${list.slice(0, 10).map((e, i) => html`<${Tooltip} key=${e.id ?? i} placement="bottom" text=${html`<div class="efftip">
|
||||||
<b>${e.name || '效果'}</b><span class="efftip__kind">${KIND[e.iconKind] || ''}${e.counterText ? ` · ${e.counterText}` : e.counter != null ? ` · ${e.counter}` : ''}</span>
|
<b>${e.name || t('效果')}</b><span class="efftip__kind">${t(KIND[e.iconKind] || '')}${e.counterText ? ` · ${bountyCounterText(e)}` : e.counter != null ? ` · ${e.counter}` : ''}</span>
|
||||||
<${RichText} as="p" text=${e.desc || ''} />
|
<${RichText} as="p" text=${e.desc || ''} />
|
||||||
</div>`}>
|
</div>`}>
|
||||||
<span class=${`effect effect--${e.iconKind || 'x'}`}>
|
<span class=${`effect effect--${e.iconKind || 'x'}`}>
|
||||||
|
|||||||
+11
-10
@@ -25,6 +25,7 @@ import { html } from './components.js';
|
|||||||
import { GIcon } from './gameComponents.js';
|
import { GIcon } from './gameComponents.js';
|
||||||
import { data, useData, localAsset, artUrls, nextArtUrl } from '../data.js';
|
import { data, useData, localAsset, artUrls, nextArtUrl } from '../data.js';
|
||||||
import { loadPref, savePref } from '../store.js';
|
import { loadPref, savePref } from '../store.js';
|
||||||
|
import { t } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
|
|
||||||
@@ -196,7 +197,7 @@ export function EmoteBubble({ id, class: cls, ttl = EMOTE_BUBBLE_MS, at }) {
|
|||||||
const e = emoteInfo(id);
|
const e = emoteInfo(id);
|
||||||
const style = [`--ebubble-ttl:${life}ms`, age && `--ebubble-age:${Math.round(age)}ms`, bg && `--ebubble-bg:url("${bg}")`].filter(Boolean).join(';');
|
const style = [`--ebubble-ttl:${life}ms`, age && `--ebubble-age:${Math.round(age)}ms`, bg && `--ebubble-bg:url("${bg}")`].filter(Boolean).join(';');
|
||||||
return html`<div class=${cx('ebubble', bg && 'has-sprite', cls)} style=${style} role="img"
|
return html`<div class=${cx('ebubble', bg && 'has-sprite', cls)} style=${style} role="img"
|
||||||
aria-label=${e ? e.label : '表情'} data-emote=${e ? e.id : ''}>
|
aria-label=${e ? t(e.label) : t('表情')} data-emote=${e ? e.id : ''}>
|
||||||
<span class="ebubble__icon"><${EmoteArt} id=${id} /></span>
|
<span class="ebubble__icon"><${EmoteArt} id=${id} /></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -310,24 +311,24 @@ export function EmoteWheel({ onSend, open, onToggle, disabled = false, cooldownM
|
|||||||
<button type="button" class=${cx('ewheel__btn', btnSprite && 'has-sprite', open && 'is-on', cooling && 'is-cooling')}
|
<button type="button" class=${cx('ewheel__btn', btnSprite && 'has-sprite', open && 'is-on', cooling && 'is-cooling')}
|
||||||
style=${btnSprite ? `--ewheel-btn:url("${btnSprite}")` : ''} onClick=${() => onToggle(!open)}
|
style=${btnSprite ? `--ewheel-btn:url("${btnSprite}")` : ''} onClick=${() => onToggle(!open)}
|
||||||
aria-expanded=${open ? 'true' : 'false'} aria-haspopup="dialog" disabled=${disabled || cooling}>
|
aria-expanded=${open ? 'true' : 'false'} aria-haspopup="dialog" disabled=${disabled || cooling}>
|
||||||
${btnSprite ? null : html`<${GIcon} name="emote" />`}<span class="ewheel__label">交流</span>
|
${btnSprite ? null : html`<${GIcon} name="emote" />`}<span class="ewheel__label">${t('交流')}</span>
|
||||||
</button>
|
</button>
|
||||||
${open ? html`<div class=${cx('ewheel__panel', panelBg && 'has-sprite', cellBg && 'has-cell')} style=${panelStyle} role="dialog" aria-label="交流">
|
${open ? html`<div class=${cx('ewheel__panel', panelBg && 'has-sprite', cellBg && 'has-cell')} style=${panelStyle} role="dialog" aria-label=${t('交流')}>
|
||||||
<div class="ewheel__viewport" onPointerDown=${onPointerDown} onPointerMove=${onPointerMove}
|
<div class="ewheel__viewport" onPointerDown=${onPointerDown} onPointerMove=${onPointerMove}
|
||||||
onPointerUp=${(e) => endDrag(e, false)} onPointerCancel=${(e) => endDrag(e, true)} onWheel=${onWheel}>
|
onPointerUp=${(e) => endDrag(e, false)} onPointerCancel=${(e) => endDrag(e, true)} onWheel=${onWheel}>
|
||||||
<div key=${theme.themeId} class=${cx('ewheel__page', dir > 0 && 'is-from-right', dir < 0 && 'is-from-left', dx !== 0 && 'is-dragging')}
|
<div key=${theme.themeId} class=${cx('ewheel__page', dir > 0 && 'is-from-right', dir < 0 && 'is-from-left', dx !== 0 && 'is-dragging')}
|
||||||
style=${dx ? `transform:translateX(${dx}px)` : ''} role="group" aria-label=${theme.name} data-theme=${theme.themeId}>
|
style=${dx ? `transform:translateX(${dx}px)` : ''} role="group" aria-label=${t(theme.name)} data-theme=${theme.themeId}>
|
||||||
${theme.emotes.map((e) => html`<button key=${e.id} type="button" class="ewheel__item" data-emote=${e.id} aria-label=${e.label}
|
${theme.emotes.map((e) => html`<button key=${e.id} type="button" class="ewheel__item" data-emote=${e.id} aria-label=${t(e.label)}
|
||||||
disabled=${cooling || disabled} onClick=${() => send(e.id)}>
|
disabled=${cooling || disabled} onClick=${() => send(e.id)}>
|
||||||
<${EmoteArt} id=${e.id} />
|
<${EmoteArt} id=${e.id} />
|
||||||
</button>`)}
|
</button>`)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="ewheel__nav is-prev" aria-label="上一组表情" disabled=${page <= 0} onClick=${() => go(page - 1)}><${GIcon} name="chevronLeft" /></button>
|
<button type="button" class="ewheel__nav is-prev" aria-label=${t('上一组表情')} disabled=${page <= 0} onClick=${() => go(page - 1)}><${GIcon} name="chevronLeft" /></button>
|
||||||
<button type="button" class="ewheel__nav is-next" aria-label="下一组表情" disabled=${page >= EMOTE_THEMES.length - 1} onClick=${() => go(page + 1)}><${GIcon} name="chevronRight" /></button>
|
<button type="button" class="ewheel__nav is-next" aria-label=${t('下一组表情')} disabled=${page >= EMOTE_THEMES.length - 1} onClick=${() => go(page + 1)}><${GIcon} name="chevronRight" /></button>
|
||||||
<div class="ewheel__dots" role="tablist" aria-label="表情主题">
|
<div class="ewheel__dots" role="tablist" aria-label=${t('表情主题')}>
|
||||||
${EMOTE_THEMES.map((t, i) => html`<button key=${t.themeId} type="button" role="tab" class=${cx('ewheel__dot', i === page && 'is-on')}
|
${EMOTE_THEMES.map((th, i) => html`<button key=${th.themeId} type="button" role="tab" class=${cx('ewheel__dot', i === page && 'is-on')}
|
||||||
aria-selected=${i === page ? 'true' : 'false'} aria-label=${`${t.name} ${i + 1}/${EMOTE_THEMES.length}`} onClick=${() => go(i)}></button>`)}
|
aria-selected=${i === page ? 'true' : 'false'} aria-label=${`${t(th.name)} ${i + 1}/${EMOTE_THEMES.length}`} onClick=${() => go(i)}></button>`)}
|
||||||
</div>
|
</div>
|
||||||
</div>` : null}
|
</div>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
+18
-17
@@ -13,9 +13,10 @@ import { groupEnemies, factionTypes, briefingBondTip } from './gameLogic.js';
|
|||||||
import { matchInfoModel, DiyBannedLine } from './matchInfo.js';
|
import { matchInfoModel, DiyBannedLine } from './matchInfo.js';
|
||||||
import { factionIconUrl } from './assetUrls.js';
|
import { factionIconUrl } from './assetUrls.js';
|
||||||
import { data } from '../data.js';
|
import { data } from '../data.js';
|
||||||
|
import { t, tParts, N_ } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
const TAG = { boss: '领袖', bounty: '悬赏', escort: '护卫' };
|
const TAG = { boss: N_('领袖'), bounty: N_('悬赏'), escort: N_('护卫') };
|
||||||
|
|
||||||
function FactionChips({ types }) {
|
function FactionChips({ types }) {
|
||||||
const f = data.get('factions')?.types || {};
|
const f = data.get('factions')?.types || {};
|
||||||
@@ -32,7 +33,7 @@ function EnemiesTab({ pub, priv, onEnemy }) {
|
|||||||
const boss = pub?.bossId ? data.lookup('bosses', pub.bossId) : null;
|
const boss = pub?.bossId ? data.lookup('bosses', pub.bossId) : null;
|
||||||
return html`<div class="edrawer__body">
|
return html`<div class="edrawer__body">
|
||||||
<div class="edrawer__sum">
|
<div class="edrawer__sum">
|
||||||
<span>第 <b class="num">${pub?.round ?? '-'}</b> 回合 · 即将迎击 <b class="num t-orange">${total}</b> 名敌人</span>
|
<span>${tParts('第 {r} 回合 · 即将迎击 {n} 名敌人', { r: html`<b class="num">${pub?.round ?? '-'}</b>`, n: html`<b class="num t-orange">${total}</b>`, count: total })}</span>
|
||||||
${types.length ? html`<${FactionChips} types=${types} />` : null}
|
${types.length ? html`<${FactionChips} types=${types} />` : null}
|
||||||
</div>
|
</div>
|
||||||
${rows.length ? html`<div class="elist">
|
${rows.length ? html`<div class="elist">
|
||||||
@@ -42,19 +43,19 @@ function EnemiesTab({ pub, priv, onEnemy }) {
|
|||||||
<span class="erow__text">
|
<span class="erow__text">
|
||||||
<b class="erow__name">${r.name}</b>
|
<b class="erow__name">${r.name}</b>
|
||||||
<span class="erow__tags">
|
<span class="erow__tags">
|
||||||
${r.tag ? html`<span class=${`etag etag--${r.tag}`}>${TAG[r.tag] || r.tag}</span>` : null}
|
${r.tag ? html`<span class=${`etag etag--${r.tag}`}>${t(TAG[r.tag] || r.tag)}</span>` : null}
|
||||||
${r.rank === 'ELITE' ? html`<span class="etag etag--elite">精英</span>` : null}
|
${r.rank === 'ELITE' ? html`<span class="etag etag--elite">${t('精英')}</span>` : null}
|
||||||
${r.fly ? html`<span class="etag etag--fly">空中</span>` : null}
|
${r.fly ? html`<span class="etag etag--fly">${t('空中')}</span>` : null}
|
||||||
${r.acTypes.map((t) => html`<span key=${t} class="etag etag--faction">${(f[t]?.name || t).replace('特训敌人·', '')}</span>`)}
|
${r.acTypes.map((ty) => html`<span key=${ty} class="etag etag--faction">${(f[ty]?.name || ty).replace(t('特训敌人·'), '')}</span>`)}
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<b class="erow__count num">×${r.count}</b>
|
<b class="erow__count num">×${r.count}</b>
|
||||||
</button>`)}
|
</button>`)}
|
||||||
</div>` : html`<p class="edrawer__empty"><${GIcon} name="target" />暂无敌方情报</p>`}
|
</div>` : html`<p class="edrawer__empty"><${GIcon} name="target" />${t('暂无敌方情报')}</p>`}
|
||||||
${boss ? html`<div class="eboss">
|
${boss ? html`<div class="eboss">
|
||||||
<${UnitThumb} kind="enemy" id=${boss.enemyKey} size="md" />
|
<${UnitThumb} kind="enemy" id=${boss.enemyKey} size="md" />
|
||||||
<div><${MicroLabel} tone="gold">ENEMY LEADER // 敌方领袖</${MicroLabel}><b>${boss.name}</b>
|
<div><${MicroLabel} tone="gold">${t('ENEMY LEADER // 敌方领袖')}</${MicroLabel}><b>${boss.name}</b>
|
||||||
<span class="t-lo">第 ${data.get('config')?.modes?.[pub.modeId]?.bossRound ?? pub.lastRound ?? 14} 回合 · 最终攻势</span></div>
|
<span class="t-lo">${t('第 {r} 回合 · 最终攻势', { r: data.get('config')?.modes?.[pub.modeId]?.bossRound ?? pub.lastRound ?? 14 })}</span></div>
|
||||||
</div>` : null}
|
</div>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -75,16 +76,16 @@ function InfoTab({ pub, priv, onChess, bandId = null, bandOwner = null }) {
|
|||||||
return html`<div class="edrawer__body">
|
return html`<div class="edrawer__body">
|
||||||
${band ? html`<div class="iband">
|
${band ? html`<div class="iband">
|
||||||
<${BandIcon} bandId=${band.bandId} size="md" />
|
<${BandIcon} bandId=${band.bandId} size="md" />
|
||||||
<div><${MicroLabel} tone="mint">STRATEGY // ${bandOwner ? `${bandOwner} 的策略` : '我的策略'}</${MicroLabel}><b>${band.name} <small class="t-lo">${band.effectName}</small></b>
|
<div><${MicroLabel} tone="mint">STRATEGY // ${bandOwner ? t('{bandOwner} 的策略', { bandOwner }) : t('我的策略')}</${MicroLabel}><b>${band.name} <small class="t-lo">${band.effectName}</small></b>
|
||||||
<${RichText} text=${band.descRaw || band.desc} class="iband__desc" /></div>
|
<${RichText} text=${band.descRaw || band.desc} class="iband__desc" /></div>
|
||||||
</div>` : null}
|
</div>` : null}
|
||||||
${stage ? html`<p class="istage"><${Icon} name="rook" />战场:<b>${stage.name || stage.id}</b></p>` : null}
|
${stage ? html`<p class="istage"><${Icon} name="rook" />${t('战场:')}<b>${stage.name || stage.id}</b></p>` : null}
|
||||||
<h4 class="ihead">本局禁用干员情况 <small class="num">${banned.length}</small></h4>
|
<h4 class="ihead">${t('本局禁用干员情况')} <small class="num">${banned.length}</small></h4>
|
||||||
<div class="ibonds">
|
<div class="ibonds">
|
||||||
${withBans.length ? withBans.map((b) => html`<span key=${b.bondId} class=${cx('ibond', disabled.has(b.bondId) && 'is-off')} title=${briefingBondTip(b.name, stateOf(b.bondId), perBond.get(b.bondId) || 0)}>
|
${withBans.length ? withBans.map((b) => html`<span key=${b.bondId} class=${cx('ibond', disabled.has(b.bondId) && 'is-off')} title=${briefingBondTip(b.name, stateOf(b.bondId), perBond.get(b.bondId) || 0)}>
|
||||||
<${BondGlyph} bondId=${b.bondId} /><span>${b.name}</span>
|
<${BondGlyph} bondId=${b.bondId} /><span>${b.name}</span>
|
||||||
${perBond.get(b.bondId) ? html`<span class="ibond__ban num" title="该盟约中被禁用的干员数"><${Icon} name="user" />${perBond.get(b.bondId)}</span>` : null}
|
${perBond.get(b.bondId) ? html`<span class="ibond__ban num" title=${t('该盟约中被禁用的干员数')}><${Icon} name="user" />${perBond.get(b.bondId)}</span>` : null}
|
||||||
</span>`) : html`<span class="t-dim">本局没有禁用盟约</span>`}
|
</span>`) : html`<span class="t-dim">${t('本局没有禁用盟约')}</span>`}
|
||||||
</div>
|
</div>
|
||||||
${banned.length ? html`<div class="ibanned">${banned.map((id) => html`<button key=${id} type="button" class="ibanned__one" onClick=${() => onChess(id)}>
|
${banned.length ? html`<div class="ibanned">${banned.map((id) => html`<button key=${id} type="button" class="ibanned__one" onClick=${() => onChess(id)}>
|
||||||
<${UnitThumb} kind="chess" id=${id} size="sm" dim=${true} /></button>`)}</div>` : null}
|
<${UnitThumb} kind="chess" id=${id} size="sm" dim=${true} /></button>`)}</div>` : null}
|
||||||
@@ -99,10 +100,10 @@ function InfoTab({ pub, priv, onChess, bandId = null, bandOwner = null }) {
|
|||||||
* replaces one's own in the 本局信息 tab (game.js scoutBandId; user playtest #2 item 2)
|
* replaces one's own in the 本局信息 tab (game.js scoutBandId; user playtest #2 item 2)
|
||||||
*/
|
*/
|
||||||
export function EnemyDrawer({ tab, onTab, pub, priv, onClose, onEnemy, onChess, bandId = null, bandOwner = null }) {
|
export function EnemyDrawer({ tab, onTab, pub, priv, onClose, onEnemy, onChess, bandId = null, bandOwner = null }) {
|
||||||
return html`<div class="edrawer brackets" role="dialog" aria-label=${tab === 'info' ? '本局信息' : '敌方情报'}>
|
return html`<div class="edrawer brackets" role="dialog" aria-label=${tab === 'info' ? t('本局信息') : t('敌方情报')}>
|
||||||
<div class="edrawer__top">
|
<div class="edrawer__top">
|
||||||
<${Tabs} size="sm" value=${tab} onChange=${onTab} items=${[{ id: 'info', label: '本局信息' }, { id: 'enemies', label: '敌方情报' }]} />
|
<${Tabs} size="sm" value=${tab} onChange=${onTab} items=${[{ id: 'info', label: t('本局信息') }, { id: 'enemies', label: t('敌方情报') }]} />
|
||||||
<button type="button" class="edrawer__close tapx" aria-label="关闭" onClick=${onClose}><${Icon} name="close" /></button>
|
<button type="button" class="edrawer__close tapx" aria-label=${t('关闭')} onClick=${onClose}><${Icon} name="close" /></button>
|
||||||
</div>
|
</div>
|
||||||
${tab === 'info' ? html`<${InfoTab} pub=${pub} priv=${priv} onChess=${onChess} bandId=${bandId} bandOwner=${bandOwner} />` : html`<${EnemiesTab} pub=${pub} priv=${priv} onEnemy=${onEnemy} />`}
|
${tab === 'info' ? html`<${InfoTab} pub=${pub} priv=${priv} onChess=${onChess} bandId=${bandId} bandOwner=${bandOwner} />` : html`<${EnemiesTab} pub=${pub} priv=${priv} onEnemy=${onEnemy} />`}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import { useEffect, useState } from '../../vendor/hooks.module.js';
|
|||||||
import { html, Modal, Button, TierChip, MicroLabel } from './components.js';
|
import { html, Modal, Button, TierChip, MicroLabel } from './components.js';
|
||||||
import { UnitThumb, RichText, GIcon } from './gameComponents.js';
|
import { UnitThumb, RichText, GIcon } from './gameComponents.js';
|
||||||
import { localAsset } from '../data.js';
|
import { localAsset } from '../data.js';
|
||||||
|
import { t, tParts } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
const isObj = (v) => !!v && typeof v === 'object';
|
const isObj = (v) => !!v && typeof v === 'object';
|
||||||
@@ -40,11 +41,11 @@ export function replaceRequest(ctx, intent, getChess = () => null, getItem = ()
|
|||||||
return {
|
return {
|
||||||
targetUid: target.uid,
|
targetUid: target.uid,
|
||||||
targetId: target.id,
|
targetId: target.id,
|
||||||
targetName: shown?.name || '该干员',
|
targetName: shown?.name || t('该干员'),
|
||||||
// the record the avatar draws (a 补位 stand-in's or a 自选 operator's composed record; the data's for any other chess)
|
// the record the avatar draws (a 补位 stand-in's or a 自选 operator's composed record; the data's for any other chess)
|
||||||
targetRec: shown && (shown.standInFor || shown.diyFor) ? shown : null,
|
targetRec: shown && (shown.standInFor || shown.diyFor) ? shown : null,
|
||||||
golden: !!target.golden,
|
golden: !!target.golden,
|
||||||
item: { uid: item.uid, id: item.id, name: rec(item.id)?.name || '装备' },
|
item: { uid: item.uid, id: item.id, name: rec(item.id)?.name || t('装备') },
|
||||||
options: equipped.map((it) => ({ uid: it.uid, id: it.id, name: rec(it.id)?.name || it.id })),
|
options: equipped.map((it) => ({ uid: it.uid, id: it.id, name: rec(it.id)?.name || it.id })),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -76,7 +77,7 @@ function ItemCard({ id, item, rec, selected = false, option = false, onPick }) {
|
|||||||
</span>
|
</span>
|
||||||
<span class="eqr__meta">
|
<span class="eqr__meta">
|
||||||
<span class="eqr__chips">${tier ? html`<${TierChip} tier=${tier} golden=${!!rec?.isGolden} size="sm" />` : null}
|
<span class="eqr__chips">${tier ? html`<${TierChip} tier=${tier} golden=${!!rec?.isGolden} size="sm" />` : null}
|
||||||
${rec?.isGolden ? html`<span class="eqr__elite">进阶</span>` : null}</span>
|
${rec?.isGolden ? html`<span class="eqr__elite">${t('进阶')}</span>` : null}</span>
|
||||||
<b class="eqr__name">${rec?.name || item?.name || id}</b>
|
<b class="eqr__name">${rec?.name || item?.name || id}</b>
|
||||||
</span>
|
</span>
|
||||||
<${RichText} as="p" class="eqr__desc" text=${rec?.descRaw || rec?.desc || ''} />`;
|
<${RichText} as="p" class="eqr__desc" text=${rec?.descRaw || rec?.desc || ''} />`;
|
||||||
@@ -85,7 +86,7 @@ function ItemCard({ id, item, rec, selected = false, option = false, onPick }) {
|
|||||||
class=${cx('eqr__card', 'eqr__opt', selected && 'is-sel')} onClick=${() => onPick(item.uid)}>
|
class=${cx('eqr__card', 'eqr__opt', selected && 'is-sel')} onClick=${() => onPick(item.uid)}>
|
||||||
${body}
|
${body}
|
||||||
<span class="eqr__flag" aria-hidden=${selected ? 'false' : 'true'}>
|
<span class="eqr__flag" aria-hidden=${selected ? 'false' : 'true'}>
|
||||||
<${GIcon} name="trash" class="eqr__flag-icon" /><span>将被销毁</span>
|
<${GIcon} name="trash" class="eqr__flag-icon" /><span>${t('将被销毁')}</span>
|
||||||
</span>
|
</span>
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
@@ -111,12 +112,12 @@ export function EquipReplaceDialog({ request, getItem = () => null, busy = false
|
|||||||
const confirmIcon = localAsset('ui/battle', 'equip_replace_comfirm_icon');
|
const confirmIcon = localAsset('ui/battle', 'equip_replace_comfirm_icon');
|
||||||
const swapIcon = localAsset('ui/battle', 'equip_replace_replace_icon');
|
const swapIcon = localAsset('ui/battle', 'equip_replace_replace_icon');
|
||||||
return html`<div class="eqr-host" style=${style} onKeyDown=${(e) => { if (e.key === 'Enter' && chosen && !busy) { e.preventDefault(); onConfirm(chosen.uid); } }}>
|
return html`<div class="eqr-host" style=${style} onKeyDown=${(e) => { if (e.key === 'Enter' && chosen && !busy) { e.preventDefault(); onConfirm(chosen.uid); } }}>
|
||||||
<${Modal} open=${true} tone="red" class="eqr" width="min(7.6rem, 96vw)" micro="EQUIPMENT · REPLACE" title="替换装备" onClose=${onCancel}
|
<${Modal} open=${true} tone="red" class="eqr" width="min(7.6rem, 96vw)" micro="EQUIPMENT · REPLACE" title=${t('替换装备')} onClose=${onCancel}
|
||||||
actions=${html`
|
actions=${html`
|
||||||
<${Button} variant="secondary" icon="close" class="eqr__cancel" onClick=${onCancel}>取消<//>
|
<${Button} variant="secondary" icon="close" class="eqr__cancel" onClick=${onCancel}>${t('取消')}<//>
|
||||||
<${Button} variant="danger" class="eqr__ok" disabled=${!chosen} loading=${busy} data-autofocus onClick=${() => chosen && onConfirm(chosen.uid)}>
|
<${Button} variant="danger" class="eqr__ok" disabled=${!chosen} loading=${busy} data-autofocus onClick=${() => chosen && onConfirm(chosen.uid)}>
|
||||||
${confirmIcon ? html`<img class="eqr__ok-icon" src=${confirmIcon} alt="" draggable=${false} />` : null}确认替换<//>`}>
|
${confirmIcon ? html`<img class="eqr__ok-icon" src=${confirmIcon} alt="" draggable=${false} />` : null}${t('确认替换')}<//>`}>
|
||||||
<p class="eqr__lead">「<b>${request.targetName}</b>」的装备栏已满(2/2)。选择一件装备进行替换,<b class="t-red">被替换的装备将被销毁</b>。</p>
|
<p class="eqr__lead">${tParts('「{name}」的装备栏已满(2/2)。选择一件装备进行替换,{warn}。', { name: html`<b>${request.targetName}</b>`, warn: html`<b class="t-red">${t('被替换的装备将被销毁')}</b>` })}</p>
|
||||||
<div class="eqr__top">
|
<div class="eqr__top">
|
||||||
<div class="eqr__op" title=${request.targetName}>
|
<div class="eqr__op" title=${request.targetName}>
|
||||||
<span class="eqr__ava"><${UnitThumb} kind="chess" id=${request.targetId} golden=${request.golden} size="md" rec=${request.targetRec || null} /></span>
|
<span class="eqr__ava"><${UnitThumb} kind="chess" id=${request.targetId} golden=${request.golden} size="md" rec=${request.targetRec || null} /></span>
|
||||||
@@ -124,16 +125,16 @@ export function EquipReplaceDialog({ request, getItem = () => null, busy = false
|
|||||||
</div>
|
</div>
|
||||||
<span class="eqr__swap" aria-hidden="true">${swapIcon ? html`<img src=${swapIcon} alt="" draggable=${false} />` : html`<${GIcon} name="refresh" />`}</span>
|
<span class="eqr__swap" aria-hidden="true">${swapIcon ? html`<img src=${swapIcon} alt="" draggable=${false} />` : html`<${GIcon} name="refresh" />`}</span>
|
||||||
<div class="eqr__incoming">
|
<div class="eqr__incoming">
|
||||||
<${MicroLabel} tone="mint">新装备</${MicroLabel}>
|
<${MicroLabel} tone="mint">${t('新装备')}</${MicroLabel}>
|
||||||
<${ItemCard} id=${request.item.id} item=${request.item} rec=${getItem(request.item.id)} />
|
<${ItemCard} id=${request.item.id} item=${request.item} rec=${getItem(request.item.id)} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="eqr__arrow" aria-hidden="true"><i class="eqr__aline"></i><i class="eqr__ahead eqr__ahead--l"></i><i class="eqr__ahead eqr__ahead--r"></i></div>
|
<div class="eqr__arrow" aria-hidden="true"><i class="eqr__aline"></i><i class="eqr__ahead eqr__ahead--l"></i><i class="eqr__ahead eqr__ahead--r"></i></div>
|
||||||
<div class="eqr__opts" role="radiogroup" aria-label="选择被替换的装备">
|
<div class="eqr__opts" role="radiogroup" aria-label=${t('选择被替换的装备')}>
|
||||||
${request.options.map((o) => html`<${ItemCard} key=${o.uid} id=${o.id} item=${o} rec=${getItem(o.id)} option=${true}
|
${request.options.map((o) => html`<${ItemCard} key=${o.uid} id=${o.id} item=${o} rec=${getItem(o.id)} option=${true}
|
||||||
selected=${o.uid === sel} onPick=${(uid) => setSel((s) => (s === uid ? null : uid))} />`)}
|
selected=${o.uid === sel} onPick=${(uid) => setSel((s) => (s === uid ? null : uid))} />`)}
|
||||||
</div>
|
</div>
|
||||||
<p class="eqr__hint">${chosen ? html`将销毁「<b>${chosen.name}</b>」并配发「<b>${request.item.name}</b>」` : '点击选择要替换的装备'}</p>
|
<p class="eqr__hint">${chosen ? html`${tParts('将销毁「{old}」并配发「{item}」', { old: html`<b>${chosen.name}</b>`, item: html`<b>${request.item.name}</b>` })}` : t('点击选择要替换的装备')}</p>
|
||||||
<//>
|
<//>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ export function UnitThumb({ kind = 'chess', id, golden, size = 'md', tier, showT
|
|||||||
<${Img} src=${src} fallback=${html`<span class="uthumb__glyph">${glyph}</span>`} />
|
<${Img} src=${src} fallback=${html`<span class="uthumb__glyph">${glyph}</span>`} />
|
||||||
</span>
|
</span>
|
||||||
${showTier && t && kind !== 'enemy' && kind !== 'token' ? html`<${TierChip} tier=${t} golden=${golden} size="sm" class="uthumb__tier" />` : null}
|
${showTier && t && kind !== 'enemy' && kind !== 'token' ? html`<${TierChip} tier=${t} golden=${golden} size="sm" class="uthumb__tier" />` : null}
|
||||||
${kind === 'token' ? html`<span class="uthumb__tag">召唤</span>` : null}
|
${kind === 'token' ? html`<span class="uthumb__tag">${tr('召唤')}</span>` : null}
|
||||||
${si ? html`<span class="uthumb__si" data-standin=${si.charId} aria-hidden="true">${tr('替补')}</span>` : null}
|
${si ? html`<span class="uthumb__si" data-standin=${si.charId} aria-hidden="true">${tr('替补')}</span>` : null}
|
||||||
${badge}
|
${badge}
|
||||||
</span>`;
|
</span>`;
|
||||||
@@ -207,11 +207,11 @@ export function GIcon({ name, class: cls, title }) {
|
|||||||
export function LpTower({ value, size = 'md', class: cls, tone, pending = 0, note = null, tip = null }) {
|
export function LpTower({ value, size = 'md', class: cls, tone, pending = 0, note = null, tip = null }) {
|
||||||
const ok = Number.isFinite(value);
|
const ok = Number.isFinite(value);
|
||||||
const p = ok && Number(pending) > 0 ? Math.min(value, Math.trunc(Number(pending))) : 0;
|
const p = ok && Number(pending) > 0 ? Math.min(value, Math.trunc(Number(pending))) : 0;
|
||||||
return html`<span class=${cx('lp', `lp--${size}`, tone && `lp--${tone}`, p > 0 && 'is-pending', cls)} title=${tip || '目标生命值'}
|
return html`<span class=${cx('lp', `lp--${size}`, tone && `lp--${tone}`, p > 0 && 'is-pending', cls)} title=${tip || tr('目标生命值')}
|
||||||
data-pending=${p > 0 ? p : null}>
|
data-pending=${p > 0 ? p : null}>
|
||||||
<${Sprite} k="hudPanel/icon_hp" class="lp__icon" fallback=${html`<${Icon} name="rook" class="lp__icon" />`} />
|
<${Sprite} k="hudPanel/icon_hp" class="lp__icon" fallback=${html`<${Icon} name="rook" class="lp__icon" />`} />
|
||||||
<b class="num lp__val">${ok ? Math.max(0, value - p) : '--'}</b>
|
<b class="num lp__val">${ok ? Math.max(0, value - p) : '--'}</b>
|
||||||
${p > 0 ? html`<span key=${p} class="lp__pend num" aria-label=${`结算时扣除 ${p}`}>−${p}</span>` : null}
|
${p > 0 ? html`<span key=${p} class="lp__pend num" aria-label=${tr('结算时扣除 {p}', { p })}>−${p}</span>` : null}
|
||||||
${note ? html`<span class="lp__note">${note}</span>` : null}
|
${note ? html`<span class="lp__note">${note}</span>` : null}
|
||||||
</span>`;
|
</span>`;
|
||||||
}
|
}
|
||||||
@@ -265,7 +265,7 @@ export function PlayerAvatar({ player, size = 'md', self = false, class: cls })
|
|||||||
<span class="pavatar__img">
|
<span class="pavatar__img">
|
||||||
<${Img} src=${src} fallback=${player?.isBot ? html`<${Icon} name="robot" class="pavatar__bot" />` : html`<span class="pavatar__glyph">${glyph}</span>`} />
|
<${Img} src=${src} fallback=${player?.isBot ? html`<${Icon} name="robot" class="pavatar__bot" />` : html`<span class="pavatar__glyph">${glyph}</span>`} />
|
||||||
</span>
|
</span>
|
||||||
${dead ? html`<span class="pavatar__x" aria-label="已淘汰"><${Icon} name="close" /></span>` : null}
|
${dead ? html`<span class="pavatar__x" aria-label=${tr('已淘汰')}><${Icon} name="close" /></span>` : null}
|
||||||
${left ? html`<span class="pavatar__door" aria-label="已离开"><${Icon} name="exit" /></span>` : null}
|
${left ? html`<span class="pavatar__door" aria-label=${tr('已离开')}><${Icon} name="exit" /></span>` : null}
|
||||||
</span>`;
|
</span>`;
|
||||||
}
|
}
|
||||||
+27
-26
@@ -16,23 +16,24 @@ import { useEffect, useMemo, useRef, useState } from '../../vendor/hooks.module.
|
|||||||
import { html, Icon, MicroLabel, Button, Spinner } from './components.js';
|
import { html, Icon, MicroLabel, Button, Spinner } from './components.js';
|
||||||
import { createStore, useStore } from '../store.js';
|
import { createStore, useStore } from '../store.js';
|
||||||
import { data, useData, artUrls, nextArtUrl } from '../data.js';
|
import { data, useData, artUrls, nextArtUrl } from '../data.js';
|
||||||
|
import { t, N_ } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
|
|
||||||
/** Page catalogue in reading order (titles transcribed from the pages). */
|
/** Page catalogue in reading order (titles transcribed from the pages). */
|
||||||
export const GUIDE_CHAPTERS = [
|
export const GUIDE_CHAPTERS = [
|
||||||
{ id: 'home', name: '基础规则', micro: 'BASICS', pages: [
|
{ id: 'home', name: N_('基础规则'), micro: 'BASICS', pages: [
|
||||||
['autochess_home_1', '卫戍协议已运行'], ['autochess_home_2', '攻防战'], ['autochess_home_3', '休整期 · 区域'],
|
['autochess_home_1', N_('卫戍协议已运行')], ['autochess_home_2', N_('攻防战')], ['autochess_home_3', N_('休整期 · 区域')],
|
||||||
['autochess_home_4', '休整期 · 资金与调度'], ['autochess_home_5', '机变阶段'], ['autochess_home_6', '限时战斗'],
|
['autochess_home_4', N_('休整期 · 资金与调度')], ['autochess_home_5', N_('机变阶段')], ['autochess_home_6', N_('限时战斗')],
|
||||||
['autochess_home_7', '作战期'], ['autochess_home_8', '协同战斗'], ['autochess_home_9', '盟约'],
|
['autochess_home_7', N_('作战期')], ['autochess_home_8', N_('协同战斗')], ['autochess_home_9', N_('盟约')],
|
||||||
] },
|
] },
|
||||||
{ id: 'shop', name: '调度手册', micro: 'HANDBOOK', pages: [
|
{ id: 'shop', name: N_('调度手册'), micro: 'HANDBOOK', pages: [
|
||||||
['autochess_shop_1', '调度手册'], ['autochess_shop_2', '干员晋级'], ['autochess_shop_3', '加成情况'],
|
['autochess_shop_1', N_('调度手册')], ['autochess_shop_2', N_('干员晋级')], ['autochess_shop_3', N_('加成情况')],
|
||||||
['autochess_shop_4', '卫戍能力'], ['autochess_shop_5', '盟约'], ['autochess_shop_6', '助战及自选编队'],
|
['autochess_shop_4', N_('卫戍能力')], ['autochess_shop_5', N_('盟约')], ['autochess_shop_6', N_('助战及自选编队')],
|
||||||
] },
|
] },
|
||||||
{ id: 'handbook', name: '进阶图鉴', micro: 'ADVANCED', pages: [
|
{ id: 'handbook', name: N_('进阶图鉴'), micro: 'ADVANCED', pages: [
|
||||||
['autochess_handbook_1', '敌人类型'], ['autochess_handbook_2', '盟约激活与叠加'], ['autochess_handbook_3', '追加盟约'],
|
['autochess_handbook_1', N_('敌人类型')], ['autochess_handbook_2', N_('盟约激活与叠加')], ['autochess_handbook_3', N_('追加盟约')],
|
||||||
['autochess_handbook_4', '策略与轮选'],
|
['autochess_handbook_4', N_('策略与轮选')],
|
||||||
] },
|
] },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -78,9 +79,9 @@ export function openGuide(page = 0) {
|
|||||||
export const closeGuide = () => guideStore.set({ open: false });
|
export const closeGuide = () => guideStore.set({ open: false });
|
||||||
|
|
||||||
/** Standard 玩法说明 trigger button. */
|
/** Standard 玩法说明 trigger button. */
|
||||||
export function GuideButton({ class: cls, size = 'sm', variant = 'ghost', label = '玩法说明', square = false }) {
|
export function GuideButton({ class: cls, size = 'sm', variant = 'ghost', label = t('玩法说明'), square = false }) {
|
||||||
return html`<${Button} variant=${variant} size=${size} icon="book" square=${square} class=${cx('guide-btn', cls)}
|
return html`<${Button} variant=${variant} size=${size} icon="book" square=${square} class=${cx('guide-btn', cls)}
|
||||||
onClick=${() => openGuide(0)} title="玩法说明" aria-label="玩法说明">${square ? null : label}<//>`;
|
onClick=${() => openGuide(0)} title=${t('玩法说明')} aria-label=${t('玩法说明')}>${square ? null : label}<//>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function preload(url) {
|
function preload(url) {
|
||||||
@@ -95,9 +96,9 @@ function preload(url) {
|
|||||||
function TipsFallback() {
|
function TipsFallback() {
|
||||||
const tips = (Array.isArray(data.get('config')?.tips) ? data.get('config').tips : []).map((t) => t?.tip).filter(Boolean);
|
const tips = (Array.isArray(data.get('config')?.tips) ? data.get('config').tips : []).map((t) => t?.tip).filter(Boolean);
|
||||||
return html`<div class="guide__tips">
|
return html`<div class="guide__tips">
|
||||||
<${MicroLabel} tone="mint">TIPS // 模拟要点</${MicroLabel}>
|
<${MicroLabel} tone="mint">${t('TIPS // 模拟要点')}</${MicroLabel}>
|
||||||
<ol>${tips.map((t, i) => html`<li key=${i}>${t}</li>`)}</ol>
|
<ol>${tips.map((t, i) => html`<li key=${i}>${t}</li>`)}</ol>
|
||||||
${!tips.length ? html`<p class="t-lo">暂无说明内容</p>` : null}
|
${!tips.length ? html`<p class="t-lo">${t('暂无说明内容')}</p>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,42 +147,42 @@ export function GuideHost() {
|
|||||||
const src = stage.kind === 'image' ? stage.src : null;
|
const src = stage.kind === 'image' ? stage.src : null;
|
||||||
const isLoaded = !!src && loaded.has(src);
|
const isLoaded = !!src && loaded.has(src);
|
||||||
return html`<div class="guide" role="presentation" onMouseDown=${(e) => { if (e.target === e.currentTarget) closeGuide(); }}>
|
return html`<div class="guide" role="presentation" onMouseDown=${(e) => { if (e.target === e.currentTarget) closeGuide(); }}>
|
||||||
<div class="guide__box brackets" role="dialog" aria-modal="true" aria-label="玩法说明" tabindex="-1" ref=${boxRef}>
|
<div class="guide__box brackets" role="dialog" aria-modal="true" aria-label=${t('玩法说明')} tabindex="-1" ref=${boxRef}>
|
||||||
<header class="guide__head">
|
<header class="guide__head">
|
||||||
<div class="guide__titles">
|
<div class="guide__titles">
|
||||||
<${MicroLabel} tone="mint">HOW TO PLAY // STRONGHOLD PROTOCOL</${MicroLabel}>
|
<${MicroLabel} tone="mint">HOW TO PLAY // STRONGHOLD PROTOCOL</${MicroLabel}>
|
||||||
<h2 class="guide__title">玩法说明</h2>
|
<h2 class="guide__title">${t('玩法说明')}</h2>
|
||||||
</div>
|
</div>
|
||||||
${n ? html`<nav class="guide__chapters" aria-label="章节">
|
${n ? html`<nav class="guide__chapters" aria-label=${t('章节')}>
|
||||||
${GUIDE_CHAPTERS.map((ch, ci) => {
|
${GUIDE_CHAPTERS.map((ch, ci) => {
|
||||||
const at = firstOf(ci);
|
const at = firstOf(ci);
|
||||||
if (at < 0) return null;
|
if (at < 0) return null;
|
||||||
return html`<button key=${ch.id} type="button" class=${cx('guide__chapter', cur?.chapter === ci && 'is-on')} onClick=${() => go(at)}>
|
return html`<button key=${ch.id} type="button" class=${cx('guide__chapter', cur?.chapter === ci && 'is-on')} onClick=${() => go(at)}>
|
||||||
<span class="guide__chname">${ch.name}</span><span class="guide__chmicro">${ch.micro}</span>
|
<span class="guide__chname">${t(ch.name)}</span><span class="guide__chmicro">${ch.micro}</span>
|
||||||
</button>`;
|
</button>`;
|
||||||
})}
|
})}
|
||||||
</nav>` : null}
|
</nav>` : null}
|
||||||
<button type="button" class="guide__close" aria-label="关闭" title="关闭 (Esc)" onClick=${closeGuide}><${Icon} name="close" /></button>
|
<button type="button" class="guide__close" aria-label=${t('关闭')} title=${t('关闭 (Esc)')} onClick=${closeGuide}><${Icon} name="close" /></button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
${src ? html`<div class="guide__stage">
|
${src ? html`<div class="guide__stage">
|
||||||
<button type="button" class="guide__nav guide__prev" aria-label="上一页" onClick=${() => go(i - 1)}><${Icon} name="chevronLeft" /></button>
|
<button type="button" class="guide__nav guide__prev" aria-label=${t('上一页')} onClick=${() => go(i - 1)}><${Icon} name="chevronLeft" /></button>
|
||||||
<div class=${cx('guide__page', isLoaded && 'is-loaded')}>
|
<div class=${cx('guide__page', isLoaded && 'is-loaded')}>
|
||||||
<img key=${src} src=${src} alt=${cur.title} draggable=${false}
|
<img key=${src} src=${src} alt=${t(cur.title)} draggable=${false}
|
||||||
onLoad=${() => setLoaded((s) => new Set(s).add(src))}
|
onLoad=${() => setLoaded((s) => new Set(s).add(src))}
|
||||||
onError=${() => setFailed((s) => new Set(s).add(src))} />
|
onError=${() => setFailed((s) => new Set(s).add(src))} />
|
||||||
${!isLoaded ? html`<span class="guide__loading"><${Spinner} size="md" /></span>` : null}
|
${!isLoaded ? html`<span class="guide__loading"><${Spinner} size="md" /></span>` : null}
|
||||||
</div>
|
</div>
|
||||||
<button type="button" class="guide__nav guide__next" aria-label="下一页" onClick=${() => go(i + 1)}><${Icon} name="chevronRight" /></button>
|
<button type="button" class="guide__nav guide__next" aria-label=${t('下一页')} onClick=${() => go(i + 1)}><${Icon} name="chevronRight" /></button>
|
||||||
</div>` : html`<div class="guide__stage guide__stage--text"><${TipsFallback} /></div>`}
|
</div>` : html`<div class="guide__stage guide__stage--text"><${TipsFallback} /></div>`}
|
||||||
|
|
||||||
${n ? html`<footer class="guide__foot">
|
${n ? html`<footer class="guide__foot">
|
||||||
<div class="guide__label">
|
<div class="guide__label">
|
||||||
<span class="guide__chtag">${chapter?.name || ''}</span>
|
<span class="guide__chtag">${t(chapter?.name || '')}</span>
|
||||||
<b class="guide__ptitle">${cur?.title || ''}</b>
|
<b class="guide__ptitle">${t(cur?.title || '')}</b>
|
||||||
</div>
|
</div>
|
||||||
<div class="guide__dots" role="tablist" aria-label="页码">
|
<div class="guide__dots" role="tablist" aria-label=${t('页码')}>
|
||||||
${pages.map((p, k) => html`<button key=${p.key} type="button" role="tab" aria-selected=${k === i ? 'true' : 'false'} title=${p.title}
|
${pages.map((p, k) => html`<button key=${p.key} type="button" role="tab" aria-selected=${k === i ? 'true' : 'false'} title=${t(p.title)}
|
||||||
class=${cx('guide__dot', k === i && 'is-on', k > 0 && pages[k - 1].chapter !== p.chapter && 'is-first')} onClick=${() => go(k)}></button>`)}
|
class=${cx('guide__dot', k === i && 'is-on', k > 0 && pages[k - 1].chapter !== p.chapter && 'is-first')} onClick=${() => go(k)}></button>`)}
|
||||||
</div>
|
</div>
|
||||||
<span class="guide__count num">${i + 1} / ${n}</span>
|
<span class="guide__count num">${i + 1} / ${n}</span>
|
||||||
|
|||||||
+32
-30
@@ -31,6 +31,7 @@ import { localAsset } from '../data.js';
|
|||||||
import { serverNow } from '../store.js';
|
import { serverNow } from '../store.js';
|
||||||
import { isCombatPhase, isBossPhase, prepCapsuleLabel, bossFrac, bossPctText, fmtNum, shopBlockReason } from './gameLogic.js';
|
import { isCombatPhase, isBossPhase, prepCapsuleLabel, bossFrac, bossPctText, fmtNum, shopBlockReason } from './gameLogic.js';
|
||||||
import { overtimeState, overtimeDrainPerSec, remainAt } from './matchStatus.js';
|
import { overtimeState, overtimeDrainPerSec, remainAt } from './matchStatus.js';
|
||||||
|
import { t, tParts, N_ } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
|
|
||||||
@@ -48,9 +49,9 @@ export function PhaseCapsule({ pub, hud, miss = null }) {
|
|||||||
return html`<div class="capsule capsule--boss" role="status">
|
return html`<div class="capsule capsule--boss" role="status">
|
||||||
<${Sprite} k="hudPanel/icon_boss" class="capsule__icon" fallback=${html`<${GIcon} name="skull" class="capsule__icon" />`} />
|
<${Sprite} k="hudPanel/icon_boss" class="capsule__icon" fallback=${html`<${GIcon} name="skull" class="capsule__icon" />`} />
|
||||||
${hud?.total != null ? html`<span class="capsule__kills num"><b>${hud.killed ?? 0}</b>/${hud.total}</span>` : null}
|
${hud?.total != null ? html`<span class="capsule__kills num"><b>${hud.killed ?? 0}</b>/${hud.total}</span>` : null}
|
||||||
<div class="bossbar" title=${boss ? `${fmtNum(boss.hp)} / ${fmtNum(boss.max)}` : '敌方领袖'}>
|
<div class="bossbar" title=${boss ? `${fmtNum(boss.hp)} / ${fmtNum(boss.max)}` : t('敌方领袖')}>
|
||||||
<div class="bossbar__fill" style=${`width:${frac == null ? 100 : frac * 100}%`}></div>
|
<div class="bossbar__fill" style=${`width:${frac == null ? 100 : frac * 100}%`}></div>
|
||||||
<span class="bossbar__txt num">${frac == null ? '敌方领袖' : bossPctText(frac)}</span>
|
<span class="bossbar__txt num">${frac == null ? t('敌方领袖') : bossPctText(frac)}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -59,7 +60,7 @@ export function PhaseCapsule({ pub, hud, miss = null }) {
|
|||||||
<${Sprite} k=${phase === PHASE.UNITE ? 'hudPanel/icon_coop' : 'hudPanel/icon_battle'} class="capsule__icon"
|
<${Sprite} k=${phase === PHASE.UNITE ? 'hudPanel/icon_coop' : 'hudPanel/icon_battle'} class="capsule__icon"
|
||||||
fallback=${html`<${Icon} name="sword" class="capsule__icon" />`} />
|
fallback=${html`<${Icon} name="sword" class="capsule__icon" />`} />
|
||||||
<span class="capsule__kills num"><b>${hud?.killed ?? 0}</b>/${hud?.total ?? '--'}</span>
|
<span class="capsule__kills num"><b>${hud?.killed ?? 0}</b>/${hud?.total ?? '--'}</span>
|
||||||
${phase === PHASE.UNITE ? html`<span class="capsule__tag">联防</span>` : null}
|
${phase === PHASE.UNITE ? html`<span class="capsule__tag">${t('联防')}</span>` : null}
|
||||||
${phase === PHASE.UNITE && Number.isFinite(miss) ? html`<${MissTag} n=${miss} />` : null}
|
${phase === PHASE.UNITE && Number.isFinite(miss) ? html`<${MissTag} n=${miss} />` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -75,8 +76,8 @@ export function PhaseCapsule({ pub, hud, miss = null }) {
|
|||||||
* @param {number} n enemies still standing @param {string|null} [name] the leaker's name (null = the viewer)
|
* @param {number} n enemies still standing @param {string|null} [name] the leaker's name (null = the viewer)
|
||||||
*/
|
*/
|
||||||
export function missTip(n, name = null) {
|
export function missTip(n, name = null) {
|
||||||
const who = name ? `${name} 漏过的敌人` : '你漏过的敌人';
|
if (n > 0) return name ? t('{name} 漏过的敌人还剩 {n} 个(联防中)', { name, n }) : t('你漏过的敌人还剩 {n} 个(队友正在迎战)', { n });
|
||||||
return n > 0 ? `${who}还剩 ${n} 个(${name ? '联防中' : '队友正在迎战'})` : `${who}已全部被击倒`;
|
return name ? t('{name} 漏过的敌人已全部被击倒', { name }) : t('你漏过的敌人已全部被击倒');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,7 +91,7 @@ export function MissTag({ n, name = null }) {
|
|||||||
const v = Math.max(0, Math.trunc(Number(n) || 0));
|
const v = Math.max(0, Math.trunc(Number(n) || 0));
|
||||||
const art = localAsset('ui/battle', 'bg_miss_enemy');
|
const art = localAsset('ui/battle', 'bg_miss_enemy');
|
||||||
return html`<span class=${cx('misstag', art && 'has-art', v === 0 && 'is-clear')} data-testid="miss-tag"
|
return html`<span class=${cx('misstag', art && 'has-art', v === 0 && 'is-clear')} data-testid="miss-tag"
|
||||||
title=${missTip(v, name)} aria-label=${`剩余敌人 ${v}`}
|
title=${missTip(v, name)} aria-label=${t('剩余敌人 {v}', { v })}
|
||||||
style=${art ? `background-image:url("${art}")` : null}>
|
style=${art ? `background-image:url("${art}")` : null}>
|
||||||
${art ? null : html`<span class="misstag__icon" aria-hidden="true"><${GIcon} name="skull" />×</span>`}<b class="misstag__n num">${v}</b>
|
${art ? null : html`<span class="misstag__icon" aria-hidden="true"><${GIcon} name="skull" />×</span>`}<b class="misstag__n num">${v}</b>
|
||||||
</span>`;
|
</span>`;
|
||||||
@@ -174,12 +175,13 @@ export function liveLp(base, { phase, round, lp, statsLeaks = null, leaks = 0, c
|
|||||||
export function pendingTip(lp, pending, { unite = false, cap = 10, left = null } = {}) {
|
export function pendingTip(lp, pending, { unite = false, cap = 10, left = null } = {}) {
|
||||||
if (!(pending > 0)) return null;
|
if (!(pending > 0)) return null;
|
||||||
if (unite && Number.isFinite(left)) {
|
if (unite && Number.isFinite(left)) {
|
||||||
const each = left > cap ? `剩余不足 ${cap} 个后,队友每击倒一个少扣 1 点` : '队友每击倒一个就少扣 1 点';
|
const each = left > cap ? t('剩余不足 {cap} 个后,队友每击倒一个少扣 1 点', { cap }) : t('队友每击倒一个就少扣 1 点');
|
||||||
return `目标生命值 ${lp}:联防中,你漏过的敌人还剩 ${left} 个,${each};按现在结算扣除 ${pending} 点(每回合至多 ${cap} 点)`;
|
return t('目标生命值 {lp}:联防中,你漏过的敌人还剩 {left} 个,{each};按现在结算扣除 {pending} 点(每回合至多 {cap} 点)', { lp, left, each, pending, cap });
|
||||||
}
|
}
|
||||||
return unite
|
return unite
|
||||||
? `目标生命值 ${lp}:联防中,队友正在迎战你漏过的敌人,结算时按联防后剩余的敌人扣除(至多 ${pending} 点)`
|
? t('目标生命值 {lp}:联防中,队友正在迎战你漏过的敌人,结算时按联防后剩余的敌人扣除(至多 {pending} 点)', { lp, pending })
|
||||||
: `目标生命值 ${lp}:本回合已有 ${pending >= cap ? `${cap} 个以上` : `${pending} 个`}敌人进入蓝门,结算时扣除 ${pending} 点(每回合至多 ${cap} 点)`;
|
: pending >= cap ? t('目标生命值 {lp}:本回合已有 {cap} 个以上敌人进入蓝门,结算时扣除 {pending} 点(每回合至多 {cap} 点)', { lp, pending, cap })
|
||||||
|
: t('目标生命值 {lp}:本回合已有 {pending} 个敌人进入蓝门,结算时扣除 {pending} 点(每回合至多 {cap} 点)', { lp, pending, cap });
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---- temp overflow row (临时整备区, user playtest #3 item 3) ---------------------------------------------------
|
// ---- temp overflow row (临时整备区, user playtest #3 item 3) ---------------------------------------------------
|
||||||
@@ -195,12 +197,12 @@ export function tempInfo(priv) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** What the temp row asks of the player (the ready button's reason, the row's label). */
|
/** What the temp row asks of the player (the ready button's reason, the row's label). */
|
||||||
export const TEMP_RULE = '放入整备区或战场、配发或使用后才能准备就绪;休整期结束时仍留在临时整备区的单位将被销毁';
|
export const TEMP_RULE = N_('放入整备区或战场、配发或使用后才能准备就绪;休整期结束时仍留在临时整备区的单位将被销毁');
|
||||||
|
|
||||||
/** Why 准备就绪 is refused while the temp row holds pieces (null when it is empty). */
|
/** Why 准备就绪 is refused while the temp row holds pieces (null when it is empty). */
|
||||||
export function tempReadyReason(priv) {
|
export function tempReadyReason(priv) {
|
||||||
const t = tempInfo(priv);
|
const info = tempInfo(priv);
|
||||||
return t.count ? `临时整备区还有 ${t.count} 个单位:${TEMP_RULE}` : null;
|
return info.count ? t('临时整备区还有 {count} 个单位:{rule}', { count: info.count, rule: t(TEMP_RULE) }) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -215,14 +217,14 @@ export function ReadyToggle({ priv, onToggle, busy, readyCount, total }) {
|
|||||||
const btn = html`<button type="button" class=${cx('readybtn', 'tapx', ready && 'is-on', busy && 'is-busy')} disabled=${!!reason || busy}
|
const btn = html`<button type="button" class=${cx('readybtn', 'tapx', ready && 'is-on', busy && 'is-busy')} disabled=${!!reason || busy}
|
||||||
aria-pressed=${ready ? 'true' : 'false'} aria-describedby=${!ready && temp.count ? 'readywrap-why' : undefined} onClick=${() => onToggle(!ready)}>
|
aria-pressed=${ready ? 'true' : 'false'} aria-describedby=${!ready && temp.count ? 'readywrap-why' : undefined} onClick=${() => onToggle(!ready)}>
|
||||||
<span class="readybtn__box">${ready ? html`<${Icon} name="check" />` : null}</span>
|
<span class="readybtn__box">${ready ? html`<${Icon} name="check" />` : null}</span>
|
||||||
<span class="readybtn__label">${ready ? '取消准备' : '准备就绪'}</span>
|
<span class="readybtn__label">${ready ? t('取消准备') : t('准备就绪')}</span>
|
||||||
<kbd class="readybtn__key">Space</kbd>
|
<kbd class="readybtn__key">Space</kbd>
|
||||||
</button>`;
|
</button>`;
|
||||||
return html`<div class="readywrap">
|
return html`<div class="readywrap">
|
||||||
${reason ? html`<${Tooltip} text=${reason} placement="bottom">${btn}<//>` : btn}
|
${reason ? html`<${Tooltip} text=${reason} placement="bottom">${btn}<//>` : btn}
|
||||||
${!ready && temp.count ? html`<span class="readywrap__why" id="readywrap-why" role="status" data-testid="ready-why">
|
${!ready && temp.count ? html`<span class="readywrap__why" id="readywrap-why" role="status" data-testid="ready-why">
|
||||||
<${Icon} name="warn" /><span>临时整备区 <b class="num">${temp.count}</b> 个单位待处理</span></span>` : null}
|
<${Icon} name="warn" /><span>${tParts('临时整备区 {n} 个单位待处理', { n: html`<b class="num">${temp.count}</b>`, count: temp.count })}</span></span>` : null}
|
||||||
${Number.isFinite(total) && total > 1 ? html`<span class="readywrap__count">已就绪 <b class="num">${readyCount}</b>/<span class="num">${total}</span></span>` : null}
|
${Number.isFinite(total) && total > 1 ? html`<span class="readywrap__count">${t('已就绪')} <b class="num">${readyCount}</b>/<span class="num">${total}</span></span>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -233,13 +235,13 @@ export function ReadyToggle({ priv, onToggle, busy, readyCount, total }) {
|
|||||||
*/
|
*/
|
||||||
export function checkButtons({ pen, penAvail, infoOpen }) {
|
export function checkButtons({ pen, penAvail, infoOpen }) {
|
||||||
const left = pen
|
const left = pen
|
||||||
? { sprite: 'btn_check_player_back', back: true, label: '返回', tip: '返回战场' }
|
? { sprite: 'btn_check_player_back', back: true, label: t('返回'), tip: t('返回战场') }
|
||||||
: { sprite: infoOpen ? 'btn_check_player_unfold' : 'btn_check_player_normal', back: false, label: '本局信息', tip: '本局信息(策略 / 禁用盟约 / 干员)' };
|
: { sprite: infoOpen ? 'btn_check_player_unfold' : 'btn_check_player_normal', back: false, label: t('本局信息'), tip: t('本局信息(策略 / 禁用盟约 / 干员)') };
|
||||||
const right = pen
|
const right = pen
|
||||||
? { sprite: 'btn_check_enemy_unfold', grey: true, label: '返回', tip: '返回' }
|
? { sprite: 'btn_check_enemy_unfold', grey: true, label: t('返回'), tip: t('返回') }
|
||||||
: penAvail
|
: penAvail
|
||||||
? { sprite: 'btn_check_enemy', grey: false, label: '敌方情报', tip: '查看即将迎击的敌方单位' }
|
? { sprite: 'btn_check_enemy', grey: false, label: t('敌方情报'), tip: t('查看即将迎击的敌方单位') }
|
||||||
: { sprite: 'btn_check_enemy_unfold', grey: true, label: '敌方情报', tip: '休整期可以查看即将迎击的敌方单位' };
|
: { sprite: 'btn_check_enemy_unfold', grey: true, label: t('敌方情报'), tip: t('休整期可以查看即将迎击的敌方单位') };
|
||||||
return { left, right };
|
return { left, right };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,7 +265,7 @@ export function OvertimeWarning({ ot }) {
|
|||||||
return html`<div class="otwarn otwarn--pending" role="alert" data-state="pending">
|
return html`<div class="otwarn otwarn--pending" role="alert" data-state="pending">
|
||||||
<${LocalSprite} name="icon_warn" class="otwarn__icon" fallback=${html`<${Icon} name="warn" class="otwarn__icon" />`} />
|
<${LocalSprite} name="icon_warn" class="otwarn__icon" fallback=${html`<${Icon} name="warn" class="otwarn__icon" />`} />
|
||||||
<span class="otwarn__tag">DOT</span>
|
<span class="otwarn__tag">DOT</span>
|
||||||
<span class="otwarn__txt"><b class="num">${ot.secs}</b> 秒后全队生命值开始流失</span>
|
<span class="otwarn__txt">${tParts('{n} 秒后全队生命值开始流失', { n: html`<b class="num">${ot.secs}</b>`, count: ot.secs })}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
return html`<div class="otwarn otwarn--drain" role="alert" data-state="drain">
|
return html`<div class="otwarn otwarn--drain" role="alert" data-state="drain">
|
||||||
@@ -271,8 +273,8 @@ export function OvertimeWarning({ ot }) {
|
|||||||
<${LocalSprite} name="blood_icon" class="otwarn__icon" fallback=${html`<${Icon} name="rook" class="otwarn__icon" />`} />
|
<${LocalSprite} name="blood_icon" class="otwarn__icon" fallback=${html`<${Icon} name="rook" class="otwarn__icon" />`} />
|
||||||
</span>
|
</span>
|
||||||
<span class="otwarn__tag">DOT</span>
|
<span class="otwarn__tag">DOT</span>
|
||||||
<span class="otwarn__txt">超时 · 生命值 <b class="num">−${ot.perSec}</b>/秒</span>
|
<span class="otwarn__txt">${tParts('超时 · 生命值 {n}/秒', { n: html`<b class="num">−${ot.perSec}</b>` })}</span>
|
||||||
${ot.lost > 0 ? html`<span class="otwarn__lost">已流失 <b class="num">${ot.lost}</b></span>` : null}
|
${ot.lost > 0 ? html`<span class="otwarn__lost">${t('已流失')} <b class="num">${ot.lost}</b></span>` : null}
|
||||||
<span key=${ot.secs} class="otwarn__tick num" aria-hidden="true">−${ot.perSec}</span>
|
<span key=${ot.secs} class="otwarn__tick num" aria-hidden="true">−${ot.perSec}</span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -287,8 +289,8 @@ function PauseGlyph() {
|
|||||||
* @param {{ paused: boolean, busy?: boolean, onToggle: () => void }} props
|
* @param {{ paused: boolean, busy?: boolean, onToggle: () => void }} props
|
||||||
*/
|
*/
|
||||||
export function PauseButton({ paused, busy = false, onToggle }) {
|
export function PauseButton({ paused, busy = false, onToggle }) {
|
||||||
const label = paused ? '继续作战' : '暂停';
|
const label = paused ? t('继续作战') : t('暂停');
|
||||||
return html`<${Tooltip} text=${paused ? '继续作战(Space)' : '暂停作战(Space)'} placement="bottom">
|
return html`<${Tooltip} text=${paused ? t('继续作战(Space)') : t('暂停作战(Space)')} placement="bottom">
|
||||||
<button type="button" class=${cx('pausebtn', 'tapx', paused && 'is-on', busy && 'is-busy')} aria-pressed=${paused ? 'true' : 'false'}
|
<button type="button" class=${cx('pausebtn', 'tapx', paused && 'is-on', busy && 'is-busy')} aria-pressed=${paused ? 'true' : 'false'}
|
||||||
aria-label=${label} disabled=${busy} data-testid="pause" onClick=${() => onToggle?.()}>
|
aria-label=${label} disabled=${busy} data-testid="pause" onClick=${() => onToggle?.()}>
|
||||||
${paused ? html`<${Icon} name="play" class="pausebtn__glyph" />` : html`<${PauseGlyph} />`}
|
${paused ? html`<${Icon} name="play" class="pausebtn__glyph" />` : html`<${PauseGlyph} />`}
|
||||||
@@ -332,7 +334,7 @@ export function TopBar({ pub, priv, conn, hud, total, drawer, onExit, onDrawer,
|
|||||||
const onRight = () => (pen ? onPen(false) : penAvail ? onPen(true) : null);
|
const onRight = () => (pen ? onPen(false) : penAvail ? onPen(true) : null);
|
||||||
return html`<header class=${cx('gtop', pen && 'is-pen')}>
|
return html`<header class=${cx('gtop', pen && 'is-pen')}>
|
||||||
<div class="gtop__left">
|
<div class="gtop__left">
|
||||||
<${Button} variant="danger" size="lg" square=${true} icon="exit" onClick=${onExit} aria-label="离开" title="离开 / 暂离" class="gtop__exit tapx" />
|
<${Button} variant="danger" size="lg" square=${true} icon="exit" onClick=${onExit} aria-label=${t('离开')} title=${t('离开 / 暂离')} class="gtop__exit tapx" />
|
||||||
<div class="gtop__meta">
|
<div class="gtop__meta">
|
||||||
<${PingPill} ms=${conn?.ping} online=${conn?.status === 'online'} />
|
<${PingPill} ms=${conn?.ping} online=${conn?.status === 'online'} />
|
||||||
${pub?.difficulty ? html`<${DifficultyTag} difficulty=${pub.difficulty} size="sm" />` : null}
|
${pub?.difficulty ? html`<${DifficultyTag} difficulty=${pub.difficulty} size="sm" />` : null}
|
||||||
@@ -345,12 +347,12 @@ export function TopBar({ pub, priv, conn, hud, total, drawer, onExit, onDrawer,
|
|||||||
chev=${btn.left.back ? '◀◀' : null} on=${!!drawer && !pen} onClick=${onLeft} testid="check-player" />
|
chev=${btn.left.back ? '◀◀' : null} on=${!!drawer && !pen} onClick=${onLeft} testid="check-player" />
|
||||||
<//>
|
<//>
|
||||||
<div class="roundbox">
|
<div class="roundbox">
|
||||||
<span class="roundbox__label">回合</span>
|
<span class="roundbox__label">${t('回合')}</span>
|
||||||
<b class="roundbox__num num">${roundText}</b>
|
<b class="roundbox__num num">${roundText}</b>
|
||||||
</div>
|
</div>
|
||||||
<${PhaseCapsule} pub=${pub} hud=${hud} miss=${!boss && Number.isFinite(live?.left) ? live.left : null} />
|
<${PhaseCapsule} pub=${pub} hud=${hud} miss=${!boss && Number.isFinite(live?.left) ? live.left : null} />
|
||||||
<${LpTower} value=${lp} size="lg" tone=${lowLp ? 'danger' : boss ? 'team' : null} pending=${pending}
|
<${LpTower} value=${lp} size="lg" tone=${lowLp ? 'danger' : boss ? 'team' : null} pending=${pending}
|
||||||
note=${pending > 0 && live?.unite ? '联防中' : null} tip=${pendingTip(lp, pending, { unite: !!live?.unite, cap, left: live?.left ?? null })} />
|
note=${pending > 0 && live?.unite ? t('联防中') : null} tip=${pendingTip(lp, pending, { unite: !!live?.unite, cap, left: live?.left ?? null })} />
|
||||||
<${Tooltip} text=${btn.right.tip} placement="bottom">
|
<${Tooltip} text=${btn.right.tip} placement="bottom">
|
||||||
<${CheckBtn} sprite=${btn.right.sprite} cls=${cx('enemybtn', btn.right.grey && 'is-grey')} label=${btn.right.label}
|
<${CheckBtn} sprite=${btn.right.sprite} cls=${cx('enemybtn', btn.right.grey && 'is-grey')} label=${btn.right.label}
|
||||||
chev=${btn.right.grey ? null : '▶▶'} disabled=${btn.right.grey && !pen} onClick=${onRight} testid="check-enemy" />
|
chev=${btn.right.grey ? null : '▶▶'} disabled=${btn.right.grey && !pen} onClick=${onRight} testid="check-enemy" />
|
||||||
@@ -373,7 +375,7 @@ export function TopBar({ pub, priv, conn, hud, total, drawer, onExit, onDrawer,
|
|||||||
/** DP counter shown at the right edge during combat. */
|
/** DP counter shown at the right edge during combat. */
|
||||||
export function DpCounter({ dp }) {
|
export function DpCounter({ dp }) {
|
||||||
if (!Number.isFinite(dp)) return null;
|
if (!Number.isFinite(dp)) return null;
|
||||||
return html`<div class="dpbox" title="部署费用(再部署消耗)">
|
return html`<div class="dpbox" title=${t('部署费用(再部署消耗)')}>
|
||||||
<${GIcon} name="dp" class="dpbox__icon" /><b class="num">${Math.floor(dp)}</b><${MicroLabel}>COST</${MicroLabel}>
|
<${GIcon} name="dp" class="dpbox__icon" /><b class="num">${Math.floor(dp)}</b><${MicroLabel}>COST</${MicroLabel}>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import { data } from '../data.js';
|
|||||||
import { html } from './components.js';
|
import { html } from './components.js';
|
||||||
|
|
||||||
/** Labels of the language switch (each in its own language). */
|
/** Labels of the language switch (each in its own language). */
|
||||||
export const LANG_LABELS = Object.freeze({ zh: '中文', en: 'English' });
|
export const LANG_LABELS = Object.freeze({ zh: '中文', en: 'English' }); // i18n-ignore: each language in its own name
|
||||||
|
/** The switch's own label, in both languages (whoever opens it may not read the current one). */
|
||||||
|
const SWITCH_LABEL = 'Language / 语言'; // i18n-ignore
|
||||||
const PREF_KEY = 'lang';
|
const PREF_KEY = 'lang';
|
||||||
/** How long boot waits for the UI translations before rendering in Chinese anyway (they apply when they arrive). */
|
/** How long boot waits for the UI translations before rendering in Chinese anyway (they apply when they arrive). */
|
||||||
const BOOT_WAIT_MS = 2500;
|
const BOOT_WAIT_MS = 2500;
|
||||||
@@ -78,7 +80,7 @@ function applyDocument(lang) {
|
|||||||
if (!doc) return;
|
if (!doc) return;
|
||||||
doc.documentElement.lang = lang === 'en' ? 'en' : 'zh-CN';
|
doc.documentElement.lang = lang === 'en' ? 'en' : 'zh-CN';
|
||||||
doc.documentElement.dataset.lang = lang;
|
doc.documentElement.dataset.lang = lang;
|
||||||
doc.title = lang === 'en' ? 'Stronghold Protocol: Alliance · Web Simulation' : '卫戍协议:盟约 · STRONGHOLD PROTOCOL';
|
doc.title = lang === 'en' ? 'Stronghold Protocol: Alliance · Web Simulation' : '卫戍协议:盟约 · STRONGHOLD PROTOCOL'; // i18n-ignore: one title per language
|
||||||
}
|
}
|
||||||
|
|
||||||
// `{ dn }` params and tName(): Chinese game-data names → the current language (data/i18n/<lang>.json names)
|
// `{ dn }` params and tName(): Chinese game-data names → the current language (data/i18n/<lang>.json names)
|
||||||
@@ -144,7 +146,7 @@ export function useLang() {
|
|||||||
*/
|
*/
|
||||||
export function LangToggle({ class: cls }) {
|
export function LangToggle({ class: cls }) {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
return html`<div class=${`set-seg lang-toggle${cls ? ` ${cls}` : ''}`} role="radiogroup" aria-label="Language / 语言" data-testid="lang-toggle">
|
return html`<div class=${`set-seg lang-toggle${cls ? ` ${cls}` : ''}`} role="radiogroup" aria-label=${SWITCH_LABEL} data-testid="lang-toggle">
|
||||||
${LANGS.map((l) => html`<button key=${l} type="button" role="radio" lang=${l === 'zh' ? 'zh-CN' : 'en'} aria-checked=${lang === l ? 'true' : 'false'}
|
${LANGS.map((l) => html`<button key=${l} type="button" role="radio" lang=${l === 'zh' ? 'zh-CN' : 'en'} aria-checked=${lang === l ? 'true' : 'false'}
|
||||||
class=${lang === l ? 'is-on' : ''} data-lang=${l} onClick=${() => { if (lang !== l) switchLang(l); }}>${LANG_LABELS[l]}</button>`)}
|
class=${lang === l ? 'is-on' : ''} data-lang=${l} onClick=${() => { if (lang !== l) switchLang(l); }}>${LANG_LABELS[l]}</button>`)}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
// (shared/protocol.js loadoutOptions / checkLoadout), so a sanitised loadout is always accepted.
|
// (shared/protocol.js loadoutOptions / checkLoadout), so a sanitised loadout is always accepted.
|
||||||
|
|
||||||
import { loadoutOptions, checkLoadout, resolveLoadout, MODULE_NONE, LOADOUT_LIMITS } from '../../../shared/protocol.js';
|
import { loadoutOptions, checkLoadout, resolveLoadout, MODULE_NONE, LOADOUT_LIMITS } from '../../../shared/protocol.js';
|
||||||
|
import { t, N_ } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
export { MODULE_NONE };
|
export { MODULE_NONE };
|
||||||
|
|
||||||
@@ -18,14 +19,14 @@ export const LOADOUT_PREF = 'loadout';
|
|||||||
export const LOADOUT_VERSION = 1;
|
export const LOADOUT_VERSION = 1;
|
||||||
|
|
||||||
export const PROF_ORDER = ['PIONEER', 'WARRIOR', 'TANK', 'SNIPER', 'CASTER', 'MEDIC', 'SUPPORT', 'SPECIAL'];
|
export const PROF_ORDER = ['PIONEER', 'WARRIOR', 'TANK', 'SNIPER', 'CASTER', 'MEDIC', 'SUPPORT', 'SPECIAL'];
|
||||||
export const PROF_NAME = Object.freeze({ PIONEER: '先锋', WARRIOR: '近卫', TANK: '重装', SNIPER: '狙击', CASTER: '术师', MEDIC: '医疗', SUPPORT: '辅助', SPECIAL: '特种' });
|
export const PROF_NAME = Object.freeze({ PIONEER: N_('先锋'), WARRIOR: N_('近卫'), TANK: N_('重装'), SNIPER: N_('狙击'), CASTER: N_('术师'), MEDIC: N_('医疗'), SUPPORT: N_('辅助'), SPECIAL: N_('特种') });
|
||||||
export const SP_TYPE = Object.freeze({ INCREASE_WITH_TIME: '自动回复', INCREASE_WHEN_ATTACK: '攻击回复', INCREASE_WHEN_TAKEN_DAMAGE: '受击回复', ON_DEPLOY: '被动', 8: '被动' });
|
export const SP_TYPE = Object.freeze({ INCREASE_WITH_TIME: N_('自动回复'), INCREASE_WHEN_ATTACK: N_('攻击回复'), INCREASE_WHEN_TAKEN_DAMAGE: N_('受击回复'), ON_DEPLOY: N_('被动'), 8: N_('被动') });
|
||||||
/** Module attribute keys (ModuleRecord.attr / battle_equip attributeBlackboard) → label + unit. */
|
/** Module attribute keys (ModuleRecord.attr / battle_equip attributeBlackboard) → label + unit. */
|
||||||
export const ATTR_LABEL = Object.freeze({
|
export const ATTR_LABEL = Object.freeze({
|
||||||
maxHp: ['生命上限', ''], max_hp: ['生命上限', ''], atk: ['攻击力', ''], def: ['防御力', ''], res: ['法术抗性', ''],
|
maxHp: [N_('生命上限'), ''], max_hp: [N_('生命上限'), ''], atk: [N_('攻击力'), ''], def: [N_('防御力'), ''], res: [N_('法术抗性'), ''],
|
||||||
magic_resistance: ['法术抗性', ''], aspd: ['攻击速度', ''], attack_speed: ['攻击速度', ''], cost: ['部署费用', ''],
|
magic_resistance: [N_('法术抗性'), ''], aspd: [N_('攻击速度'), ''], attack_speed: [N_('攻击速度'), ''], cost: [N_('部署费用'), ''],
|
||||||
blockCnt: ['阻挡数', ''], block_cnt: ['阻挡数', ''], respawnTime: ['再部署时间', '秒'], respawn_time: ['再部署时间', '秒'],
|
blockCnt: [N_('阻挡数'), ''], block_cnt: [N_('阻挡数'), ''], respawnTime: [N_('再部署时间'), N_('秒')], respawn_time: [N_('再部署时间'), N_('秒')],
|
||||||
baseAttackTime: ['攻击间隔', '秒'], base_attack_time: ['攻击间隔', '秒'], moveSpeed: ['移动速度', ''], hpRecoveryPerSec: ['每秒回复', ''],
|
baseAttackTime: [N_('攻击间隔'), N_('秒')], base_attack_time: [N_('攻击间隔'), N_('秒')], moveSpeed: [N_('移动速度'), ''], hpRecoveryPerSec: [N_('每秒回复'), ''],
|
||||||
});
|
});
|
||||||
|
|
||||||
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
|
||||||
@@ -102,19 +103,19 @@ export function serializeExport(entries, opts) {
|
|||||||
export function parseImport(input) {
|
export function parseImport(input) {
|
||||||
let raw = input;
|
let raw = input;
|
||||||
if (typeof raw === 'string') {
|
if (typeof raw === 'string') {
|
||||||
if (raw.length > LOADOUT_IMPORT_MAX_BYTES) return { ok: false, error: '内容过长,无法导入' };
|
if (raw.length > LOADOUT_IMPORT_MAX_BYTES) return { ok: false, error: t('内容过长,无法导入') };
|
||||||
const text = raw.trim();
|
const text = raw.trim();
|
||||||
if (!text) return { ok: false, error: '没有可导入的内容' };
|
if (!text) return { ok: false, error: t('没有可导入的内容') };
|
||||||
try { raw = JSON.parse(text); } catch { return { ok: false, error: '无法识别的内容' }; }
|
try { raw = JSON.parse(text); } catch { return { ok: false, error: t('无法识别的内容') }; }
|
||||||
}
|
}
|
||||||
if (!isObj(raw)) return { ok: false, error: '无法识别的格式' };
|
if (!isObj(raw)) return { ok: false, error: t('无法识别的格式') };
|
||||||
const v = isInt(raw.v) ? raw.v : null;
|
const v = isInt(raw.v) ? raw.v : null;
|
||||||
// a newer envelope may reshuffle fields — refuse instead of silently reading it as something else
|
// a newer envelope may reshuffle fields — refuse instead of silently reading it as something else
|
||||||
if (v != null && v > LOADOUT_VERSION) return { ok: false, error: `这份调配来自更新的版本(v${v}),请先更新游戏` };
|
if (v != null && v > LOADOUT_VERSION) return { ok: false, error: t('这份调配来自更新的版本(v{v}),请先更新游戏', { v }) };
|
||||||
const kind = typeof raw.kind === 'string' ? raw.kind : null;
|
const kind = typeof raw.kind === 'string' ? raw.kind : null;
|
||||||
if (kind && kind !== LOADOUT_EXPORT_KIND) return { ok: false, error: '这不是干员调配的数据' };
|
if (kind && kind !== LOADOUT_EXPORT_KIND) return { ok: false, error: t('这不是干员调配的数据') };
|
||||||
const entries = parseStored(raw);
|
const entries = parseStored(raw);
|
||||||
if (!Object.keys(entries).length) return { ok: false, error: '里面没有有效的调配条目' };
|
if (!Object.keys(entries).length) return { ok: false, error: t('里面没有有效的调配条目') };
|
||||||
return { ok: true, entries };
|
return { ok: true, entries };
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -292,7 +293,7 @@ export function filterRoster(roster, f = {}, entries = {}, getChess = () => null
|
|||||||
if (!effectiveChoice(entries, c, golden).changed) return false;
|
if (!effectiveChoice(entries, c, golden).changed) return false;
|
||||||
}
|
}
|
||||||
if (q) {
|
if (q) {
|
||||||
const hay = [c.name, c.appellation, c.subProfessionName, PROF_NAME[c.profession], ...(c.bonds || []).map((b) => getBond(b)?.name)]
|
const hay = [c.name, c.appellation, c.subProfessionName, t(PROF_NAME[c.profession]), ...(c.bonds || []).map((b) => getBond(b)?.name)]
|
||||||
.filter(Boolean).join(' ').toLowerCase();
|
.filter(Boolean).join(' ').toLowerCase();
|
||||||
if (!hay.includes(q)) return false;
|
if (!hay.includes(q)) return false;
|
||||||
}
|
}
|
||||||
@@ -336,7 +337,7 @@ export function attrRows(attr) {
|
|||||||
if (typeof v !== 'number' || !Number.isFinite(v) || v === 0) continue;
|
if (typeof v !== 'number' || !Number.isFinite(v) || v === 0) continue;
|
||||||
const [label, unit] = ATTR_LABEL[k] || [k, ''];
|
const [label, unit] = ATTR_LABEL[k] || [k, ''];
|
||||||
const n = Math.abs(v) < 10 && !Number.isInteger(v) ? Number(v.toFixed(2)) : Math.round(v);
|
const n = Math.abs(v) < 10 && !Number.isInteger(v) ? Number(v.toFixed(2)) : Math.round(v);
|
||||||
out.push({ key: k, label, text: `${v > 0 ? '+' : ''}${n}${unit}`, positive: k === 'cost' || k === 'respawnTime' || k === 'respawn_time' || k === 'baseAttackTime' || k === 'base_attack_time' ? v < 0 : v > 0 });
|
out.push({ key: k, label: t(label), text: `${v > 0 ? '+' : ''}${n}${t(unit)}`, positive: k === 'cost' || k === 'respawnTime' || k === 'respawn_time' || k === 'baseAttackTime' || k === 'base_attack_time' ? v < 0 : v > 0 });
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
@@ -350,10 +351,10 @@ export function skillTags(rec) {
|
|||||||
const passive = rec.skillType === 'PASSIVE' || rec.spType === 'ON_DEPLOY' || rec.spType === 8;
|
const passive = rec.skillType === 'PASSIVE' || rec.spType === 'ON_DEPLOY' || rec.spType === 8;
|
||||||
const spKind = passive ? 'passive' : rec.spType === 'INCREASE_WHEN_ATTACK' ? 'atk' : rec.spType === 'INCREASE_WHEN_TAKEN_DAMAGE' ? 'def' : 'time';
|
const spKind = passive ? 'passive' : rec.spType === 'INCREASE_WHEN_ATTACK' ? 'atk' : rec.spType === 'INCREASE_WHEN_TAKEN_DAMAGE' ? 'def' : 'time';
|
||||||
let duration = null;
|
let duration = null;
|
||||||
if (rec.durationType === 'AMMO') duration = '弹药';
|
if (rec.durationType === 'AMMO') duration = t('弹药');
|
||||||
else if (Number(rec.duration) > 0) duration = `${Number(rec.duration)}秒`;
|
else if (Number(rec.duration) > 0) duration = t('{n}秒', { n: Number(rec.duration) });
|
||||||
return {
|
return {
|
||||||
sp: SP_TYPE[rec.spType] || (passive ? '被动' : '技力'),
|
sp: t(SP_TYPE[rec.spType]) || (passive ? t('被动') : t('技力')),
|
||||||
spKind,
|
spKind,
|
||||||
init: passive ? null : Number.isFinite(rec.initSp) ? rec.initSp : 0,
|
init: passive ? null : Number.isFinite(rec.initSp) ? rec.initSp : 0,
|
||||||
cost: passive ? null : Number.isFinite(rec.spCost) ? rec.spCost : 0,
|
cost: passive ? null : Number.isFinite(rec.spCost) ? rec.spCost : 0,
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import { LOADOUT_PREF, parseStored, toStored, sanitizeEntries } from './loadoutM
|
|||||||
import { OWNERSHIP_PREF, parseStoredOwnership, toStoredOwnership, cleanIds, sanitizeNotOwned } from './ownershipModel.js';
|
import { OWNERSHIP_PREF, parseStoredOwnership, toStoredOwnership, cleanIds, sanitizeNotOwned } from './ownershipModel.js';
|
||||||
import { DIY_PREF, parseStoredDiy, toStoredDiy, cleanPicks, sanitizeDiyPicks } from './diyModel.js';
|
import { DIY_PREF, parseStoredDiy, toStoredDiy, cleanPicks, sanitizeDiyPicks } from './diyModel.js';
|
||||||
import { toast } from './toasts.js';
|
import { toast } from './toasts.js';
|
||||||
import { t } from '../../../shared/i18n.js';
|
import { t, N_ } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
export const SYNC_DEBOUNCE_MS = 500;
|
export const SYNC_DEBOUNCE_MS = 500;
|
||||||
export const RETRY_MS = 1500;
|
export const RETRY_MS = 1500;
|
||||||
@@ -234,7 +234,7 @@ export function installLoadoutSync({ net, getChessReady, lookupChess, timers, ta
|
|||||||
const lookup = lookupChess || ((id) => data.lookup('chess', id));
|
const lookup = lookupChess || ((id) => data.lookup('chess', id));
|
||||||
return installPrefSync({
|
return installPrefSync({
|
||||||
net, timers, target, notify, key: 'entries', stateKey: 'sync', msgType: 'room.loadout', field: 'entries', tag: 'loadout',
|
net, timers, target, notify, key: 'entries', stateKey: 'sync', msgType: 'room.loadout', field: 'entries', tag: 'loadout',
|
||||||
lockedText: '本局的干员调配已锁定,修改将在下一局生效',
|
lockedText: N_('本局的干员调配已锁定,修改将在下一局生效'),
|
||||||
async prepare() {
|
async prepare() {
|
||||||
const current = target.get().entries;
|
const current = target.get().entries;
|
||||||
// an empty loadout needs no data (nothing to sanitise): a player who never opened 干员调配 does not download
|
// an empty loadout needs no data (nothing to sanitise): a player who never opened 干员调配 does not download
|
||||||
@@ -257,7 +257,7 @@ export function installLoadoutSync({ net, getChessReady, lookupChess, timers, ta
|
|||||||
export function installOwnershipSync({ net, timers, target = loadoutStore, notify } = {}) {
|
export function installOwnershipSync({ net, timers, target = loadoutStore, notify } = {}) {
|
||||||
return installPrefSync({
|
return installPrefSync({
|
||||||
net, timers, target, notify, key: 'notOwned', stateKey: 'ownSync', msgType: 'room.ownership', field: 'notOwned', tag: 'ownership',
|
net, timers, target, notify, key: 'notOwned', stateKey: 'ownSync', msgType: 'room.ownership', field: 'notOwned', tag: 'ownership',
|
||||||
lockedText: '干员持有是局外设置,修改将在下一局生效',
|
lockedText: N_('干员持有是局外设置,修改将在下一局生效'),
|
||||||
prepare: async () => cleanIds(target.get().notOwned),
|
prepare: async () => cleanIds(target.get().notOwned),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -277,7 +277,7 @@ export function installDiySync({ net, timers, target = loadoutStore, notify } =
|
|||||||
});
|
});
|
||||||
const sync = installPrefSync({
|
const sync = installPrefSync({
|
||||||
net, timers, target, notify, key: 'diy', stateKey: 'diySync', msgType: 'room.diy', field: 'picks', tag: 'diy',
|
net, timers, target, notify, key: 'diy', stateKey: 'diySync', msgType: 'room.diy', field: 'picks', tag: 'diy',
|
||||||
lockedText: '自选编队是局外设置,修改将在下一局生效',
|
lockedText: N_('自选编队是局外设置,修改将在下一局生效'),
|
||||||
prepare: async () => cleanPicks(target.get().diy),
|
prepare: async () => cleanPicks(target.get().diy),
|
||||||
});
|
});
|
||||||
return { flush: sync.flush, dispose() { offKit?.(); sync.dispose(); } };
|
return { flush: sync.flush, dispose() { offKit?.(); sync.dispose(); } };
|
||||||
|
|||||||
+20
-19
@@ -12,14 +12,15 @@ import { net } from '../net.js';
|
|||||||
import { store, useStore, shallowEqual, emptyMatch, createStore, isSpectating } from '../store.js';
|
import { store, useStore, shallowEqual, emptyMatch, createStore, isSpectating } from '../store.js';
|
||||||
import { GIcon } from './gameComponents.js';
|
import { GIcon } from './gameComponents.js';
|
||||||
import { GuideButton } from './guide.js';
|
import { GuideButton } from './guide.js';
|
||||||
|
import { t, tParts, N_ } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Exit dialog lines. 放弃模拟 (g.leave → room.leave) ends the run on the server as 'abandoned' and returns to the
|
* Exit dialog lines. 放弃模拟 (g.leave → room.leave) ends the run on the server as 'abandoned' and returns to the
|
||||||
* lobby straight away — no settlement screen follows, so the solo text must not promise one.
|
* lobby straight away — no settlement screen follows, so the solo text must not promise one.
|
||||||
*/
|
*/
|
||||||
export const EXIT_TEXT = Object.freeze({
|
export const EXIT_TEXT = Object.freeze({
|
||||||
soloRest: '独立模拟的休整期没有时间限制,你可以随时继续。',
|
soloRest: N_('独立模拟的休整期没有时间限制,你可以随时继续。'),
|
||||||
soloQuit: '放弃模拟将立即结束本局并返回大厅,本局进度不会保留,也不会进行结算。',
|
soloQuit: N_('放弃模拟将立即结束本局并返回大厅,本局进度不会保留,也不会进行结算。'),
|
||||||
});
|
});
|
||||||
|
|
||||||
/** Local "暂离 / AI 托管" flag (the server keeps no per-client autoplay view). */
|
/** Local "暂离 / AI 托管" flag (the server keeps no per-client autoplay view). */
|
||||||
@@ -67,10 +68,10 @@ export function ExitModal({ open, onClose, solo, onAway }) {
|
|||||||
onClose();
|
onClose();
|
||||||
};
|
};
|
||||||
if (spectator) {
|
if (spectator) {
|
||||||
return html`<${Modal} open=${open} onClose=${onClose} tone="red" title="离开观战" micro="LEAVE SPECTATING" width="6.8rem"
|
return html`<${Modal} open=${open} onClose=${onClose} tone="red" title=${t('离开观战')} micro="LEAVE SPECTATING" width="6.8rem"
|
||||||
actions=${html`<${Button} variant="secondary" onClick=${onClose}>取消<//>
|
actions=${html`<${Button} variant="secondary" onClick=${onClose}>${t('取消')}<//>
|
||||||
<${Button} variant="danger" icon="exit" loading=${busy === 'quit'} onClick=${quit}>离开观战<//>`}>
|
<${Button} variant="danger" icon="exit" loading=${busy === 'quit'} onClick=${quit}>${t('离开观战')}<//>`}>
|
||||||
<div class="exitm"><p>离开观战席并返回大厅,本局模拟不受影响;观战席空着时可以凭同盟密钥再次观战。</p></div>
|
<div class="exitm"><p>${t('离开观战席并返回大厅,本局模拟不受影响;观战席空着时可以凭同盟密钥再次观战。')}</p></div>
|
||||||
<//>`;
|
<//>`;
|
||||||
}
|
}
|
||||||
const away = async () => {
|
const away = async () => {
|
||||||
@@ -80,16 +81,16 @@ export function ExitModal({ open, onClose, solo, onAway }) {
|
|||||||
onClose();
|
onClose();
|
||||||
if (ok) { awayStore.set({ away: true }); onAway?.(); }
|
if (ok) { awayStore.set({ away: true }); onAway?.(); }
|
||||||
};
|
};
|
||||||
return html`<${Modal} open=${open} onClose=${onClose} tone="red" title="离开模拟" micro="LEAVE SIMULATION" width="6.8rem"
|
return html`<${Modal} open=${open} onClose=${onClose} tone="red" title=${t('离开模拟')} micro="LEAVE SIMULATION" width="6.8rem"
|
||||||
actions=${html`
|
actions=${html`
|
||||||
<${Button} variant="secondary" onClick=${onClose}>取消<//>
|
<${Button} variant="secondary" onClick=${onClose}>${t('取消')}<//>
|
||||||
${!solo ? html`<${Button} variant="ice" icon="robot" loading=${busy === 'away'} onClick=${away}>暂离(AI 托管)<//>` : null}
|
${!solo ? html`<${Button} variant="ice" icon="robot" loading=${busy === 'away'} onClick=${away}>${t('暂离(AI 托管)')}<//>` : null}
|
||||||
<${Button} variant="danger" icon="exit" loading=${busy === 'quit'} onClick=${quit}>放弃模拟<//>`}>
|
<${Button} variant="danger" icon="exit" loading=${busy === 'quit'} onClick=${quit}>${t('放弃模拟')}<//>`}>
|
||||||
<div class="exitm">
|
<div class="exitm">
|
||||||
${solo
|
${solo
|
||||||
? html`<p>${EXIT_TEXT.soloRest}</p><p class="t-lo">${EXIT_TEXT.soloQuit}</p>`
|
? html`<p>${t(EXIT_TEXT.soloRest)}</p><p class="t-lo">${t(EXIT_TEXT.soloQuit)}</p>`
|
||||||
: html`<p><b class="t-ice">暂离</b>:由 AI 托管你的席位(自动部署、准备与选择),随时可以返回。</p>
|
: html`<p>${tParts('{away}:由 AI 托管你的席位(自动部署、准备与选择),随时可以返回。', { away: html`<b class="t-ice">${t('暂离')}</b>` })}</p>
|
||||||
<p><b class="t-red">放弃模拟</b>:离开后无法返回本局,你的干员将回到共享卡池。</p>`}
|
<p>${tParts('{quit}:离开后无法返回本局,你的干员将回到共享卡池。', { quit: html`<b class="t-red">${t('放弃模拟')}</b>` })}</p>`}
|
||||||
</div>
|
</div>
|
||||||
<//>`;
|
<//>`;
|
||||||
}
|
}
|
||||||
@@ -97,13 +98,13 @@ export function ExitModal({ open, onClose, solo, onAway }) {
|
|||||||
/** Full-screen "AI 托管中" overlay with 返回模拟. */
|
/** Full-screen "AI 托管中" overlay with 返回模拟. */
|
||||||
export function AwayOverlay({ onBack = () => {} }) {
|
export function AwayOverlay({ onBack = () => {} }) {
|
||||||
const [busy, setBusy] = useState(false);
|
const [busy, setBusy] = useState(false);
|
||||||
return html`<div class="awayov" role="dialog" aria-label="AI 托管中">
|
return html`<div class="awayov" role="dialog" aria-label=${t('AI 托管中')}>
|
||||||
<div class="awayov__box brackets">
|
<div class="awayov__box brackets">
|
||||||
<${GIcon} name="robot" class="awayov__icon" />
|
<${GIcon} name="robot" class="awayov__icon" />
|
||||||
<${MicroLabel} tone="mint">AUTOPILOT // AI 托管</${MicroLabel}>
|
<${MicroLabel} tone="mint">${t('AUTOPILOT // AI 托管')}</${MicroLabel}>
|
||||||
<h2>AI 托管中</h2>
|
<h2>${t('AI 托管中')}</h2>
|
||||||
<p class="t-lo">AI 正在代为操作你的席位</p>
|
<p class="t-lo">${t('AI 正在代为操作你的席位')}</p>
|
||||||
<${Button} variant="primary" size="lg" icon="play" loading=${busy} onClick=${async () => { setBusy(true); const ok = await actions.autoplay(false); setBusy(false); if (ok) { awayStore.set({ away: false }); onBack(); } }}>返回模拟<//>
|
<${Button} variant="primary" size="lg" icon="play" loading=${busy} onClick=${async () => { setBusy(true); const ok = await actions.autoplay(false); setBusy(false); if (ok) { awayStore.set({ away: false }); onBack(); } }}>${t('返回模拟')}<//>
|
||||||
</div>
|
</div>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -116,7 +117,7 @@ export function StepHeader({ step, of, title, micro, pub, total, onExit }) {
|
|||||||
const conn = useStore((s) => s.connection, shallowEqual);
|
const conn = useStore((s) => s.connection, shallowEqual);
|
||||||
return html`<header class="stephead">
|
return html`<header class="stephead">
|
||||||
<div class="stephead__left">
|
<div class="stephead__left">
|
||||||
<${Button} variant="danger" size="lg" square=${true} icon="exit" onClick=${onExit} aria-label="离开" title="离开" />
|
<${Button} variant="danger" size="lg" square=${true} icon="exit" onClick=${onExit} aria-label=${t('离开')} title=${t('离开')} />
|
||||||
<div class="stephead__meta">
|
<div class="stephead__meta">
|
||||||
<${PingPill} ms=${conn.ping} online=${conn.status === 'online'} />
|
<${PingPill} ms=${conn.ping} online=${conn.status === 'online'} />
|
||||||
${pub?.difficulty ? html`<${DifficultyTag} difficulty=${pub.difficulty} />` : null}
|
${pub?.difficulty ? html`<${DifficultyTag} difficulty=${pub.difficulty} />` : null}
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ export function MatchBondRow({ title, micro, bonds, model }) {
|
|||||||
* @param {{ model: MatchInfoModel }} props
|
* @param {{ model: MatchInfoModel }} props
|
||||||
*/
|
*/
|
||||||
export function MatchLegend({ model }) {
|
export function MatchLegend({ model }) {
|
||||||
return html`<p class="brief-legend"><span class="brief-legend__off"></span>灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)${model.sets.off.size ? ',或本模式禁用' : ''} · <span class="brief-legend__ban"><${Icon} name="user" /></span>该盟约中无法出现的干员数</p>`;
|
return html`<p class="brief-legend"><span class="brief-legend__off"></span>${t('灰色:部分盟约所含干员阵容不完整(仍可通过其他盟约的干员或装备激活)')}${model.sets.off.size ? t(',或本模式禁用') : ''} · <span class="brief-legend__ban"><${Icon} name="user" /></span>${t('该盟约中无法出现的干员数')}</p>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -111,10 +111,10 @@ export function MatchLegend({ model }) {
|
|||||||
export function BannedOperators({ model }) {
|
export function BannedOperators({ model }) {
|
||||||
const { banned } = model;
|
const { banned } = model;
|
||||||
return html`<div class="brief-banned">
|
return html`<div class="brief-banned">
|
||||||
<h3 class="brief-h"><span>本局禁用干员</span><${MicroLabel}>BANNED OPERATORS</${MicroLabel}><b class="num brief-banned__n">${banned.length}</b></h3>
|
<h3 class="brief-h"><span>${t('本局禁用干员')}</span><${MicroLabel}>BANNED OPERATORS</${MicroLabel}><b class="num brief-banned__n">${banned.length}</b></h3>
|
||||||
${banned.length ? html`<div class="brief-banned__grid">
|
${banned.length ? html`<div class="brief-banned__grid">
|
||||||
${banned.map((id) => html`<${UnitThumb} key=${id} kind="chess" id=${id} size="sm" dim=${true} />`)}
|
${banned.map((id) => html`<${UnitThumb} key=${id} kind="chess" id=${id} size="sm" dim=${true} />`)}
|
||||||
</div>` : html`<p class="t-dim">本局没有禁用干员</p>`}
|
</div>` : html`<p class="t-dim">${t('本局没有禁用干员')}</p>`}
|
||||||
<${DiyBannedLine} model=${model} />
|
<${DiyBannedLine} model=${model} />
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -124,8 +124,8 @@ export function BannedOperators({ model }) {
|
|||||||
* @param {{ model: MatchInfoModel }} props
|
* @param {{ model: MatchInfoModel }} props
|
||||||
*/
|
*/
|
||||||
export function MatchInfo({ model }) {
|
export function MatchInfo({ model }) {
|
||||||
return html`<${MatchBondRow} title="核心盟约" micro="CORE BONDS" bonds=${model.core} model=${model} />
|
return html`<${MatchBondRow} title=${t('核心盟约')} micro="CORE BONDS" bonds=${model.core} model=${model} />
|
||||||
<${MatchBondRow} title="附加盟约" micro="ADD-ON BONDS" bonds=${model.addon} model=${model} />
|
<${MatchBondRow} title=${t('附加盟约')} micro="ADD-ON BONDS" bonds=${model.addon} model=${model} />
|
||||||
<${MatchLegend} model=${model} />
|
<${MatchLegend} model=${model} />
|
||||||
<${BannedOperators} model=${model} />`;
|
<${BannedOperators} model=${model} />`;
|
||||||
}
|
}
|
||||||
@@ -136,9 +136,9 @@ export function MatchInfo({ model }) {
|
|||||||
* @param {{ open: boolean, onClose: Function, model: MatchInfoModel|null, status?: any }} props
|
* @param {{ open: boolean, onClose: Function, model: MatchInfoModel|null, status?: any }} props
|
||||||
*/
|
*/
|
||||||
export function MatchInfoDialog({ open, onClose, model, status = null }) {
|
export function MatchInfoDialog({ open, onClose, model, status = null }) {
|
||||||
return html`<${Modal} open=${open && !!model} onClose=${onClose} title="本局信息" micro="MATCH INFO // BONDS & BANNED OPERATORS" class="minfo-dlg"
|
return html`<${Modal} open=${open && !!model} onClose=${onClose} title=${t('本局信息')} micro="MATCH INFO // BONDS & BANNED OPERATORS" class="minfo-dlg"
|
||||||
actions=${html`${status ? html`<div class="minfo-dlg__status" data-testid="match-info-status">${status}</div>` : null}
|
actions=${html`${status ? html`<div class="minfo-dlg__status" data-testid="match-info-status">${status}</div>` : null}
|
||||||
<${Button} variant="secondary" icon="close" data-autofocus data-testid="match-info-close" onClick=${onClose}>关闭<//>`}>
|
<${Button} variant="secondary" icon="close" data-autofocus data-testid="match-info-close" onClick=${onClose}>${t('关闭')}<//>`}>
|
||||||
${open && model ? html`<${MatchInfo} model=${model} />` : null}
|
${open && model ? html`<${MatchInfo} model=${model} />` : null}
|
||||||
<//>`;
|
<//>`;
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@
|
|||||||
|
|
||||||
import { checkNotOwned, OWNERSHIP_LIMITS } from '../../../shared/protocol.js';
|
import { checkNotOwned, OWNERSHIP_LIMITS } from '../../../shared/protocol.js';
|
||||||
import { isDroppableChess, standInRecord } from '../../../shared/standIn.js';
|
import { isDroppableChess, standInRecord } from '../../../shared/standIn.js';
|
||||||
|
import { t } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
/** localStorage key (store.js loadPref/savePref prefix `sp.pref.`) and format version. */
|
/** localStorage key (store.js loadPref/savePref prefix `sp.pref.`) and format version. */
|
||||||
export const OWNERSHIP_PREF = 'ownership';
|
export const OWNERSHIP_PREF = 'ownership';
|
||||||
@@ -153,17 +154,17 @@ export const serializeOwnership = (notOwned, opts) => JSON.stringify(exportOwner
|
|||||||
export function parseOwnershipImport(input) {
|
export function parseOwnershipImport(input) {
|
||||||
let raw = input;
|
let raw = input;
|
||||||
if (typeof raw === 'string') {
|
if (typeof raw === 'string') {
|
||||||
if (raw.length > OWNERSHIP_IMPORT_MAX_BYTES) return { ok: false, error: '内容过长,无法导入' };
|
if (raw.length > OWNERSHIP_IMPORT_MAX_BYTES) return { ok: false, error: t('内容过长,无法导入') };
|
||||||
const text = raw.trim();
|
const text = raw.trim();
|
||||||
if (!text) return { ok: false, error: '没有可导入的内容' };
|
if (!text) return { ok: false, error: t('没有可导入的内容') };
|
||||||
try { raw = JSON.parse(text); } catch { return { ok: false, error: '无法识别的内容' }; }
|
try { raw = JSON.parse(text); } catch { return { ok: false, error: t('无法识别的内容') }; }
|
||||||
}
|
}
|
||||||
if (Array.isArray(raw)) return { ok: true, notOwned: cleanIds(raw) };
|
if (Array.isArray(raw)) return { ok: true, notOwned: cleanIds(raw) };
|
||||||
if (!isObj(raw)) return { ok: false, error: '无法识别的格式' };
|
if (!isObj(raw)) return { ok: false, error: t('无法识别的格式') };
|
||||||
const v = Number.isInteger(raw.v) ? raw.v : null;
|
const v = Number.isInteger(raw.v) ? raw.v : null;
|
||||||
if (v != null && v > OWNERSHIP_VERSION) return { ok: false, error: `这份数据来自更新的版本(v${v}),请先更新游戏` };
|
if (v != null && v > OWNERSHIP_VERSION) return { ok: false, error: t('这份数据来自更新的版本(v{v}),请先更新游戏', { v }) };
|
||||||
const kind = typeof raw.kind === 'string' ? raw.kind : null;
|
const kind = typeof raw.kind === 'string' ? raw.kind : null;
|
||||||
if (kind && kind !== OWNERSHIP_EXPORT_KIND) return { ok: false, error: '这不是干员持有的数据' };
|
if (kind && kind !== OWNERSHIP_EXPORT_KIND) return { ok: false, error: t('这不是干员持有的数据') };
|
||||||
if (!Array.isArray(raw.notOwned)) return { ok: false, error: '里面没有干员持有的数据' };
|
if (!Array.isArray(raw.notOwned)) return { ok: false, error: t('里面没有干员持有的数据') };
|
||||||
return { ok: true, notOwned: cleanIds(raw.notOwned) };
|
return { ok: true, notOwned: cleanIds(raw.notOwned) };
|
||||||
}
|
}
|
||||||
+31
-31
@@ -40,7 +40,7 @@ function PriceHex({ slot, free, poor = false }) {
|
|||||||
if (free || price === 0) return html`<span class="scard__free">FREE</span>`;
|
if (free || price === 0) return html`<span class="scard__free">FREE</span>`;
|
||||||
// like the original (img_bg_price_not_enough) the price turns grey while the funds don't cover it
|
// like the original (img_bg_price_not_enough) the price turns grey while the funds don't cover it
|
||||||
return html`<${HexBadge} value=${price} tone=${poor ? 'dark' : tone} size="md" class=${cx('scard__price', poor && 'is-poor')}
|
return html`<${HexBadge} value=${price} tone=${poor ? 'dark' : tone} size="md" class=${cx('scard__price', poor && 'is-poor')}
|
||||||
title=${poor ? '资金不足' : tone === 'discount' ? `折扣价(原价 ${slot.basePrice})` : tone === 'premium' ? `加价(原价 ${slot.basePrice})` : '价格'} />`;
|
title=${poor ? t('资金不足') : tone === 'discount' ? t('折扣价(原价 {basePrice})', { basePrice: slot.basePrice }) : tone === 'premium' ? t('加价(原价 {basePrice})', { basePrice: slot.basePrice }) : t('价格')} />`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Data lookups (merge progress / target, the loadout; shopBlockReason ignores them — a full hand refuses every purchase). */
|
/** Data lookups (merge progress / target, the loadout; shopBlockReason ignores them — a full hand refuses every purchase). */
|
||||||
@@ -53,13 +53,13 @@ const LOOKUPS = { getChess: (id) => data.lookup('chess', id), getItem: (id) => d
|
|||||||
export function mergeHint(priv, chessId) {
|
export function mergeHint(priv, chessId) {
|
||||||
const prog = mergeProgress(priv, chessId, LOOKUPS.getChess);
|
const prog = mergeProgress(priv, chessId, LOOKUPS.getChess);
|
||||||
if (!(prog.copies > 0 && prog.copies + 1 >= prog.need)) return null;
|
if (!(prog.copies > 0 && prog.copies + 1 >= prog.need)) return null;
|
||||||
return mergeTarget(priv, chessId, LOOKUPS.getChess) ? '精锐干员将出现在作战区原位置' : '精锐干员将进入整备区';
|
return mergeTarget(priv, chessId, LOOKUPS.getChess) ? t('精锐干员将出现在作战区原位置') : t('精锐干员将进入整备区');
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The armed (first-tapped) card's confirm strip: 确认购买 / 确认选择, or 无法购买 + why. */
|
/** The armed (first-tapped) card's confirm strip: 确认购买 / 确认选择, or 无法购买 + why. */
|
||||||
function ArmedTag({ reason, free }) {
|
function ArmedTag({ reason, free }) {
|
||||||
if (reason) return html`<span class="scard__confirm is-no" role="status"><b>无法购买</b><small>${reason}</small></span>`;
|
if (reason) return html`<span class="scard__confirm is-no" role="status"><b>${t('无法购买')}</b><small>${reason}</small></span>`;
|
||||||
return html`<span class="scard__confirm" role="status"><b>${free ? '确认选择' : '确认购买'}</b><small>再次点击</small></span>`;
|
return html`<span class="scard__confirm" role="status"><b>${free ? t('确认选择') : t('确认购买')}</b><small>${t('再次点击')}</small></span>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -85,17 +85,17 @@ export function ChessCard({ slot, idx, priv, frozen = false, reason = null, free
|
|||||||
const disabled = !!reason;
|
const disabled = !!reason;
|
||||||
const lo = si ? standInLoadout(si, LOOKUPS.getChess, data.get('backups')) : c ? chessLoadout(c, priv?.loadout, dr ? diyGetter(LOOKUPS.getChess, priv, diyData) : LOOKUPS.getChess) : null;
|
const lo = si ? standInLoadout(si, LOOKUPS.getChess, data.get('backups')) : c ? chessLoadout(c, priv?.loadout, dr ? diyGetter(LOOKUPS.getChess, priv, diyData) : LOOKUPS.getChess) : null;
|
||||||
const tap = () => { if (onTap) onTap(idx); else if (!disabled) onBuy(idx); else onDetail(slot.id, 'chess', hint); };
|
const tap = () => { if (onTap) onTap(idx); else if (!disabled) onBuy(idx); else onDetail(slot.id, 'chess', hint); };
|
||||||
const name = `${c?.name || '干员'}${si ? `(${standInForText(c0.name)})` : ''}`;
|
const name = `${c?.name || t('干员')}${si ? t('({note})', { note: standInForText(c0.name) }) : ''}`;
|
||||||
const card = html`<button type="button" class=${cx('scard', `scard--t${tier}`, frozen && 'is-frozen', disabled && 'is-disabled', willMerge && 'is-merge', armed && 'is-armed', si && 'is-standin')}
|
const card = html`<button type="button" class=${cx('scard', `scard--t${tier}`, frozen && 'is-frozen', disabled && 'is-disabled', willMerge && 'is-merge', armed && 'is-armed', si && 'is-standin')}
|
||||||
onClick=${tap} onContextMenu=${(e) => { e.preventDefault(); onDetail(slot.id, 'chess', hint); }}
|
onClick=${tap} onContextMenu=${(e) => { e.preventDefault(); onDetail(slot.id, 'chess', hint); }}
|
||||||
aria-label=${`${name},价格 ${slot.price}${armed ? (disabled ? ',无法购买' : ',再次点击确认') : ''}`} aria-pressed=${onTap ? String(!!armed) : undefined}>
|
aria-label=${`${t('{name},价格 {price}', { name, price: slot.price })}${armed ? (disabled ? t(',无法购买') : t(',再次点击确认')) : ''}`} aria-pressed=${onTap ? String(!!armed) : undefined}>
|
||||||
<span class="scard__bg" aria-hidden="true"></span>
|
<span class="scard__bg" aria-hidden="true"></span>
|
||||||
<span class="scard__water" aria-hidden="true">${bonds[0] ? html`<${BondGlyph} bondId=${bonds[0]} />` : null}</span>
|
<span class="scard__water" aria-hidden="true">${bonds[0] ? html`<${BondGlyph} bondId=${bonds[0]} />` : null}</span>
|
||||||
<${Img} src=${chessPortraitUrl(m, c)} class="scard__art" />
|
<${Img} src=${chessPortraitUrl(m, c)} class="scard__art" />
|
||||||
<span class="scard__top">
|
<span class="scard__top">
|
||||||
<${TierChip} tier=${tier} size="md" />
|
<${TierChip} tier=${tier} size="md" />
|
||||||
<${PriceHex} slot=${slot} free=${free} poor=${reason === '资金不足'} />
|
<${PriceHex} slot=${slot} free=${free} poor=${reason === t('资金不足')} />
|
||||||
${prog.copies > 0 ? html`<span class="scard__pips" title=${`已拥有 ${prog.copies}/${prog.need}`}>
|
${prog.copies > 0 ? html`<span class="scard__pips" title=${t('已拥有 {copies}/{need}', { copies: prog.copies, need: prog.need })}>
|
||||||
${Array.from({ length: prog.need }, (_, i) => html`<i key=${i} class=${i < prog.copies ? 'on' : ''}></i>`)}
|
${Array.from({ length: prog.need }, (_, i) => html`<i key=${i} class=${i < prog.copies ? 'on' : ''}></i>`)}
|
||||||
</span>` : null}
|
</span>` : null}
|
||||||
</span>
|
</span>
|
||||||
@@ -116,11 +116,11 @@ export function ChessCard({ slot, idx, priv, frozen = false, reason = null, free
|
|||||||
<span>${c?.subProfessionName || ''}</span>
|
<span>${c?.subProfessionName || ''}</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
${willMerge ? html`<span class="scard__mergetag" title=${hint}>可晋升</span>` : null}
|
${willMerge ? html`<span class="scard__mergetag" title=${hint}>${t('可晋升')}</span>` : null}
|
||||||
${frozen ? html`<span class="scard__ice" aria-hidden="true"><${Icon} name="snow" /></span>` : null}
|
${frozen ? html`<span class="scard__ice" aria-hidden="true"><${Icon} name="snow" /></span>` : null}
|
||||||
${armed ? html`<${ArmedTag} reason=${reason} free=${free} />` : null}
|
${armed ? html`<${ArmedTag} reason=${reason} free=${free} />` : null}
|
||||||
</button>`;
|
</button>`;
|
||||||
return reason && reason !== '已售出' && !armed ? html`<${Tooltip} text=${reason} block=${true} class="scard-wrap">${card}<//>` : card;
|
return reason && reason !== t('已售出') && !armed ? html`<${Tooltip} text=${reason} block=${true} class="scard-wrap">${card}<//>` : card;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The loadout's skill (icon; name + 已调配 in the tooltip) and an elite's module type, on an operator card. */
|
/** The loadout's skill (icon; name + 已调配 in the tooltip) and an elite's module type, on an operator card. */
|
||||||
@@ -131,7 +131,7 @@ function SkillBadge({ chess, lo, standIn = false }) {
|
|||||||
const src = custom ? skillRecordIconUrl(m, lo.skill, { empty: false }) : skillIconUrl(m, chess);
|
const src = custom ? skillRecordIconUrl(m, lo.skill, { empty: false }) : skillIconUrl(m, chess);
|
||||||
const slot = Number.isInteger(lo.skill.index) ? `S${lo.skill.index + 1}` : null;
|
const slot = Number.isInteger(lo.skill.index) ? `S${lo.skill.index + 1}` : null;
|
||||||
const mod = lo.module && !lo.module.none ? lo.module : null;
|
const mod = lo.module && !lo.module.none ? lo.module : null;
|
||||||
const tip = `技能${slot ? ` ${slot}` : ''}:${lo.skill.name || ''}${custom ? '(已调配)' : ''}${mod ? ` · 模组:${mod.name}` : lo.module?.none ? ' · 未装备模组' : ''}`;
|
const tip = `${slot ? t('技能 {slot}:{name}', { slot, name: lo.skill.name || '' }) : t('技能:{name}', { name: lo.skill.name || '' })}${custom ? t('(已调配)') : ''}${mod ? t(' · 模组:{name}', { name: mod.name }) : lo.module?.none ? t(' · 未装备模组') : ''}`;
|
||||||
return html`<span class=${cx('scard__skill', custom && 'is-custom')} title=${tip} aria-label=${tip} data-skill=${lo.skill.skillId || ''}>
|
return html`<span class=${cx('scard__skill', custom && 'is-custom')} title=${tip} aria-label=${tip} data-skill=${lo.skill.skillId || ''}>
|
||||||
<${Img} src=${src} fallback=${slot ? html`<span class="scard__sglyph num">${slot}</span>` : html`<${GIcon} name="bolt" />`} />
|
<${Img} src=${src} fallback=${slot ? html`<span class="scard__sglyph num">${slot}</span>` : html`<${GIcon} name="bolt" />`} />
|
||||||
${mod && mod.typeName ? html`<span class="scard__mod" data-type=${mod.typeName}><${Img} src=${moduleTypeIconUrl(data.get('local'), mod.typeName)} class="scard__modicon" />${mod.typeName}</span>` : null}
|
${mod && mod.typeName ? html`<span class="scard__mod" data-type=${mod.typeName}><${Img} src=${moduleTypeIconUrl(data.get('local'), mod.typeName)} class="scard__modicon" />${mod.typeName}</span>` : null}
|
||||||
@@ -150,12 +150,12 @@ export function ItemCard({ slot, idx, frozen = false, reason = null, free = fals
|
|||||||
const tap = () => { if (onTap) onTap(idx); else if (!disabled) onBuy(idx); else onDetail(slot.id, 'item'); };
|
const tap = () => { if (onTap) onTap(idx); else if (!disabled) onBuy(idx); else onDetail(slot.id, 'item'); };
|
||||||
const card = html`<button type="button" class=${cx('scard', 'scard--item', frozen && 'is-frozen', disabled && 'is-disabled', armed && 'is-armed')}
|
const card = html`<button type="button" class=${cx('scard', 'scard--item', frozen && 'is-frozen', disabled && 'is-disabled', armed && 'is-armed')}
|
||||||
onClick=${tap} onContextMenu=${(e) => { e.preventDefault(); onDetail(slot.id, 'item'); }}
|
onClick=${tap} onContextMenu=${(e) => { e.preventDefault(); onDetail(slot.id, 'item'); }}
|
||||||
aria-label=${`${it?.name || '装备'},价格 ${slot.price}${armed ? (disabled ? ',无法购买' : ',再次点击确认') : ''}`}
|
aria-label=${`${t('{name},价格 {price}', { name: it?.name || t('装备'), price: slot.price })}${armed ? (disabled ? t(',无法购买') : t(',再次点击确认')) : ''}`}
|
||||||
aria-pressed=${onTap ? String(!!armed) : undefined}>
|
aria-pressed=${onTap ? String(!!armed) : undefined}>
|
||||||
<span class="scard__bg" aria-hidden="true"></span>
|
<span class="scard__bg" aria-hidden="true"></span>
|
||||||
<span class="scard__top">
|
<span class="scard__top">
|
||||||
<${TierChip} tier=${it?.tier ?? 1} size="md" />
|
<${TierChip} tier=${it?.tier ?? 1} size="md" />
|
||||||
<${PriceHex} slot=${slot} free=${free} poor=${reason === '资金不足'} />
|
<${PriceHex} slot=${slot} free=${free} poor=${reason === t('资金不足')} />
|
||||||
</span>
|
</span>
|
||||||
<span class="scard__itemart"><${Img} src=${itemIconUrl(m, it)} fallback=${html`<${GIcon} name="bolt" />`} /></span>
|
<span class="scard__itemart"><${Img} src=${itemIconUrl(m, it)} fallback=${html`<${GIcon} name="bolt" />`} /></span>
|
||||||
<span class="scard__body">
|
<span class="scard__body">
|
||||||
@@ -165,12 +165,12 @@ export function ItemCard({ slot, idx, frozen = false, reason = null, free = fals
|
|||||||
${frozen ? html`<span class="scard__ice" aria-hidden="true"><${Icon} name="snow" /></span>` : null}
|
${frozen ? html`<span class="scard__ice" aria-hidden="true"><${Icon} name="snow" /></span>` : null}
|
||||||
${armed ? html`<${ArmedTag} reason=${reason} free=${free} />` : null}
|
${armed ? html`<${ArmedTag} reason=${reason} free=${free} />` : null}
|
||||||
</button>`;
|
</button>`;
|
||||||
return reason && reason !== '已售出' && !armed ? html`<${Tooltip} text=${reason} block=${true} class="scard-wrap">${card}<//>` : card;
|
return reason && reason !== t('已售出') && !armed ? html`<${Tooltip} text=${reason} block=${true} class="scard-wrap">${card}<//>` : card;
|
||||||
}
|
}
|
||||||
|
|
||||||
function SoldCard({ item = false }) {
|
function SoldCard({ item = false }) {
|
||||||
return html`<div class=${cx('scard', 'scard--sold', item && 'scard--item')} aria-label="已售出">
|
return html`<div class=${cx('scard', 'scard--sold', item && 'scard--item')} aria-label=${t('已售出')}>
|
||||||
<span class="scard__soldtxt"><${MicroLabel}>SOLD OUT</${MicroLabel}><span>${item ? '已购买' : '已招募'}</span></span>
|
<span class="scard__soldtxt"><${MicroLabel}>SOLD OUT</${MicroLabel}><span>${item ? t('已购买') : t('已招募')}</span></span>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -179,14 +179,14 @@ function LevelCard({ shop, reason, armed = false, onTap }) {
|
|||||||
const max = lv >= (shop?.maxLevel ?? 6);
|
const max = lv >= (shop?.maxLevel ?? 6);
|
||||||
const price = shop?.upgradePrice ?? 0;
|
const price = shop?.upgradePrice ?? 0;
|
||||||
return html`<button type="button" class=${cx('lvcard', max && 'is-max', reason && 'is-disabled', armed && 'is-armed')} onClick=${() => !reason && onTap()}
|
return html`<button type="button" class=${cx('lvcard', max && 'is-max', reason && 'is-disabled', armed && 'is-armed')} onClick=${() => !reason && onTap()}
|
||||||
title=${reason || (armed ? `再次点击确认升级(${price} 资金)` : `升级调度中心(${price} 资金) · D`)} aria-disabled=${reason ? 'true' : 'false'}
|
title=${reason || (armed ? t('再次点击确认升级({price} 资金)', { price }) : t('升级调度中心({price} 资金) · D', { price }))} aria-disabled=${reason ? 'true' : 'false'}
|
||||||
aria-pressed=${String(!!armed)}>
|
aria-pressed=${String(!!armed)}>
|
||||||
${!max ? html`<${HexBadge} value=${price} tone=${reason && reason !== '调度中心已达最高等级' ? 'dark' : 'gold'} size="md" class="lvcard__price" />` : null}
|
${!max ? html`<${HexBadge} value=${price} tone=${reason && reason !== t('调度中心已达最高等级') ? 'dark' : 'gold'} size="md" class="lvcard__price" />` : null}
|
||||||
<span class="lvcard__frame">
|
<span class="lvcard__frame">
|
||||||
<span class="lvcard__micro">LEVEL</span>
|
<span class="lvcard__micro">LEVEL</span>
|
||||||
<b class="lvcard__num num">${lv}</b>
|
<b class="lvcard__num num">${lv}</b>
|
||||||
</span>
|
</span>
|
||||||
<span class="lvcard__label">${max ? '已满级' : armed ? '确认升级' : '升级'}</span>
|
<span class="lvcard__label">${max ? t('已满级') : armed ? t('确认升级') : t('升级')}</span>
|
||||||
<kbd class="lvcard__key">D</kbd>
|
<kbd class="lvcard__key">D</kbd>
|
||||||
</button>`;
|
</button>`;
|
||||||
}
|
}
|
||||||
@@ -249,11 +249,11 @@ export function RewardCards({ offer, priv, editable, onPick, onDetail, onLater,
|
|||||||
<span class="rwtag__micro">${head.micro}</span>
|
<span class="rwtag__micro">${head.micro}</span>
|
||||||
<span class="rwtag__sub">${head.sub}</span>
|
<span class="rwtag__sub">${head.sub}</span>
|
||||||
${head.more ? html`<span class="rwtag__sub rwtag__more">${head.more}</span>` : null}
|
${head.more ? html`<span class="rwtag__sub rwtag__more">${head.more}</span>` : null}
|
||||||
<button type="button" class="rwtag__later" onClick=${onLater} title="稍后选择(回合结束后消失)"><${Icon} name="minus" />稍后</button>
|
<button type="button" class="rwtag__later" onClick=${onLater} title=${t('稍后选择(回合结束后消失)')}><${Icon} name="minus" />${t('稍后')}</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="shopbar__rwcards">
|
<div class="shopbar__rwcards">
|
||||||
${offer.slots.map((s, i) => {
|
${offer.slots.map((s, i) => {
|
||||||
if (!s || s.sold) return html`<div key=${`rw${i}`} class=${cx('scard', 'scard--sold', s && s.kind === 'item' && 'scard--item')}><span class="scard__soldtxt"><span>已选择</span></span></div>`;
|
if (!s || s.sold) return html`<div key=${`rw${i}`} class=${cx('scard', 'scard--sold', s && s.kind === 'item' && 'scard--item')}><span class="scard__soldtxt"><span>${t('已选择')}</span></span></div>`;
|
||||||
const kind = s.kind === 'item' ? 'item' : 'chess';
|
const kind = s.kind === 'item' ? 'item' : 'chess';
|
||||||
const reason = shopBlockReason('reward', { priv, editable, slot: s, ...LOOKUPS });
|
const reason = shopBlockReason('reward', { priv, editable, slot: s, ...LOOKUPS });
|
||||||
const props = { slot: { ...s, price: 0 }, idx: i, free: true, reason, armed: armed === armKey('r', i, s), onBuy: onPick, onDetail,
|
const props = { slot: { ...s, price: 0 }, idx: i, free: true, reason, armed: armed === armKey('r', i, s), onBuy: onPick, onDetail,
|
||||||
@@ -319,22 +319,22 @@ export function ShopBar({ priv, editable, collapsed, onCollapse, onBuy, onLevel,
|
|||||||
if (collapsed) {
|
if (collapsed) {
|
||||||
return html`<div class="shopbar-tab" ref=${barRef}>
|
return html`<div class="shopbar-tab" ref=${barRef}>
|
||||||
<div class="shopbar-tab__funds"><${CoinGlyph} /><b class="num">${funds}</b></div>
|
<div class="shopbar-tab__funds"><${CoinGlyph} /><b class="num">${funds}</b></div>
|
||||||
<button type="button" class="shopbar-tab__btn" onClick=${() => onCollapse(false)}><${Icon} name="chevronLeft" />展开商店</button>
|
<button type="button" class="shopbar-tab__btn" onClick=${() => onCollapse(false)}><${Icon} name="chevronLeft" />${t('展开商店')}</button>
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
return html`<section class=${cx('shopbar', frozen && 'is-frozen', !editable && 'is-locked', showReward && 'has-reward', armed && 'has-armed')} ref=${barRef} aria-label="调度中心">
|
return html`<section class=${cx('shopbar', frozen && 'is-frozen', !editable && 'is-locked', showReward && 'has-reward', armed && 'has-armed')} ref=${barRef} aria-label=${t('调度中心')}>
|
||||||
<div class="shopbar__tools">
|
<div class="shopbar__tools">
|
||||||
<span class="shopbar__remain">剩余可放置角色:<b class=${cx('num', remaining === 0 && 't-orange')}>${remaining}</b></span>
|
<span class="shopbar__remain">${t('剩余可放置角色:')}<b class=${cx('num', remaining === 0 && 't-orange')}>${remaining}</b></span>
|
||||||
<button type="button" class=${cx('toolbtn', 'toolbtn--ice', frozen && 'is-on')} disabled=${!!frzReason} onClick=${onFreeze}
|
<button type="button" class=${cx('toolbtn', 'toolbtn--ice', frozen && 'is-on')} disabled=${!!frzReason} onClick=${onFreeze}
|
||||||
title=${frzReason || (frozen ? '解冻商店 · F' : '冻结商店(下回合保留) · F')}>
|
title=${frzReason || (frozen ? t('解冻商店 · F') : t('冻结商店(下回合保留) · F'))}>
|
||||||
<${Img} src=${uiUrl(data.get('assets'), frozen ? 'shopPanel/frozen_icon2' : 'shopPanel/frozen_icon')} class="toolbtn__img" fallback=${html`<${Icon} name="snow" />`} />
|
<${Img} src=${uiUrl(data.get('assets'), frozen ? 'shopPanel/frozen_icon2' : 'shopPanel/frozen_icon')} class="toolbtn__img" fallback=${html`<${Icon} name="snow" />`} />
|
||||||
<span>${frozen ? '解冻' : '冻结'}</span><kbd>F</kbd>
|
<span>${frozen ? t('解冻') : t('冻结')}</span><kbd>F</kbd>
|
||||||
</button>
|
</button>
|
||||||
<button type="button" class="toolbtn toolbtn--amber" disabled=${!!refReason} onClick=${onRefresh} title=${refReason || '刷新商店 · R'}>
|
<button type="button" class="toolbtn toolbtn--amber" disabled=${!!refReason} onClick=${onRefresh} title=${refReason || t('刷新商店 · R')}>
|
||||||
<${Img} src=${uiUrl(data.get('assets'), 'shopPanel/refresh_icon')} class="toolbtn__img" fallback=${html`<${Icon} name="refresh" />`} />
|
<${Img} src=${uiUrl(data.get('assets'), 'shopPanel/refresh_icon')} class="toolbtn__img" fallback=${html`<${Icon} name="refresh" />`} />
|
||||||
<span>刷新</span>
|
<span>${t('刷新')}</span>
|
||||||
${free > 0 ? html`<span class="toolbtn__free">免费 ×${free}</span>` : html`<${HexBadge} value=${shop.refreshPrice ?? 1} tone=${refReason ? 'dark' : 'gold'} size="sm" />`}
|
${free > 0 ? html`<span class="toolbtn__free">${t('免费 ×{free}', { free })}</span>` : html`<${HexBadge} value=${shop.refreshPrice ?? 1} tone=${refReason ? 'dark' : 'gold'} size="sm" />`}
|
||||||
<kbd>R</kbd>
|
<kbd>R</kbd>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@@ -365,8 +365,8 @@ export function ShopBar({ priv, editable, collapsed, onCollapse, onBuy, onLevel,
|
|||||||
<${CoinGlyph} class="funds__coin" />
|
<${CoinGlyph} class="funds__coin" />
|
||||||
<b class="funds__num num">${funds}</b>
|
<b class="funds__num num">${funds}</b>
|
||||||
</div>
|
</div>
|
||||||
<span class="funds__label">目前资金</span>
|
<span class="funds__label">${t('目前资金')}</span>
|
||||||
<button type="button" class="funds__collapse" onClick=${() => onCollapse(true)}><${Icon} name="close" />收起</button>
|
<button type="button" class="funds__collapse" onClick=${() => onCollapse(true)}><${Icon} name="close" />${t('收起')}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>`;
|
</section>`;
|
||||||
|
|||||||
+12
-10
@@ -22,6 +22,7 @@ import { EmoteBubble } from './emotes.js';
|
|||||||
import { STATUS_META, sortedPlayers } from './gameLogic.js';
|
import { STATUS_META, sortedPlayers } from './gameLogic.js';
|
||||||
import { MissTag, uniteRemaining } from './hud.js';
|
import { MissTag, uniteRemaining } from './hud.js';
|
||||||
import { localAsset } from '../data.js';
|
import { localAsset } from '../data.js';
|
||||||
|
import { t } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
|
|
||||||
@@ -60,8 +61,9 @@ export function rowLp(p, pub, self = null, { uniteLocal = null, cap = 10 } = {})
|
|||||||
*/
|
*/
|
||||||
export function rowLpTip(lp, cap = 10) {
|
export function rowLpTip(lp, cap = 10) {
|
||||||
if (!lp || !(lp.pending > 0)) return null;
|
if (!lp || !(lp.pending > 0)) return null;
|
||||||
if (lp.unite && lp.left != null) return `目标生命值 ${lp.lp},联防中:漏过的敌人还剩 ${lp.left} 个,按现在结算扣除 ${lp.pending} 点(每回合至多 ${cap} 点)`;
|
if (lp.unite && lp.left != null) return t('目标生命值 {lp},联防中:漏过的敌人还剩 {left} 个,按现在结算扣除 {pending} 点(每回合至多 {cap} 点)', { lp: lp.lp, left: lp.left, pending: lp.pending, cap });
|
||||||
return `目标生命值 ${lp.lp},${lp.unite ? '联防中,' : ''}结算时扣除${lp.unite ? '至多' : ''} ${lp.pending} 点`;
|
return lp.unite ? t('目标生命值 {lp},联防中,结算时扣除至多 {pending} 点', { lp: lp.lp, pending: lp.pending })
|
||||||
|
: t('目标生命值 {lp},结算时扣除 {pending} 点', { lp: lp.lp, pending: lp.pending });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -84,7 +86,7 @@ export function TeamPanel({ pub, myId, watching, bubbles, onWatch, compact = fal
|
|||||||
if (!t.fieldId) { setOpenPid(null); onWatch(p); return; } // the game screen toasts the reason
|
if (!t.fieldId) { setOpenPid(null); onWatch(p); return; } // the game screen toasts the reason
|
||||||
setOpenPid((cur) => (cur === p.playerId ? null : p.playerId));
|
setOpenPid((cur) => (cur === p.playerId ? null : p.playerId));
|
||||||
};
|
};
|
||||||
return html`<aside class=${cx('team', compact && 'team--compact')} aria-label="同盟成员">
|
return html`<aside class=${cx('team', compact && 'team--compact')} aria-label=${t('同盟成员')}>
|
||||||
${players.map((p) => {
|
${players.map((p) => {
|
||||||
const self = p.playerId === myId;
|
const self = p.playerId === myId;
|
||||||
const status = p.alive === false ? 'dead' : p.status;
|
const status = p.alive === false ? 'dead' : p.status;
|
||||||
@@ -94,7 +96,7 @@ export function TeamPanel({ pub, myId, watching, bubbles, onWatch, compact = fal
|
|||||||
const offline = p.connected === false && !p.isBot;
|
const offline = p.connected === false && !p.isBot;
|
||||||
const open = !!observe && openPid === p.playerId && !self;
|
const open = !!observe && openPid === p.playerId && !self;
|
||||||
const back = !!observe && self && observe.observing;
|
const back = !!observe && self && observe.observing;
|
||||||
const title = observe ? (self ? (observe.observing ? '返回战场' : '你自己') : `查看 ${p.name} 的战场`) : (self ? '查看自己的阵地' : `查看 ${p.name} 的阵地`);
|
const title = observe ? (self ? (observe.observing ? t('返回战场') : t('你自己')) : t('查看 {name} 的战场', { name: p.name })) : (self ? t('查看自己的阵地') : t('查看 {name} 的阵地', { name: p.name }));
|
||||||
const lp = rowLp(p, pub, self ? selfLive : null, { uniteLocal, cap });
|
const lp = rowLp(p, pub, self ? selfLive : null, { uniteLocal, cap });
|
||||||
return html`<div key=${p.playerId} class=${cx('team__row', self && 'is-self', watched && 'is-watched', p.alive === false && 'is-dead', open && 'is-open')}>
|
return html`<div key=${p.playerId} class=${cx('team__row', self && 'is-self', watched && 'is-watched', p.alive === false && 'is-dead', open && 'is-open')}>
|
||||||
<button type="button" class="team__btn" onClick=${() => click(p, self)} title=${title} aria-expanded=${observe && !self ? String(open) : undefined}>
|
<button type="button" class="team__btn" onClick=${() => click(p, self)} title=${title} aria-expanded=${observe && !self ? String(open) : undefined}>
|
||||||
@@ -104,24 +106,24 @@ export function TeamPanel({ pub, myId, watching, bubbles, onWatch, compact = fal
|
|||||||
${self ? html`<span class="team__you"><${Icon} name="user" /></span>` : null}
|
${self ? html`<span class="team__you"><${Icon} name="user" /></span>` : null}
|
||||||
</button>
|
</button>
|
||||||
<div class="team__info">
|
<div class="team__info">
|
||||||
<span class="team__name">${p.name || '博士'}</span>
|
<span class="team__name">${p.name || t('博士')}</span>
|
||||||
<div class="team__line">
|
<div class="team__line">
|
||||||
<${LpTower} value=${lp.lp} size="sm" tone=${Number.isFinite(lp.lp) && lp.lp - lp.pending <= 5 ? 'danger' : null} pending=${lp.pending}
|
<${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)} />
|
tip=${rowLpTip(lp, cap)} />
|
||||||
${lp.left != null ? html`<${MissTag} n=${lp.left} name=${self ? null : p.name || '博士'} />` : null}
|
${lp.left != null ? html`<${MissTag} n=${lp.left} name=${self ? null : p.name || t('博士')} />` : null}
|
||||||
<${Tooltip} text=${offline ? '连接已断开' : meta.text} placement="right">
|
<${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}>
|
<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}>
|
||||||
${offline ? html`<${LocalSprite} name="icon_lost_connect" fallback=${html`<${Icon} name="wifiOff" />`} />`
|
${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} />`} />`
|
: STATUS_SPRITE[status] ? html`<${LocalSprite} name=${STATUS_SPRITE[status]} fallback=${html`<${GIcon} name=${meta.glyph} />`} />`
|
||||||
: html`<${GIcon} name=${meta.glyph} />`}
|
: html`<${GIcon} name=${meta.glyph} />`}
|
||||||
</span>
|
</span>
|
||||||
<//>
|
<//>
|
||||||
${watched && !self ? html`<span class="team__eye" title="正在查看"><${GIcon} name="eye" /></span>` : null}
|
${watched && !self ? html`<span class="team__eye" title=${t('正在查看')}><${GIcon} name="eye" /></span>` : null}
|
||||||
</div>
|
</div>
|
||||||
${open ? html`<button type="button" class="btn btn--primary btn--sm team__ob"
|
${open ? html`<button type="button" class="btn btn--primary btn--sm team__ob"
|
||||||
onClick=${() => { setOpenPid(null); onWatch(p); }}><span class="btn__label">前往查看</span></button>` : null}
|
onClick=${() => { setOpenPid(null); onWatch(p); }}><span class="btn__label">${t('前往查看')}</span></button>` : null}
|
||||||
${back ? html`<button type="button" class="btn btn--secondary btn--sm team__back"
|
${back ? html`<button type="button" class="btn btn--secondary btn--sm team__back"
|
||||||
onClick=${() => observe.onBack()}><span class="btn__label">返回战场</span></button>` : null}
|
onClick=${() => observe.onBack()}><span class="btn__label">${t('返回战场')}</span></button>` : null}
|
||||||
</div>
|
</div>
|
||||||
${bubble ? html`<${EmoteBubble} key=${bubble.seq} id=${bubble.id} class="team__bubble" />` : null}
|
${bubble ? html`<${EmoteBubble} key=${bubble.seq} id=${bubble.id} class="team__bubble" />` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
+13
-11
@@ -29,6 +29,7 @@ import { GIcon } from './gameComponents.js';
|
|||||||
import { useTileScreen } from './facingWheel.js';
|
import { useTileScreen } from './facingWheel.js';
|
||||||
import { localAsset } from '../data.js';
|
import { localAsset } from '../data.js';
|
||||||
import { GEO } from '../../../shared/constants.js';
|
import { GEO } from '../../../shared/constants.js';
|
||||||
|
import { t, tParts } from '../../../shared/i18n.js';
|
||||||
|
|
||||||
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
const cx = (...p) => p.flat().filter(Boolean).join(' ');
|
||||||
|
|
||||||
@@ -85,25 +86,25 @@ export function Underframe({ view, uid = null, row, col, actions, name = '', bus
|
|||||||
const half = s * 1.05;
|
const half = s * 1.05;
|
||||||
const stop = (e) => e.stopPropagation();
|
const stop = (e) => e.stopPropagation();
|
||||||
return html`<div class="uframe" data-uid=${uid} style=${`left:${g.x}px;top:${g.y}px;width:${half * 2}px;height:${half * 2}px`} role="group"
|
return html`<div class="uframe" data-uid=${uid} style=${`left:${g.x}px;top:${g.y}px;width:${half * 2}px;height:${half * 2}px`} role="group"
|
||||||
aria-label=${`${name || '单位'} 操作`}>
|
aria-label=${t('{name} 操作', { name: name || t('单位') })}>
|
||||||
<svg class="uframe__dia" viewBox="-110 -110 220 220" aria-hidden="true">
|
<svg class="uframe__dia" viewBox="-110 -110 220 220" aria-hidden="true">
|
||||||
<path class="uframe__outer" d="M0 -100 L100 0 L0 100 L-100 0 Z" />
|
<path class="uframe__outer" d="M0 -100 L100 0 L0 100 L-100 0 Z" />
|
||||||
<path class="uframe__corner" d="M-100 0 L-86 -14 M-100 0 L-86 14 M100 0 L86 -14 M100 0 L86 14 M0 -100 L-14 -86 M0 -100 L14 -86 M0 100 L-14 86 M0 100 L14 86" />
|
<path class="uframe__corner" d="M-100 0 L-86 -14 M-100 0 L-86 14 M100 0 L86 -14 M100 0 L86 14 M0 -100 L-14 -86 M0 -100 L14 -86 M0 100 L-14 86 M0 100 L14 86" />
|
||||||
</svg>
|
</svg>
|
||||||
${actions.retreat ? html`<button type="button" class="uframe__btn uframe__btn--retreat" disabled=${busy} onPointerDown=${stop}
|
${actions.retreat ? html`<button type="button" class="uframe__btn uframe__btn--retreat" disabled=${busy} onPointerDown=${stop}
|
||||||
onClick=${(e) => { stop(e); onRetreat?.(); }} title=${actions.sell != null ? '撤退至整备区(Q)' : '撤退至整备区'} aria-label="撤退" aria-keyshortcuts=${actions.sell != null ? 'Q' : undefined}>
|
onClick=${(e) => { stop(e); onRetreat?.(); }} title=${actions.sell != null ? t('撤退至整备区(Q)') : t('撤退至整备区')} aria-label=${t('撤退')} aria-keyshortcuts=${actions.sell != null ? 'Q' : undefined}>
|
||||||
<${RetreatGlyph} /><span class="uframe__label">${actions.sell != null ? '撤退[Q]' : '撤退'}</span>
|
<${RetreatGlyph} /><span class="uframe__label">${actions.sell != null ? t('撤退[Q]') : t('撤退')}</span>
|
||||||
</button>` : null}
|
</button>` : null}
|
||||||
${actions.sell != null ? html`<button type="button" class="uframe__btn uframe__btn--sell" disabled=${busy} onPointerDown=${stop}
|
${actions.sell != null ? html`<button type="button" class="uframe__btn uframe__btn--sell" disabled=${busy} onPointerDown=${stop}
|
||||||
onClick=${(e) => { stop(e); onSell?.(); }} title=${`出售(+${actions.sell} 资金,X)`} aria-label=${`出售,获得 ${actions.sell} 资金`} aria-keyshortcuts="X">
|
onClick=${(e) => { stop(e); onSell?.(); }} title=${t('出售(+{sell} 资金,X)', { sell: actions.sell })} aria-label=${t('出售,获得 {sell} 资金', { sell: actions.sell })} aria-keyshortcuts="X">
|
||||||
<${PlateIcon} sprite="icon_sell" glyph="sell" tone="sell" />
|
<${PlateIcon} sprite="icon_sell" glyph="sell" tone="sell" />
|
||||||
<span class="uframe__label">出售[X]</span>
|
<span class="uframe__label">${t('出售[X]')}</span>
|
||||||
<${HexBadge} value=${`+${actions.sell}`} tone="gold" size="sm" class="uframe__price" />
|
<${HexBadge} value=${`+${actions.sell}`} tone="gold" size="sm" class="uframe__price" />
|
||||||
</button>` : null}
|
</button>` : null}
|
||||||
${actions.destroy ? html`<button type="button" class=${cx('uframe__btn', 'uframe__btn--destroy')} disabled=${busy} onPointerDown=${stop}
|
${actions.destroy ? html`<button type="button" class=${cx('uframe__btn', 'uframe__btn--destroy')} disabled=${busy} onPointerDown=${stop}
|
||||||
onClick=${(e) => { stop(e); onDestroy?.(); }} title="销毁道具" aria-label="销毁">
|
onClick=${(e) => { stop(e); onDestroy?.(); }} title=${t('销毁道具')} aria-label=${t('销毁')}>
|
||||||
<${PlateIcon} sprite="icon_destory" glyph="trash" tone="destroy" />
|
<${PlateIcon} sprite="icon_destory" glyph="trash" tone="destroy" />
|
||||||
<span class="uframe__label">销毁</span>
|
<span class="uframe__label">${t('销毁')}</span>
|
||||||
</button>` : null}
|
</button>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
@@ -143,8 +144,8 @@ export function tempRowFrame(a, b, { labelW = 0, gap = 8, vw = Infinity } = {})
|
|||||||
*/
|
*/
|
||||||
export function tempRowRule(ready) {
|
export function tempRowRule(ready) {
|
||||||
return ready
|
return ready
|
||||||
? '已准备就绪后进入的单位保留到下个休整期,届时仍在此处的将被销毁(取消准备则在本休整期结束时销毁)'
|
? t('已准备就绪后进入的单位保留到下个休整期,届时仍在此处的将被销毁(取消准备则在本休整期结束时销毁)')
|
||||||
: '放入整备区或战场、配发或使用后才能准备;休整期结束时仍在此处的将被销毁';
|
: t('放入整备区或战场、配发或使用后才能准备;休整期结束时仍在此处的将被销毁');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -163,11 +164,12 @@ export function TempRowNotice({ view, count, items = 0, label = true, ready = fa
|
|||||||
const pts = f.quad.map((p) => `${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
|
const pts = f.quad.map((p) => `${p[0].toFixed(1)},${p[1].toFixed(1)}`).join(' ');
|
||||||
const pad = rem * 0.1;
|
const pad = rem * 0.1;
|
||||||
const style = f.side === 'left' ? `left:${(f.left - pad).toFixed(1)}px;top:${f.y.toFixed(1)}px` : `left:${(f.right + pad).toFixed(1)}px;top:${f.y.toFixed(1)}px`;
|
const style = f.side === 'left' ? `left:${(f.left - pad).toFixed(1)}px;top:${f.y.toFixed(1)}px` : `left:${(f.right + pad).toFixed(1)}px;top:${f.y.toFixed(1)}px`;
|
||||||
const what = items > 0 && items === count ? '件道具' : '个单位';
|
|
||||||
return html`<div class="tempnote" aria-hidden="false" data-testid="temp-notice">
|
return html`<div class="tempnote" aria-hidden="false" data-testid="temp-notice">
|
||||||
<svg class="tempnote__frame" aria-hidden="true"><polygon points=${pts} /></svg>
|
<svg class="tempnote__frame" aria-hidden="true"><polygon points=${pts} /></svg>
|
||||||
${label ? html`<div class=${`tempnote__label is-${f.side}`} style=${style} role="status">
|
${label ? html`<div class=${`tempnote__label is-${f.side}`} style=${style} role="status">
|
||||||
<b class="tempnote__title"><${Icon} name="warn" />临时整备区 <span class="num">${count}</span> ${what}待处理</b>
|
<b class="tempnote__title"><${Icon} name="warn" />${items > 0 && items === count
|
||||||
|
? tParts('临时整备区 {n} 件道具待处理', { n: html`<span class="num">${count}</span>`, count })
|
||||||
|
: tParts('临时整备区 {n} 个单位待处理', { n: html`<span class="num">${count}</span>`, count })}</b>
|
||||||
<span class="tempnote__rule">${tempRowRule(ready)}</span>
|
<span class="tempnote__rule">${tempRowRule(ready)}</span>
|
||||||
</div>` : null}
|
</div>` : null}
|
||||||
</div>`;
|
</div>`;
|
||||||
|
|||||||
@@ -180,4 +180,8 @@ test('tools/i18n.mjs: msgids of template literals name their params; the codemod
|
|||||||
const scanned = await scanSource("const v = html`<span>${tParts('第 {r} 回合', { r: html`<b>${n}</b>` })}</span>`;", 'x.js');
|
const scanned = await scanSource("const v = html`<span>${tParts('第 {r} 回合', { r: html`<b>${n}</b>` })}</span>`;", 'x.js');
|
||||||
assert.deepEqual(scanned.msgids.map((m) => [m.msgid, m.via]), [['第 {r} 回合', 'tParts']]);
|
assert.deepEqual(scanned.msgids.map((m) => [m.msgid, m.via]), [['第 {r} 回合', 'tParts']]);
|
||||||
assert.deepEqual(scanned.literals.filter((l) => !l.reason), []);
|
assert.deepEqual(scanned.literals.filter((l) => !l.reason), []);
|
||||||
|
// an aliased import (`t as tr`, where t is a local variable) is the same msgid call
|
||||||
|
const aliased = await scanSource("import { t as tr } from '../../../shared/i18n.js';\nconst t = 3;\nexport const f = () => tr('替补') + t;", 'x.js');
|
||||||
|
assert.deepEqual(aliased.msgids.map((m) => [m.msgid, m.via]), [['替补', 't']]);
|
||||||
|
assert.deepEqual(aliased.literals.filter((l) => !l.reason), []);
|
||||||
});
|
});
|
||||||
@@ -114,7 +114,7 @@ describe('#7 the 联防 leak counter (HUD)', () => {
|
|||||||
assert.equal(missTip(0, 'Doctor·B'), 'Doctor·B 漏过的敌人已全部被击倒');
|
assert.equal(missTip(0, 'Doctor·B'), 'Doctor·B 漏过的敌人已全部被击倒');
|
||||||
assert.equal(MissTag({ n: 5, name: 'Doctor·B' }).props.title, 'Doctor·B 漏过的敌人还剩 5 个(联防中)');
|
assert.equal(MissTag({ n: 5, name: 'Doctor·B' }).props.title, 'Doctor·B 漏过的敌人还剩 5 个(联防中)');
|
||||||
assert.equal(MissTag({ n: 5 }).props.title, '你漏过的敌人还剩 5 个(队友正在迎战)');
|
assert.equal(MissTag({ n: 5 }).props.title, '你漏过的敌人还剩 5 个(队友正在迎战)');
|
||||||
assert.match(read('public/js/ui/teamPanel.js'), /<\$\{MissTag\} n=\$\{lp\.left\} name=\$\{self \? null : p\.name \|\| '博士'\} \/>/);
|
assert.match(read('public/js/ui/teamPanel.js'), /<\$\{MissTag\} n=\$\{lp\.left\} name=\$\{self \? null : p\.name \|\| t\('博士'\)\} \/>/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the phase capsule carries the official runner tag ×N in 联防 only; the tag reads the uncapped number', () => {
|
test('the phase capsule carries the official runner tag ×N in 联防 only; the tag reads the uncapped number', () => {
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ describe('the drawer shows the watched player\'s 策略 while scouting', () => {
|
|||||||
test('InfoTab overrides the band and labels it with the owner (source); the detail card has no 策略 section', () => {
|
test('InfoTab overrides the band and labels it with the owner (source); the detail card has no 策略 section', () => {
|
||||||
const drawer = read('public/js/ui/enemyDrawer.js');
|
const drawer = read('public/js/ui/enemyDrawer.js');
|
||||||
assert.match(drawer, /const band = \(bandId \|\| priv\?\.bandId\) \? data\.lookup\('bands', bandId \|\| priv\.bandId\) : null/);
|
assert.match(drawer, /const band = \(bandId \|\| priv\?\.bandId\) \? data\.lookup\('bands', bandId \|\| priv\.bandId\) : null/);
|
||||||
assert.match(drawer, /bandOwner \? `\$\{bandOwner\} 的策略` : '我的策略'/);
|
assert.match(drawer, /bandOwner \? t\('\{bandOwner\} 的策略', \{ bandOwner \}\) : t\('我的策略'\)/);
|
||||||
const panel = read('public/js/ui/detailPanel.js');
|
const panel = read('public/js/ui/detailPanel.js');
|
||||||
assert.doesNotMatch(panel, /key="band"/, 'no 策略 section on the detail card');
|
assert.doesNotMatch(panel, /key="band"/, 'no 策略 section on the detail card');
|
||||||
assert.doesNotMatch(panel, /'band'/, "CHESS_SECTIONS has no 'band' block");
|
assert.doesNotMatch(panel, /'band'/, "CHESS_SECTIONS has no 'band' block");
|
||||||
|
|||||||
+17
-9
@@ -144,15 +144,15 @@ const calleeName = (call) => {
|
|||||||
};
|
};
|
||||||
const isConsoleCall = (call) => call?.callee?.type === 'MemberExpression' && call.callee.object?.type === 'Identifier' && call.callee.object.name === 'console';
|
const isConsoleCall = (call) => call?.callee?.type === 'MemberExpression' && call.callee.object?.type === 'Identifier' && call.callee.object.name === 'console';
|
||||||
|
|
||||||
/** Why a literal must not be wrapped (null = wrap it). */
|
/** Why a literal must not be wrapped (null = wrap it). `aliases`: local name → shared/i18n.js export (`t as tr`). */
|
||||||
function skipReason(node, ancestors) {
|
function skipReason(node, ancestors, aliases = new Map()) {
|
||||||
const parent = ancestors[ancestors.length - 1];
|
const parent = ancestors[ancestors.length - 1];
|
||||||
for (let i = ancestors.length - 1; i >= 0; i--) {
|
for (let i = ancestors.length - 1; i >= 0; i--) {
|
||||||
const a = ancestors[i];
|
const a = ancestors[i];
|
||||||
if (a.type === 'CallExpression') {
|
if (a.type === 'CallExpression') {
|
||||||
if (isConsoleCall(a)) return 'console';
|
if (isConsoleCall(a)) return 'console';
|
||||||
const name = calleeName(a);
|
const name = calleeName(a);
|
||||||
if (SKIP_CALLS.has(name) && a.callee.type === 'Identifier') return 'in-call';
|
if (a.callee.type === 'Identifier' && SKIP_CALLS.has(aliases.get(name) ?? name)) return 'in-call';
|
||||||
}
|
}
|
||||||
if (a.type === 'NewExpression' && a.callee.type === 'Identifier' && /Error$/.test(a.callee.name)) return 'error';
|
if (a.type === 'NewExpression' && a.callee.type === 'Identifier' && /Error$/.test(a.callee.name)) return 'error';
|
||||||
if (a.type === 'ThrowStatement') return 'error';
|
if (a.type === 'ThrowStatement') return 'error';
|
||||||
@@ -324,17 +324,25 @@ export async function scanSource(src, file = '<src>') {
|
|||||||
const msgids = [];
|
const msgids = [];
|
||||||
const literals = [];
|
const literals = [];
|
||||||
const lineOf = (pos) => src.slice(0, pos).split('\n').length;
|
const lineOf = (pos) => src.slice(0, pos).split('\n').length;
|
||||||
|
// shared/i18n.js functions imported under another name (`import { t as tr }` where `t` is a local variable)
|
||||||
|
const aliases = new Map();
|
||||||
|
for (const n of ast.body) {
|
||||||
|
if (n.type !== 'ImportDeclaration' || !/shared\/i18n\.js$/.test(String(n.source.value))) continue;
|
||||||
|
for (const sp of n.specifiers) if (sp.type === 'ImportSpecifier' && sp.imported.name !== sp.local.name) aliases.set(sp.local.name, sp.imported.name);
|
||||||
|
}
|
||||||
|
const fnName = (callee) => (callee.type === 'Identifier' ? aliases.get(callee.name) ?? callee.name : null);
|
||||||
// a line marked `i18n-ignore` (in a comment) holds no UI text (font samples, data keys …)
|
// a line marked `i18n-ignore` (in a comment) holds no UI text (font samples, data keys …)
|
||||||
const ignored = new Set(src.split('\n').map((l, i) => (/i18n-ignore/.test(l) ? i + 1 : 0)).filter(Boolean));
|
const ignored = new Set(src.split('\n').map((l, i) => (/i18n-ignore/.test(l) ? i + 1 : 0)).filter(Boolean));
|
||||||
walk(ast, (node, ancestors) => {
|
walk(ast, (node, ancestors) => {
|
||||||
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && node.callee.name === 'tc') {
|
if (node.type === 'CallExpression' && fnName(node.callee) === 'tc') {
|
||||||
const [c, a] = node.arguments;
|
const [c, a] = node.arguments;
|
||||||
if (c?.type === 'Literal' && a?.type === 'Literal' && typeof a.value === 'string') msgids.push({ msgid: `${c.value}::${a.value}`, line: lineOf(a.start), via: 'tc' });
|
if (c?.type === 'Literal' && a?.type === 'Literal' && typeof a.value === 'string') msgids.push({ msgid: `${c.value}::${a.value}`, line: lineOf(a.start), via: 'tc' });
|
||||||
}
|
}
|
||||||
if (node.type === 'CallExpression' && node.callee.type === 'Identifier' && MSGID_CALLS.has(node.callee.name)) {
|
if (node.type === 'CallExpression' && MSGID_CALLS.has(fnName(node.callee))) {
|
||||||
const a = node.arguments[0];
|
const a = node.arguments[0];
|
||||||
if (a && a.type === 'Literal' && typeof a.value === 'string') msgids.push({ msgid: a.value, line: lineOf(a.start), via: node.callee.name });
|
const via = fnName(node.callee);
|
||||||
else if (a && a.type === 'TemplateLiteral' && !a.expressions.length) msgids.push({ msgid: a.quasis[0].value.cooked, line: lineOf(a.start), via: node.callee.name });
|
if (a && a.type === 'Literal' && typeof a.value === 'string') msgids.push({ msgid: a.value, line: lineOf(a.start), via });
|
||||||
|
else if (a && a.type === 'TemplateLiteral' && !a.expressions.length) msgids.push({ msgid: a.quasis[0].value.cooked, line: lineOf(a.start), via });
|
||||||
}
|
}
|
||||||
if (node.type === 'TaggedTemplateExpression' && node.tag.type === 'Identifier' && node.tag.name === 'html') {
|
if (node.type === 'TaggedTemplateExpression' && node.tag.type === 'Identifier' && node.tag.name === 'html') {
|
||||||
for (const seg of htmlSegments(node.quasi)) {
|
for (const seg of htmlSegments(node.quasi)) {
|
||||||
@@ -357,7 +365,7 @@ export async function scanSource(src, file = '<src>') {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
const line = lineOf(node.start);
|
const line = lineOf(node.start);
|
||||||
const reason = ignored.has(line) ? 'ignored' : skipReason(node, ancestors);
|
const reason = ignored.has(line) ? 'ignored' : skipReason(node, ancestors, aliases);
|
||||||
literals.push({ kind: 'str', start: node.start, end: node.end, line, msgid: node.value, params: [], module: !inFunction(ancestors), reason });
|
literals.push({ kind: 'str', start: node.start, end: node.end, line, msgid: node.value, params: [], module: !inFunction(ancestors), reason });
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -366,7 +374,7 @@ export async function scanSource(src, file = '<src>') {
|
|||||||
if (parent && parent.type === 'TaggedTemplateExpression') return true;
|
if (parent && parent.type === 'TaggedTemplateExpression') return true;
|
||||||
const cooked = node.quasis.map((q) => q.value.cooked ?? q.value.raw);
|
const cooked = node.quasis.map((q) => q.value.cooked ?? q.value.raw);
|
||||||
if (!cooked.some((s) => HAN.test(s))) return true;
|
if (!cooked.some((s) => HAN.test(s))) return true;
|
||||||
const reason = ignored.has(lineOf(node.start)) ? 'ignored' : skipReason(node, ancestors);
|
const reason = ignored.has(lineOf(node.start)) ? 'ignored' : skipReason(node, ancestors, aliases);
|
||||||
const { msgid, params } = templateMsgid(cooked, node.expressions.map((e) => src.slice(e.start, e.end)));
|
const { msgid, params } = templateMsgid(cooked, node.expressions.map((e) => src.slice(e.start, e.end)));
|
||||||
literals.push({ kind: 'tpl', start: node.start, end: node.end, line: lineOf(node.start), msgid, params, exprs: node.expressions.map((e) => [e.start, e.end]), module: !inFunction(ancestors), reason });
|
literals.push({ kind: 'tpl', start: node.start, end: node.end, line: lineOf(node.start), msgid, params, exprs: node.expressions.map((e) => [e.start, e.end]), module: !inFunction(ancestors), reason });
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
Reference in new issue
Block a user