Fall back to the nearest forme with an icon

xyicons/ is built from src/minisprites/pokemon/gen6 alone, which holds no
totem, no Mimikyu-Busted and no Gourgeist or Pumpkaboo size forme. Nor does
anywhere else in the tree: home/ has the size formes as 70x112 HOME renders
against gen6's 20x26 XY icons, and no directory anywhere has a totem icon at
all. There is nothing upstream to take either, since PS ships icons as one
dexnum-indexed sheet rather than as files per name.

That is because the games drew those formes no icon of their own. PS's client
sends each to the nearest forme that has art, and ps-pokemon.sheet.mjs already
carries the list, commented out, under "alt forms with duplicate icons". Walk
the same chain rather than leave 22 names 404ing.

The chain cannot be read off a published name, since a base name carries dashes
too (kommo-o, mr-mime, porygon-z), and dropping one word blindly strands
Toxtricity-Low-Key on a Toxtricity-Low that does not exist. So
tools/spritedata/formes.ts writes data/lib/formes.ts, pointing each forme at the
longest run of its leading words that names a forme of its own, and at the base
species otherwise.

xyicons/ gains 22: the 12 totems, Mimikyu-Busted, the six Gourgeist and
Pumpkaboo sizes, Greninja-Bond, Rockruff-Dusk and Pichu-Spiky-Eared. Each is
byte for byte the sprite it fell back to. Togedemaru-Totem is among them and the
server has never had it, so that one is a gain rather than a restoration. Only
this set walks the chain; xy/ has real art for all 22. The species PS keeps but
nobody ever drew, missingno and the 15 pokestar entries, have no ancestor with
an icon and so still get nothing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christopher Monsanto
2026-08-23 01:33:32 -04:00
parent 35102330aa
commit f92f04716a
3 changed files with 523 additions and 1 deletions

418
data/lib/formes.ts Normal file
View File

@@ -0,0 +1,418 @@
// Generated by tools/spritedata/formes.ts; do not edit.
// Each forme's published name -> the name it falls back on for art.
export const FORME_PARENTS: Record<string, string> = {
'venusaur-mega': 'venusaur',
'charizard-mega-x': 'charizard',
'charizard-mega-y': 'charizard',
'blastoise-mega': 'blastoise',
'beedrill-mega': 'beedrill',
'pidgeot-mega': 'pidgeot',
'rattata-alola': 'rattata',
'raticate-alola': 'raticate',
'raticate-alola-totem': 'raticate-alola',
'pikachu-original': 'pikachu',
'pikachu-hoenn': 'pikachu',
'pikachu-sinnoh': 'pikachu',
'pikachu-unova': 'pikachu',
'pikachu-kalos': 'pikachu',
'pikachu-alola': 'pikachu',
'pikachu-partner': 'pikachu',
'pikachu-starter': 'pikachu',
'pikachu-world': 'pikachu',
'pikachu-rock-star': 'pikachu',
'pikachu-belle': 'pikachu',
'pikachu-pop-star': 'pikachu',
'pikachu-phd': 'pikachu',
'pikachu-libre': 'pikachu',
'pikachu-cosplay': 'pikachu',
'raichu-alola': 'raichu',
'raichu-mega-x': 'raichu',
'raichu-mega-y': 'raichu',
'sandshrew-alola': 'sandshrew',
'sandslash-alola': 'sandslash',
'clefable-mega': 'clefable',
'vulpix-alola': 'vulpix',
'ninetales-alola': 'ninetales',
'diglett-alola': 'diglett',
'dugtrio-alola': 'dugtrio',
'meowth-alola': 'meowth',
'meowth-galar': 'meowth',
'persian-alola': 'persian',
'growlithe-hisui': 'growlithe',
'arcanine-hisui': 'arcanine',
'alakazam-mega': 'alakazam',
'victreebel-mega': 'victreebel',
'geodude-alola': 'geodude',
'graveler-alola': 'graveler',
'golem-alola': 'golem',
'ponyta-galar': 'ponyta',
'rapidash-galar': 'rapidash',
'slowpoke-galar': 'slowpoke',
'slowbro-mega': 'slowbro',
'slowbro-galar': 'slowbro',
'farfetchd-galar': 'farfetchd',
'grimer-alola': 'grimer',
'muk-alola': 'muk',
'gengar-mega': 'gengar',
'voltorb-hisui': 'voltorb',
'electrode-hisui': 'electrode',
'exeggutor-alola': 'exeggutor',
'marowak-alola': 'marowak',
'marowak-alola-totem': 'marowak-alola',
'weezing-galar': 'weezing',
'kangaskhan-mega': 'kangaskhan',
'starmie-mega': 'starmie',
'mr-mime-galar': 'mr-mime',
'pinsir-mega': 'pinsir',
'tauros-paldea-combat': 'tauros',
'tauros-paldea-blaze': 'tauros',
'tauros-paldea-aqua': 'tauros',
'gyarados-mega': 'gyarados',
'eevee-starter': 'eevee',
'aerodactyl-mega': 'aerodactyl',
'articuno-galar': 'articuno',
'zapdos-galar': 'zapdos',
'moltres-galar': 'moltres',
'dragonite-mega': 'dragonite',
'mewtwo-mega-x': 'mewtwo',
'mewtwo-mega-y': 'mewtwo',
'meganium-mega': 'meganium',
'typhlosion-hisui': 'typhlosion',
'feraligatr-mega': 'feraligatr',
'pichu-spiky-eared': 'pichu',
'ampharos-mega': 'ampharos',
'wooper-paldea': 'wooper',
'slowking-galar': 'slowking',
'unown-b': 'unown',
'unown-c': 'unown',
'unown-d': 'unown',
'unown-e': 'unown',
'unown-f': 'unown',
'unown-g': 'unown',
'unown-h': 'unown',
'unown-i': 'unown',
'unown-j': 'unown',
'unown-k': 'unown',
'unown-l': 'unown',
'unown-m': 'unown',
'unown-n': 'unown',
'unown-o': 'unown',
'unown-p': 'unown',
'unown-q': 'unown',
'unown-r': 'unown',
'unown-s': 'unown',
'unown-t': 'unown',
'unown-u': 'unown',
'unown-v': 'unown',
'unown-w': 'unown',
'unown-x': 'unown',
'unown-y': 'unown',
'unown-z': 'unown',
'unown-exclamation': 'unown',
'unown-question': 'unown',
'steelix-mega': 'steelix',
'qwilfish-hisui': 'qwilfish',
'scizor-mega': 'scizor',
'heracross-mega': 'heracross',
'sneasel-hisui': 'sneasel',
'corsola-galar': 'corsola',
'skarmory-mega': 'skarmory',
'houndoom-mega': 'houndoom',
'tyranitar-mega': 'tyranitar',
'sceptile-mega': 'sceptile',
'blaziken-mega': 'blaziken',
'swampert-mega': 'swampert',
'zigzagoon-galar': 'zigzagoon',
'linoone-galar': 'linoone',
'gardevoir-mega': 'gardevoir',
'sableye-mega': 'sableye',
'mawile-mega': 'mawile',
'aggron-mega': 'aggron',
'medicham-mega': 'medicham',
'manectric-mega': 'manectric',
'sharpedo-mega': 'sharpedo',
'camerupt-mega': 'camerupt',
'altaria-mega': 'altaria',
'castform-sunny': 'castform',
'castform-rainy': 'castform',
'castform-snowy': 'castform',
'banette-mega': 'banette',
'chimecho-mega': 'chimecho',
'absol-mega': 'absol',
'absol-mega-z': 'absol-mega',
'glalie-mega': 'glalie',
'salamence-mega': 'salamence',
'metagross-mega': 'metagross',
'latias-mega': 'latias',
'latios-mega': 'latios',
'kyogre-primal': 'kyogre',
'groudon-primal': 'groudon',
'rayquaza-mega': 'rayquaza',
'deoxys-attack': 'deoxys',
'deoxys-defense': 'deoxys',
'deoxys-speed': 'deoxys',
'staraptor-mega': 'staraptor',
'burmy-sandy': 'burmy',
'burmy-trash': 'burmy',
'wormadam-sandy': 'wormadam',
'wormadam-trash': 'wormadam',
'cherrim-sunshine': 'cherrim',
'shellos-east': 'shellos',
'gastrodon-east': 'gastrodon',
'lopunny-mega': 'lopunny',
'garchomp-mega': 'garchomp',
'garchomp-mega-z': 'garchomp-mega',
'lucario-mega': 'lucario',
'lucario-mega-z': 'lucario-mega',
'abomasnow-mega': 'abomasnow',
'gallade-mega': 'gallade',
'froslass-mega': 'froslass',
'rotom-heat': 'rotom',
'rotom-wash': 'rotom',
'rotom-frost': 'rotom',
'rotom-fan': 'rotom',
'rotom-mow': 'rotom',
'dialga-origin': 'dialga',
'palkia-origin': 'palkia',
'heatran-mega': 'heatran',
'giratina-origin': 'giratina',
'darkrai-mega': 'darkrai',
'shaymin-sky': 'shaymin',
'arceus-fighting': 'arceus',
'arceus-flying': 'arceus',
'arceus-poison': 'arceus',
'arceus-ground': 'arceus',
'arceus-rock': 'arceus',
'arceus-bug': 'arceus',
'arceus-ghost': 'arceus',
'arceus-steel': 'arceus',
'arceus-fire': 'arceus',
'arceus-water': 'arceus',
'arceus-grass': 'arceus',
'arceus-electric': 'arceus',
'arceus-psychic': 'arceus',
'arceus-ice': 'arceus',
'arceus-dragon': 'arceus',
'arceus-dark': 'arceus',
'arceus-fairy': 'arceus',
'emboar-mega': 'emboar',
'samurott-hisui': 'samurott',
'excadrill-mega': 'excadrill',
'audino-mega': 'audino',
'scolipede-mega': 'scolipede',
'lilligant-hisui': 'lilligant',
'basculin-blue-striped': 'basculin',
'basculin-white-striped': 'basculin',
'darumaka-galar': 'darumaka',
'darmanitan-zen': 'darmanitan',
'darmanitan-galar': 'darmanitan',
'darmanitan-galar-zen': 'darmanitan-galar',
'scrafty-mega': 'scrafty',
'yamask-galar': 'yamask',
'zorua-hisui': 'zorua',
'zoroark-hisui': 'zoroark',
'deerling-summer': 'deerling',
'deerling-autumn': 'deerling',
'deerling-winter': 'deerling',
'sawsbuck-summer': 'sawsbuck',
'sawsbuck-autumn': 'sawsbuck',
'sawsbuck-winter': 'sawsbuck',
'eelektross-mega': 'eelektross',
'chandelure-mega': 'chandelure',
'stunfisk-galar': 'stunfisk',
'golurk-mega': 'golurk',
'braviary-hisui': 'braviary',
'tornadus-therian': 'tornadus',
'thundurus-therian': 'thundurus',
'landorus-therian': 'landorus',
'kyurem-white': 'kyurem',
'kyurem-black': 'kyurem',
'keldeo-resolute': 'keldeo',
'meloetta-pirouette': 'meloetta',
'genesect-douse': 'genesect',
'genesect-shock': 'genesect',
'genesect-burn': 'genesect',
'genesect-chill': 'genesect',
'chesnaught-mega': 'chesnaught',
'delphox-mega': 'delphox',
'greninja-bond': 'greninja',
'greninja-ash': 'greninja',
'greninja-mega': 'greninja',
'vivillon-icy-snow': 'vivillon',
'vivillon-polar': 'vivillon',
'vivillon-tundra': 'vivillon',
'vivillon-continental': 'vivillon',
'vivillon-garden': 'vivillon',
'vivillon-elegant': 'vivillon',
'vivillon-modern': 'vivillon',
'vivillon-marine': 'vivillon',
'vivillon-archipelago': 'vivillon',
'vivillon-high-plains': 'vivillon',
'vivillon-sandstorm': 'vivillon',
'vivillon-river': 'vivillon',
'vivillon-monsoon': 'vivillon',
'vivillon-savanna': 'vivillon',
'vivillon-sun': 'vivillon',
'vivillon-ocean': 'vivillon',
'vivillon-jungle': 'vivillon',
'vivillon-fancy': 'vivillon',
'vivillon-pokeball': 'vivillon',
'pyroar-mega': 'pyroar',
'flabebe-yellow': 'flabebe',
'flabebe-orange': 'flabebe',
'flabebe-blue': 'flabebe',
'flabebe-white': 'flabebe',
'floette-yellow': 'floette',
'floette-orange': 'floette',
'floette-blue': 'floette',
'floette-white': 'floette',
'floette-eternal': 'floette',
'floette-mega': 'floette',
'florges-yellow': 'florges',
'florges-orange': 'florges',
'florges-blue': 'florges',
'florges-white': 'florges',
'furfrou-heart': 'furfrou',
'furfrou-star': 'furfrou',
'furfrou-diamond': 'furfrou',
'furfrou-debutante': 'furfrou',
'furfrou-matron': 'furfrou',
'furfrou-dandy': 'furfrou',
'furfrou-la-reine': 'furfrou',
'furfrou-kabuki': 'furfrou',
'furfrou-pharaoh': 'furfrou',
'meowstic-f': 'meowstic',
'meowstic-m-mega': 'meowstic',
'meowstic-f-mega': 'meowstic-f',
'aegislash-blade': 'aegislash',
'malamar-mega': 'malamar',
'barbaracle-mega': 'barbaracle',
'dragalge-mega': 'dragalge',
'hawlucha-mega': 'hawlucha',
'sliggoo-hisui': 'sliggoo',
'goodra-hisui': 'goodra',
'pumpkaboo-small': 'pumpkaboo',
'pumpkaboo-large': 'pumpkaboo',
'pumpkaboo-super': 'pumpkaboo',
'gourgeist-small': 'gourgeist',
'gourgeist-large': 'gourgeist',
'gourgeist-super': 'gourgeist',
'avalugg-hisui': 'avalugg',
'xerneas-neutral': 'xerneas',
'zygarde-10': 'zygarde',
'zygarde-complete': 'zygarde',
'zygarde-mega': 'zygarde',
'diancie-mega': 'diancie',
'hoopa-unbound': 'hoopa',
'decidueye-hisui': 'decidueye',
'gumshoos-totem': 'gumshoos',
'vikavolt-totem': 'vikavolt',
'crabominable-mega': 'crabominable',
'oricorio-pom-pom': 'oricorio',
'oricorio-pau': 'oricorio',
'oricorio-sensu': 'oricorio',
'ribombee-totem': 'ribombee',
'rockruff-dusk': 'rockruff',
'lycanroc-midnight': 'lycanroc',
'lycanroc-dusk': 'lycanroc',
'wishiwashi-school': 'wishiwashi',
'araquanid-totem': 'araquanid',
'lurantis-totem': 'lurantis',
'salazzle-totem': 'salazzle',
'golisopod-mega': 'golisopod',
'silvally-fighting': 'silvally',
'silvally-flying': 'silvally',
'silvally-poison': 'silvally',
'silvally-ground': 'silvally',
'silvally-rock': 'silvally',
'silvally-bug': 'silvally',
'silvally-ghost': 'silvally',
'silvally-steel': 'silvally',
'silvally-fire': 'silvally',
'silvally-water': 'silvally',
'silvally-grass': 'silvally',
'silvally-electric': 'silvally',
'silvally-psychic': 'silvally',
'silvally-ice': 'silvally',
'silvally-dragon': 'silvally',
'silvally-dark': 'silvally',
'silvally-fairy': 'silvally',
'minior-meteor': 'minior',
'minior-orange': 'minior',
'minior-yellow': 'minior',
'minior-green': 'minior',
'minior-blue': 'minior',
'minior-indigo': 'minior',
'minior-violet': 'minior',
'togedemaru-totem': 'togedemaru',
'mimikyu-busted': 'mimikyu',
'mimikyu-totem': 'mimikyu',
'mimikyu-busted-totem': 'mimikyu-busted',
'drampa-mega': 'drampa',
'kommo-o-totem': 'kommo-o',
'necrozma-dusk-mane': 'necrozma',
'necrozma-dawn-wings': 'necrozma',
'necrozma-ultra': 'necrozma',
'magearna-original': 'magearna',
'magearna-mega': 'magearna',
'magearna-original-mega': 'magearna-original',
'zeraora-mega': 'zeraora',
'cramorant-gulping': 'cramorant',
'cramorant-gorging': 'cramorant',
'toxtricity-low-key': 'toxtricity',
'sinistea-antique': 'sinistea',
'polteageist-antique': 'polteageist',
'alcremie-ruby-cream': 'alcremie',
'alcremie-matcha-cream': 'alcremie',
'alcremie-mint-cream': 'alcremie',
'alcremie-lemon-cream': 'alcremie',
'alcremie-salted-cream': 'alcremie',
'alcremie-ruby-swirl': 'alcremie',
'alcremie-caramel-swirl': 'alcremie',
'alcremie-rainbow-swirl': 'alcremie',
'falinks-mega': 'falinks',
'eiscue-noice': 'eiscue',
'indeedee-f': 'indeedee',
'morpeko-hangry': 'morpeko',
'zacian-crowned': 'zacian',
'zamazenta-crowned': 'zamazenta',
'eternatus-eternamax': 'eternatus',
'urshifu-rapid-strike': 'urshifu',
'zarude-dada': 'zarude',
'calyrex-ice': 'calyrex',
'calyrex-shadow': 'calyrex',
'ursaluna-bloodmoon': 'ursaluna',
'basculegion-f': 'basculegion',
'enamorus-therian': 'enamorus',
'oinkologne-f': 'oinkologne',
'maushold-four': 'maushold',
'squawkabilly-blue': 'squawkabilly',
'squawkabilly-yellow': 'squawkabilly',
'squawkabilly-white': 'squawkabilly',
'scovillain-mega': 'scovillain',
'palafin-hero': 'palafin',
'glimmora-mega': 'glimmora',
'tatsugiri-droopy': 'tatsugiri',
'tatsugiri-stretchy': 'tatsugiri',
'tatsugiri-curly-mega': 'tatsugiri',
'tatsugiri-droopy-mega': 'tatsugiri-droopy',
'tatsugiri-stretchy-mega': 'tatsugiri-stretchy',
'dudunsparce-three-segment': 'dudunsparce',
'baxcalibur-mega': 'baxcalibur',
'gimmighoul-roaming': 'gimmighoul',
'poltchageist-artisan': 'poltchageist',
'sinistcha-masterpiece': 'sinistcha',
'ogerpon-wellspring': 'ogerpon',
'ogerpon-hearthflame': 'ogerpon',
'ogerpon-cornerstone': 'ogerpon',
'ogerpon-teal-tera': 'ogerpon',
'ogerpon-wellspring-tera': 'ogerpon-wellspring',
'ogerpon-hearthflame-tera': 'ogerpon-hearthflame',
'ogerpon-cornerstone-tera': 'ogerpon-cornerstone',
'terapagos-terastal': 'terapagos',
'terapagos-stellar': 'terapagos',
'crucibelle-mega': 'crucibelle',
'venomicon-epilogue': 'venomicon',
'ramnarok-radiant': 'ramnarok',
};

View File

@@ -1,4 +1,6 @@
import {FORME_PARENTS} from '@smogon/sprite-data/formes.ts';
import {gen10Modelslike} from './rules/modelslike.ts';
import {type Sprite, publishedNames} from './rules/publish.ts';
import {forEachRule} from './tools/build/artifact.ts';
@@ -75,8 +77,31 @@ let xyIcons = forEachRule('src/minisprites/pokemon/gen6/*.png', {
}, '%b');
deploy(ctx => {
let byName = new Map<string, Sprite>();
for (let f of xyIcons) {
smogonSpritecopy(ctx, f, 'xyicons', publishedNames(f));
for (let name of publishedNames(f)) {
byName.set(name, f);
}
}
// The games drew no icon for some formes, so PS's own sheet sends them to
// the nearest one that has art: Gourgeist-Super to Gourgeist,
// Mimikyu-Busted-Totem to Mimikyu-Busted and on to Mimikyu. Walk the same
// chain, so a name the data knows gets an icon rather than a 404. Only
// this set needs it, since xy/ has real art for every one of them.
for (let name of Object.keys(FORME_PARENTS)) {
if (byName.has(name)) {
continue;
}
for (let p = FORME_PARENTS[name]; p !== undefined; p = FORME_PARENTS[p]) {
let f = byName.get(p);
if (f) {
byName.set(name, f);
break;
}
}
}
for (let [name, f] of byName) {
smogonSpritecopy(ctx, f, 'xyicons', [name]);
}
});

View File

@@ -0,0 +1,79 @@
import {encode, publishedName, smogon} from '@smogon/sprite-data/index.ts';
import {species} from './pokedex.ts';
// Regenerates data/lib/formes.ts from a pokemon-showdown checkout:
//
// node tools/spritedata/formes.ts ~/smogon/pokemon-showdown > data/lib/formes.ts
//
// The games drew some formes no art of their own, and a set that has none
// falls back on the nearest forme it is a refinement of, up to the base
// species: PS's own icon sheet points Gourgeist-Super at Gourgeist's slot and
// Mimikyu-Busted-Totem at Mimikyu's. That chain can't be recovered from a
// published name alone, since a base name carries dashes too (kommo-o,
// mr-mime, porygon-z), so it is written down here.
//
// The parent is the longest run of leading words that names a forme of its
// own, and the base species otherwise. Dropping one word blindly would strand
// Toxtricity-Low-Key on a Toxtricity-Low that does not exist.
let ps = process.argv[2];
if (!ps) {
throw new Error('usage: node tools/spritedata/formes.ts <pokemon-showdown checkout>');
}
// `forme` comes in encoded; `base` does not.
function published(base: string, forme: string): string {
let extra = new Map<string, string>();
if (forme) {
extra.set('o', forme);
}
return publishedName({kind: 's', name: encode(base), extra}, smogon);
}
let all = species(ps);
// Which formes each base species actually has, so a parent can be checked.
let formes = new Map<string, Set<string>>();
for (let {base, forme} of all) {
let set = formes.get(base);
if (!set) {
set = new Set();
formes.set(base, set);
}
set.add(encode(forme));
}
function parent(base: string, forme: string): string {
let words = forme.split('_');
for (let n = words.length - 1; n > 0; n--) {
let shorter = words.slice(0, n).join('_');
if (formes.get(base)?.has(shorter)) {
return shorter;
}
}
return '';
}
let out = '';
out += '// Generated by tools/spritedata/formes.ts; do not edit.\n';
out += "// Each forme's published name -> the name it falls back on for art.\n\n";
out += 'export const FORME_PARENTS: Record<string, string> = {\n';
let seen = new Set<string>();
for (let {base, forme} of all) {
if (!forme) {
continue;
}
let name = published(base, encode(forme));
// Where formeOrder gives one name to several slots, they share a parent.
if (seen.has(name)) {
continue;
}
seen.add(name);
out += ` '${name}': '${published(base, parent(base, encode(forme)))}',\n`;
}
out += '};\n';
process.stdout.write(out);