Name the icons that share one, instead of deriving them

The last commit answered "these 22 formes have no icon" with a generated
418-line table of every forme's parent and a loop that walked it, which is a lot
of apparatus for a list that fits on a screen and changes when a generation
does. Write the list.

Same 1556 files out of xyicons/, byte for byte. The two throws are the part the
derivation gave for free: an alias whose source icon has gone, and an alias for
a name that has since been drawn, are both mistakes worth stopping the build
for rather than quietly serving the wrong picture.

data/lib/formes.ts and tools/spritedata/formes.ts go; nothing else read either.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christopher Monsanto
2026-08-23 01:45:36 -04:00
parent 593f1c5c2d
commit 7493d948e6
3 changed files with 37 additions and 513 deletions

View File

@@ -1,418 +0,0 @@
// 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,6 +1,4 @@
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';
@@ -76,6 +74,36 @@ let xyIcons = forEachRule('src/minisprites/pokemon/gen6/*.png', {
cmds: [trimimg(), compresspng({config: 'MINISPRITE'})],
}, '%b');
// The games drew these formes no icon of their own, so gen 6 has none and
// nothing upstream does either: PS ships icons as one dexnum-indexed sheet, and
// its own copy of this list sits commented out in ps-pokemon.sheet.mjs under
// "alt forms with duplicate icons". Serve each the icon it shares. Only this
// set needs them; xy/ has real art for all 22.
let xyIconAliases: Record<string, string> = {
'araquanid-totem': 'araquanid',
'gourgeist-large': 'gourgeist',
'gourgeist-small': 'gourgeist',
'gourgeist-super': 'gourgeist',
'greninja-bond': 'greninja',
'gumshoos-totem': 'gumshoos',
'kommo-o-totem': 'kommo-o',
'lurantis-totem': 'lurantis',
'marowak-alola-totem': 'marowak-alola',
'mimikyu-busted': 'mimikyu',
'mimikyu-busted-totem': 'mimikyu',
'mimikyu-totem': 'mimikyu',
'pichu-spiky-eared': 'pichu',
'pumpkaboo-large': 'pumpkaboo',
'pumpkaboo-small': 'pumpkaboo',
'pumpkaboo-super': 'pumpkaboo',
'raticate-alola-totem': 'raticate-alola',
'ribombee-totem': 'ribombee',
'rockruff-dusk': 'rockruff',
'salazzle-totem': 'salazzle',
'togedemaru-totem': 'togedemaru',
'vikavolt-totem': 'vikavolt',
};
deploy(ctx => {
let byName = new Map<string, Sprite>();
for (let f of xyIcons) {
@@ -83,22 +111,15 @@ deploy(ctx => {
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)) {
for (let [name, from] of Object.entries(xyIconAliases)) {
let f = byName.get(from);
if (f === undefined) {
throw new Error(`No ${from} icon to publish as ${name}`);
}
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;
}
throw new Error(`${name} has an icon of its own now; drop the alias`);
}
byName.set(name, f);
}
for (let [name, f] of byName) {
smogonSpritecopy(ctx, f, 'xyicons', [name]);

View File

@@ -1,79 +0,0 @@
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);