Lend the borrowed icons to the sheet and forumsprites too

The alias table sat in smogon.build.ts, so only xyicons/ got it. Two other sets
publish the same directory and had the same holes: the smogdex sheet, where the
dex asks for a sprite- class per species it renders, and forumsprites. Neither
had a class or a file for a totem, a Gourgeist size or Mimikyu-Busted.

So the table belongs beside the names, not beside one set that uses them. It
moves to data/lib as ICON_ALIASES with an iconNames() to apply it, which the
sheet calls in place of smogonNames() and the two deploys reach through a
NameOpts on publishedNames(). Keyed by the name holding the icon now, since
that is the direction all three consumers read it in. It stays out of
smogonNames(): src/models has real art for every one of these, and in xy/ the
lending name would be claimed by the base sprite and shadow the forme's own.

Greninja-Bond goes the other way, into SPECIES_ALIASES, because it is not an
icon shortage. Battle Bond's form looks like any other Greninja, and the tree
agrees: all seven -obond files under src/, in gen5, pmd and previews/gen9, are
byte for byte their base, and no model was ever drawn. xy/ was serving the gen 5
rendition of a Greninja; it now serves the animation, and the seven duplicates
go.

The sheet gains 22 classes, forumsprites 22 files. xy/ changes one file,
greninja-bond.gif, which is now greninja.gif.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Christopher Monsanto
2026-08-23 02:08:40 -04:00
parent da25d70f78
commit 79f0ef6497
12 changed files with 64 additions and 53 deletions

View File

@@ -69,7 +69,7 @@ deploy(async ctx => {
await itemspritecopy(manifest, f, {dir: `${ASSETS}/forumsprites`});
}
for (let f of forumG6) {
await spritecopy(manifest, f, {dir: `${ASSETS}/forumsprites`}, true);
await spritecopy(manifest, f, {dir: `${ASSETS}/forumsprites`}, {allowUnknown: true, icons: true});
}
manifest.write('__meta/forumsprites/manifest.json');
});

View File

@@ -84,16 +84,54 @@ export const ITEM_ALIASES: Record<string, string[]> = {
// The second name a sprite answers to, where one picture publishes twice.
// Meowstic is a disagreement: PS's is the male, baseForme M with Meowstic-F the
// alt forme, while the dex splits the pair evenly and calls that entry
// Meowstic-M. Toxtricity is a shortage: the games drew one Gigantamax
// Toxtricity and not two, which is why PS's own icon sheet gives Amped and
// Low-Key a single slot and its animations no low-key gmax at all. Keyed and
// valued in published smogon form, because only that side asks; PS wants
// `meowstic` and `toxtricitygmax`, which is what the filenames already say.
// Meowstic-M. The other two are one picture wearing two names. The games drew
// one Gigantamax Toxtricity and not two, which is why PS's own icon sheet gives
// Amped and Low-Key a single slot and its animations no low-key gmax at all.
// Greninja-Bond is Battle Bond's form and looks like any other Greninja, unlike
// Greninja-Ash, which is drawn: the gen 5 renditions of the two are byte for
// byte the base sprite, and nothing ever drew a model. Keyed and valued in
// published smogon form, because only that side asks; PS wants `meowstic`,
// `toxtricitygmax` and `greninja`, which is what the filenames already say.
export const SPECIES_ALIASES: Record<string, string[]> = {
greninja: ['greninja-bond'],
meowstic: ['meowstic-m'],
'toxtricity-gmax': ['toxtricity-low-key-gmax'],
};
// Formes the gen 6 icon set has no art for, because the games drew them none.
// PS's client says the same thing in its own sheet, where each of these sits at
// the slot of the forme it shares, under "alt forms with duplicate icons".
// Keyed by the name that has the icon, in published smogon form.
//
// The icon sets alone borrow this way. src/models has a real animation for
// every one of these, so smogonNames() must not know about them: in xy/ the
// borrowing name would be claimed by the base sprite and shadow the forme's
// own art.
export const ICON_ALIASES: Record<string, string[]> = {
araquanid: ['araquanid-totem'],
gourgeist: ['gourgeist-large', 'gourgeist-small', 'gourgeist-super'],
gumshoos: ['gumshoos-totem'],
'kommo-o': ['kommo-o-totem'],
lurantis: ['lurantis-totem'],
'marowak-alola': ['marowak-alola-totem'],
mimikyu: ['mimikyu-busted', 'mimikyu-busted-totem', 'mimikyu-totem'],
pichu: ['pichu-spiky-eared'],
pumpkaboo: ['pumpkaboo-large', 'pumpkaboo-small', 'pumpkaboo-super'],
'raticate-alola': ['raticate-alola-totem'],
ribombee: ['ribombee-totem'],
rockruff: ['rockruff-dusk'],
salazzle: ['salazzle-totem'],
togedemaru: ['togedemaru-totem'],
vikavolt: ['vikavolt-totem'],
};
// Every name a gen 6 icon answers to: its own, and any forme with no icon of
// its own that borrows it.
export function iconNames(sn: SpriteFilename): string[] {
let names = smogonNames(sn);
return [...names, ...names.flatMap(n => ICON_ALIASES[n] ?? [])];
}
// Every name a sprite answers to on the smogon side: its own, and any alias.
export function smogonNames(sn: SpriteFilename): string[] {
let name = publishedName(sn, smogon);

View File

@@ -69,7 +69,15 @@ function extOf(f: Sprite, ext?: string): string {
// copies, since the mapping isn't one name per filename in either direction:
// Meowstic answers to two, and the forme slots the games gave one sprite (the
// six Minior meteors, Zygarde's Power Construct pair) answer to the same one.
export function publishedNames(f: Sprite, allowUnknown = false): string[] {
export type NameOpts = {
// forumsprites publishes Unknown; no other set does.
allowUnknown?: boolean,
// The gen 6 icon sets publish the formes they have no icon for under a
// borrowed one. See ICON_ALIASES.
icons?: boolean,
};
export function publishedNames(f: Sprite, opts: NameOpts = {}): string[] {
let sn = spritedata.parseFilename(f.name);
// Skip asymmetrical for now
@@ -79,19 +87,19 @@ export function publishedNames(f: Sprite, allowUnknown = false): string[] {
if (sn.kind === 'x') {
// Skip these, we don't use Unknown/Substitute
if (!allowUnknown || sn.name !== 'unknown') {
if (!opts.allowUnknown || sn.name !== 'unknown') {
return [];
}
} else if (sn.kind !== 's') {
throw new Error(`Not a specie sprite: ${f.name}`);
}
return spritedata.smogonNames(sn);
return opts.icons ? spritedata.iconNames(sn) : spritedata.smogonNames(sn);
}
export async function spritecopy(manifest: Manifest, f: Sprite, dest: Dest,
allowUnknown = false): Promise<void> {
for (let name of publishedNames(f, allowUnknown)) {
opts: NameOpts = {}): Promise<void> {
for (let name of publishedNames(f, opts)) {
await manifest.copy(f, dest, name);
}
}

View File

@@ -74,53 +74,18 @@ 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 => {
// icons: the gen 6 set has no art for some formes and lends them another's,
// which the smogdex sheet and forumsprites do off the same directory.
let byName = new Map<string, Sprite>();
for (let f of xyIcons) {
for (let name of publishedNames(f)) {
for (let name of publishedNames(f, {icons: true})) {
if (byName.has(name)) {
throw new Error(`Two icons published as ${name}`);
}
byName.set(name, f);
}
}
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)) {
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]);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 551 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 798 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 799 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 945 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 945 B

View File

@@ -33,7 +33,7 @@ for (let [filename, sprite] of Object.entries(result.coordinates)) {
continue;
}
// TODO would like to use psid here, mess with it later.
for (let name of spritedata.smogonNames(parsed)) {
for (let name of spritedata.iconNames(parsed)) {
sprites.set(name, sprite);
}
}