diff --git a/play.pokemonshowdown.com/src/battle-dex.ts b/play.pokemonshowdown.com/src/battle-dex.ts
index 637baf7d2..f63bef0e9 100644
--- a/play.pokemonshowdown.com/src/battle-dex.ts
+++ b/play.pokemonshowdown.com/src/battle-dex.ts
@@ -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++) {
diff --git a/play.pokemonshowdown.com/src/battle-searchresults.tsx b/play.pokemonshowdown.com/src/battle-searchresults.tsx
index 421b34bd2..8d6ebb87e 100644
--- a/play.pokemonshowdown.com/src/battle-searchresults.tsx
+++ b/play.pokemonshowdown.com/src/battle-searchresults.tsx
@@ -101,10 +101,12 @@ export class PSSearchResults extends preact.Component<{
const pokemon = search.dex.species.get(id);
if (!pokemon) return `
Unrecognized pokemon`;
- 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)}">` +
`${escapeHTML(search.getTier(pokemon))}` +
`` +
- `${this.renderNameHTML(displayName, matchStart, matchEnd, displayTagStart)}`;
+ `${this.renderNameHTML(
+ displayName, matchStart, matchEnd, baseStart, baseEnd
+ )}`;
if (errorMessage) return `${buf}${errorMessage}`;
buf += `${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 `(no ability)`;
+ 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)) +
+ `${escapeHTML(name.slice(boldStart, boldEnd))}` +
+ escapeHTML(name.slice(boldEnd, end));
+ };
+ return (tagStart ? `${renderPart(0, tagStart)}` : '') +
+ renderPart(tagStart, tagEnd) +
+ (tagEnd < name.length ? `${renderPart(tagEnd, name.length)}` : '');
+ }
if (!matchEnd) {
if (!tagStart) return escapeHTML(name);
diff --git a/play.pokemonshowdown.com/src/battle-text-parser.ts b/play.pokemonshowdown.com/src/battle-text-parser.ts
index 5a0e11e42..57f3bfc0d 100644
--- a/play.pokemonshowdown.com/src/battle-text-parser.ts
+++ b/play.pokemonshowdown.com/src/battle-text-parser.ts
@@ -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'),
diff --git a/play.pokemonshowdown.com/src/globals.d.ts b/play.pokemonshowdown.com/src/globals.d.ts
index 28f79e3be..bb0e98480 100644
--- a/play.pokemonshowdown.com/src/globals.d.ts
+++ b/play.pokemonshowdown.com/src/globals.d.ts
@@ -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 },
diff --git a/test/battle-searchresults.test.js b/test/battle-searchresults.test.js
new file mode 100644
index 000000000..383980874
--- /dev/null
+++ b/test/battle-searchresults.test.js
@@ -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'),
+ /Thundurus-Incarnate<\/small><\/span>/);
+ assert.match(renderRow('Thundurus-Therian', 'Thundurus-Therian', 'Thundurus'),
+ /Thundurus-Therian<\/small><\/span>/);
+ assert.match(renderRow('Mewtwo-Mega-X', 'Mega Mewtwo X', 'Mewtwo'),
+ /Mega <\/small>Mewtwo X<\/small><\/span>/);
+ assert.match(renderRow('Thundurus', 'Inkarnations-Voltolos', 'Voltolos'),
+ /Inkarnations-<\/small>Voltolos<\/span>/);
+ assert.match(renderRow('Thundurus', 'ボルトロス・けしん', 'ボルトロス'),
+ /ボルトロス・けしん<\/small><\/span>/);
+ assert.match(renderRow('Greninja-Ash', 'Sachanobi', 'Amphinobi'),
+ /Sachanobi<\/small><\/span>/);
+ });
+});
diff --git a/test/battle-text-parser.test.js b/test/battle-text-parser.test.js
index 02ea6340f..b1566154a 100644
--- a/test/battle-text-parser.test.js
+++ b/test/battle-text-parser.test.js
@@ -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');