TypeScript!

Currently TypeScript is validating tools.js and is not particularly
strict about anything and we use 'any' a lot and it's not part of
'npm test' yet, but everything has to start somewhere!

tools.js has also been refactored majorly to use accessors rather
than loader functions. This basically means you don't need to do
Tools.includeData() or anything like that anymore. The new system is
also easier to make TypeScript-compatible.

See #3278
This commit is contained in:
Guangcong Luo 2017-05-03 16:17:26 -07:00
parent c201e22841
commit 3716f360ff
8 changed files with 120 additions and 62 deletions

View File

@ -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;

View File

@ -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) {

View File

@ -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

View File

@ -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));
}
}

View File

@ -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;

152
tools.js
View File

@ -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'];

View File

@ -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)]) {

14
tsconfig.json Normal file
View File

@ -0,0 +1,14 @@
{
"compilerOptions": {
"lib": ["es2017"],
"noEmit": true,
"target": "ESNext",
"strictNullChecks": true,
"allowJs": true,
"checkJs": true
},
"types": ["node"],
"include": [
"./tools.js"
]
}