Files

166 lines
9.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Server → client texts in the player's language (docs/I18N.md): m.toast / m.ticker frames carry the Chinese `text`
// (older clients) plus `msgid` + `params`, a config.broadcasts line its `args`; the client renders them with
// translateWire / ui/lang.js tickerText; error codes through ui/toasts.js describeError.
import { test, before, after } from 'node:test';
import assert from 'node:assert/strict';
import { readFileSync, existsSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { MatchMessaging } from '../server/match/match/messaging.js';
import { addMessages, setLang, setMessages, translateWire, msg, dn } from '../shared/i18n.js';
import { makeMatch, give, DATA } from './match/harness.js';
import { createRegistry } from '../server/match/effectsMeta.js';
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const EN_UI = JSON.parse(readFileSync(path.join(ROOT, 'public/i18n/en.json'), 'utf8'));
const realFetch = globalThis.fetch;
let lang;
let data;
let toasts;
before(async () => {
// the client data store (singleton) reads data/ through fetch: serve it from the repo
globalThis.fetch = async (url) => {
const abs = path.join(ROOT, String(url).replace(/^\//, ''));
if (!existsSync(abs)) return { ok: false, status: 404, json: async () => null };
const text = readFileSync(abs, 'utf8');
return { ok: true, status: 200, json: async () => JSON.parse(text) };
};
lang = await import('../public/js/ui/lang.js');
({ data } = await import('../public/js/data.js'));
toasts = await import('../public/js/ui/toasts.js');
await data.loadAll('config', 'chess', 'tokens', 'bonds');
addMessages('en', EN_UI);
});
after(() => { globalThis.fetch = realFetch; setLang('zh'); setMessages('en', {}); });
/** A Match-shaped stub for the messaging methods. */
function stubMatch() {
const sent = [];
const broadcast = [];
return {
sent, bc: broadcast,
disposed: false,
players: new Map([['p1', { playerId: 'p1', connected: true }]]),
spectators: new Map(),
gd: { config: DATA.config },
sendTo(id, m) { sent.push([id, m]); return true; },
broadcast(m) { broadcast.push(m); },
};
}
test('m.toast frames: a string stays { text }; a msg() adds msgid + params beside its Chinese text', () => {
const m = stubMatch();
const ps = m.players.get('p1');
MatchMessaging.prototype.toast.call(m, ps, 'warn', '整备区已满,获得的装备已销毁');
MatchMessaging.prototype.toast.call(m, ps, 'warn', msg('{names}只能部署在召唤者攻击范围内,已退回整备区', { names: ['医疗探机', '狼影'].map(dn) }));
assert.deepEqual(m.sent[0][1], { t: 'm.toast', kind: 'warn', text: '整备区已满,获得的装备已销毁' });
assert.deepEqual(m.sent[1][1], {
t: 'm.toast', kind: 'warn', text: '医疗探机、狼影只能部署在召唤者攻击范围内,已退回整备区',
msgid: '{names}只能部署在召唤者攻击范围内,已退回整备区', params: { names: [{ dn: '医疗探机' }, { dn: '狼影' }] },
});
});
test('m.ticker frames: broadcast lines carry their args; CUSTOM lines a msgid', () => {
const m = stubMatch();
MatchMessaging.prototype.tickerFor.call(m, 'GOLDEN_CHAR', ['阿米娅', '琳琅诗怀雅'], { playerId: 'p1' });
MatchMessaging.prototype.tickerText.call(m, msg('{name}博士中途退出了模拟', { name: '阿米娅' }), 5);
const [gold, custom] = m.bc;
assert.equal(gold.type, 'GOLDEN_CHAR');
assert.deepEqual(gold.args, ['阿米娅', '琳琅诗怀雅']);
assert.ok(gold.text.includes('阿米娅') && gold.text.includes('琳琅诗怀雅'), gold.text);
assert.deepEqual({ ...custom }, { t: 'm.ticker', text: '阿米娅博士中途退出了模拟', msgid: '{name}博士中途退出了模拟', params: { name: '阿米娅' }, id: null, type: 'CUSTOM', priority: 5, playerId: null });
});
test('client: the same frames in English — msgid + params, broadcast template + args (player names never translated)', async () => {
setLang('en');
await data.setLocale('en');
try {
const toastFrame = { text: '医疗探机只能部署在召唤者攻击范围内,已退回整备区', msgid: '{names}只能部署在召唤者攻击范围内,已退回整备区', params: { names: [{ dn: '医疗探机' }] } };
assert.equal(translateWire(toastFrame), "Medic Drone can only be deployed within the summoner's attack range and returned to the Bench");
assert.equal(translateWire({ text: '整备区已满,获得的装备已销毁' }), EN_UI['整备区已满,获得的装备已销毁'], 'a static server text is its own msgid');
const m = stubMatch();
MatchMessaging.prototype.tickerFor.call(m, 'GOLDEN_CHAR', ['阿米娅', '琳琅诗怀雅'], { playerId: 'p1' });
const line = lang.tickerText(m.bc[0]);
assert.ok(line.includes('阿米娅'), `player name kept: ${line}`);
assert.ok(line.includes('Swire the Elegant Wit'), `operator name translated: ${line}`);
assert.ok(!/[一-鿿]/.test(line.replace('阿米娅', '')), line);
// an older server (no args): the Chinese line as it came
assert.equal(lang.tickerText({ text: m.bc[0].text, id: m.bc[0].id }), m.bc[0].text);
// error codes: ERR_TEXT / net.js CLIENT_ERR_TEXT are msgids
assert.equal(toasts.describeError({ code: 'HAND_FULL', msg: '整备区已满' }), 'Bench is full');
assert.equal(toasts.describeError('NO_FUNDS'), EN_UI['资金不足']);
} finally {
setLang('zh');
await data.setLocale('zh');
}
});
test('a real match: 歌蕾蒂娅 grants a chess → 「歌蕾蒂娅:获得X」 in Chinese, "Gladiia: obtained …" in English', async () => {
const h = makeMatch({ mode: 'solo', seed: 11, registry: createRegistry({ log: { warn() {}, error() {}, info() {} } }), fake: true }).start();
h.toPrep(1);
const m = h.m;
const ps = h.ps('p_0');
for (const p of [...ps.board.values(), ...ps.hand.filter(Boolean)]) if (p.kind === 'chess') ps.returnCopies(p);
ps.board.clear(); ps.hand.fill(null); ps.temp.fill(null); ps.offers.length = 0; ps.bandId = null; ps.recompute();
const prepFree = (c) => (c.garrisonIds || []).every((g) => DATA.garrisons[g].eventType === 'IN_BATTLE');
const row = Object.values(DATA.chess).filter((c) => c.visible && !c.isGolden && prepFree(c)).map((c) => c.chessId).sort().slice(0, 2);
give(m, ps, 'chess_char_4_12_a', 'board', [10, 4]);
give(m, ps, row[0], 'board', [10, 6]);
give(m, ps, row[1], 'board', [10, 8]);
const n0 = h.allTo('p_0', 'm.toast').length;
m.dispatch(ps, 'onRoundStart', { round: m.round });
const frame = h.allTo('p_0', 'm.toast').slice(n0).find((t) => t.msgid === '{who}:获得{name}');
assert.ok(frame, JSON.stringify(h.allTo('p_0', 'm.toast').slice(n0)));
assert.ok(frame.text.startsWith('歌蕾蒂娅:获得'), frame.text);
assert.deepEqual(frame.params.who, { dn: '歌蕾蒂娅' });
setLang('en');
await data.setLocale('en');
try {
const en = translateWire(JSON.parse(JSON.stringify(frame)));
assert.match(en, /^Gladiia: obtained [A-Z]/, en);
assert.ok(!/[一-鿿]/.test(en), en);
} finally {
setLang('zh');
await data.setLocale('zh');
m.dispose();
}
});
test('server-sent game texts in English: 机变 cards, the draft name, effect entries and offer labels come from the localized data; a reworded text stays as sent; Chinese unchanged', async () => {
await data.loadAll('choices', 'effects', 'items', 'bands', 'garrisons');
const { resolveSpCard } = await import('../public/js/ui/choiceOverlay.js');
const { effectDesc } = await import('../public/js/ui/effectsList.js');
const { offerHeader } = await import('../public/js/ui/gameLogic/shop.js');
const HAN = /[一-鿿]/;
const raw = data.getRaw('choices');
// a 战术决策 card as server/match/choices.js tacticCard sends it (the card's Chinese name and plain text)
const tac = raw.cards.tactic.find((c) => c.effectId && c.name && c.desc && data.lookupRaw('effects', c.effectId)?.descRaw);
const card = { kind: 'tactic', id: tac.effectId, name: tac.name, desc: tac.desc };
const reworded = { ...card, desc: `${tac.desc}(服务器改写)` };
const zh = resolveSpCard(card, 'tactic');
assert.equal(zh.name, tac.name, 'Chinese: the name as sent');
const eff = data.lookupRaw('effects', tac.effectId);
const entry = { id: tac.effectId, iconId: tac.effectId, iconKind: 'choice', name: eff.name, desc: eff.descRaw };
assert.equal(effectDesc(entry), eff.descRaw, 'Chinese: the description as sent');
const band = data.list('bands').find((b) => b && b.effectName);
const offer = { source: 'special', label: band.effectName, slots: [{ kind: 'chess', id: 'x' }] };
assert.equal(offerHeader(offer).title, band.effectName);
setLang('en');
await data.setLocale('en');
try {
const en = resolveSpCard(card, 'tactic');
assert.ok(en.name && !HAN.test(en.name), `card name: ${en.name}`);
if (zh.desc === data.lookupRaw('effects', tac.effectId).descRaw) assert.equal(en.desc, data.lookup('effects', tac.effectId).descRaw, 'card text: the localized record');
assert.equal(resolveSpCard(reworded, 'tactic').desc, reworded.desc, 'a reworded text stays as the server sent it');
assert.equal(effectDesc(entry), data.lookup('effects', tac.effectId).descRaw);
assert.equal(effectDesc({ ...entry, desc: `${eff.descRaw}!` }), `${eff.descRaw}!`);
assert.ok(!HAN.test(offerHeader(offer).title), offerHeader(offer).title);
assert.equal(lang.sentText(raw.families.bounty.name, raw.families.bounty.name, data.get('choices').families.bounty.name), 'Bounty Decision');
} finally {
setLang('zh');
await data.setLocale('zh');
}
});