diff --git a/build-tools/build-indexes b/build-tools/build-indexes index e417b9a86..f622686e4 100755 --- a/build-tools/build-indexes +++ b/build-tools/build-indexes @@ -1760,15 +1760,55 @@ function buildModSprites() { const subFolders = ['anifront', 'anifront-shiny','aniback','aniback-shiny','front', 'front-shiny', 'back', 'back-shiny', 'icons', 'types', 'items', 'cries']; for (const j in subFolders) { const subF = subFolders[j]; - const spritePath = 'caches/DH2/data/mods/' + modName + (j === 'cries' ? '/audio/' : '/sprites/') + subF; + const spritePath = 'caches/DH2/data/mods/' + modName + (subF === 'cries' ? '/audio/cries' : ('/sprites/' + subF)); const spriteDir = fs.existsSync(spritePath) ? fs.readdirSync(spritePath) : ''; + let inheritDataPresent = false; for (const sprI in spriteDir) { let id = spriteDir[sprI]; + if (id === 'inherit') { + inheritDataPresent = true; + continue; + } const ext = id.split(".")[1]; id = toID(id.slice(0, id.length - 4)); modSprites[id] ||= {}; modSprites[id][modName] ||= []; - modSprites[id][modName].push(subF); + modSprites[id][modName][subF] = null; + } + if (!inheritDataPresent) continue; + try { + //We're not outright making "inherit" a JSON because the other files would attempt to copypaste it and it would cause problems + const mappings = JSON.parse(fs.readFileSync(spritePath + "/inherit")); + for (const mon in mappings) { + //null is our indicator for the presence of a custom sprite already being uploaded + //Also you can't inherit from yourself + if (mappings[mon] === null || mappings[mon] === mon) continue; + modSprites[mon] ||= {}; + modSprites[mon][modName] ||= {}; + //Skip if we already have custom data on this mon + if (modSprites[mon][modName].hasOwnProperty(subF)) continue; + let inherited = mappings[mon]; + if (modSprites[inherited] && modSprites[inherited][modName] && modSprites[inherited][modName].hasOwnProperty(subF)) { + //If we already decided that the inheritor inherits in of itself we inherit from that + //(cycles result in neither inheriting) + modSprites[mon][modName][subF] = modSprites[inherited][modName][subF]; + } else { + //We don't have inherit data for this mapping, so let us inherit + while (mappings.hasOwnProperty(inherited) && inherited !== mon) { + //If there is a chain of inheritance track it to the source + inherited = mappings[inherited]; + } + if (inherited === mon) { + //CYCLE SPOTTED, DO NOT INHERIT + modSprites[mon][modName][subF] = null; + } else { + //Now we inherit + modSprites[mon][modName][subF] = inherited; + } + } + } + } catch (e) { + console.log("WARNING: Failed to read inherit file under " + spritePath + " (it's meant to be formatted like a JSON)"); } } } diff --git a/play.pokemonshowdown.com/js/client-teambuilder.js b/play.pokemonshowdown.com/js/client-teambuilder.js index 45cf3becf..a941b19e2 100644 --- a/play.pokemonshowdown.com/js/client-teambuilder.js +++ b/play.pokemonshowdown.com/js/client-teambuilder.js @@ -3584,8 +3584,8 @@ var baseid = toID(species.baseSpecies); var forms = [baseid].concat(species.cosmeticFormes.map(toID)); - let modSprite = Dex.getSpriteMod(mod, baseid, 'front', species.exists !== false) - || Dex.getSpriteMod(mod, species.id, 'front', species.exists !== false); + let modSprite = Dex.getSpriteMod(mod, baseid, 'front', species.exists !== false).mod + || Dex.getSpriteMod(mod, species.id, 'front', species.exists !== false).mod; let resourcePrefix; let d; if (modSprite) { diff --git a/play.pokemonshowdown.com/src/battle-dex.ts b/play.pokemonshowdown.com/src/battle-dex.ts index 67e46810e..00017e73b 100644 --- a/play.pokemonshowdown.com/src/battle-dex.ts +++ b/play.pokemonshowdown.com/src/battle-dex.ts @@ -475,18 +475,28 @@ const Dex = new class implements ModdedDex { // getSpriteMod is used to find the correct mod folder for the sprite url to use // id is the name of the pokemon, type, or item. folder refers to "front", or "back-shiny" etc. overrideStandard is false for custom elements and true for canon elements getSpriteMod(optionsMod: string, spriteId: string, filepath: string, overrideStandard: boolean = false) { - if (!window.ModSprites[spriteId]) return ''; - if ((!optionsMod || !window.ModSprites[spriteId][optionsMod]) && !overrideStandard) { // for custom elements only, it will use sprites from another mod if the mod provided doesn't have one - for (const modName in window.ModSprites[spriteId]) { - if (window.ModSprites[spriteId][modName].includes(filepath)) return modName; - if (window.ModSprites[spriteId][modName].includes('ani' + filepath)) return modName; + //Setting default value; This is returned if realmon or otherwise no custom sprite data + //(Implementing it this way helps prioritize mods where it has a sprite over mods where it borrows for custom elements) + let pick = {mod: '', inherit: null}; + if (!window.ModSprites[spriteId]) return pick; + if (optionsMod && window.ModSprites[spriteId][optionsMod]) { + for (const prefix of ['ani', '']) { + if (window.ModSprites[spriteId][optionsMod].hasOwnProperty(prefix + filepath)) + //It won't matter if it inherits in another mod or not, point is it has data in this mod + return {mod: optionsMod, inherit: window.ModSprites[spriteId][optionsMod][prefix + filepath]}; } } - if (optionsMod && window.ModSprites[spriteId][optionsMod]) { - if (window.ModSprites[spriteId][optionsMod].includes('ani' + filepath)) return optionsMod; - if (window.ModSprites[spriteId][optionsMod].includes(filepath)) return optionsMod; + else if (!overrideStandard) { // for custom elements only, it will use sprites from another mod if the mod provided doesn't have one + for (const modName in window.ModSprites[spriteId]) { + for (const prefix of ['', 'ani']) { + const entry = window.ModSprites[spriteId][modName][prefix + filepath]; + //With other mods, we prioritize ones that actually have the sprite + if (entry === null) return {mod: modName, inherit: null}; + if (entry) pick = {mod: modName, inherit: entry}; + } + } } - return ''; // must be a real Pokemon or not have custom sprite data + return pick; } loadSpriteData(gen: 'xy' | 'bw') { @@ -502,6 +512,46 @@ const Dex = new class implements ModdedDex { document.getElementsByTagName('body')[0].appendChild(el); } + + getCryUrl(miscData: any, species: Species, crymod: string, overrideStandard: boolean = false, resourcePrefix: string) { + const data = this.getSpriteMod(crymod, toID(species.spriteid), 'cries', overrideStandard); + //This is null if we're either using the provided cry or didn't find a cry in the first place + if (data.inherit) { + const newspecies = Dex.species.get(data.inherit); + miscData.num = newspecies.num + return this.getCryUrl(miscData, Dex.species.get(data.inherit), crymod, overrideStandard, resourcePrefix); + } + let url = ''; + if (species.exists && miscData.num !== 0 && miscData.num > -5000) { + let baseSpeciesid = toID(species.baseSpecies); + if (BattlePokemonSprites[baseSpeciesid] || BattlePokemonSpritesBW[baseSpeciesid]) { + url = 'audio/cries/' + baseSpeciesid; + let formeid = species.formeid; + if (species.isMega || formeid && ( + ['-crowned','-eternal','-eternamax','-four','-hangry','-hero', + '-lowkey','-noice','-primal','-rapidstrike', '-roaming', '-school', + '-sky', '-starter', '-super', '-therian', '-unbound'].includes(formeid) + || ['calyrex','kyurem','cramorant','indeedee','lycanroc','necrozma', + 'oinkologne','oricorio','slowpoke','tatsugiri','zygarde'].includes(baseSpeciesid) + )) { + url += formeid; + } + url += '.mp3'; + } + } + + // Mod Cries + if (crymod === 'digimon') { + url = `sprites/${options.mod}/audio/${toID(species.baseSpecies)}.mp3`; + } + //If we already have a cry url we load from the main server, otherwise we attempt to load from our repository + if ((!(url &&= 'https://' + Config.routes.psmain + '/' + url)) && data.mod) { + url = resourcePrefix + data.mod + '/audio/cries/' + species.id + '.mp3'; + } + //console.log ("URL for " + species.id + " cry is " + url); + return url; + } + getSpriteData(pokemon: Pokemon | Species | string, isFront: boolean, options: { gen?: number, shiny?: boolean, @@ -537,13 +587,16 @@ const Dex = new class implements ModdedDex { let spriteDir = 'sprites/'; let hasCustomSprite = false; let modSpriteId = toID(modSpecies.spriteid); - options.mod = this.getSpriteMod(options.mod, modSpriteId, isFront ? 'front' : 'back', modSpecies.exists); + let firstmod = options.mod; + const searchedMod = this.getSpriteMod(options.mod, modSpriteId, isFront ? 'front' : 'back', modSpecies.exists); + options.mod = searchedMod.mod; + let cryResourcePrefix = resourcePrefix; if (options.mod) { resourcePrefix = Dex.modResourcePrefix; spriteDir = `${options.mod}/sprites/`; hasCustomSprite = true; - if (this.getSpriteMod(options.mod, modSpriteId, (isFront ? 'front' : 'back') + '-shiny', modSpecies.exists) === '') options.shiny = false; - } + if (this.getSpriteMod(options.mod, modSpriteId, (isFront ? 'front' : 'back') + '-shiny', modSpecies.exists).mod === '') options.shiny = false; + } else if (this.getSpriteMod(firstmod, modSpriteId, 'cries', modSpecies.exists).mod) cryResourcePrefix = Dex.modResourcePrefix; const species = Dex.species.get(pokemon); // Gmax sprites are already extremely large, so we don't need to double. @@ -603,45 +656,22 @@ const Dex = new class implements ModdedDex { if (!animationData) animationData = {}; if (!miscData) miscData = {}; - if (miscData.num !== 0 && miscData.num > -5000) { - let baseSpeciesid = toID(species.baseSpecies); - spriteData.cryurl = 'audio/cries/' + baseSpeciesid; - let formeid = species.formeid; - if (species.isMega || formeid && ( - formeid === '-crowned' || - formeid === '-eternal' || - formeid === '-eternamax' || - formeid === '-four' || - formeid === '-hangry' || - formeid === '-hero' || - formeid === '-lowkey' || - formeid === '-noice' || - formeid === '-primal' || - formeid === '-rapidstrike' || - formeid === '-roaming' || - formeid === '-school' || - formeid === '-sky' || - formeid === '-starter' || - formeid === '-super' || - formeid === '-therian' || - formeid === '-unbound' || - baseSpeciesid === 'calyrex' || - baseSpeciesid === 'kyurem' || - baseSpeciesid === 'cramorant' || - baseSpeciesid === 'indeedee' || - baseSpeciesid === 'lycanroc' || - baseSpeciesid === 'necrozma' || - baseSpeciesid === 'oinkologne' || - baseSpeciesid === 'oricorio' || - baseSpeciesid === 'slowpoke' || - baseSpeciesid === 'tatsugiri' || - baseSpeciesid === 'zygarde' - )) { - spriteData.cryurl += formeid; - } - spriteData.cryurl += '.mp3'; + spriteData.cryurl = Dex.getCryUrl(miscData, species, firstmod, modSpecies.exists, cryResourcePrefix); + if (searchedMod.inherit) { + let newoptions = { + gen: mechanicsGen, + shiny: spriteData.shiny, + gender: options.gender || 'N', + afd: options.afd || false, + noScale: options.noScale || false, + mod: firstmod, + dynamax: isDynamax + }; + let overwriteData = Dex.getSpriteData(searchedMod.inherit, isFront, newoptions); + overwriteData.cryurl = spriteData.cryurl; + return overwriteData; } - + if (options.shiny && mechanicsGen > 1) dir += '-shiny'; // April Fool's 2014 @@ -661,21 +691,6 @@ const Dex = new class implements ModdedDex { } return spriteData; } - - // Mod Cries - if (options.mod === 'digimon') { - spriteData.cryurl = `sprites/${options.mod}/audio/${toID(species.baseSpecies)}.mp3`; - } - //If we already have a cry url we load from the main server, otherwise we try to search for the presence of a custom cry - if (!(spriteData.cryurl &&= 'https://' + Config.routes.psmain + '/' + spriteData.cryurl)) { - //For whatever reason if there is a cry but no true sprite data then options.mod becomes '' regardless of mod - //TODO: Possibly fix that? I wouldn't prioritize it though - if (window.ModSprites[modSpriteId]?.[options.mod]?.includes('cries')) { - spriteData.cryurl = resourcePrefix + options.mod + '/audio/cries/' + speciesid + '.mp3'; - } else { //We couldn't find a cry - spriteData.cryurl = ''; - } - } let hasCustomAnim = false; if (hasCustomSprite && window.ModSprites[modSpriteId][options.mod].includes('ani' + facing)){ @@ -694,7 +709,6 @@ const Dex = new class implements ModdedDex { spriteData.w = animationData[facing].w; spriteData.h = animationData[facing].h; spriteData.url += dir + '/' + name + '.gif'; - console.log(animationData[facing]); } else { // There is no entry or enough data in pokedex-mini.js // Handle these in case-by-case basis; either using BW sprites or matching the played gen. @@ -807,8 +821,18 @@ const Dex = new class implements ModdedDex { let fainted = ((pokemon as Pokemon | ServerPokemon)?.fainted ? `;opacity:.3;filter:grayscale(100%) brightness(.5)` : ``); Dex.species.get(id); let species = window.BattlePokedexAltForms && window.BattlePokedexAltForms[id] ? window.BattlePokedexAltForms[id] : Dex.species.get(id); - mod = this.getSpriteMod(mod, id, 'icons', species.exists !== false); - if (mod) return `background:transparent url(${this.modResourcePrefix}${mod}/sprites/icons/${id}.png) no-repeat scroll -0px -0px${fainted}`; + const moddata = this.getSpriteMod(mod, id, 'icons', species.exists !== false); + //TODO: Figure out a way for inherited sprites to show up as fainted + //("?" icons will be used as placeholders for fainted inherited sprites for the time being) + if (!moddata.inherit) { + mod = moddata.mod; + if (mod) { + //TODO: If female try and check if "../icons/${id}f" is available, fall back on the default if no such file is found (which is to say there are no gender differences in the icon) + return `background:transparent url(${this.modResourcePrefix}${mod}/sprites/icons/${id}.png) no-repeat scroll -0px -0px${fainted}`; + } + } else if (!fainted) { + return this.getPokemonIcon(moddata.inherit, facingLeft || false, mod); + } return `background:transparent url(${Dex.resourcePrefix}sprites/pokemonicons-sheet.png?v16) no-repeat scroll -${left}px -${top}px${fainted}`; } @@ -821,12 +845,14 @@ const Dex = new class implements ModdedDex { spriteid = species.spriteid || toID(pokemon.species); } if (mod && window.ModConfig[mod].spriteGen) gen = window.ModConfig[mod].spriteGen; - mod = this.getSpriteMod(mod, id, 'front', species.exists !== false); + const moddata = this.getSpriteMod(mod, id, 'front', species.exists !== false); + if (moddata.inherit) return this.getTeambuilderSpriteData({species: moddata.inherit, spriteid: moddata.inherit, shiny: pokemon.shiny}, gen, mod); + mod = moddata.mod; if (mod) { return { spriteDir: `${mod}/sprites/front`, spriteid, - shiny: (this.getSpriteMod(mod, id, 'front-shiny', species.exists !== false) !== null && pokemon.shiny), + shiny: (this.getSpriteMod(mod, id, 'front-shiny', species.exists !== false).mod !== null && pokemon.shiny), x: 10, y: 5, }; @@ -892,7 +918,9 @@ const Dex = new class implements ModdedDex { getItemIcon(item: any, mod: string = '') { let num = 0; if (typeof item === 'string' && exports.BattleItems) item = exports.BattleItems[toID(item)]; - mod = this.getSpriteMod(mod, item.id, 'items'); + const moddata = this.getSpriteMod(mod, item.id, 'items'); + if (moddata.inherit) return this.getItemIcon(mod, moddata.inherit, 'items'); + mod = moddata.mod; if (mod) return `background:transparent url(${this.modResourcePrefix}${mod}/sprites/items/${item.id}.png) no-repeat`; if (item?.spritenum) num = item.spritenum; @@ -905,7 +933,10 @@ const Dex = new class implements ModdedDex { type = this.types.get(type).name; if (!type) type = '???'; let sanitizedType = type.replace(/\?/g, '%3f'); - mod = this.getSpriteMod(mod, toID(type), 'types'); + const moddata = this.getSpriteMod(mod, toID(type), 'types'); + //Only real application I could see is mixing up type visuals but eh, consistency + if (moddata.inherit) return this.getTypeIcon(moddata.inherit, mod); + mod = moddata.mod; if (mod && (type !== '???')) { return `${type}`; } else { @@ -913,6 +944,7 @@ const Dex = new class implements ModdedDex { } } + //TODO: Support replaced sprites from mods maybe? getCategoryIcon(category: string | null) { const categoryID = toID(category); let sanitizedCategory = '';