mirror of
https://github.com/smogon/pokemon-showdown-client.git
synced 2026-10-25 03:31:07 -05:00
Support latest server-data scheme
This commit is contained in:
@@ -26,6 +26,7 @@ import {
|
||||
import type * as DexData from "./battle-dex-data";
|
||||
import type { Teams } from "./battle-teams";
|
||||
import { Config } from "./client-main";
|
||||
import { BattleLog } from "./battle-log";
|
||||
|
||||
export declare namespace Dex {
|
||||
/* eslint-disable @typescript-eslint/no-shadow */
|
||||
@@ -100,15 +101,17 @@ const TEXT_TABLES = {
|
||||
Move: 'Moves',
|
||||
} as const;
|
||||
|
||||
interface ClientDexTextEntry extends BattleTextEntry {
|
||||
interface ClientEffectTextEntry extends BattleTextEntry {
|
||||
name: string;
|
||||
desc: string;
|
||||
shortDesc: string;
|
||||
baseSpecies: string;
|
||||
forme: string | null;
|
||||
}
|
||||
|
||||
interface ClientDexText {
|
||||
getLanguage(): string;
|
||||
get(effect: TranslatableEffect, lang?: string): ClientDexTextEntry;
|
||||
get(effect: TranslatableEffect, lang?: string): ClientEffectTextEntry;
|
||||
termName(name: string, lang?: string): string;
|
||||
typeName(name: string, lang?: string): string;
|
||||
natureName(name: string, lang?: string): string;
|
||||
@@ -208,22 +211,28 @@ function getOtherName(table: OtherNameTable, name: string, lang: string): string
|
||||
return BattleText[lang]?.[table]?.[id] || BattleText.en?.[table]?.[id] || name;
|
||||
}
|
||||
|
||||
function getTextEntry(effect: TranslatableEffect, gen: number, lang: string): ClientDexTextEntry {
|
||||
/**
|
||||
* Does actually match ClientEffectTextEntry exactly.
|
||||
*/
|
||||
function getTextEntry(effect: TranslatableEffect, gen: number, lang: string): ClientEffectTextEntry {
|
||||
if (effect.effectType === 'Species') {
|
||||
const name = BattleText[lang]?.PokedexNames?.[effect.id] ||
|
||||
BattleText.en?.PokedexNames?.[effect.id] || effect.name;
|
||||
return { name, desc: '', shortDesc: '' };
|
||||
const entry = BattleText[lang]?.Pokedex?.[effect.id] || BattleText.en?.Pokedex?.[effect.id];
|
||||
return {
|
||||
name: entry?.name || effect.name,
|
||||
baseSpecies: entry?.baseSpecies || effect.baseSpecies,
|
||||
forme: entry?.forme || null,
|
||||
} as ClientEffectTextEntry;
|
||||
}
|
||||
if (effect.effectType === 'Type') {
|
||||
return { name: getOtherName('TypeNames', effect.name, lang), desc: '', shortDesc: '' };
|
||||
return { name: getOtherName('TypeNames', effect.name, lang) } as ClientEffectTextEntry;
|
||||
}
|
||||
if (effect.effectType === 'Nature') {
|
||||
return { name: getOtherName('NatureNames', effect.name, lang), desc: '', shortDesc: '' };
|
||||
return { name: getOtherName('NatureNames', effect.name, lang) } as ClientEffectTextEntry;
|
||||
}
|
||||
const tableName = TEXT_TABLES[effect.effectType];
|
||||
const english = BattleText.en?.[tableName]?.[effect.id] || {};
|
||||
const localized = BattleText[lang]?.[tableName]?.[effect.id] || {};
|
||||
const entry = {} as ClientDexTextEntry;
|
||||
const entry = {} as ClientEffectTextEntry;
|
||||
assignTextFields(entry, english);
|
||||
assignTextFields(entry, localized);
|
||||
for (let i = 1; i <= 8; i++) {
|
||||
|
||||
@@ -101,10 +101,12 @@ export class PSSearchResults extends preact.Component<{
|
||||
const pokemon = search.dex.species.get(id);
|
||||
if (!pokemon) return `<li class="result" value="${index}">Unrecognized pokemon</li>`;
|
||||
|
||||
const tagStart = (pokemon.forme ? pokemon.name.length - pokemon.forme.length - 1 : 0);
|
||||
const displayName = search.dex.text.get(pokemon).name;
|
||||
const pokemonText = search.dex.text.get(pokemon);
|
||||
const displayName = pokemonText.name;
|
||||
[matchStart, matchEnd] = getLocalizedMatch(displayName, pokemon.name, matchStart, matchEnd);
|
||||
const displayTagStart = displayName === pokemon.name ? tagStart : 0;
|
||||
const baseIndex = pokemonText.baseSpecies ? displayName.indexOf(pokemonText.baseSpecies) : -1;
|
||||
const baseStart = Math.max(0, baseIndex);
|
||||
const baseEnd = baseIndex < 0 ? 0 : baseIndex + pokemonText.baseSpecies.length;
|
||||
const stats = pokemon.baseStats;
|
||||
let bst = 0;
|
||||
for (const stat of Object.values(stats)) bst += stat;
|
||||
@@ -115,7 +117,9 @@ export class PSSearchResults extends preact.Component<{
|
||||
`data-entry="pokemon|${escapeHTML(pokemon.name)}">` +
|
||||
`<span class="col numcol">${escapeHTML(search.getTier(pokemon))}</span>` +
|
||||
`<span class="col iconcol"><span class="pixelated" style="${escapeHTML(Dex.getPokemonIcon(pokemon.id))}"></span></span>` +
|
||||
`<span class="col pokemonnamecol">${this.renderNameHTML(displayName, matchStart, matchEnd, displayTagStart)}</span>`;
|
||||
`<span class="col pokemonnamecol">${this.renderNameHTML(
|
||||
displayName, matchStart, matchEnd, baseStart, baseEnd
|
||||
)}</span>`;
|
||||
if (errorMessage) return `${buf}${errorMessage}</a></li>`;
|
||||
|
||||
buf += `<span class="col typecol">${pokemon.types.map(type =>
|
||||
@@ -158,8 +162,21 @@ export class PSSearchResults extends preact.Component<{
|
||||
return buf;
|
||||
}
|
||||
|
||||
renderNameHTML(name: string, matchStart: number, matchEnd: number, tagStart?: number) {
|
||||
renderNameHTML(name: string, matchStart: number, matchEnd: number, tagStart?: number, tagEnd?: number) {
|
||||
if (name === 'No Ability') return `<i>(no ability)</i>`;
|
||||
if (tagStart !== undefined && tagEnd !== undefined) {
|
||||
const renderPart = (start: number, end: number) => {
|
||||
if (!matchEnd || matchEnd <= start || matchStart >= end) return escapeHTML(name.slice(start, end));
|
||||
const boldStart = Math.max(start, matchStart);
|
||||
const boldEnd = Math.min(end, matchEnd);
|
||||
return escapeHTML(name.slice(start, boldStart)) +
|
||||
`<b>${escapeHTML(name.slice(boldStart, boldEnd))}</b>` +
|
||||
escapeHTML(name.slice(boldEnd, end));
|
||||
};
|
||||
return (tagStart ? `<small>${renderPart(0, tagStart)}</small>` : '') +
|
||||
renderPart(tagStart, tagEnd) +
|
||||
(tagEnd < name.length ? `<small>${renderPart(tagEnd, name.length)}</small>` : '');
|
||||
}
|
||||
|
||||
if (!matchEnd) {
|
||||
if (!tagStart) return escapeHTML(name);
|
||||
|
||||
@@ -535,7 +535,12 @@ export class BattleTextParser {
|
||||
const species = details.split(',')[0];
|
||||
const localizedSpecies = this.speciesName(species);
|
||||
if (nickname === localizedSpecies) return [pokemon.slice(0, 2), `**${localizedSpecies}**`];
|
||||
return [pokemon.slice(0, 2), `${nickname} (**${localizedSpecies}**)`];
|
||||
const template = BattleText[this.language]?.TermNames?.nicknamespecies ||
|
||||
BattleText.en?.TermNames?.nicknamespecies || '[NICKNAME] ([SPECIES])';
|
||||
return [pokemon.slice(0, 2), this.render(template, {
|
||||
NICKNAME: nickname,
|
||||
SPECIES: `**${localizedSpecies}**`,
|
||||
})];
|
||||
}
|
||||
|
||||
trainer(side: string) {
|
||||
@@ -622,8 +627,7 @@ export class BattleTextParser {
|
||||
speciesName(name?: string) {
|
||||
if (!name) return '';
|
||||
name = name.trim();
|
||||
const id = toID(name);
|
||||
return BattleText[this.language]?.PokedexNames?.[id] || BattleText.en?.PokedexNames?.[id] || name;
|
||||
return Dex.text.get(Dex.species.get(name), this.language).name || name;
|
||||
}
|
||||
|
||||
template(type: string, ...namespaces: (string | undefined)[]) {
|
||||
@@ -676,8 +680,7 @@ export class BattleTextParser {
|
||||
|
||||
static stat(stat: string, language = Dex.text.getLanguage()) {
|
||||
const id = stat || 'stats';
|
||||
const table = id === 'stats' ? 'TermNames' : 'StatNames';
|
||||
const name = BattleText[language]?.[table]?.[id] || BattleText.en?.[table]?.[id];
|
||||
const name = BattleText[language]?.StatNames?.[id] || BattleText.en?.StatNames?.[id];
|
||||
return typeof name === 'string' ? name : `???stat:${stat}???`;
|
||||
}
|
||||
static statMediumName(stat: string, language = Dex.text.getLanguage()) {
|
||||
@@ -690,9 +693,8 @@ export class BattleTextParser {
|
||||
}
|
||||
private statValue(stat: string): RenderValue {
|
||||
const id = stat || 'stats';
|
||||
const table = id === 'stats' ? 'TermNames' : 'StatNames';
|
||||
const grammar = BattleText[this.language]?.[table]?.[`${id}:grammar`] ||
|
||||
BattleText.en?.[table]?.[`${id}:grammar`];
|
||||
const grammar = BattleText[this.language]?.StatNames?.[`${id}:grammar`] ||
|
||||
BattleText.en?.StatNames?.[`${id}:grammar`];
|
||||
return {
|
||||
value: BattleTextParser.stat(stat, this.language),
|
||||
category: grammar || (stat ? 's' : 'p'),
|
||||
|
||||
2
play.pokemonshowdown.com/src/globals.d.ts
vendored
2
play.pokemonshowdown.com/src/globals.d.ts
vendored
@@ -8,7 +8,7 @@ type BattleTextEntry = { [templateName: string]: string | null | BattleTextEntry
|
||||
type BattleTextTable = { [id: string]: BattleTextEntry };
|
||||
type BattleTextData = {
|
||||
Default: BattleTextTable,
|
||||
PokedexNames: { [id: string]: string },
|
||||
Pokedex: { [id: string]: { name: string, baseSpecies: string, forme?: string, grammar?: string } },
|
||||
TermNames: { [id: string]: string },
|
||||
TypeNames: { [id: string]: string },
|
||||
NatureNames: { [id: string]: string },
|
||||
|
||||
55
test/battle-searchresults.test.js
Normal file
55
test/battle-searchresults.test.js
Normal file
@@ -0,0 +1,55 @@
|
||||
const assert = require('assert').strict;
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const {describe, it} = require('node:test');
|
||||
const vm = require('vm');
|
||||
|
||||
global.preact = {Component: function () {}};
|
||||
global.Config = {routes: {dex: 'dex.pokemonshowdown.com'}};
|
||||
global.Dex = {resourcePrefix: '', getPokemonIcon() { return ''; }};
|
||||
|
||||
const searchResultsPath = path.resolve(__dirname, '../play.pokemonshowdown.com/js/battle-searchresults.js');
|
||||
const PSSearchResults = vm.runInThisContext(
|
||||
`${fs.readFileSync(searchResultsPath, 'utf8')}\nPSSearchResults;`,
|
||||
{filename: searchResultsPath}
|
||||
);
|
||||
|
||||
describe('PSSearchResults', () => {
|
||||
it('renders everything outside the localized base species in small text', () => {
|
||||
const renderRow = (canonicalName, name, baseSpecies) => {
|
||||
const pokemon = {
|
||||
id: canonicalName.toLowerCase().replace(/[^a-z0-9]+/g, ''),
|
||||
name: canonicalName,
|
||||
baseStats: {hp: 1, atk: 1, def: 1, spa: 1, spd: 1, spe: 1},
|
||||
types: [],
|
||||
abilities: {},
|
||||
};
|
||||
const search = {
|
||||
dex: {
|
||||
gen: 2,
|
||||
species: {get() { return pokemon; }},
|
||||
text: {get() { return {name, baseSpecies}; }},
|
||||
},
|
||||
getTier() { return ''; },
|
||||
};
|
||||
const renderer = Object.create(PSSearchResults.prototype);
|
||||
renderer.props = {search};
|
||||
renderer.URL_ROOT = '//dex.pokemonshowdown.com/';
|
||||
renderer.speciesId = '';
|
||||
return renderer.renderPokemonRowHTML(0, pokemon.id, 0, 0);
|
||||
};
|
||||
|
||||
assert.match(renderRow('Thundurus', 'Thundurus-Incarnate', 'Thundurus'),
|
||||
/<span class="col pokemonnamecol">Thundurus<small>-Incarnate<\/small><\/span>/);
|
||||
assert.match(renderRow('Thundurus-Therian', 'Thundurus-Therian', 'Thundurus'),
|
||||
/<span class="col pokemonnamecol">Thundurus<small>-Therian<\/small><\/span>/);
|
||||
assert.match(renderRow('Mewtwo-Mega-X', 'Mega Mewtwo X', 'Mewtwo'),
|
||||
/<span class="col pokemonnamecol"><small>Mega <\/small>Mewtwo<small> X<\/small><\/span>/);
|
||||
assert.match(renderRow('Thundurus', 'Inkarnations-Voltolos', 'Voltolos'),
|
||||
/<span class="col pokemonnamecol"><small>Inkarnations-<\/small>Voltolos<\/span>/);
|
||||
assert.match(renderRow('Thundurus', 'ボルトロス・けしん', 'ボルトロス'),
|
||||
/<span class="col pokemonnamecol">ボルトロス<small>・けしん<\/small><\/span>/);
|
||||
assert.match(renderRow('Greninja-Ash', 'Sachanobi', 'Amphinobi'),
|
||||
/<span class="col pokemonnamecol"><small>Sachanobi<\/small><\/span>/);
|
||||
});
|
||||
});
|
||||
@@ -77,10 +77,12 @@ describe('BattleTextParser', {skip: hasBuiltText ? false : 'text data has not be
|
||||
it('uses canonical language tables with English field fallback', () => {
|
||||
global.BattleText.ja = {
|
||||
Default: {default: {hitCount: '[NUMBER]回 当たった!'}},
|
||||
PokedexNames: {ironleaves: 'テツノイサハ'},
|
||||
Pokedex: {ironleaves: {name: 'テツノイサハ', baseSpecies: 'テツノイサハ'}},
|
||||
TypeNames: {fire: 'ほのお'},
|
||||
NatureNames: {adamant: 'いじっぱり'},
|
||||
TermNames: {egggroup: 'タマゴグループ', moves: '技'},
|
||||
TermNames: {
|
||||
egggroup: 'タマゴグループ', moves: '技', nicknamespecies: '[NICKNAME]([SPECIES])',
|
||||
},
|
||||
TagNames: {physical: 'ぶつり'},
|
||||
GenderNames: {female: 'メス'},
|
||||
EggGroupNames: {humanlike: 'ひとがた'},
|
||||
@@ -117,13 +119,17 @@ describe('BattleTextParser', {skip: hasBuiltText ? false : 'text data has not be
|
||||
assert.equal(global.Dex.text.colorName('Purple', 'ja'), 'むらさき');
|
||||
assert.deepEqual(
|
||||
parser.pokemonFull('p1a: Salad', 'Iron Leaves'),
|
||||
['p1', 'Salad (**テツノイサハ**)']
|
||||
['p1', 'Salad(**テツノイサハ**)']
|
||||
);
|
||||
assert.deepEqual(
|
||||
parser.pokemonFull('p1a: Iron Leaves', 'Iron Leaves'),
|
||||
['p1', 'Iron Leaves (**テツノイサハ**)']
|
||||
['p1', 'Iron Leaves(**テツノイサハ**)']
|
||||
);
|
||||
assert.deepEqual(parser.pokemonFull('p1a: テツノイサハ', 'Iron Leaves'), ['p1', '**テツノイサハ**']);
|
||||
assert.equal(
|
||||
parser.extractMessage('|switch|p1a: Salad|Iron Leaves, L50|100/100'),
|
||||
'Go! Salad(**テツノイサハ**)!\n'
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back by field for past-generation text', () => {
|
||||
@@ -340,14 +346,14 @@ describe('BattleTextParser', {skip: hasBuiltText ? false : 'text data has not be
|
||||
it('uses localized stat grammar', () => {
|
||||
global.BattleText.fr = {
|
||||
Default: {default: {boost: '[STAT:definite:capitalize] de [POKEMON] augmente !'}},
|
||||
TermNames: {stats: 'Stats', 'stats:grammar': 'fp'},
|
||||
StatNames: {atk: 'Attaque', 'atk:grammar': 'fs'},
|
||||
TermNames: {stats: 'Stats'},
|
||||
StatNames: {stats: 'stats', 'stats:grammar': 'fp', atk: 'Attaque', 'atk:grammar': 'fs'},
|
||||
StatMediumNames: {atk: 'Atq.'},
|
||||
StatShortNames: {atk: 'Atq'},
|
||||
};
|
||||
const parser = new BattleTextParser('p1', 'fr');
|
||||
assert.equal(BattleTextParser.stat('atk', 'fr'), 'Attaque');
|
||||
assert.equal(BattleTextParser.stat('', 'fr'), 'Stats');
|
||||
assert.equal(BattleTextParser.stat('', 'fr'), 'stats');
|
||||
assert.equal(BattleTextParser.statMediumName('atk', 'fr'), 'Atq.');
|
||||
assert.equal(BattleTextParser.statShortName('atk', 'fr'), 'Atq');
|
||||
assert.equal(parser.extractMessage('|-boost|p1a: Mew|atk|1'), 'L’Attaque de Mew augmente !\n');
|
||||
|
||||
Reference in New Issue
Block a user