mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-09-08 11:59:08 -05:00
Support translated battles (#2740)
Some checks failed
Node.js CI / build (22.x) (push) Has been cancelled
Some checks failed
Node.js CI / build (22.x) (push) Has been cancelled
This commit is contained in:
@@ -1400,8 +1400,9 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
|
||||
const Moves = requireNoCache('../caches/pokemon-showdown/dist/data/moves.js').Moves;
|
||||
for (const id in Moves) {
|
||||
const move = Dex.moves.get(Moves[id].name);
|
||||
if (move.desc) Moves[id].desc = move.desc;
|
||||
if (move.shortDesc) Moves[id].shortDesc = move.shortDesc;
|
||||
const text = Dex.text.get(move);
|
||||
if (text.desc) Moves[id].desc = text.desc;
|
||||
if (text.shortDesc) Moves[id].shortDesc = text.shortDesc;
|
||||
if (move.basePowerCallback) Moves[id].basePowerCallback = true;
|
||||
}
|
||||
const buf = 'exports.BattleMovedex = ' + es3stringify(Moves) + ';';
|
||||
@@ -1417,8 +1418,9 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
|
||||
const Items = requireNoCache('../caches/pokemon-showdown/dist/data/items.js').Items;
|
||||
for (const id in Items) {
|
||||
const item = Dex.items.get(Items[id].name);
|
||||
if (item.desc) Items[id].desc = item.desc;
|
||||
if (item.shortDesc) Items[id].shortDesc = item.shortDesc;
|
||||
const text = Dex.text.get(item);
|
||||
if (text.desc) Items[id].desc = text.desc;
|
||||
if (text.shortDesc) Items[id].shortDesc = text.shortDesc;
|
||||
}
|
||||
const buf = 'exports.BattleItems = ' + es3stringify(Items) + ';';
|
||||
fs.writeFileSync('play.pokemonshowdown.com/data/items.js', buf);
|
||||
@@ -1432,8 +1434,9 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
|
||||
const Abilities = requireNoCache('../caches/pokemon-showdown/dist/data/abilities.js').Abilities;
|
||||
for (const id in Abilities) {
|
||||
const ability = Dex.abilities.get(Abilities[id].name);
|
||||
if (ability.desc) Abilities[id].desc = ability.desc;
|
||||
if (ability.shortDesc) Abilities[id].shortDesc = ability.shortDesc;
|
||||
const text = Dex.text.get(ability);
|
||||
if (text.desc) Abilities[id].desc = text.desc;
|
||||
if (text.shortDesc) Abilities[id].shortDesc = text.shortDesc;
|
||||
}
|
||||
const buf = 'exports.BattleAbilities = ' + es3stringify(Abilities) + ';';
|
||||
fs.writeFileSync('play.pokemonshowdown.com/data/abilities.js', buf);
|
||||
@@ -1491,34 +1494,24 @@ process.stdout.write("Building `data/moves,items,abilities,typechart,learnsets.j
|
||||
}
|
||||
|
||||
/*********************************************************
|
||||
* Build text.js
|
||||
* Build text/*.js
|
||||
*********************************************************/
|
||||
|
||||
{
|
||||
const textData = Dex.loadTextData();
|
||||
const Text = textData.Default;
|
||||
const LANGS = fs.readdirSync('caches/pokemon-showdown/data/text', { withFileTypes: true })
|
||||
.filter(entry => entry.isDirectory())
|
||||
.map(entry => entry.name)
|
||||
.concat('en');
|
||||
|
||||
function assignData(id, entry) {
|
||||
for (const key in entry) {
|
||||
if (['name', 'desc', 'shortDesc'].includes(key)) continue;
|
||||
const textEntry = Text[id] || (Text[id] = {});
|
||||
if (key.startsWith('gen')) {
|
||||
for (const modKey in entry[key]) {
|
||||
if (['desc', 'shortDesc'].includes(key)) continue;
|
||||
textEntry[modKey + 'Gen' + key.charAt(3)] = entry[key][modKey];
|
||||
}
|
||||
} else {
|
||||
textEntry[key] = entry[key];
|
||||
}
|
||||
}
|
||||
fs.mkdirSync('play.pokemonshowdown.com/data/text', { recursive: true });
|
||||
for (const lang of LANGS) {
|
||||
const translation = Dex.loadTextData(lang);
|
||||
fs.writeFileSync(
|
||||
`play.pokemonshowdown.com/data/text/${lang}.js`,
|
||||
'exports.BattleText = exports.BattleText || {};\n' +
|
||||
`exports.BattleText[${JSON.stringify(lang)}] = ` + es3stringify(translation) + ';'
|
||||
);
|
||||
}
|
||||
|
||||
for (const id in textData.Moves) assignData(id, textData.Moves[id]);
|
||||
for (const id in textData.Abilities) assignData(id, textData.Abilities[id]);
|
||||
for (const id in textData.Items) assignData(id, textData.Items[id]);
|
||||
|
||||
const buf = 'exports.BattleText = ' + es3stringify(Text) + ';';
|
||||
fs.writeFileSync('play.pokemonshowdown.com/data/text.js', buf);
|
||||
}
|
||||
|
||||
console.log("DONE");
|
||||
|
||||
@@ -114,8 +114,8 @@ compiledFiles += compiler.compileToDir(`replay.pokemonshowdown.com/src`, `replay
|
||||
compiledFiles += compiler.compileToDir(`teams.pokemonshowdown.com/src`, `teams.pokemonshowdown.com/js`, compileOpts);
|
||||
|
||||
try {
|
||||
fs.statSync('play.pokemonshowdown.com/data/text.js');
|
||||
// text.js exists, we can compile battledata
|
||||
fs.statSync('play.pokemonshowdown.com/data/text/en.js');
|
||||
// English text exists, we can compile battledata
|
||||
compiledFiles += compiler.compileToFile(
|
||||
[
|
||||
'play.pokemonshowdown.com/src/battle-dex.ts',
|
||||
@@ -124,8 +124,7 @@ try {
|
||||
'play.pokemonshowdown.com/src/battle-log.ts',
|
||||
'play.pokemonshowdown.com/src/battle-log-misc.js',
|
||||
'caches/pokemon-showdown/server/chat-formatter.ts',
|
||||
'play.pokemonshowdown.com/data/text.js',
|
||||
'play.pokemonshowdown.com/data/text-afd.js',
|
||||
'play.pokemonshowdown.com/data/text/en.js',
|
||||
'play.pokemonshowdown.com/src/battle-text-parser.ts',
|
||||
],
|
||||
'play.pokemonshowdown.com/js/battledata.js',
|
||||
|
||||
@@ -349,7 +349,11 @@ export const defaultRulesES3TSChecked = {
|
||||
// Map/Set can be polyfilled but it's nontrivial and it's easier just to use bare objects
|
||||
// fetch can be polyfilled, but our standard is to use $.get or Net as appropriate.
|
||||
"no-restricted-globals": ["error", "Proxy", "Reflect", "Symbol", "WeakSet", "WeakMap", "Set", "Map", "fetch"],
|
||||
"no-restricted-syntax": ["error", "YieldExpression", "AwaitExpression", "BigIntLiteral"],
|
||||
"no-restricted-syntax": ["error",
|
||||
"YieldExpression", "AwaitExpression", "BigIntLiteral",
|
||||
{ selector: "MethodDefinition[kind='get']", message: "Getters cannot be compiled to ES3" },
|
||||
{ selector: "Property[kind='get']", message: "Getters cannot be compiled to ES3" },
|
||||
],
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
exports.BattleTextAFD = {
|
||||
default: {
|
||||
// April Fool's 2019
|
||||
// April Fool's 2025: tone down the most common text
|
||||
// (I think the jokes work best as a surprise, when most of the
|
||||
// text is the same as before.)
|
||||
|
||||
// startBattle: "[TRAINER] and [TRAINER] commence battle!",
|
||||
// winBattle: "**[TRAINER]** is victorious!",
|
||||
// tieBattle: "The feud between [TRAINER] and [TRAINER] remains unresolved!",
|
||||
|
||||
// pokemon: "[NICKNAME]",
|
||||
// opposingPokemon: "a villainous [NICKNAME]",
|
||||
// team: "your faithful alliance",
|
||||
// opposingTeam: "the forces of evil",
|
||||
// party: "our blessed allies",
|
||||
// opposingParty: "the barbarous opposition",
|
||||
|
||||
// turn: "== Phase [NUMBER] ==",
|
||||
// switchIn: "[TRAINER] would have us contend with [FULLNAME]!",
|
||||
// switchInOwn: "[FULLNAME]! Honor demands your presence!",
|
||||
// switchOut: "[TRAINER] has other plans for [NICKNAME]!",
|
||||
// switchOutOwn: "[NICKNAME], the time for retreat is upon us!",
|
||||
// drag: "[FULLNAME] was unprepared to join us!",
|
||||
// faint: "[POKEMON]'s service has concluded.",
|
||||
// swap: "[POKEMON] and [TARGET] switched places!",
|
||||
// swapCenter: "[POKEMON] moved to the center!",
|
||||
|
||||
// zEffect: " [POKEMON] isn't holding back anymore!",
|
||||
// move: "[POKEMON] unleashes **[MOVE]**!",
|
||||
// abilityActivation: " [[POKEMON]'s [ABILITY]]",
|
||||
|
||||
mega: " [POKEMON]'s [ITEM] glows!",
|
||||
megaNoItem: " [POKEMON]'s lack of Mega Stone glows!",
|
||||
megaGen6: " [POKEMON]'s [ITEM] glows!",
|
||||
transformMega: "[POKEMON] thinks it's a big deal!",
|
||||
primal: "[POKEMON]'s reversion! It got nostalgic about the old days!",
|
||||
zPower: " [POKEMON] is about to stop holding back!",
|
||||
zBroken: " [POKEMON]'s shields are failing!",
|
||||
terastallize: " [POKEMON] is cosplaying as [TYPE]-type!",
|
||||
|
||||
// in case the different default messages didn't make it obvious, the difference
|
||||
// is that the `cant` message REPLACES "Pokemon used Move!", while the `fail`
|
||||
// message happens AFTER "Pokemon used Move!"
|
||||
cant: "[POKEMON] can't use [MOVE]!",
|
||||
cantNoMove: "[POKEMON] can't move!",
|
||||
fail: " Things did not go as planned!",
|
||||
|
||||
// n.b. this is the default message for in-battle forme changes
|
||||
// for the move Transform and ability Imposter, see the entry for the move Transform
|
||||
transform: "[POKEMON] transformed!",
|
||||
typeChange: " [POKEMON] transformed into the [TYPE] type!",
|
||||
typeChangeFromEffect: " [POKEMON]'s [EFFECT] made it the [TYPE] type!",
|
||||
typeAdd: " [TYPE] type was added to [POKEMON]!",
|
||||
|
||||
start: " ([EFFECT] started on [POKEMON]!)",
|
||||
end: " [POKEMON] was freed from [EFFECT]!",
|
||||
activate: " ([EFFECT] activated!)",
|
||||
startTeamEffect: " ([EFFECT] started on [TEAM]!)",
|
||||
endTeamEffect: " ([EFFECT] ended on [TEAM]!)",
|
||||
startFieldEffect: " ([EFFECT] started!)",
|
||||
endFieldEffect: " ([EFFECT] ended!)",
|
||||
|
||||
changeAbility: " [POKEMON] acquired [ABILITY]!",
|
||||
addItem: " [POKEMON] obtained one [ITEM].", // Trick, Switcheroo
|
||||
takeItem: " [POKEMON] stole [SOURCE]'s [ITEM]!", // Thief, Covet, Magician, Pickpocket
|
||||
eatItem: " [POKEMON] ate its [ITEM]!",
|
||||
useGem: " The [ITEM] strengthened [POKEMON]'s power!",
|
||||
eatItemWeaken: " The [ITEM] weakened damage to [POKEMON]!",
|
||||
removeItem: " [POKEMON] lost its [ITEM]!",
|
||||
activateItem: " ([POKEMON] used its [ITEM]!)",
|
||||
activateWeaken: " The [ITEM] weakened the damage to [POKEMON]!",
|
||||
|
||||
damage: " ([POKEMON] was hurt!)",
|
||||
damagePercentage: " ([POKEMON] lost [PERCENTAGE] of its health!)",
|
||||
damageFromPokemon: " [POKEMON] is hurt by [SOURCE]'s [ITEM]!", // Jaboca/Rowap Berry
|
||||
damageFromItem: " [POKEMON] is hurt by its [ITEM]!", // Sticky Barb
|
||||
damageFromPartialTrapping: " [POKEMON] is hurt by [MOVE]!",
|
||||
heal: " [POKEMON] restored its HP.",
|
||||
healFromZEffect: " [POKEMON] restored its HP using its Z-Power!",
|
||||
healFromEffect: " [POKEMON] restored HP using its [EFFECT]!",
|
||||
|
||||
boost: " [POKEMON]'s [STAT] increased!",
|
||||
boost2: " [POKEMON]'s [STAT] increased twice!",
|
||||
boost3: " [POKEMON]'s [STAT] increased a lot!",
|
||||
boost0: " [POKEMON]'s [STAT] won't go any higher!",
|
||||
boostFromItem: " The [ITEM] raised [POKEMON]'s [STAT]!",
|
||||
boost2FromItem: " The [ITEM] raised [POKEMON]'s [STAT] twice!",
|
||||
boost3FromItem: " The [ITEM] raised [POKEMON]'s [STAT] a lot!",
|
||||
boostFromZEffect: " [POKEMON] boosted its [STAT] using its Z-Power!",
|
||||
boost2FromZEffect: " [POKEMON] boosted its [STAT] twice using its Z-Power!",
|
||||
boost3FromZEffect: " [POKEMON] boosted its [STAT] a lot using its Z-Power!",
|
||||
boostMultipleFromZEffect: " [POKEMON] boosted its stats using its Z-Power!",
|
||||
|
||||
unboost: " [POKEMON]'s [STAT] was lowered!",
|
||||
unboost2: " [POKEMON]'s [STAT] was lowered twice!",
|
||||
unboost3: " [POKEMON]'s [STAT] was lowered a lot!",
|
||||
unboost0: " [POKEMON]'s [STAT] won't go any lower!",
|
||||
unboostFromItem: " The [ITEM] lowered [POKEMON]'s [STAT]!",
|
||||
unboost2FromItem: " The [ITEM] lowered [POKEMON]'s [STAT] twice!",
|
||||
unboost3FromItem: " The [ITEM] lowered [POKEMON]'s [STAT] a lot!",
|
||||
|
||||
swapBoost: " [POKEMON] switched stat changes with its target!",
|
||||
swapOffensiveBoost: " [POKEMON] switched all changes to its Strength and Intelligence with its target!",
|
||||
swapDefensiveBoost: " [POKEMON] switched all changes to its Armor and Resistance with its target!",
|
||||
copyBoost: " [POKEMON] copied [TARGET]'s stat changes!",
|
||||
clearBoost: " [POKEMON]'s stat changes were removed!",
|
||||
clearBoostFromZEffect: " [POKEMON] returned its decreased stats to normal using its Z-Power!",
|
||||
invertBoost: " [POKEMON]'s stat changes were inverted!",
|
||||
clearAllBoost: " All stat changes were eliminated!",
|
||||
|
||||
superEffective: " It hit its enemy's weakness!",
|
||||
superEffectiveSpread: " It hit [POKEMON]'s weakness!",
|
||||
resisted: " It was resisted...",
|
||||
resistedSpread: " [POKEMON] resisted the attack.",
|
||||
crit: " A lucky hit! Remember to buy crit insurance!",
|
||||
critSpread: " A lucky hit on [POKEMON]! How dare you!",
|
||||
immune: " [POKEMON] is immune to such dastardly tricks!",
|
||||
immuneNoPokemon: " The foe was immune!", // old gens
|
||||
immuneOHKO: " [POKEMON] is unaffected!",
|
||||
miss: " [POKEMON] avoided the attack!",
|
||||
missNoPokemon: " [SOURCE]'s attack missed!", // old gens
|
||||
|
||||
center: " Automatic center!",
|
||||
noTarget: " But there was no target...", // gen 5 and earlier
|
||||
ohko: " It's a one-hit KO!",
|
||||
combine: " The two moves have become one! It's a combined move!",
|
||||
hitCount: " Hit [NUMBER] times!",
|
||||
hitCountSingular: " Hit 1 time!",
|
||||
},
|
||||
dynamax: {
|
||||
// April Fool's 2020
|
||||
start: " ([POKEMON]'s Tinymax!)",
|
||||
end: " ([POKEMON] returned to normal!)",
|
||||
block: " The move was blocked by the power of Tinymax!",
|
||||
fail: " [POKEMON] shook its head. It seems like it can't use this move...",
|
||||
},
|
||||
|
||||
// stats
|
||||
hp: {
|
||||
statName: "Constitution",
|
||||
statShortName: "HP",
|
||||
},
|
||||
atk: {
|
||||
statName: "Strength",
|
||||
statShortName: "Atk",
|
||||
},
|
||||
def: {
|
||||
statName: "Armor",
|
||||
statShortName: "Def",
|
||||
},
|
||||
spa: {
|
||||
statName: "Intelligence",
|
||||
statShortName: "SpA",
|
||||
},
|
||||
spd: {
|
||||
statName: "Resistance",
|
||||
statShortName: "SpD",
|
||||
},
|
||||
spe: {
|
||||
statName: "Agility",
|
||||
statShortName: "Spe",
|
||||
},
|
||||
accuracy: {
|
||||
statName: "accuracy",
|
||||
},
|
||||
evasion: {
|
||||
statName: "evasiveness",
|
||||
},
|
||||
spc: {
|
||||
statName: "Intelligence",
|
||||
statShortName: "Spc",
|
||||
},
|
||||
stats: {
|
||||
statName: "stats",
|
||||
},
|
||||
|
||||
// misc effects
|
||||
confusion: {
|
||||
start: " [POKEMON] became confused!",
|
||||
startFromFatigue: " [POKEMON] became confused due to fatigue!",
|
||||
end: " [POKEMON] snapped out of its confusion!",
|
||||
endFromItem: " [POKEMON]'s [ITEM] snapped it out of its confusion!",
|
||||
alreadyStarted: " [POKEMON] is already confused!",
|
||||
activate: " [POKEMON] is confused!",
|
||||
damage: "It hurt itself in its confusion!",
|
||||
},
|
||||
drain: {
|
||||
heal: " [SOURCE] will find its attacker's health restored!",
|
||||
},
|
||||
flinch: {
|
||||
cant: "[POKEMON] flinched and couldn't move!",
|
||||
},
|
||||
healreplacement: {
|
||||
activate: " [POKEMON] will restore its replacement's HP using its Z-Power!",
|
||||
},
|
||||
nopp: {
|
||||
cant: "[POKEMON] used [MOVE]!\n But there was no PP left for the move!",
|
||||
},
|
||||
recharge: {
|
||||
cant: "[POKEMON] must recharge!",
|
||||
},
|
||||
recoil: {
|
||||
damage: " [POKEMON] is damaged by the recoil!",
|
||||
},
|
||||
unboost: {
|
||||
fail: " [POKEMON]'s stats were not lowered!",
|
||||
failSingular: " [POKEMON]'s [STAT] was not lowered!",
|
||||
},
|
||||
struggle: {
|
||||
activate: " [POKEMON] has no moves left!",
|
||||
},
|
||||
trapped: {
|
||||
start: " [POKEMON] can no longer escape!",
|
||||
},
|
||||
};
|
||||
|
||||
exports.BattleTextNotAFD = exports.BattleText;
|
||||
@@ -115,8 +115,7 @@ https://psim.us/dev
|
||||
<script defer src="/js/battle-sound.js"></script>
|
||||
<script defer src="/js/lib/jquery-2.2.4.min.js"></script>
|
||||
<script defer src="/data/graphics.js?"></script>
|
||||
<script defer src="/data/text.js?"></script>
|
||||
<script defer src="/data/text-afd.js?"></script>
|
||||
<script defer src="/data/text/en.js?"></script>
|
||||
<script defer src="/js/battle-tooltips.js?"></script>
|
||||
<script defer src="/js/battle.js?"></script>
|
||||
<script defer src="/js/battle-choices.js?"></script>
|
||||
|
||||
@@ -86,6 +86,45 @@ export function toUserid(text: any) {
|
||||
return toID(text);
|
||||
}
|
||||
|
||||
const TEXT_LANGUAGES: { [language: string]: string } = {
|
||||
english: 'en', german: 'de', spanish: 'es', french: 'fr', italian: 'it',
|
||||
japanese: 'ja', korean: 'ko', simplifiedchinese: 'zh-cn', traditionalchinese: 'zh-tw',
|
||||
};
|
||||
|
||||
interface ClientDexText {
|
||||
getLanguage(): string;
|
||||
get(effect: Species | Item | Ability | Move, lang?: string): BattleTextEntry;
|
||||
}
|
||||
|
||||
function getTextLanguage() {
|
||||
const preference = Dex.prefs('language') || Dex.prefs('serversettings')?.language || 'english';
|
||||
const language = TEXT_LANGUAGES[preference] || 'en';
|
||||
return language === 'en' && Dex.afdMode === true ? 'en-afd' : language;
|
||||
}
|
||||
|
||||
function getTextEntry(effect: Species | Item | Ability | Move, gen: number, lang: string): BattleTextEntry {
|
||||
const tableName = `${effect.effectType === 'Species' ? 'Pokedex' : `${effect.effectType}s`}` as keyof BattleTextData;
|
||||
const english = BattleText.en?.[tableName]?.[effect.id] || {};
|
||||
const localized = BattleText[lang]?.[tableName]?.[effect.id] || {};
|
||||
const entry: BattleTextEntry = { ...english, ...localized };
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
const genName = `gen${i}`;
|
||||
const englishGen = english[genName];
|
||||
const localizedGen = localized[genName];
|
||||
if (typeof englishGen === 'object' || typeof localizedGen === 'object') {
|
||||
entry[genName] = {
|
||||
...(typeof englishGen === 'object' ? englishGen : {}),
|
||||
...(typeof localizedGen === 'object' ? localizedGen : {}),
|
||||
};
|
||||
}
|
||||
}
|
||||
for (let i = 8; i >= gen; i--) {
|
||||
const genName = `gen${i}`;
|
||||
Object.assign(entry, english[genName] || {}, localized[genName] || {});
|
||||
}
|
||||
return entry;
|
||||
}
|
||||
|
||||
type Comparable = number | string | boolean | Comparable[] | { reverse: Comparable };
|
||||
export const PSUtils = new class {
|
||||
/**
|
||||
@@ -254,6 +293,7 @@ export const Dex = new class implements ModdedDex {
|
||||
})();
|
||||
|
||||
loadedSpriteData = { xy: 1, bw: 0 };
|
||||
loadedTextData: { [lang: string]: 1 | Promise<void> } = { en: 1 };
|
||||
moddedDexes: { [mod: string]: ModdedDex } = {};
|
||||
|
||||
/**
|
||||
@@ -343,6 +383,13 @@ export const Dex = new class implements ModdedDex {
|
||||
return window.Storage?.prefs ? window.Storage.prefs(prop) : window.PS?.prefs?.[prop];
|
||||
}
|
||||
|
||||
text: ClientDexText = {
|
||||
getLanguage: getTextLanguage,
|
||||
get: (effect: Species | Item | Ability | Move, lang = getTextLanguage()) => {
|
||||
return getTextEntry(effect, 9, lang);
|
||||
},
|
||||
};
|
||||
|
||||
getShortName(name: string) {
|
||||
let shortName = name.replace(/[^A-Za-z0-9]+$/, '');
|
||||
if (shortName.includes('(')) {
|
||||
@@ -583,6 +630,29 @@ export const Dex = new class implements ModdedDex {
|
||||
el.src = path + 'data/pokedex-mini-bw.js' + qs;
|
||||
document.getElementsByTagName('body')[0].appendChild(el);
|
||||
}
|
||||
loadTextData(lang = this.text.getLanguage()): Promise<void> {
|
||||
lang = TEXT_LANGUAGES[lang] || lang;
|
||||
if (BattleText[lang] || typeof document === 'undefined') return Promise.resolve();
|
||||
const existing = this.loadedTextData[lang];
|
||||
if (existing) return existing === 1 ? Promise.resolve() : existing;
|
||||
|
||||
const loadScript = (src: string) => new Promise<void>((resolve, reject) => {
|
||||
const el = document.createElement('script');
|
||||
el.src = src;
|
||||
el.onload = () => resolve();
|
||||
el.onerror = () => reject(new Error(`Failed to load text data from ${src}`));
|
||||
document.getElementsByTagName('body')[0].appendChild(el);
|
||||
});
|
||||
let loading = loadScript(Config.testclient ? `data/text/${lang}.js` : `${this.resourcePrefix}data/text/${lang}.js`);
|
||||
if (Config.testclient) {
|
||||
loading = loading.catch(() => loadScript(`https://play.pokemonshowdown.com/data/text/${lang}.js`));
|
||||
}
|
||||
loading = loading.catch(() => {
|
||||
delete this.loadedTextData[lang];
|
||||
});
|
||||
this.loadedTextData[lang] = loading;
|
||||
return loading;
|
||||
}
|
||||
getSpriteData(pokemon: Pokemon | Species | string, isFront: boolean, options: {
|
||||
gen?: number,
|
||||
shiny?: boolean,
|
||||
@@ -980,6 +1050,12 @@ export class ModdedDex {
|
||||
if ((modid !== 'champions' && !modid.startsWith('gen')) || !gen) throw new Error("Unsupported modid");
|
||||
this.gen = gen;
|
||||
}
|
||||
text: ClientDexText = {
|
||||
getLanguage: getTextLanguage,
|
||||
get: (effect: Species | Item | Ability | Move, lang = getTextLanguage()) => {
|
||||
return getTextEntry(effect, this.gen, lang);
|
||||
},
|
||||
};
|
||||
moves = {
|
||||
get: (name: string): Move => {
|
||||
let id = toID(name);
|
||||
|
||||
@@ -105,6 +105,10 @@ export class BattleLog {
|
||||
this.atBottom = (distanceFromBottom < 30);
|
||||
};
|
||||
reset() {
|
||||
if (this.battleParser) {
|
||||
this.battleParser.language = Dex.text.getLanguage();
|
||||
this.battleParser.lowercaseRegExp = undefined;
|
||||
}
|
||||
this.innerElem.innerHTML = '';
|
||||
if (this.preemptElem) this.preemptElem.innerHTML = '';
|
||||
this.atBottom = true;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1519,7 +1519,7 @@ export class BattleTooltips {
|
||||
if (this.battle.gen === 1 && statName === 'spd') continue;
|
||||
let statLabel = this.battle.gen === 1 && statName === 'spa' ? 'spc' : statName;
|
||||
buf += statName === 'atk' ? '<small>' : '<small> / ';
|
||||
buf += `${BattleText[statLabel].statShortName} </small>`;
|
||||
buf += `${BattleTextParser.statShortName(statLabel)} </small>`;
|
||||
buf += `${stats[statName]}`;
|
||||
if (modifiedStats[statName] !== stats[statName]) hasModifiedStat = true;
|
||||
}
|
||||
@@ -1535,7 +1535,7 @@ export class BattleTooltips {
|
||||
if (this.battle.gen === 1 && statName === 'spd') continue;
|
||||
let statLabel = this.battle.gen === 1 && statName === 'spa' ? 'spc' : statName;
|
||||
buf += statName === 'atk' ? '<small>' : '<small> / ';
|
||||
buf += `${BattleText[statLabel].statShortName} </small>`;
|
||||
buf += `${BattleTextParser.statShortName(statLabel)} </small>`;
|
||||
if (modifiedStats[statName] === stats[statName]) {
|
||||
buf += `${modifiedStats[statName]}`;
|
||||
} else if (modifiedStats[statName] < stats[statName]) {
|
||||
|
||||
@@ -20,9 +20,6 @@ import type { BattleRoom } from './panel-battle';
|
||||
import { Teams } from './battle-teams';
|
||||
import type preact from '../js/lib/preact';
|
||||
|
||||
declare const BattleTextAFD: any;
|
||||
declare const BattleTextNotAFD: any;
|
||||
|
||||
export const VERTICAL_HEADER_WIDTH = 240;
|
||||
export const NARROW_MODE_HEADER_WIDTH = 280;
|
||||
|
||||
@@ -291,17 +288,6 @@ class PSPrefs extends PSStreamModel<string | null> {
|
||||
|
||||
setAFD(mode?: typeof this['afd']) {
|
||||
if (mode === undefined) {
|
||||
// init
|
||||
if (typeof BattleTextAFD !== 'undefined') {
|
||||
for (const id in BattleTextNotAFD) {
|
||||
if (!BattleTextAFD[id]) {
|
||||
BattleTextAFD[id] = BattleTextNotAFD[id];
|
||||
} else {
|
||||
BattleTextAFD[id] = { ...BattleTextNotAFD[id], ...BattleTextAFD[id] };
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.server?.afd) {
|
||||
mode = true;
|
||||
} else if (this.afd !== undefined) {
|
||||
@@ -313,14 +299,7 @@ class PSPrefs extends PSStreamModel<string | null> {
|
||||
}
|
||||
|
||||
Dex.afdMode = mode;
|
||||
|
||||
if (typeof BattleTextAFD !== 'undefined') {
|
||||
if (mode === true) {
|
||||
(BattleText as any) = BattleTextAFD;
|
||||
} else {
|
||||
(BattleText as any) = BattleTextNotAFD;
|
||||
}
|
||||
}
|
||||
if (mode === true) Dex.loadTextData('en-afd');
|
||||
}
|
||||
setShowDebug(mode = this.showdebug) {
|
||||
const css = mode ? `.debug {display: block;}` : `.debug {display: none;}`;
|
||||
@@ -1551,11 +1530,12 @@ export class PSRoom extends PSStreamModel<Args | null> implements RoomOptions {
|
||||
this.add('||AFD is currently set to ' + curMode);
|
||||
this.send('/help afd');
|
||||
}
|
||||
for (let roomid in PS.rooms) {
|
||||
let battle = PS.rooms[roomid] && (PS.rooms[roomid] as BattleRoom).battle;
|
||||
if (!battle) continue;
|
||||
battle.resetToCurrentTurn();
|
||||
}
|
||||
void Dex.loadTextData().then(() => {
|
||||
for (const roomid in PS.rooms) {
|
||||
const battle = (PS.rooms[roomid] as BattleRoom)?.battle;
|
||||
if (battle) battle.resetToCurrentTurn();
|
||||
}
|
||||
});
|
||||
},
|
||||
'clearpms'() {
|
||||
let rooms = PS.miniRoomList.filter(roomid => roomid.startsWith('dm-'));
|
||||
|
||||
11
play.pokemonshowdown.com/src/globals.d.ts
vendored
11
play.pokemonshowdown.com/src/globals.d.ts
vendored
@@ -4,7 +4,16 @@
|
||||
///////////
|
||||
|
||||
type AnyObject = { [k: string]: any };
|
||||
declare const BattleText: { [id: string]: { [templateName: string]: string } };
|
||||
type BattleTextEntry = { [templateName: string]: string | BattleTextEntry };
|
||||
type BattleTextTable = { [id: string]: BattleTextEntry };
|
||||
type BattleTextData = {
|
||||
Default: BattleTextTable,
|
||||
Pokedex: BattleTextTable,
|
||||
Moves: BattleTextTable,
|
||||
Abilities: BattleTextTable,
|
||||
Items: BattleTextTable,
|
||||
};
|
||||
declare const BattleText: { [lang: string]: BattleTextData };
|
||||
declare const BattleFormats: { [id: string]: import('./panel-teamdropdown').FormatData };
|
||||
declare const BattlePokedex: { [id: string]: AnyObject };
|
||||
declare const BattleMovedex: { [id: string]: AnyObject };
|
||||
|
||||
@@ -1174,11 +1174,12 @@
|
||||
this.add('AFD is currently set to ' + mode);
|
||||
this.parseCommand('/help afd');
|
||||
}
|
||||
for (var roomid in app.rooms) {
|
||||
var battle = app.rooms[roomid] && app.rooms[roomid].battle;
|
||||
if (!battle) continue;
|
||||
battle.resetToCurrentTurn();
|
||||
}
|
||||
Dex.loadTextData().then(function () {
|
||||
for (var roomid in app.rooms) {
|
||||
var battle = app.rooms[roomid] && app.rooms[roomid].battle;
|
||||
if (battle) battle.resetToCurrentTurn();
|
||||
}
|
||||
});
|
||||
return false;
|
||||
|
||||
// documentation of client commands
|
||||
|
||||
@@ -550,8 +550,9 @@
|
||||
"Türkçe": 'turkish',
|
||||
"हिंदी": 'hindi',
|
||||
"日本語": 'japanese',
|
||||
"한국어": 'korean',
|
||||
"简体中文": 'simplifiedchinese',
|
||||
"中文": 'traditionalchinese'
|
||||
"繁體中文": 'traditionalchinese'
|
||||
};
|
||||
buf += '<p><label class="optlabel">Language: <select name="language" class="button">';
|
||||
for (var name in possibleLanguages) {
|
||||
@@ -631,7 +632,14 @@
|
||||
Storage.prefs('tournaments', tournaments);
|
||||
},
|
||||
setLanguage: function (e) {
|
||||
app.user.updateSetting('language', e.currentTarget.value);
|
||||
var language = e.currentTarget.value;
|
||||
app.user.updateSetting('language', language);
|
||||
Dex.loadTextData().then(function () {
|
||||
for (var roomid in app.rooms) {
|
||||
var battle = app.rooms[roomid] && app.rooms[roomid].battle;
|
||||
if (battle) battle.resetToCurrentTurn();
|
||||
}
|
||||
});
|
||||
},
|
||||
setBlockpms: function (e) {
|
||||
app.user.updateSetting('blockPMs', !!e.currentTarget.checked);
|
||||
|
||||
@@ -742,20 +742,6 @@ function toId() {
|
||||
},
|
||||
setAFD: function (mode) {
|
||||
if (mode === undefined) {
|
||||
// init
|
||||
if (typeof BattleTextAFD !== 'undefined') {
|
||||
for (var id in BattleTextNotAFD) {
|
||||
if (!BattleTextAFD[id]) {
|
||||
BattleTextAFD[id] = BattleTextNotAFD[id];
|
||||
} else {
|
||||
var combined = {};
|
||||
Object.assign(combined, BattleTextNotAFD[id]);
|
||||
Object.assign(combined, BattleTextAFD[id]);
|
||||
BattleTextAFD[id] = combined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Config.server.afd) {
|
||||
mode = true;
|
||||
} else if (Dex.prefs('afd') !== undefined) {
|
||||
@@ -767,12 +753,7 @@ function toId() {
|
||||
}
|
||||
|
||||
Dex.afdMode = mode;
|
||||
|
||||
if (mode === true) {
|
||||
BattleText = BattleTextAFD;
|
||||
} else {
|
||||
BattleText = BattleTextNotAFD;
|
||||
}
|
||||
if (mode === true) Dex.loadTextData('en-afd');
|
||||
},
|
||||
/**
|
||||
* This function establishes the actual connection to the sim server.
|
||||
|
||||
@@ -147,6 +147,7 @@ export class MainMenuRoom extends PSRoom {
|
||||
if (named) PS.user.initializing = false;
|
||||
if (settingsJSON) {
|
||||
PS.prefs.set('serversettings', { ...PS.prefs.serversettings, ...JSON.parse(settingsJSON) });
|
||||
Dex.loadTextData();
|
||||
}
|
||||
PS.user.setName(fullName, named, avatar);
|
||||
PS.teams.loadRemoteTeams();
|
||||
|
||||
@@ -656,6 +656,12 @@ class OptionsPanel extends PSRoomPanel {
|
||||
}
|
||||
case 'language': {
|
||||
PS.prefs.set('serversettings', { ...PS.prefs.serversettings, language: elem.value });
|
||||
void Dex.loadTextData().then(() => {
|
||||
for (const roomid in PS.rooms) {
|
||||
const battle = (PS.rooms[roomid] as BattleRoom)?.battle;
|
||||
if (battle) battle.resetToCurrentTurn();
|
||||
}
|
||||
});
|
||||
PS.send(`/language ${elem.value}`);
|
||||
break;
|
||||
}
|
||||
@@ -696,7 +702,29 @@ class OptionsPanel extends PSRoomPanel {
|
||||
<button className="button" data-href="register">Register</button>)}
|
||||
|
||||
<hr />
|
||||
<h3>Graphics</h3>
|
||||
<h3>General</h3>
|
||||
<p>
|
||||
<label class="optlabel">
|
||||
<i class="fa fa-globe" aria-hidden></i> Language: {}
|
||||
<select name="language" onChange={this.handleOnChange} class="select" value={serverSettings.language || 'english'}>
|
||||
<option value="english">English</option>
|
||||
<option value="german">Deutsch</option>
|
||||
<option value="spanish">Español</option>
|
||||
<option value="french">Français</option>
|
||||
<option value="italian">Italiano</option>
|
||||
<option value="dutch">Nederlands</option>
|
||||
<option value="portuguese">Português</option>
|
||||
<option value="turkish">Türkçe</option>
|
||||
<option value="hindi">हिंदी</option>
|
||||
<option value="japanese">日本語</option>
|
||||
<option value="korean">한국어</option>
|
||||
<option value="simplifiedchinese">简体中文</option>
|
||||
<option value="traditionalchinese">繁體中文</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<hr />
|
||||
<h3>Appearance</h3>
|
||||
<p>
|
||||
<label class="optlabel">Theme: <select
|
||||
name="theme" class="select" onChange={this.setTheme} value={PS.prefs.theme || 'light'}
|
||||
@@ -774,25 +802,6 @@ class OptionsPanel extends PSRoomPanel {
|
||||
name="refreshprompt" checked={PS.prefs.refreshprompt || false} type="checkbox" onChange={this.handleOnChange}
|
||||
/> Confirm before refreshing</label>
|
||||
</p>
|
||||
<p>
|
||||
<label class="optlabel">
|
||||
Language: {}
|
||||
<select name="language" onChange={this.handleOnChange} class="select" value={serverSettings.language || 'english'}>
|
||||
<option value="english">English</option>
|
||||
<option value="german">Deutsch</option>
|
||||
<option value="spanish">Español</option>
|
||||
<option value="french">Français</option>
|
||||
<option value="italian">Italiano</option>
|
||||
<option value="dutch">Nederlands</option>
|
||||
<option value="portuguese">Português</option>
|
||||
<option value="turkish">Türkçe</option>
|
||||
<option value="hindi">हिंदी</option>
|
||||
<option value="japanese">日本語</option>
|
||||
<option value="simplifiedchinese">简体中文</option>
|
||||
<option value="traditionalchinese">中文</option>
|
||||
</select>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label class="optlabel">
|
||||
Tournaments: <select
|
||||
|
||||
@@ -138,8 +138,7 @@
|
||||
<script src="js/battle-sound.js"></script>
|
||||
<script src="js/lib/jquery-2.2.4.min.js"></script>
|
||||
<script src="data/graphics.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="data/text.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="data/text-afd.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="data/text/en.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="data/commands.js?" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="js/battle-tooltips.js"></script>
|
||||
<script src="js/battle.js"></script>
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
<!-- fallback for if you've never done a full build -->
|
||||
<script src="js/server/chat-formatter.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="js/battledata.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="data/text.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="data/text/en.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="data/pokedex-mini.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="data/pokedex-mini-bw.js" onerror="loadRemoteData(this.src)"></script>
|
||||
<script src="data/typechart.js" onerror="loadRemoteData(this.src)"></script>
|
||||
|
||||
202
test/battle-text-parser.test.js
Normal file
202
test/battle-text-parser.test.js
Normal file
@@ -0,0 +1,202 @@
|
||||
const assert = require('assert').strict;
|
||||
const {describe, it} = require('node:test');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const englishTextPath = path.resolve(__dirname, '../play.pokemonshowdown.com/data/text/en.js');
|
||||
const afdTextPath = path.resolve(__dirname, '../play.pokemonshowdown.com/data/text/en-afd.js');
|
||||
const hasBuiltText = fs.existsSync(englishTextPath) && fs.existsSync(afdTextPath);
|
||||
|
||||
let BattleTextParser;
|
||||
if (hasBuiltText) {
|
||||
global.window = global;
|
||||
global.BattleText = require(englishTextPath).BattleText;
|
||||
global.BattleText['en-afd'] = require(afdTextPath).BattleText['en-afd'];
|
||||
global.BattleText.en.Default.default.hitCount =
|
||||
" The Pok\u00E9mon was hit [NUMBER] [INFLECT:NUMBER:s=time:p=times]!";
|
||||
global.BattleText.en.Default.unboost.fail =
|
||||
" [POKEMON]'s [STAT] [INFLECT:STAT:s=was:p=were] not lowered!";
|
||||
|
||||
require('../play.pokemonshowdown.com/js/battle-dex-data.js');
|
||||
require('../play.pokemonshowdown.com/js/battle-dex.js');
|
||||
require('../play.pokemonshowdown.com/js/battle-text-parser.js');
|
||||
BattleTextParser = global.BattleTextParser;
|
||||
}
|
||||
|
||||
describe('BattleTextParser', {skip: hasBuiltText ? false : 'text data has not been built'}, () => {
|
||||
it('uses en-afd as a sparse English overlay', () => {
|
||||
global.Dex.afdMode = true;
|
||||
const parser = new BattleTextParser();
|
||||
assert.equal(parser.extractMessage('|-hitcount|p1a: Mew|3'), ' Hit 3 times!\n');
|
||||
global.Dex.afdMode = false;
|
||||
});
|
||||
|
||||
it('uses canonical language tables with English field fallback', () => {
|
||||
global.BattleText.ja = {
|
||||
Default: {default: {hitCount: '[NUMBER]回 当たった!'}},
|
||||
Pokedex: {ironleaves: {name: 'テツノイサハ'}},
|
||||
Moves: {knockoff: {name: 'はたきおとす'}},
|
||||
Abilities: {levitate: {name: 'ふゆう'}},
|
||||
Items: {lifeorb: {name: 'いのちのたま'}},
|
||||
};
|
||||
const parser = new BattleTextParser('p1', 'ja');
|
||||
assert.equal(parser.extractMessage('|-hitcount|p1a: Mew|3'), '3回 当たった!\n');
|
||||
assert.equal(parser.pokemon('p1a: Mew'), 'Mew');
|
||||
});
|
||||
|
||||
it('translates effect and species names without translating nicknames', () => {
|
||||
const parser = new BattleTextParser('p1', 'ja');
|
||||
assert.equal(parser.moveName('Knock Off'), 'はたきおとす');
|
||||
assert.equal(parser.itemName('Life Orb'), 'いのちのたま');
|
||||
assert.equal(parser.abilityName('Levitate'), 'ふゆう');
|
||||
assert.deepEqual(
|
||||
parser.pokemonFull('p1a: Salad', 'Iron Leaves'),
|
||||
['p1', 'Salad (**テツノイサハ**)']
|
||||
);
|
||||
assert.deepEqual(
|
||||
parser.pokemonFull('p1a: Iron Leaves', 'Iron Leaves'),
|
||||
['p1', 'Iron Leaves (**テツノイサハ**)']
|
||||
);
|
||||
assert.deepEqual(parser.pokemonFull('p1a: テツノイサハ', 'Iron Leaves'), ['p1', '**テツノイサハ**']);
|
||||
});
|
||||
|
||||
it('falls back by field for past-generation text', () => {
|
||||
global.BattleText.en.Moves.testmove = {start: 'modern', gen4: {start: 'old'}};
|
||||
global.BattleText.ja.Moves.testmove = {start: '現代'};
|
||||
const parser = new BattleTextParser('p1', 'ja');
|
||||
parser.gen = 4;
|
||||
assert.equal(parser.template('start', 'move: Test Move'), 'old\n');
|
||||
});
|
||||
|
||||
it('Dex.text.get returns localized entries with English fallback', () => {
|
||||
global.BattleText.en.Moves.testmove = {name: 'Test Move', desc: 'English description'};
|
||||
global.BattleText.ja.Moves.testmove = {name: 'テスト技'};
|
||||
const move = new global.Dex.Move('testmove', 'Test Move', {});
|
||||
assert.deepEqual(global.Dex.text.get(move, 'ja'), {
|
||||
name: 'テスト技', desc: 'English description',
|
||||
});
|
||||
});
|
||||
|
||||
it('selects and unescapes INFLECT values', () => {
|
||||
const template = String.raw`[INFLECT:ITEM:s=one:p=two\:three\=four\]five\\six]`;
|
||||
assert.equal(BattleTextParser.inflect(template, {ITEM: 's'}), 'one');
|
||||
assert.equal(BattleTextParser.inflect(template, {ITEM: 'p'}), 'two:three=four]five\\six');
|
||||
});
|
||||
|
||||
it('leaves unresolved INFLECT expressions visible', () => {
|
||||
const template = '[INFLECT:ITEM:s=one:p=many]';
|
||||
assert.equal(BattleTextParser.inflect(template, {}), template);
|
||||
assert.equal(BattleTextParser.inflect(template, {ITEM: 'z'}), template);
|
||||
});
|
||||
|
||||
it('inflects hit counts', () => {
|
||||
const parser = new BattleTextParser();
|
||||
assert.equal(parser.extractMessage('|-hitcount|p1a: Mew|1'), ' The Pok\u00E9mon was hit 1 time!\n');
|
||||
assert.equal(parser.extractMessage('|-hitcount|p1a: Mew|3'), ' The Pok\u00E9mon was hit 3 times!\n');
|
||||
});
|
||||
|
||||
it('lets translations place the percentage symbol', () => {
|
||||
global.BattleText.en.Default.default.damagePercentage =
|
||||
' ([POKEMON] lost [PERCENTAGE]% of its health!)';
|
||||
const parser = new BattleTextParser();
|
||||
assert.equal(
|
||||
parser.extractMessage('|-damage|p1a: Mew|58/100|42%'),
|
||||
' (Mew lost 42% of its health!)\n'
|
||||
);
|
||||
assert.equal(parser.extractMessage('|-damage|p1a: Mew|58/100'), ' (Mew was hurt!)\n');
|
||||
});
|
||||
|
||||
it('inflects single-stat and all-stat failures', () => {
|
||||
const parser = new BattleTextParser();
|
||||
assert.equal(parser.extractMessage('|-fail|p1a: Mew|unboost|atk'), " Mew's Attack was not lowered!\n");
|
||||
assert.equal(parser.extractMessage('|-fail|p1a: Mew|unboost'), " Mew's stats were not lowered!\n");
|
||||
});
|
||||
|
||||
it('applies placeholder modifiers after substitution', () => {
|
||||
global.BattleText.fr = {Default: {default: {
|
||||
faint: "[POKEMON] voit le clone [POKEMON:de] disparaître !",
|
||||
}}};
|
||||
const parser = new BattleTextParser('p1', 'fr');
|
||||
assert.equal(parser.extractMessage('|faint|p1a: Mew'), 'Mew voit le clone de Mew disparaître !\n');
|
||||
assert.equal(parser.extractMessage('|faint|p1a: Eevee'), 'Eevee voit le clone d’Eevee disparaître !\n');
|
||||
});
|
||||
|
||||
it('renders repeated placeholders with distinct values and literal dollar signs', () => {
|
||||
const original = global.BattleText.en.Default.default.startBattle;
|
||||
global.BattleText.en.Default.default.startBattle = '[TRAINER] vs. [TRAINER]... start!';
|
||||
const parser = new BattleTextParser();
|
||||
assert.equal(parser.extractMessage('|player|p1|Alice$1'), '');
|
||||
assert.equal(parser.extractMessage('|player|p2|Bob$2'), '');
|
||||
assert.equal(parser.extractMessage('|start'), 'Alice$1 vs. Bob$2... start!\n');
|
||||
global.BattleText.en.Default.default.startBattle = original;
|
||||
});
|
||||
|
||||
it('applies Korean placeholder particles', () => {
|
||||
global.BattleText.ko = {Default: {default: {
|
||||
faint: '[POKEMON:topic] 쓰러졌다!',
|
||||
}}};
|
||||
const parser = new BattleTextParser('p1', 'ko');
|
||||
assert.equal(parser.extractMessage('|faint|p1a: 뮤'), '뮤는 쓰러졌다!\n');
|
||||
assert.equal(parser.extractMessage('|faint|p1a: 팬텀'), '팬텀은 쓰러졌다!\n');
|
||||
});
|
||||
|
||||
it('applies articles, contractions, and capitalization', () => {
|
||||
assert.equal(BattleTextParser.modify('Baie Oran', ['indefinite'], 'fr', 'fs'), 'une Baie Oran');
|
||||
assert.equal(BattleTextParser.modify('Angriff', ['definite', 'accusative'], 'de'), 'den Angriff');
|
||||
assert.equal(BattleTextParser.modify('avversario', ['a', 'definite'], 'it'), "all’avversario");
|
||||
assert.equal(BattleTextParser.modify('equipo', ['capitalize'], 'es'), 'Equipo');
|
||||
assert.equal(
|
||||
BattleTextParser.modify('Agua Mística', ['definite'], 'es', 'fu', 'stressed-a'),
|
||||
'el Agua Mística'
|
||||
);
|
||||
assert.equal(
|
||||
BattleTextParser.modify('Aguas Místicas', ['definite'], 'es', 'fp', 'stressed-a'),
|
||||
'las Aguas Místicas'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses localized grammar and classified item forms', () => {
|
||||
global.BattleText.fr = {
|
||||
Default: {default: {
|
||||
addItem: '[POKEMON] obtient [ITEM:indefinite:classified] !',
|
||||
useGem: '[ITEM:definite:capitalize:classified] agi[INFLECT:ITEM:s=t:p=ssent] !',
|
||||
}},
|
||||
Items: {brightpowder: {
|
||||
name: 'Poudre Claire', grammar: 'fu',
|
||||
classified: {name: 'sac de Poudre Claire', grammar: 'ms'},
|
||||
}},
|
||||
};
|
||||
const parser = new BattleTextParser('p1', 'fr');
|
||||
assert.equal(
|
||||
parser.extractMessage('|-item|p1a: Mew|Bright Powder|[from] move: Not Real'),
|
||||
'Mew obtient un sac de Poudre Claire !\n'
|
||||
);
|
||||
assert.equal(
|
||||
parser.extractMessage('|-enditem|p1a: Mew|Bright Powder|[from] gem|[move] Tackle'),
|
||||
'Le sac de Poudre Claire agit !\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses localized article rules', () => {
|
||||
global.BattleText.es = {
|
||||
Default: {default: {activateItem: '[ITEM:definite:capitalize] actúa!'}},
|
||||
Items: {mysticwater: {
|
||||
name: 'Agua Mística', grammar: 'fu', articleRule: 'stressed-a',
|
||||
}},
|
||||
};
|
||||
const parser = new BattleTextParser('p1', 'es');
|
||||
assert.equal(
|
||||
parser.extractMessage('|-enditem|p1a: Mew|Mystic Water'),
|
||||
'El Agua Mística actúa!\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('uses localized stat grammar', () => {
|
||||
global.BattleText.fr = {Default: {
|
||||
default: {boost: '[STAT:definite:capitalize] de [POKEMON] augmente !'},
|
||||
atk: {statName: 'Attaque', grammar: 'fs'},
|
||||
}};
|
||||
const parser = new BattleTextParser('p1', 'fr');
|
||||
assert.equal(parser.extractMessage('|-boost|p1a: Mew|atk|1'), 'L’Attaque de Mew augmente !\n');
|
||||
});
|
||||
});
|
||||
@@ -6,7 +6,7 @@ window = global;
|
||||
require('../play.pokemonshowdown.com/js/battle-dex-data.js');
|
||||
require('../play.pokemonshowdown.com/js/battle-dex.js');
|
||||
require('../play.pokemonshowdown.com/js/battle-scene-stub.js');
|
||||
// global.BattleText = require('../play.pokemonshowdown.com/data/text.js').BattleText;
|
||||
// global.BattleText = require('../play.pokemonshowdown.com/data/text/en.js').BattleText;
|
||||
require('../play.pokemonshowdown.com/js/battle-text-parser.js');
|
||||
require('../play.pokemonshowdown.com/js/battle.js');
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ const path = require('path');
|
||||
process.chdir(path.resolve(__dirname, '..'));
|
||||
|
||||
require(`../play.pokemonshowdown.com/js/battle-dex.js`); // only needed for toID
|
||||
global.BattleText = require(`../play.pokemonshowdown.com/data/text.js`).BattleText;
|
||||
global.BattleText = require(`../play.pokemonshowdown.com/data/text/en.js`).BattleText;
|
||||
require(`../play.pokemonshowdown.com/js/battle-text-parser.js`);
|
||||
|
||||
const parser = new BattleTextParser();
|
||||
|
||||
Reference in New Issue
Block a user