diff --git a/rules/avatars.ts b/rules/avatars.ts new file mode 100644 index 00000000..ef6b5de3 --- /dev/null +++ b/rules/avatars.ts @@ -0,0 +1,737 @@ + +// The forum's auto avatars: a frozen pool of pictures it assigns to users who +// have uploaded none. It picks one with crc32(username) % count, so the pool is +// an index and not a set -- a name added, removed or respelled reassigns every +// user's avatar. That is why this is a list and not a glob over the gen 5 +// sources: a sprite landing in src/ must not join the pool on its own. +// +// The names are the ones the 2019 set was built under, which are PS ids rather +// than the smogon names every other set here publishes: `hooh`, not `ho-oh`. +// publishedName(sn, psid) spells all but two of them. + +export let AVATARS: string[] = [ + 'abomasnow', + 'abra', + 'absol', + 'accelgor', + 'aerodactyl', + 'aggron', + 'aipom', + 'alakazam', + 'alomomola', + 'altaria', + 'ambipom', + 'amoonguss', + 'ampharos', + 'anorith', + 'arbok', + 'arcanine', + 'arceus', + 'arceus-bug', + 'arceus-dark', + 'arceus-dragon', + 'arceus-electric', + 'arceus-ghost', + 'arceus-grass', + 'arceus-ground', + 'arceus-ice', + 'arceus-normal', + 'arceus-poison', + 'arceus-psychic', + 'arceus-rock', + 'arceus-steel', + 'arceus-water', + 'archen', + 'archeops', + 'ariados', + 'armaldo', + 'aron', + 'articuno', + 'audino', + 'aurumoth', + 'axew', + 'azelf', + 'azumarill', + 'azurill', + 'bagon', + 'baltoy', + 'banette', + 'barboach', + 'basculin', + 'bastiodon', + 'bayleef', + 'beartic', + 'beautifly', + 'beedrill', + 'beheeyem', + 'beldum', + 'bellossom', + 'bellsprout', + 'bibarel', + 'bidoof', + 'bisharp', + 'blastoise', + 'blaziken', + 'blissey', + 'blitzle', + 'boldore', + 'bonsly', + 'bouffalant', + 'braviary', + 'breloom', + 'bronzong', + 'bronzor', + 'budew', + 'buizel', + 'bulbasaur', + 'buneary', + 'burmy', + 'burmy-sandy', + 'burmy-trash', + 'butterfree', + 'cacnea', + 'cacturne', + 'camerupt', + 'carnivine', + 'carracosta', + 'carvanha', + 'cascoon', + 'castform', + 'castform-rainy', + 'castform-snowy', + 'castform-sunny', + 'caterpie', + 'celebi', + 'chandelure', + 'chansey', + 'charizard', + 'charmander', + 'charmeleon', + 'chatot', + 'cherrim', + 'cherrim-sunshine', + 'cherubi', + 'chikorita', + 'chimchar', + 'chimecho', + 'chinchou', + 'chingling', + 'cinccino', + 'clamperl', + 'claydol', + 'clefable', + 'clefairy', + 'cleffa', + 'cloyster', + 'cobalion', + 'cofagrigus', + 'combee', + 'combusken', + 'conkeldurr', + 'corphish', + 'corsola', + 'cottonee', + 'cradily', + 'cranidos', + 'crawdaunt', + 'cresselia', + 'croagunk', + 'crobat', + 'croconaw', + 'crustle', + 'cryogonal', + 'cubchoo', + 'cubone', + 'cyndaquil', + 'darkrai', + 'darmanitan', + 'darmanitan-zen', + 'darumaka', + 'deerling', + 'deerling-autumn', + 'deerling-summer', + 'deerling-winter', + 'deino', + 'delcatty', + 'delibird', + 'deoxys', + 'deoxys-attack', + 'deoxys-defense', + 'deoxys-speed', + 'dewgong', + 'dewott', + 'dialga', + 'diglett', + 'ditto', + 'dodrio', + 'doduo', + 'donphan', + 'dragonair', + 'dragonite', + 'drapion', + 'dratini', + 'drifblim', + 'drifloon', + 'drilbur', + 'drowzee', + 'druddigon', + 'ducklett', + 'dugtrio', + 'dunsparce', + 'duosion', + 'durant', + 'dusclops', + 'dusknoir', + 'duskull', + 'dustox', + 'dwebble', + 'eelektrik', + 'eelektross', + 'eevee', + 'ekans', + 'electabuzz', + 'electivire', + 'electrike', + 'electrode', + 'elekid', + 'elgyem', + 'emboar', + 'emolga', + 'empoleon', + 'entei', + 'escavalier', + 'espeon', + 'excadrill', + 'exeggcute', + 'exeggutor', + 'exploud', + 'farfetchd', + 'fearow', + 'feebas', + 'feraligatr', + 'ferroseed', + 'ferrothorn', + 'finneon', + 'flaaffy', + 'flareon', + 'floatzel', + 'flygon', + 'foongus', + 'forretress', + 'fraxure', + 'frillish', + 'froslass', + 'furret', + 'gabite', + 'gallade', + 'galvantula', + 'garbodor', + 'garchomp', + 'gardevoir', + 'gastly', + 'gastrodon', + 'gastrodon-east', + 'genesect', + 'gengar', + 'geodude', + 'gible', + 'gigalith', + 'girafarig', + 'giratina', + 'giratina-origin', + 'glaceon', + 'glalie', + 'glameow', + 'gligar', + 'gliscor', + 'gloom', + 'golbat', + 'goldeen', + 'golduck', + 'golem', + 'golett', + 'golurk', + 'gorebyss', + 'gothita', + 'gothitelle', + 'gothorita', + 'granbull', + 'graveler', + 'grimer', + 'grotle', + 'groudon', + 'grovyle', + 'growlithe', + 'grumpig', + 'gulpin', + 'gurdurr', + 'gyarados', + 'happiny', + 'hariyama', + 'haunter', + 'haxorus', + 'heatmor', + 'heatran', + 'heracross', + 'herdier', + 'hippopotas', + 'hippowdon', + 'hitmonchan', + 'hitmonlee', + 'hitmontop', + 'honchkrow', + 'hooh', + 'hoothoot', + 'hoppip', + 'horsea', + 'houndoom', + 'houndour', + 'huntail', + 'hydreigon', + 'hypno', + 'igglybuff', + 'illumise', + 'infernape', + 'ivysaur', + 'jellicent', + 'jigglypuff', + 'jirachi', + 'jolteon', + 'joltik', + 'jumpluff', + 'jynx', + 'kabuto', + 'kabutops', + 'kadabra', + 'kakuna', + 'kangaskhan', + 'karrablast', + 'kecleon', + 'keldeo', + 'keldeo-resolute', + 'kingdra', + 'kingler', + 'kirlia', + 'klang', + 'klink', + 'klinklang', + 'koffing', + 'krabby', + 'kricketot', + 'kricketune', + 'krokorok', + 'krookodile', + 'kyogre', + 'kyurem', + 'kyurem-black', + 'kyurem-white', + 'lairon', + 'lampent', + 'landorus', + 'landorus-therian', + 'lanturn', + 'lapras', + 'larvesta', + 'larvitar', + 'latias', + 'latios', + 'leafeon', + 'leavanny', + 'ledian', + 'ledyba', + 'lickilicky', + 'lickitung', + 'liepard', + 'lileep', + 'lilligant', + 'lillipup', + 'linoone', + 'litwick', + 'lombre', + 'lopunny', + 'lotad', + 'loudred', + 'lucario', + 'ludicolo', + 'lugia', + 'lumineon', + 'lunatone', + 'luvdisc', + 'luxio', + 'luxray', + 'machamp', + 'machoke', + 'machop', + 'magby', + 'magcargo', + 'magikarp', + 'magmar', + 'magmortar', + 'magnemite', + 'magneton', + 'magnezone', + 'makuhita', + 'malaconda', + 'mamoswine', + 'manaphy', + 'mandibuzz', + 'manectric', + 'mankey', + 'mantine', + 'mantyke', + 'maractus', + 'mareep', + 'marill', + 'marowak', + 'marshtomp', + 'masquerain', + 'mawile', + 'medicham', + 'meditite', + 'meganium', + 'meloetta', + 'meloetta-pirouette', + 'meowth', + 'mesprit', + 'metagross', + 'metang', + 'metapod', + 'mew', + 'mewtwo', + 'mienfoo', + 'mienshao', + 'mightyena', + 'milotic', + 'miltank', + 'mimejr', + 'minccino', + 'minun', + 'misdreavus', + 'mismagius', + 'mollux', + 'moltres', + 'monferno', + 'mothim', + 'mrmime', + 'mudkip', + 'muk', + 'munchlax', + 'munna', + 'murkrow', + 'musharna', + 'natu', + 'necturna', + 'nidoking', + 'nidoqueen', + 'nidoranf', + 'nidoranm', + 'nidorina', + 'nidorino', + 'nincada', + 'ninetales', + 'ninjask', + 'noctowl', + 'nosepass', + 'numel', + 'nuzleaf', + 'octillery', + 'oddish', + 'omanyte', + 'omastar', + 'onix', + 'oshawott', + 'pachirisu', + 'palkia', + 'palpitoad', + 'panpour', + 'pansage', + 'pansear', + 'paras', + 'parasect', + 'patrat', + 'pawniard', + 'pelipper', + 'persian', + 'petilil', + 'phanpy', + 'phione', + 'pichu', + 'pidgeot', + 'pidgeotto', + 'pidgey', + 'pidove', + 'pignite', + 'pikachu', + 'piloswine', + 'pineco', + 'pinsir', + 'piplup', + 'plusle', + 'politoed', + 'poliwag', + 'poliwhirl', + 'poliwrath', + 'ponyta', + 'poochyena', + 'porygon', + 'porygon2', + 'porygonz', + 'primeape', + 'prinplup', + 'probopass', + 'psyduck', + 'pupitar', + 'purrloin', + 'purugly', + 'quagsire', + 'quilava', + 'qwilfish', + 'raichu', + 'raikou', + 'ralts', + 'rampardos', + 'rapidash', + 'raticate', + 'rattata', + 'rayquaza', + 'regice', + 'regigigas', + 'regirock', + 'registeel', + 'relicanth', + 'remoraid', + 'reshiram', + 'reuniclus', + 'rhydon', + 'rhyhorn', + 'rhyperior', + 'riolu', + 'roggenrola', + 'roselia', + 'roserade', + 'rotom', + 'rotom-heat', + 'rotom-mow', + 'rotom-wash', + 'rufflet', + 'sableye', + 'salamence', + 'samurott', + 'sandile', + 'sandshrew', + 'sandslash', + 'sawk', + 'sawsbuck', + 'sawsbuck-autumn', + 'sawsbuck-summer', + 'sawsbuck-winter', + 'sceptile', + 'scizor', + 'scolipede', + 'scrafty', + 'scraggy', + 'scyther', + 'seadra', + 'seaking', + 'sealeo', + 'seedot', + 'seel', + 'seismitoad', + 'sentret', + 'serperior', + 'servine', + 'seviper', + 'sewaddle', + 'sharpedo', + 'shaymin', + 'shaymin-sky', + 'shedinja', + 'shelgon', + 'shellder', + 'shellos', + 'shellos-east', + 'shelmet', + 'shieldon', + 'shiftry', + 'shinx', + 'shroomish', + 'shuckle', + 'shuppet', + 'sigilyph', + 'silcoon', + 'simipour', + 'simisage', + 'simisear', + 'skarmory', + 'skiploom', + 'skitty', + 'skorupi', + 'skuntank', + 'slaking', + 'slakoth', + 'slowbro', + 'slowking', + 'slowpoke', + 'slugma', + 'smeargle', + 'smoochum', + 'sneasel', + 'snivy', + 'snorlax', + 'snorunt', + 'snover', + 'snubbull', + 'solosis', + 'solrock', + 'spearow', + 'spheal', + 'spinarak', + 'spinda', + 'spiritomb', + 'spoink', + 'squirtle', + 'stantler', + 'staraptor', + 'staravia', + 'starly', + 'starmie', + 'staryu', + 'steelix', + 'stoutland', + 'stunfisk', + 'stunky', + 'substitute', + 'sudowoodo', + 'suicune', + 'sunflora', + 'sunkern', + 'surskit', + 'swablu', + 'swadloon', + 'swalot', + 'swampert', + 'swanna', + 'swanna-2', + 'swellow', + 'swinub', + 'swoobat', + 'taillow', + 'tangela', + 'tangrowth', + 'tauros', + 'teddiursa', + 'tentacool', + 'tentacruel', + 'tepig', + 'terrakion', + 'throh', + 'thundurus', + 'thundurus-therian', + 'timburr', + 'tirtouga', + 'togekiss', + 'togepi', + 'togetic', + 'tomohawk', + 'torchic', + 'torkoal', + 'tornadus', + 'tornadus-therian', + 'torterra', + 'totodile', + 'toxicroak', + 'tranquill', + 'trapinch', + 'treecko', + 'tropius', + 'trubbish', + 'turtwig', + 'tympole', + 'tynamo', + 'typhlosion', + 'tyranitar', + 'tyrogue', + 'umbreon', + 'unfezant', + 'unown', + 'ursaring', + 'uxie', + 'vanillish', + 'vanillite', + 'vanilluxe', + 'vaporeon', + 'venipede', + 'venomoth', + 'venonat', + 'venusaur', + 'vespiquen', + 'vibrava', + 'victini', + 'victreebel', + 'vigoroth', + 'vileplume', + 'virizion', + 'volbeat', + 'volcarona', + 'voltorb', + 'vullaby', + 'vulpix', + 'wailmer', + 'wailord', + 'walrein', + 'wartortle', + 'watchog', + 'weavile', + 'weedle', + 'weepinbell', + 'weezing', + 'whimsicott', + 'whirlipede', + 'whiscash', + 'whismur', + 'wigglytuff', + 'wingull', + 'wobbuffet', + 'woobat', + 'wooper', + 'wormadam', + 'wormadam-sandy', + 'wormadam-trash', + 'wurmple', + 'wynaut', + 'xatu', + 'yamask', + 'yanma', + 'yanmega', + 'zangoose', + 'zapdos', + 'zebstrika', + 'zekrom', + 'zigzagoon', + 'zoroark', + 'zorua', + 'zubat', + 'zweilous', +]; + +// Two names the 2019 tree carried a second copy of. Swanna's two files were +// byte for byte the same picture; Arceus and Arceus-Normal were two palettes of +// one pose, and only the one is drawn today. Both spend their slot on the +// picture already in the pool. +export let AVATAR_ALIASES: Record = { + 'arceus-normal': 'arceus', + 'swanna-2': 'swanna', +}; + +// The pool in the order the forum indexes it, which is the directory listing +// scandir() used to return. Two things about that listing are the index and so +// are reproduced rather than tidied away: it sorted filenames with the +// extension attached, which orders `arceus-bug.gif` ahead of `arceus.gif` where +// the bare names sort the other way round, and it led with `.` and `..`, which +// named no avatar but counted toward the divisor. Those two slots rendered a +// broken image; they get Substitute, which the pool already carries, so they +// cost nothing but the slot. +export function avatarSlots(): string[] { + let files = [...AVATARS].sort((a, b) => `${a}.gif` < `${b}.gif` ? -1 : 1); + return ['substitute', 'substitute', ...files]; +} diff --git a/rules/publish.ts b/rules/publish.ts index a7c6c41a..d5e27300 100644 --- a/rules/publish.ts +++ b/rules/publish.ts @@ -55,10 +55,20 @@ export class Manifest { this.#ctx.copy(f, `${this.#tree.root}/${dir}/${stamped}`); } + // The whole url the copy `name` answers to. Exposed on its own for a set + // that publishes some other projection of this mapping than write()'s. + url(name: string): string { + let e = this.#entries.get(name); + if (e === undefined) { + throw new Error(`no sprite named ${name}`); + } + return `${this.#tree.served}/${e.dir}/${e.stamped}`; + } + write(dst: string): void { let sorted: Record = {}; - for (let [k, e] of this.#sorted()) { - sorted[k] = `${this.#tree.served}/${e.dir}/${e.stamped}`; + for (let [k] of this.#sorted()) { + sorted[k] = this.url(k); } this.#ctx.write(dst, JSON.stringify(sorted, null, 4) + '\n'); } diff --git a/smogon.build.ts b/smogon.build.ts index 914148aa..1ba26310 100644 --- a/smogon.build.ts +++ b/smogon.build.ts @@ -1,9 +1,12 @@ +import * as spritedata from '@smogon/sprite-data/index.ts'; + +import {AVATARS, AVATAR_ALIASES, avatarSlots} from './rules/avatars.ts'; import {gen10Modelslike} from './rules/modelslike.ts'; import {GEN4_CAPS, forEachFront, fronts, named} from './rules/oldgen.ts'; import {Manifest, type Sprite, type Tree, firstWins, itemspritecopy, spritecopy} from './rules/publish.ts'; -import {type CmdSpec, forEachRule, rule} from './tools/build/artifact.ts'; -import {compresspng, pad, spriteglob, trimimg} from './tools/build/helpers.ts'; +import {type Artifact, type CmdSpec, forEachRule, rule} from './tools/build/artifact.ts'; +import {base, compresspng, pad, spriteglob, trimimg} from './tools/build/helpers.ts'; import {type DeployCtx, deploy} from './tools/deploy/api.ts'; // The tar root maps onto the served tree: sprites/x is served at @@ -220,6 +223,60 @@ deploy(async ctx => { manifest.write('__meta/forumsprites/manifest.json'); }); +// Forum auto avatars: the gen 5 animations on a uniform 96x96 box, which is +// what XenForo's avatar container is. It scales an to fill, so a sprite +// published at its own aspect would arrive stretched; the box is the games' own +// sprite size, so nothing is scaled up, and the few animations that overflow it +// are cropped to it, which is what the set the forum used to carry did too. + +// The pool with its two second slots resolved, which is the set of pictures to +// build. See rules/avatars.ts for why it is a list and not a glob. +let avatarPool = new Set(AVATARS.map(name => AVATAR_ALIASES[name] ?? name)); + +let avatarArt = new Map(); +for (let f of spriteglob(['src/sprites/gen5/*.gif', 'src/sprites/gen5/xsubstitute.png'], + {b: false, s: false})) { + // publishedNames() is no use here: it resolves smogon names where the pool + // is spelled in PS ids, and it refuses the one x-kind sprite the pool + // carries. Backs and shinies are filtered above rather than here, since + // publishedName spells them exactly as it spells their front. + let name = spritedata.publishedName(spritedata.parseFilename(base(f)), spritedata.psid); + if (!avatarPool.has(name)) { + continue; + } + if (avatarArt.has(name)) { + throw new Error(`avatar ${name}: two gen 5 sources publish it`); + } + avatarArt.set(name, rule(f, { + display: 'avatar %f', + cmds: [ + 'magick convert %f -coalesce -background none -gravity center -extent 96x96 %o', + 'gifsicle -O3 -b %o', + ], + }, `${name}.gif`)); +} + +// A pool member with no source would leave the forum a shorter list and +// reassign every user, so a source renamed out from under one fails the build +// rather than the deploy. +for (let name of avatarPool) { + if (!avatarArt.has(name)) { + throw new Error(`avatar ${name}: no gen 5 source`); + } +} + +deploy(async ctx => { + let manifest = new Manifest(ctx, TREE); + for (let [name, art] of avatarArt) { + await manifest.copy(art, {dir: 'avatars'}, name); + } + // Published by slot rather than by name: nothing looks an avatar up, the + // forum picks one with crc32(username) % count, so the order is the whole + // mapping and the length is the divisor. + let slots = avatarSlots().map(name => manifest.url(AVATAR_ALIASES[name] ?? name)); + ctx.write('__meta/avatars/manifest.json', JSON.stringify(slots, null, 4) + '\n'); +}); + // PMD sprites ship as-is, stamped. deploy(async ctx => {