feedback5 WI: server toasts and ticker lines carry msgid + params so the client shows them in the player's language
messaging.js: a toast / CUSTOM ticker line is a string (its own msgid) or a shared/i18n.js msg(); frames keep
the Chinese `text` (older clients) and add `msgid` / `params`; config.broadcasts lines carry their `args`
(the client fills the localized broadcast template; the player name {0} is never translated). Templated call
sites send msg(): the grant toast 「X:获得Y」, the summon-range and terrain withdrawals, the 联防 line,
eliminated / left lines, the 远见 / 奇迹 stack payouts and the 维多利亚 hammers; game-data names go as dn().
The protocol stays backward compatible (new fields optional). Tests that captured m.toast's argument render
it with renderMessage. No gameplay change: golden unchanged.
This commit is contained in:
1 parent
a12a539e4d
commit
5398a0cfc5
12 files changed
+166
-18
No files matched your search
@@ -41,6 +41,7 @@ import { boardOrder, parseKey, tileKey } from './board.js';
|
||||
import { pieceBonds as bondsOfPiece } from './bondsMeta.js';
|
||||
import { registerAllMeta } from '../sim/content/index.js';
|
||||
import { registerBuiltins } from './builtinMeta.js';
|
||||
import { msg, dn } from '../../shared/i18n.js';
|
||||
|
||||
export const HOOKS = Object.freeze([
|
||||
'onRoundStart', 'onIncome', 'onPrepStart', 'onPrepEnd', 'onGain', 'onSold', 'onRefresh', 'onPrice', 'onBuy',
|
||||
@@ -525,7 +526,7 @@ export function makeCtx(m, ps, source, hook, ev = null) {
|
||||
const name = got && got.name;
|
||||
if (name) {
|
||||
const who = grantSpeaker(gd, source);
|
||||
m.toast(ps, 'info', who ? `${who}:获得${name}` : `获得${name}`);
|
||||
m.toast(ps, 'info', who ? msg('{who}:获得{name}', { who: dn(who), name: dn(name) }) : msg('获得{name}', { name: dn(name) }));
|
||||
}
|
||||
}
|
||||
return p ? view(p) : null;
|
||||
@@ -635,8 +636,9 @@ export function makeCtx(m, ps, source, hook, ev = null) {
|
||||
addBounty: (card) => m.addBounty(ps, card),
|
||||
|
||||
// ---- messaging
|
||||
toast: (text, kind = 'info') => m.toast(ps, kind, String(text)),
|
||||
ticker: (text) => m.tickerText(String(text)),
|
||||
// a string or a shared/i18n.js msg(msgid, params)
|
||||
toast: (text, kind = 'info') => m.toast(ps, kind, text && typeof text === 'object' ? text : String(text)),
|
||||
ticker: (text) => m.tickerText(text && typeof text === 'object' ? text : String(text)),
|
||||
/** CHAR_GIFT broadcast to this player: "{0}博士给你赠送了{1}". */
|
||||
giftTicker: (fromName, chessId) => { const c = gd.chess(chessId); m.tickerFor('CHAR_GIFT', [String(fromName), c ? c.name : String(chessId)], { to: ps.playerId }); },
|
||||
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
// server/match/match/messaging.js — Match methods: messaging — unicast, broadcast, toasts, the ticker lines
|
||||
// (config.broadcasts by type; the remake's CUSTOM lines), the dirty marks and flush: m.private per player only when it
|
||||
// changed (plus the prep scouts of that player's board), m.public throttled (DELAYS.PUBLIC_THROTTLE) and deduplicated.
|
||||
// i18n (docs/I18N.md): a toast / CUSTOM line is a string (its own msgid) or a shared/i18n.js msg(msgid, params); the frame
|
||||
// carries `text` (the Chinese rendering, what older clients show) plus `msgid` / `params`; a broadcast line carries its
|
||||
// `args` so the client can fill the localized config.broadcasts template.
|
||||
// Installed on Match.prototype by server/match/Match.js (a method container: never instantiated; `this` is the match).
|
||||
|
||||
import { DELAYS } from './common.js';
|
||||
import { wireMessage } from '../../../shared/i18n.js';
|
||||
|
||||
export class MatchMessaging {
|
||||
sendTo(playerId, msg) {
|
||||
@@ -18,9 +22,10 @@ export class MatchMessaging {
|
||||
try { this.broadcastFn(msg); } catch (e) { this.reportError('broadcast', e); }
|
||||
}
|
||||
|
||||
/** @param {string|{ msgid: string, params?: object }} text a string or msg() (shared/i18n.js) */
|
||||
toast(ps, kind, text) {
|
||||
if (!ps || ps.isBot || ps.left || !ps.connected) return;
|
||||
this.sendTo(ps.playerId, { t: 'm.toast', kind, text });
|
||||
this.sendTo(ps.playerId, { t: 'm.toast', kind, ...wireMessage(text) });
|
||||
}
|
||||
|
||||
/** Broadcast ticker from config.broadcasts by type; `param` picks the variant (SHOP_LEVEL level, BOSS_HIT share…). */
|
||||
@@ -31,7 +36,7 @@ export class MatchMessaging {
|
||||
const tpl = b && typeof b.text === 'string' ? b.text : null;
|
||||
if (!tpl) return;
|
||||
const text = tpl.replace(/\{(\d)\}/g, (_, i) => (args[Number(i)] != null ? String(args[Number(i)]) : ''));
|
||||
const msg = { t: 'm.ticker', text, id: b.id, type, priority: Number(b.priority) || 0, playerId };
|
||||
const msg = { t: 'm.ticker', text, id: b.id, type, priority: Number(b.priority) || 0, playerId, args: args.map((a) => String(a ?? '')) };
|
||||
if (to) this.sendTo(to, msg);
|
||||
else this.broadcast(msg);
|
||||
}
|
||||
@@ -42,7 +47,8 @@ export class MatchMessaging {
|
||||
*/
|
||||
tickerText(text, priority = 0) {
|
||||
if (!text) return;
|
||||
this.broadcast({ t: 'm.ticker', text: String(text).slice(0, 200), id: null, type: 'CUSTOM', priority: Number(priority) || 0, playerId: null });
|
||||
const w = wireMessage(text);
|
||||
this.broadcast({ t: 'm.ticker', ...w, text: w.text.slice(0, 200), id: null, type: 'CUSTOM', priority: Number(priority) || 0, playerId: null });
|
||||
}
|
||||
|
||||
markPublic() { this._pubDirty = true; }
|
||||
|
||||
@@ -8,6 +8,7 @@ import { C2S } from '../../../shared/protocol.js';
|
||||
import { PHASE, ERR } from '../../../shared/constants.js';
|
||||
import { syntheticResult } from '../fields.js';
|
||||
import { FLOW_TICKER_PRIORITY, OK, fail } from './common.js';
|
||||
import { msg } from '../../../shared/i18n.js';
|
||||
|
||||
const GAME_TYPES = new Set(Object.keys(C2S).filter((t) => Object.hasOwn(C2S, t) && (t.startsWith('g.') || t.startsWith('b.'))));
|
||||
|
||||
@@ -171,7 +172,7 @@ export class MatchPlatform {
|
||||
}
|
||||
ps.lp = 0;
|
||||
ps.eliminate(passedRound);
|
||||
this.tickerText(`${ps.name}博士中途退出了模拟`, FLOW_TICKER_PRIORITY);
|
||||
this.tickerText(msg('{name}博士中途退出了模拟', { name: ps.name }), FLOW_TICKER_PRIORITY);
|
||||
if (this.bossWaves && (phase === PHASE.ROUND_START || phase === PHASE.SP_DRAFT || phase === PHASE.PREP)) {
|
||||
// before the boss fight: pair the players left again (the prep preview shows the new partner / template); a
|
||||
// player moved to the other half re-checks its board there at once (recompute → deployMap, marks it private)
|
||||
|
||||
@@ -7,6 +7,7 @@ import { PHASE, layerGainRoom } from '../../../shared/constants.js';
|
||||
import { uniteSurvivors } from '../unite.js';
|
||||
import { buildResult } from '../results.js';
|
||||
import { FLOW_TICKER_PRIORITY, DELAYS } from './common.js';
|
||||
import { msg } from '../../../shared/i18n.js';
|
||||
|
||||
export class MatchSettle {
|
||||
settle(plan, uniteResult) {
|
||||
@@ -63,7 +64,7 @@ export class MatchSettle {
|
||||
ps.lp = 0;
|
||||
ps.eliminate(this.round);
|
||||
this.toast(ps, 'error', '你的目标生命值耗尽,已被淘汰');
|
||||
this.tickerText(`${ps.name}博士的目标生命值已耗尽`, FLOW_TICKER_PRIORITY);
|
||||
this.tickerText(msg('{name}博士的目标生命值已耗尽', { name: ps.name }), FLOW_TICKER_PRIORITY);
|
||||
}
|
||||
}
|
||||
this.fields = [];
|
||||
|
||||
@@ -9,6 +9,7 @@ import { uniteBattleOpts, uniteSurvivors, uniteStageId } from '../unite.js';
|
||||
import { FieldRunner, timelineAt, uniteBillBounds } from '../fields.js';
|
||||
import { uniteLeft } from '../../sim/spec.js';
|
||||
import { FLOW_TICKER_PRIORITY, DELAYS } from './common.js';
|
||||
import { msg } from '../../../shared/i18n.js';
|
||||
|
||||
export class MatchUnite {
|
||||
startUnite(plan) {
|
||||
@@ -21,7 +22,7 @@ export class MatchUnite {
|
||||
this.deadline = this.sched.instant ? 0 : this.sched.now() + Math.round((limit / this.gameSpeed) * 1000);
|
||||
this._defaultWatch();
|
||||
this.markPublic();
|
||||
this.tickerText(`联防阶段:${plan.helpers.map((p) => p.name).join('、')} 迎战突破防线的敌人`, FLOW_TICKER_PRIORITY);
|
||||
this.tickerText(msg('联防阶段:{names} 迎战突破防线的敌人', { names: plan.helpers.map((p) => p.name) }), FLOW_TICKER_PRIORITY);
|
||||
this._uniteLeftKey = null;
|
||||
this.runner = new FieldRunner(this, this.fields, {
|
||||
onTick: (runner) => this._uniteTick(runner),
|
||||
@@ -79,7 +80,7 @@ export class MatchUnite {
|
||||
this._sendStart(ps.playerId, f, { watch: !f.players.includes(ps.playerId) });
|
||||
}
|
||||
this.markPublic();
|
||||
this.tickerText(`联防阶段:${plan.helpers.map((p) => p.name).join('、')} 迎战突破防线的敌人`, FLOW_TICKER_PRIORITY);
|
||||
this.tickerText(msg('联防阶段:{names} 迎战突破防线的敌人', { names: plan.helpers.map((p) => p.name) }), FLOW_TICKER_PRIORITY);
|
||||
}
|
||||
|
||||
_finishUniteClient() {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
// the player state).
|
||||
|
||||
import { PHASE } from '../../../shared/constants.js';
|
||||
import { msg, dn } from '../../../shared/i18n.js';
|
||||
import { checkLoadout, resolveLoadout } from '../../../shared/protocol.js';
|
||||
import { tileKey, boardOrder } from '../board.js';
|
||||
|
||||
@@ -126,7 +127,7 @@ export class PlayerBasics {
|
||||
}
|
||||
}
|
||||
}
|
||||
if (names.length) this.m.toast(this, 'warn', `地形变化:${names.join('、')}无法停留在原位置,已撤回整备区`);
|
||||
if (names.length) this.m.toast(this, 'warn', msg('地形变化:{names}无法停留在原位置,已撤回整备区', { names: names.map(dn) }));
|
||||
return moved;
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
// the player state).
|
||||
|
||||
import { ERR } from '../../../shared/constants.js';
|
||||
import { msg, dn } from '../../../shared/i18n.js';
|
||||
import { tileKey, parseKey, inField, canPlace, placeClass, freeSlot, pieceDir, parseDir, ownerRangeKeys } from '../board.js';
|
||||
import { attackRangeGrid, loadoutRecord, resolveRecordLoadout } from '../../../shared/loadoutRecord.js';
|
||||
import { OK, fail } from './common.js';
|
||||
@@ -91,8 +92,8 @@ export class PlayerPlacement {
|
||||
this.board.delete(k);
|
||||
(this._returnToken(p, null, { allowTemp: true }) ? back : gone).push(this.gd.token(p.id)?.name || p.id);
|
||||
}
|
||||
if (back.length) this.m.toast(this, 'warn', `${back.join('、')}只能部署在召唤者攻击范围内,已退回整备区`);
|
||||
if (gone.length) this.m.toast(this, 'warn', `${gone.join('、')}只能部署在召唤者攻击范围内,整备区已满,下回合返还`);
|
||||
if (back.length) this.m.toast(this, 'warn', msg('{names}只能部署在召唤者攻击范围内,已退回整备区', { names: back.map(dn) }));
|
||||
if (gone.length) this.m.toast(this, 'warn', msg('{names}只能部署在召唤者攻击范围内,整备区已满,下回合返还', { names: gone.map(dn) }));
|
||||
return back.length + gone.length;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
// assumed 1).
|
||||
// Latched state lives in player counters (prefix `bondaddon:`); onPrice only reads them (pure).
|
||||
|
||||
import { msg, dn } from '../../../../../shared/i18n.js';
|
||||
|
||||
const C_VISI_PAID = 'bondaddon:visi:paid';
|
||||
const C_VISI_DISC = 'bondaddon:visi:disc';
|
||||
const C_MIRA_PAID = 'bondaddon:mira:paid';
|
||||
@@ -50,10 +52,10 @@ function settleCoins(ctx, bondId, counterKey, label) {
|
||||
const gain = (due - paid) * count;
|
||||
if (prepEnded(ctx)) {
|
||||
ctx.addPendingFunds(gain);
|
||||
ctx.toast(`【${label}】层数达成,下回合开始时获得${gain}资金`, 'info');
|
||||
ctx.toast(msg('【{label}】层数达成,下回合开始时获得{gain}资金', { label: dn(label), gain }), 'info');
|
||||
} else {
|
||||
ctx.addFunds(gain, `bond:${bondId}`);
|
||||
ctx.toast(`【${label}】层数达成,获得${gain}资金`, 'info');
|
||||
ctx.toast(msg('【{label}】层数达成,获得{gain}资金', { label: dn(label), gain }), 'info');
|
||||
}
|
||||
return gain;
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ import { spawnYanyou } from '../tokens.js';
|
||||
import { kjeragColdWind } from '../devices.js';
|
||||
import * as items from '../items.js';
|
||||
import { FORCED_EXIT } from '../../constants.js';
|
||||
import { msg } from '../../../../shared/i18n.js';
|
||||
|
||||
const num = S.num;
|
||||
|
||||
@@ -637,7 +638,7 @@ export function payHammers(ctx) {
|
||||
if (id && ctx.grantItem(id, { source: 'bond:victoriaShip' })) granted++;
|
||||
}
|
||||
}
|
||||
if (granted && typeof ctx.toast === 'function') ctx.toast(`【维多利亚】获得${granted}件维式重锤`, 'info');
|
||||
if (granted && typeof ctx.toast === 'function') ctx.toast(msg('【维多利亚】获得{n}件维式重锤', { n: granted }), 'info');
|
||||
return granted;
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user