diff --git a/battle-engine.js b/battle-engine.js index 7931595716..e204646afe 100644 --- a/battle-engine.js +++ b/battle-engine.js @@ -2817,7 +2817,7 @@ class Battle extends Tools.BattleDex { if (!rest.length) throw new TypeError("Event handlers must have a callback"); if (target.effectType !== 'Format') { - throw new TypeError(`${target.effectType} targets are not supported at this time`); + throw new TypeError(`${target.name} is a ${target.effectType} but only Format targets are supported right now`); } let callback, priority, order, subOrder, data; diff --git a/chat-plugins/datasearch.js b/chat-plugins/datasearch.js index 5c27132de1..bb2fad297f 100644 --- a/chat-plugins/datasearch.js +++ b/chat-plugins/datasearch.js @@ -592,7 +592,7 @@ function runDexsearch(target, cmd, canAll, message) { if (alts.tiers[dex[mon].tier]) continue; if (Object.values(alts.tiers).includes(false) && alts.tiers[dex[mon].tier] !== false) continue; // some LC Pokemon are also in other tiers and need to be handled separately - if (alts.tiers.LC && !dex[mon].prevo && dex[mon].nfe && dex[mon].tier !== 'LC Uber' && !Tools.data.Formats.lc.banlist.includes(dex[mon].species)) continue; + if (alts.tiers.LC && !dex[mon].prevo && dex[mon].nfe && dex[mon].tier !== 'LC Uber' && !Tools.formats.lc.banlist.includes(dex[mon].species)) continue; } for (let type in alts.types) { diff --git a/chat-plugins/info.js b/chat-plugins/info.js index 18d3e4e156..9fe6e63bb2 100644 --- a/chat-plugins/info.js +++ b/chat-plugins/info.js @@ -1335,7 +1335,7 @@ exports.commands = { let format = Tools.getFormat(targetId); if (format.effectType === 'Format') formatList = [targetId]; if (!formatList) { - formatList = Object.keys(Tools.data.Formats).filter(formatid => Tools.data.Formats[formatid].effectType === 'Format'); + formatList = Object.keys(Tools.formats); } // Filter formats and group by section diff --git a/ladders.js b/ladders.js index 11e35e2030..c600d1e775 100644 --- a/ladders.js +++ b/ladders.js @@ -313,8 +313,8 @@ class Ladder { */ static visualizeAll(username) { let ratings = []; - for (let i in Tools.data.Formats) { - if (Tools.data.Formats[i].searchShow) { + for (let i in Tools.formats) { + if (Tools.formats[i].searchShow) { ratings.push(Ladders(i).visualize(username)); } } diff --git a/rooms.js b/rooms.js index e8ff13b7f3..e12c680f2d 100644 --- a/rooms.js +++ b/rooms.js @@ -418,8 +418,8 @@ class GlobalRoom { this.formatList = '|formats' + (Ladders.formatsListPrefix || ''); let section = '', prevSection = ''; let curColumn = 1; - for (let i in Tools.data.Formats) { - let format = Tools.data.Formats[i]; + for (let i in Tools.formats) { + let format = Tools.formats[i]; if (format.section) section = format.section; if (format.column) curColumn = format.column; if (!format.name) continue; diff --git a/tools.js b/tools.js index 115be32b89..b20ebdec55 100644 --- a/tools.js +++ b/tools.js @@ -21,7 +21,7 @@ * Note that you don't need this for Dex.mod, Dex.mod will * automatically populate this. * - Dex.includeFormats() ~30ms - * As above, but will also populate Dex.data.Formats, giving an object + * As above, but will also populate Dex.formats, giving an object * containing formats. * - Dex.includeData() ~500ms * As above, but will also populate all of Dex.data, giving access to @@ -34,6 +34,9 @@ * Note that Dex.mod(...) will automatically populate .data, so use * this only if you need to manually iterate Dex.dexes. * + * Note that preloading is unnecessary. The getters for Dex.data etc + * will automatically load this data as needed. + * * @license MIT license */ @@ -49,6 +52,7 @@ if (!Object.values) { for (let k in object) values.push(object[k]); return values; }; + // @ts-ignore Object.entries = function (object) { let entries = []; for (let k in object) entries.push([k, object[k]]); @@ -68,8 +72,10 @@ if (!Object.values) { // }; // } +/** @type {{[mod: string]: BattleDex}} */ let dexes = {}; +/** @typedef {'Pokedex' | 'FormatsData' | 'Learnsets' | 'Movedex' | 'Statuses' | 'TypeChart' | 'Scripts' | 'Items' | 'Abilities' | 'Natures' | 'Formats' | 'Aliases'} DataType */ const DATA_TYPES = ['Pokedex', 'FormatsData', 'Learnsets', 'Movedex', 'Statuses', 'TypeChart', 'Scripts', 'Items', 'Abilities', 'Natures', 'Formats', 'Aliases']; const DATA_FILES = { @@ -87,6 +93,13 @@ const DATA_FILES = { 'Natures': 'natures', }; +/** @typedef {{ + id: string, + name: string, +}} DexTemplate */ + +/** @typedef {{Pokedex: object, Movedex: object, Statuses: object, TypeChart: object, Scripts: object, Items: object, Abilities: object, FormatsData: object, Learnsets: object, Aliases: object, Natures: object}} BattleDexData */ + const BattleNatures = { adamant: {name:"Adamant", plus:'atk', minus:'spa'}, bashful: {name:"Bashful"}, @@ -128,32 +141,60 @@ function toId(text) { class BattleDex { - constructor(mod) { - if (!mod) mod = 'base'; + /** + * @param {string=} mod + */ + constructor(mod = 'base') { + this.gen = 0; - this.formatsLoaded = false; - this.modsLoaded = false; - this.dataLoaded = false; + this.name = "[BattleDex]"; this.isBase = (mod === 'base'); this.currentMod = mod; this.parentMod = ''; - this.data = null; - this.dexes = dexes; + + /** @type {any} */ + this.dataCache = null; + /** @type {any} */ + this.formatsCache = null; + this.modsLoaded = false; + + this.BattleDex = BattleDex; } + get data() { + this.includeData(); + return this.dataCache; + } + get formats() { + this.includeFormats(); + return this.formatsCache; + } + get dexes() { + this.includeMods(); + return dexes; + } + + /** + * @param {string} mod + * @return {BattleDex} + */ mod(mod) { if (!dexes['base'].modsLoaded) dexes['base'].includeMods(); if (!mod) mod = 'base'; - return dexes[mod].includeData(); + return dexes[mod]; } format(format) { if (!this.modsLoaded) this.includeMods(); const mod = this.getFormat(format).mod; // TODO: change default format mod as gen7 becomes stable - if (!mod) return dexes['gen6'].includeData(); - return dexes[mod].includeData(); + if (!mod) return dexes['gen6']; + return dexes[mod]; } + /** + * @param {DataType} dataType + * @param {string} id + */ modData(dataType, id) { if (this.isBase) return this.data[dataType][id]; if (this.data[dataType][id] !== dexes[this.parentMod].data[dataType][id]) return this.data[dataType][id]; @@ -173,7 +214,7 @@ class BattleDex { * or isn't a function. Instead, Tools.getString simply returns '' if the * passed variable isn't a string or a number. * - * @param {mixed} str + * @param {any} str * @return {string} */ getString(str) { @@ -202,7 +243,7 @@ class BattleDex { * characters in the name, although this is not strictly necessary for * safety. * - * @param {mixed} name + * @param {any} name * @return {string} */ getName(name) { @@ -228,7 +269,7 @@ class BattleDex { * Tools.getId is generally assigned to the global toId, because of how * commonly it's used. * - * @param {mixed} text + * @param {any} text * @return {string} */ getId(text) { @@ -421,6 +462,7 @@ class BattleDex { } if (id.substr(0, 11) === 'hiddenpower') { let matches = /([a-z]*)([0-9]*)/.exec(id); + // @ts-ignore id = matches[1]; } if (id && this.data.Movedex.hasOwnProperty(id)) { @@ -814,9 +856,15 @@ class BattleDex { return arr; } - levenshtein(s, t, l) { // s = string 1, t = string 2, l = limit + /** + * @param {string} s - string 1 + * @param {string} t - string 2 + * @param {number} l - limit + */ + levenshtein(s, t, l) { // Original levenshtein distance function by James Westgate, turned out to be the fastest - let d = []; // 2d matrix + /** @type {number[][]} */ + let d = []; // Step 1 let n = s.length; @@ -895,6 +943,7 @@ class BattleDex { let cmpTarget = target.toLowerCase(); let maxLd = 3; + let fuzzyResults = []; if (cmpTarget.length <= 1) { return false; } else if (cmpTarget.length <= 4) { @@ -919,18 +968,19 @@ class BattleDex { let ld = this.levenshtein(cmpTarget, word.toLowerCase(), maxLd); if (ld <= maxLd) { - searchResults.push({word: word, ld: ld}); + fuzzyResults.push({word: word, ld: ld}); } } } - if (searchResults.length) { + if (fuzzyResults.length) { + /** @type {any} */ let newTarget = ""; let newLD = 10; - for (let i = 0, l = searchResults.length; i < l; i++) { - if (searchResults[i].ld < newLD) { - newTarget = searchResults[i]; - newLD = searchResults[i].ld; + for (let i = 0, l = fuzzyResults.length; i < l; i++) { + if (fuzzyResults[i].ld < newLD) { + newTarget = fuzzyResults[i]; + newLD = fuzzyResults[i].ld; } } @@ -1193,7 +1243,7 @@ class BattleDex { } includeMods() { - if (!this.isBase) throw new Error("This must be called on the base Dex."); + if (!this.isBase) throw new Error(`This must be called on the base Dex`); if (this.modsLoaded) return this; let modList = fs.readdirSync(path.resolve(__dirname, 'mods')); @@ -1206,16 +1256,15 @@ class BattleDex { } includeModData() { - this.includeMods(); - for (const mod in dexes) { + for (const mod in this.dexes) { dexes[mod].includeData(); } } includeData() { - if (this.dataLoaded) return this; + if (this.dataCache) return this; dexes['base'].includeMods(); - if (!this.data) this.data = {mod: this.currentMod}; + this.dataCache = {}; let basePath = this.isBase ? './data/' : './mods/' + this.currentMod + '/'; @@ -1226,29 +1275,29 @@ class BattleDex { if (this.parentMod) { parentDex = dexes[this.parentMod]; if (!parentDex || parentDex === this) throw new Error("Unable to load " + this.currentMod + ". `inherit` should specify a parent mod from which to inherit data, or must be not specified."); - parentDex.includeData(); } for (let dataType of DATA_TYPES) { if (dataType === 'Natures' && this.isBase) { - this.data[dataType] = BattleNatures; + this.dataCache[dataType] = BattleNatures; continue; } let BattleData = this.loadDataFile(basePath, dataType); if (!BattleData || typeof BattleData !== 'object') throw new TypeError("Exported property `Battle" + dataType + "`from `" + './data/' + DATA_FILES[dataType] + "` must be an object except `null`."); - if (BattleData !== this.data[dataType]) this.data[dataType] = Object.assign(BattleData, this.data[dataType]); + if (BattleData !== this.dataCache[dataType]) this.dataCache[dataType] = Object.assign(BattleData, this.dataCache[dataType]); + if (dataType === 'Formats' && !parentDex) Object.assign(BattleData, this.formats); } - this.data['MoveCache'] = new Map(); - this.data['ItemCache'] = new Map(); - this.data['AbilityCache'] = new Map(); - this.data['TemplateCache'] = new Map(); - if (this.isBase) { + this.dataCache['MoveCache'] = new Map(); + this.dataCache['ItemCache'] = new Map(); + this.dataCache['AbilityCache'] = new Map(); + this.dataCache['TemplateCache'] = new Map(); + if (!parentDex) { // Formats are inherited by mods this.includeFormats(); } else { for (let dataType of DATA_TYPES) { const parentTypedData = parentDex.data[dataType]; - const childTypedData = this.data[dataType] || (this.data[dataType] = {}); + const childTypedData = this.dataCache[dataType] || (this.dataCache[dataType] = {}); for (let entryId in parentTypedData) { if (childTypedData[entryId] === null) { // null means don't inherit @@ -1278,25 +1327,20 @@ class BattleDex { } // Flag the generation. Required for team validator. - this.gen = this.data.Scripts.gen || 7; + this.gen = this.dataCache.Scripts.gen || 7; // Execute initialization script. if (BattleScripts.init) BattleScripts.init.call(this); - this.dataLoaded = true; return this; } includeFormats() { + if (!this.isBase) throw new Error(`This should only be run on the base mod`); this.includeMods(); - if (this.formatsLoaded) return this; + if (this.formatsCache) return this; - if (!this.data) this.data = {mod: this.currentMod}; - if (!this.data.Formats) this.data.Formats = {}; - - // Load [formats] aliases - let BattleAliases = this.loadDataFile('./data/', 'Aliases'); - this.data.Aliases = BattleAliases; + if (!this.formatsCache) this.formatsCache = {}; // Load formats let Formats; @@ -1305,7 +1349,7 @@ class BattleDex { } catch (e) { if (e.code !== 'MODULE_NOT_FOUND') throw e; } - if (!Array.isArray(Formats)) throw new TypeError("Exported property `Formats` from `" + "./config/formats.js" + "` must be an array."); + if (!Array.isArray(Formats)) throw new TypeError(`Exported property 'Formats' from "./config/formats.js" must be an array`); let section = ''; let column = 1; @@ -1315,27 +1359,28 @@ class BattleDex { if (format.section) section = format.section; if (format.column) column = format.column; if (!format.name && format.section) continue; - if (!id) throw new RangeError("Format #" + (i + 1) + " must have a name with alphanumeric characters"); + if (!id) throw new RangeError(`Format #${i + 1} must have a name with alphanumeric characters, not '${format.name}'`); if (!format.section) format.section = section; if (!format.column) format.column = column; - if (this.data.Formats[id]) throw new Error("Format #" + (i + 1) + " has a duplicate ID: `" + id + "`"); + if (this.formatsCache[id]) throw new Error(`Format #${i + 1} has a duplicate ID: '${id}'`); format.effectType = 'Format'; if (format.challengeShow === undefined) format.challengeShow = true; if (format.searchShow === undefined) format.searchShow = true; if (format.tournamentShow === undefined) format.tournamentShow = true; if (format.mod === undefined) format.mod = 'gen6'; - if (!dexes[format.mod]) throw new Error("Format `" + format.name + "` requires nonexistent mod: `" + format.mod + "`"); - this.installFormat(id, format); + if (!dexes[format.mod]) throw new Error(`Format "${format.name}" requires nonexistent mod: '${format.mod}'`); + this.formatsCache[id] = format; } - this.formatsLoaded = true; return this; } installFormat(id, format) { - this.data.Formats[id] = format; + dexes['base'].includeFormats(); + dexes['base'].formatsCache[id] = format; + if (this.dataCache) this.dataCache.Formats[id] = format; if (!this.isBase) { - dexes['base'].data.Formats[id] = format; + if (dexes['base'].dataCache) dexes['base'].dataCache.Formats[id] = format; } } @@ -1350,7 +1395,6 @@ class BattleDex { } dexes['base'] = new BattleDex(); -dexes['base'].BattleDex = BattleDex; // "gen7" is an alias for the current base data dexes['gen7'] = dexes['base']; diff --git a/tournaments/index.js b/tournaments/index.js index 13c7f5d178..c2ba3ae12b 100644 --- a/tournaments/index.js +++ b/tournaments/index.js @@ -965,7 +965,7 @@ function createTournament(room, format, generator, playerCap, isRated, args, out format = Tools.getFormat(format); if (format.effectType !== 'Format' || !format.tournamentShow) { output.errorReply(format.id + " is not a valid tournament format."); - output.errorReply("Valid formats: " + Object.values(Tools.data.Formats).filter(f => f.effectType === 'Format' && f.tournamentShow).map(format => format.name).join(", ")); + output.errorReply("Valid formats: " + Object.values(Tools.formats).filter(f => f.tournamentShow).map(format => format.name).join(", ")); return; } if (!TournamentGenerators[toId(generator)]) { diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000000..e64f536db1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "lib": ["es2017"], + "noEmit": true, + "target": "ESNext", + "strictNullChecks": true, + "allowJs": true, + "checkJs": true + }, + "types": ["node"], + "include": [ + "./tools.js" + ] +}