Skip to content

Commit

Permalink
Merge pull request #42 from KirbyRider1337/sprite-and-cry-sharing
Browse files Browse the repository at this point in the history
Mons in a mod can share cries and/or sprites
  • Loading branch information
Rezzo64 authored Apr 3, 2024
2 parents 34c0b37 + 63ee159 commit 52ad87c
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 79 deletions.
67 changes: 64 additions & 3 deletions build-tools/build-indexes
Original file line number Diff line number Diff line change
Expand Up @@ -1762,13 +1762,74 @@ function buildModSprites() {
const subF = subFolders[j];
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 === 'reused') {
inheritDataPresent = true;
continue;
}
const ext = id.split(".")[1];
//These are our supported file extensions, if missing we skip
if (!["png","gif","mp3"].includes(ext)) continue;
id = toID(id.slice(0, id.length - 4));
modSprites[id] ||= {};
modSprites[id][modName] ||= [];
modSprites[id][modName].push(subF);
if (!modSprites[id]) modSprites[id] = {[modName]: [subF]};
else if (!modSprites[id][modName]) modSprites[id][modName] = [subF];
else modSprites[id][modName].push(subF);
}
//We're not adding the .JSON extension outright because the other files would attempt to copypaste it and it would cause problems
if (!inheritDataPresent) continue;
try {
const mappings = JSON.parse(fs.readFileSync(spritePath + "/reused"));
for (const mon in mappings) {
let inherited = mappings[mon];
//null is not valid to inherit from
//Also you can't inherit from yourself
if (inherited === null || inherited === mon) continue;
//Skip if we already have custom data on this mon
if (modSprites[mon] && modSprites[mon][modName] && modSprites[mon][modName].includes(subF)) continue;
if (!modSprites.ReusedResources[mon]) {
modSprites.ReusedResources[mon] = {[modName]: {}};
} else if (!modSprites.ReusedResources[mon][modName]) {
modSprites.ReusedResources[mon][modName] = {};
} else if (modSprites.ReusedResources[mon][modName].hasOwnProperty(subF)){
//We already handled this, it was presumably caught in an inherit chain
continue;
}
//If we already decided that the inheritor inherits in of itself we inherit from that
//(cycles result in neither inheriting)
if (modSprites.ReusedResources[inherited] && modSprites.ReusedResources[inherited][modName]
&& modSprites.ReusedResources[inherited][modName].hasOwnProperty(subF)) {
modSprites.ReusedResources[mon][modName][subF] = modSprites.ReusedResources[inherited][modName][subF];
} else {
//We don't have inherit data for this mapping, so let us inherit
const inheritChain = [mon];
//If there is a chain of inheritance track it to the source
//"inherited !== mon" breaks the loop if the inheritance is cyclic
while (mappings.hasOwnProperty(inherited) && inherited !== mon
//We're cutting off the inherit chain if we come across a preexisting element.
&& !(modSprites.hasOwnProperty(inherited) && modSprites[inherited].hasOwnProperty(modName)
&& modSprites[inherited][modName].contains(subF))) {
inheritChain.push(inherited);
inherited = mappings[inherited];
}
//If there's a cycle none of these guys are inheriting
if (inherited === mon) {
for (const chainInherit of inheritChain) {
modSprites[chainInherit] ||= {};
if (!modSprites[chainInherit].hasOwnProperty(modName)) modSprites[chainInherit][modName] = [subF];
else modSprites[chainInherit][modName].push(subF);
}
} else {
//We're inheriting here
for (const chainInherit of inheritChain) {
modSprites.ReusedResources[chainInherit][modName][subF] = inherited;
}
}
}
}
} catch (e) {
console.log("WARNING: Failed to read \"reused\" file under " + spritePath + " (it's meant to be formatted like a JSON)");
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions play.pokemonshowdown.com/js/client-teambuilder.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
201 changes: 127 additions & 74 deletions play.pokemonshowdown.com/src/battle-dex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,18 +475,47 @@ 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] && !window.ModSprites.ReusedResources[spriteId]) return pick;
const reuseLocation = window.ModSprites.ReusedResources[spriteId];
if (optionsMod) {
if (window.ModSprites[spriteId] && window.ModSprites[spriteId][optionsMod]) {
for (const prefix of ['ani', '']) {
if (window.ModSprites[spriteId][optionsMod].includes(prefix + filepath))
return {mod: optionsMod, inherit: null};
}
}
if (reuseLocation && reuseLocation[optionsMod]) {
console.log("Checking reuse in " + optionsMod + " for " + spriteId);
for (const prefix of ['ani', '']) {
if (reuseLocation[optionsMod].hasOwnProperty(prefix + filepath))
return {mod: optionsMod, inherit: reuseLocation[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]) {
if (window.ModSprites[spriteId] && window.ModSprites[spriteId][modName]) {
for (const prefix of ['', 'ani']) {
if (window.ModSprites[spriteId][modName].includes(prefix + filepath))
return {mod: modName, inherit: null};
}
}
if (reuseLocation && !pick.mod && reuseLocation[modName]) {
console.log("Checking reuse in " + optionsMod + " for " + spriteId);
for (const prefix of ['', 'ani']) {
const entry = reuseLocation[modName][prefix + filepath];
if (entry) {
pick = {mod: modName, inherit: entry};
break;
}
}
}
}
}
return ''; // must be a real Pokemon or not have custom sprite data
return pick;
}

loadSpriteData(gen: 'xy' | 'bw') {
Expand All @@ -502,6 +531,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,
Expand Down Expand Up @@ -537,13 +606,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;
cryResourcePrefix = 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.
Expand Down Expand Up @@ -603,45 +675,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
Expand All @@ -661,21 +710,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)){
Expand All @@ -694,7 +728,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.
Expand All @@ -708,7 +741,7 @@ const Dex = new class implements ModdedDex {
name += '-f';
}
//If it's a custom sprite, does it have separate sprites for male and female?
else if (window.ModSprites[modSpriteId] && window.ModSprites[modSpriteId + 'f']) {
else if (window.ModSprites[modSpriteId] && window.ModSprites[modSpriteId + 'f'] && window.ModSprites[modSpriteId + 'f'][options.mod]) {
name += 'f';
}
}
Expand Down Expand Up @@ -742,10 +775,12 @@ const Dex = new class implements ModdedDex {
}
if (window.BattlePokemonSprites) {
if (!window.ModSprites[modSpriteId] && !window.BattlePokemonSprites[modSpriteId] && pokemon !== 'substitute') {
let currentcry = spriteData.cryurl;
spriteData = Dex.getSpriteData('substitute', spriteData.isFrontSprite, {
gen: options.gen,
mod: options.mod,
});
spriteData.cryurl = currentcry;
}
}
return spriteData;
Expand Down Expand Up @@ -807,8 +842,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}`;

}
Expand All @@ -821,12 +866,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,
};
Expand Down Expand Up @@ -892,7 +939,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;

Expand All @@ -905,14 +954,18 @@ 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 `<img src="${this.modResourcePrefix}${mod}/sprites/types/${toID(type)}.png" alt="${type}" class="pixelated${b ? ' b' : ''}" />`;
} else {
return `<img src="${Dex.resourcePrefix}sprites/types/${sanitizedType}.png" alt="${type}" height="14" width="32" class="pixelated${b ? ' b' : ''}" />`;
}
}

//TODO: Support replaced category icons from mods maybe?
getCategoryIcon(category: string | null) {
const categoryID = toID(category);
let sanitizedCategory = '';
Expand Down

0 comments on commit 52ad87c

Please sign in to comment.