Improve TypeScript typing

sim/dex.js and sim/prng.js are now valid strict TypeScript! Also they
have slightly less "any" use than before.
This commit is contained in:
Guangcong Luo 2017-05-08 02:58:55 -05:00
parent 1c96b80694
commit 22186f1903
8 changed files with 274 additions and 130 deletions

View File

@ -1126,7 +1126,7 @@ exports.BattleScripts = {
// Spammable attacks are: Thunderbolt, Psychic, Surf, Blizzard, Earthquake.
let pokemonWeaknesses = [];
for (let type in weaknessCount) {
let increaseCount = Dex.getImmunity(type, template) && Dex.getEffectiveness(type, template) > 0;
let increaseCount = this.getImmunity(type, template) && this.getEffectiveness(type, template) > 0;
if (!increaseCount) continue;
if (weaknessCount[type] >= 2) {
skip = true;

View File

@ -1408,7 +1408,7 @@ class Battle extends Dex.ModdedDex {
if (this.rated) {
this.add('rated');
}
this.add('seed', Battle.logReplay.bind(this, this.prngSeed.join(',')));
this.add('seed', side => Battle.logReplay(this.prngSeed.join(','), side));
if (format.onBegin) {
format.onBegin.call(this);
@ -1429,6 +1429,50 @@ class Battle extends Dex.ModdedDex {
this.midTurn = true;
if (!this.currentRequest) this.go();
}
/**
* returns false if the target is immune; true otherwise
*
* also checks immunity to some statuses
* @param {Sim.Pokemon} source
* @param {Sim.Pokemon} target
*/
getImmunity(source, target) {
let sourceType = source.type || source;
let targetTyping = target.getTypes && target.getTypes() || target.types || target;
if (Array.isArray(targetTyping)) {
for (let i = 0; i < targetTyping.length; i++) {
if (!this.getImmunity(sourceType, targetTyping[i])) return false;
}
return true;
}
let typeData = this.data.TypeChart[targetTyping];
if (typeData && typeData.damageTaken[sourceType] === 3) return false;
return true;
}
/**
* @param {Sim.Pokemon} source
* @param {Sim.Pokemon} target
*/
getEffectiveness(source, target) {
let sourceType = source.type || source;
let totalTypeMod = 0;
let targetTyping = target.getTypes && target.getTypes() || target.types || target;
if (Array.isArray(targetTyping)) {
for (let i = 0; i < targetTyping.length; i++) {
totalTypeMod += this.getEffectiveness(sourceType, targetTyping[i]);
}
return totalTypeMod;
}
let typeData = this.data.TypeChart[targetTyping];
if (!typeData) return 0;
switch (typeData.damageTaken[sourceType]) {
case 1: return 1; // super-effective
case 2: return -1; // resist
// in case of weird situations like Gravity, immunity is
// handled elsewhere
default: return 0;
}
}
boost(boost, target, source, effect, isSecondary, isSelf) {
if (this.event) {
if (!target) target = this.event.target;

0
sim/dex-data.js Normal file
View File

View File

@ -51,6 +51,7 @@ const FORMATS = path.resolve(__dirname, '../config/formats');
// shim Object.values
if (!Object.values) {
// @ts-ignore
Object.values = function (object) {
let values = [];
for (let k in object) values.push(object[k]);
@ -76,10 +77,14 @@ if (!Object.values) {
// };
// }
/** @typedef {{[k: string]: any}} AnyObject */
/** @type {{[mod: string]: ModdedDex}} */
let dexes = {};
/** @typedef {'Pokedex' | 'FormatsData' | 'Learnsets' | 'Movedex' | 'Statuses' | 'TypeChart' | 'Scripts' | 'Items' | 'Abilities' | 'Natures' | 'Formats' | 'Aliases'} DataType */
/** @type {DataType[]} */
// @ts-ignore TypeScript bug
const DATA_TYPES = ['Pokedex', 'FormatsData', 'Learnsets', 'Movedex', 'Statuses', 'TypeChart', 'Scripts', 'Items', 'Abilities', 'Natures', 'Formats', 'Aliases'];
const DATA_FILES = {
@ -97,12 +102,10 @@ const DATA_FILES = {
'Natures': 'natures',
};
/** @typedef {{
id: string,
name: string,
}} DexTemplate */
/** @typedef {{id: string, name: string, [k: string]: any}} DexTemplate */
/** @typedef {{[id: string]: AnyObject}} DexTable */
/** @typedef {{Pokedex: object, Movedex: object, Statuses: object, TypeChart: object, Scripts: object, Items: object, Abilities: object, FormatsData: object, Learnsets: object, Aliases: object, Natures: object}} ModdedDex */
/** @typedef {{Pokedex: DexTable, Movedex: DexTable, Statuses: DexTable, TypeChart: DexTable, Scripts: DexTable, Items: DexTable, Abilities: DexTable, FormatsData: DexTable, Learnsets: DexTable, Aliases: DexTable, Natures: DexTable, Formats: DexTable, MoveCache: Map, ItemCache: Map, AbilityCache: Map, TemplateCache: Map}} DexData */
const BattleNatures = {
adamant: {name:"Adamant", plus:'atk', minus:'spa'},
@ -132,6 +135,10 @@ const BattleNatures = {
timid: {name:"Timid", plus:'spe', minus:'atk'},
};
/**
* @param {any} text
* @return {string}
*/
function toId(text) {
// this is a duplicate of Dex.getId, for performance reasons
if (text && text.id) {
@ -157,23 +164,32 @@ class ModdedDex {
this.currentMod = mod;
this.parentMod = '';
/** @type {any} */
/** @type {?DexData} */
this.dataCache = null;
/** @type {any} */
/** @type {?DexTable} */
this.formatsCache = null;
this.modsLoaded = false;
this.ModdedDex = ModdedDex;
}
/**
* @return {DexData}
*/
get data() {
this.includeData();
return this.dataCache;
return this.loadData();
}
/**
* @return {DexTable}
*/
get formats() {
this.includeFormats();
// @ts-ignore
return this.formatsCache;
}
/**
* @return {{[mod: string]: ModdedDex}}
*/
get dexes() {
this.includeMods();
return dexes;
@ -188,6 +204,10 @@ class ModdedDex {
if (!mod) mod = 'base';
return dexes[mod];
}
/**
* @param {AnyObject | string} format
* @return {ModdedDex}
*/
format(format) {
if (!this.modsLoaded) this.includeMods();
const mod = this.getFormat(format).mod;
@ -286,51 +306,12 @@ class ModdedDex {
return ('' + text).toLowerCase().replace(/[^a-z0-9]+/g, '');
}
/**
* returns false if the target is immune; true otherwise
*
* also checks immunity to some statuses
*/
getImmunity(source, target) {
let sourceType = source.type || source;
let targetTyping = target.getTypes && target.getTypes() || target.types || target;
if (Array.isArray(targetTyping)) {
for (let i = 0; i < targetTyping.length; i++) {
if (!this.getImmunity(sourceType, targetTyping[i])) return false;
}
return true;
}
let typeData = this.data.TypeChart[targetTyping];
if (typeData && typeData.damageTaken[sourceType] === 3) return false;
return true;
}
getEffectiveness(source, target) {
let sourceType = source.type || source;
let totalTypeMod = 0;
let targetTyping = target.getTypes && target.getTypes() || target.types || target;
if (Array.isArray(targetTyping)) {
for (let i = 0; i < targetTyping.length; i++) {
totalTypeMod += this.getEffectiveness(sourceType, targetTyping[i]);
}
return totalTypeMod;
}
let typeData = this.data.TypeChart[targetTyping];
if (!typeData) return 0;
switch (typeData.damageTaken[sourceType]) {
case 1: return 1; // super-effective
case 2: return -1; // resist
// in case of weird situations like Gravity, immunity is
// handled elsewhere
default: return 0;
}
}
/**
* Convert a pokemon name, ID, or template into its species name, preserving
* form name (which is the main way Dex.getSpecies(id) differs from
* Dex.getTemplate(id).species).
*
* @param {string|DexTemplate} species
* @param {string | AnyObject} species
* @return {string}
*/
getSpecies(species) {
@ -345,6 +326,10 @@ class ModdedDex {
return species;
}
/**
* @param {string | AnyObject} template
* @return {AnyObject}
*/
getTemplate(template) {
if (!template || typeof template === 'string') {
let name = (template || '').trim();
@ -446,11 +431,19 @@ class ModdedDex {
}
return template;
}
/**
* @param {string | AnyObject} template
* @return {AnyObject}
*/
getLearnset(template) {
const id = toId(template);
if (!this.data.Learnsets[id]) return null;
return this.data.Learnsets[id].learnset;
}
/**
* @param {string | AnyObject} move
* @return {AnyObject}
*/
getMove(move) {
if (!move || typeof move === 'string') {
let name = (move || '').trim();
@ -518,83 +511,98 @@ class ModdedDex {
* If you really want to, use:
* moveCopyCopy = Dex.getMoveCopy(moveCopy.id)
*
* @param move Move ID, move object, or movecopy object describing move to copy
* @return movecopy object
* @param {AnyObject | string} move - Move ID, move object, or movecopy object describing move to copy
* @return {AnyObject} movecopy object
*/
getMoveCopy(move) {
// @ts-ignore
if (move && move.isCopy) return move;
move = this.getMove(move);
let moveCopy = this.deepClone(move);
moveCopy.isCopy = true;
return moveCopy;
}
getEffect(effect) {
if (!effect || typeof effect === 'string') {
let name = (effect || '').trim();
let id = toId(name);
effect = {};
if (id && this.data.Statuses[id]) {
effect = this.data.Statuses[id];
effect.name = effect.name || this.data.Statuses[id].name;
} else if (id && this.data.Movedex[id] && this.data.Movedex[id].effect) {
effect = this.data.Movedex[id].effect;
effect.name = effect.name || this.data.Movedex[id].name;
} else if (id && this.data.Abilities[id] && this.data.Abilities[id].effect) {
effect = this.data.Abilities[id].effect;
effect.name = effect.name || this.data.Abilities[id].name;
} else if (id && this.data.Items[id] && this.data.Items[id].effect) {
effect = this.data.Items[id].effect;
effect.name = effect.name || this.data.Items[id].name;
} else if (id && this.data.Formats[id]) {
effect = this.data.Formats[id];
effect.name = effect.name || this.data.Formats[id].name;
if (!effect.mod) effect.mod = 'gen6';
if (!effect.effectType) effect.effectType = 'Format';
} else if (id === 'recoil') {
effect = {
effectType: 'Recoil',
};
} else if (id === 'drain') {
effect = {
effectType: 'Drain',
};
}
if (!effect.id) effect.id = id;
if (!effect.name) effect.name = name;
if (!effect.fullname) effect.fullname = effect.name;
effect.toString = this.effectToString;
if (!effect.category) effect.category = 'Effect';
if (!effect.effectType) effect.effectType = 'Effect';
/**
* @param {string | AnyObject} name
* @return {AnyObject}
*/
getEffect(name) {
if (name && typeof name !== 'string') {
return name;
}
name = (name || '').trim();
let id = toId(name);
let effect = {};
if (id && this.data.Statuses[id]) {
effect = this.data.Statuses[id];
effect.name = effect.name || this.data.Statuses[id].name;
} else if (id && this.data.Movedex[id] && this.data.Movedex[id].effect) {
effect = this.data.Movedex[id].effect;
effect.name = effect.name || this.data.Movedex[id].name;
} else if (id && this.data.Abilities[id] && this.data.Abilities[id].effect) {
effect = this.data.Abilities[id].effect;
effect.name = effect.name || this.data.Abilities[id].name;
} else if (id && this.data.Items[id] && this.data.Items[id].effect) {
effect = this.data.Items[id].effect;
effect.name = effect.name || this.data.Items[id].name;
} else if (id && this.data.Formats[id]) {
effect = this.data.Formats[id];
effect.name = effect.name || this.data.Formats[id].name;
if (!effect.mod) effect.mod = 'gen6';
if (!effect.effectType) effect.effectType = 'Format';
} else if (id === 'recoil') {
effect = {
effectType: 'Recoil',
};
} else if (id === 'drain') {
effect = {
effectType: 'Drain',
};
}
if (!effect.id) effect.id = id;
if (!effect.name) effect.name = name;
if (!effect.fullname) effect.fullname = effect.name;
effect.toString = this.effectToString;
if (!effect.category) effect.category = 'Effect';
if (!effect.effectType) effect.effectType = 'Effect';
return effect;
}
getFormat(effect) {
if (!effect || typeof effect === 'string') {
let name = (effect || '').trim();
let id = toId(name);
if (this.data.Aliases[id]) {
name = this.data.Aliases[id];
id = toId(name);
}
effect = {};
if (this.data.Formats.hasOwnProperty(id)) {
effect = this.data.Formats[id];
if (effect.cached) return effect;
effect.cached = true;
effect.exists = true;
effect.name = effect.name || this.data.Formats[id].name;
if (!effect.mod) effect.mod = 'gen6';
if (!effect.effectType) effect.effectType = 'Format';
}
if (!effect.id) effect.id = id;
if (!effect.name) effect.name = name;
if (!effect.fullname) effect.fullname = effect.name;
effect.toString = this.effectToString;
if (!effect.category) effect.category = 'Effect';
if (!effect.effectType) effect.effectType = 'Effect';
/**
* @param {string | AnyObject} name
* @return {AnyObject}
*/
getFormat(name) {
if (name && typeof name !== 'string') {
return name;
}
name = (name || '').trim();
let id = toId(name);
if (this.data.Aliases[id]) {
name = this.data.Aliases[id];
id = toId(name);
}
let effect = {};
if (this.data.Formats.hasOwnProperty(id)) {
effect = this.data.Formats[id];
if (effect.cached) return effect;
effect.cached = true;
effect.exists = true;
effect.name = effect.name || this.data.Formats[id].name;
if (!effect.mod) effect.mod = 'gen6';
if (!effect.effectType) effect.effectType = 'Format';
}
if (!effect.id) effect.id = id;
if (!effect.name) effect.name = name;
if (!effect.fullname) effect.fullname = effect.name;
effect.toString = this.effectToString;
if (!effect.category) effect.category = 'Effect';
if (!effect.effectType) effect.effectType = 'Effect';
return effect;
}
/**
* @param {string | AnyObject} item
* @return {AnyObject}
*/
getItem(item) {
if (!item || typeof item === 'string') {
let name = (item || '').trim();
@ -648,6 +656,10 @@ class ModdedDex {
}
return item;
}
/**
* @param {string | AnyObject} ability
* @return {AnyObject}
*/
getAbility(ability) {
if (!ability || typeof ability === 'string') {
let name = (ability || '').trim();
@ -687,6 +699,10 @@ class ModdedDex {
}
return ability;
}
/**
* @param {string | AnyObject} type
* @return {AnyObject}
*/
getType(type) {
if (!type || typeof type === 'string') {
let id = toId(type);
@ -708,6 +724,10 @@ class ModdedDex {
}
return type;
}
/**
* @param {string | AnyObject} nature
* @return {AnyObject}
*/
getNature(nature) {
if (!nature || typeof nature === 'string') {
let name = (nature || '').trim();
@ -726,6 +746,11 @@ class ModdedDex {
}
return nature;
}
/**
* @param {AnyObject} stats
* @param {AnyObject} set
* @return {AnyObject}
*/
spreadModify(stats, set) {
const modStats = {atk:10, def:10, spa:10, spd:10, spe:10};
for (let statName in modStats) {
@ -738,6 +763,11 @@ class ModdedDex {
}
return this.natureModify(modStats, set.nature);
}
/**
* @param {AnyObject} stats
* @param {string | AnyObject} nature
* @return {AnyObject}
*/
natureModify(stats, nature) {
nature = this.getNature(nature);
if (nature.plus) stats[nature.plus] = Math.floor(stats[nature.plus] * 1.1);
@ -745,6 +775,9 @@ class ModdedDex {
return stats;
}
/**
* @param {AnyObject} ivs
*/
getHiddenPower(ivs) {
const hpTypes = ['Fighting', 'Flying', 'Poison', 'Ground', 'Rock', 'Bug', 'Ghost', 'Steel', 'Fire', 'Water', 'Grass', 'Electric', 'Psychic', 'Ice', 'Dragon', 'Dark'];
const stats = {hp: 31, atk: 31, def: 31, spe: 31, spa: 31, spd: 31};
@ -775,6 +808,12 @@ class ModdedDex {
}
}
/**
* @param {AnyObject} format
* @param {AnyObject=} subformat
* @param {number=} depth
* @return {AnyObject}
*/
getBanlistTable(format, subformat, depth) {
let banlistTable;
if (!depth) depth = 0;
@ -849,6 +888,11 @@ class ModdedDex {
return banlistTable;
}
/**
* TODO: TypeScript generics
* @param {Array} arr
* @return {Array}
*/
shuffle(arr) {
// In-place shuffle by Fisher-Yates algorithm
for (let i = arr.length - 1; i > 0; i--) {
@ -864,6 +908,7 @@ class ModdedDex {
* @param {string} s - string 1
* @param {string} t - string 2
* @param {number} l - limit
* @return {number} - distance
*/
levenshtein(s, t, l) {
// Original levenshtein distance function by James Westgate, turned out to be the fastest
@ -913,6 +958,13 @@ class ModdedDex {
return d[n][m];
}
/**
* Forces num to be an integer (between min and max).
* @param {any} num
* @param {number=} min
* @param {number=} max
* @return {number}
*/
clampIntRange(num, min, max) {
if (typeof num !== 'number') num = 0;
num = Math.floor(num);
@ -921,6 +973,12 @@ class ModdedDex {
return num;
}
/**
* @param {string} target
* @param {DataType[] | null=} searchIn
* @param {boolean=} isInexact
* @return {AnyObject[] | false}
*/
dataSearch(target, searchIn, isInexact) {
if (!target) {
return false;
@ -932,6 +990,8 @@ class ModdedDex {
let searchTypes = {Pokedex: 'pokemon', Movedex: 'move', Abilities: 'ability', Items: 'item', Natures: 'nature'};
let searchResults = [];
for (let i = 0; i < searchIn.length; i++) {
/** @type {AnyObject} */
// @ts-ignore TypeScript bug
let res = this[searchFunctions[searchIn[i]]](target);
if (res.exists) {
searchResults.push({
@ -997,6 +1057,10 @@ class ModdedDex {
return false;
}
/**
* @param {AnyObject[]} team
* @return {string}
*/
packTeam(team) {
if (!team) return '';
@ -1099,6 +1163,10 @@ class ModdedDex {
return buf;
}
/**
* @param {string} buf
* @return {AnyObject[]}
*/
fastUnpackTeam(buf) {
if (!buf) return null;
@ -1218,6 +1286,10 @@ class ModdedDex {
return team;
}
/**
* @param {AnyObject} obj
* @return {AnyObject}
*/
deepClone(obj) {
if (typeof obj === 'function') return obj;
if (obj === null || typeof obj !== 'object') return obj;
@ -1230,6 +1302,11 @@ class ModdedDex {
return clone;
}
/**
* @param {string} basePath
* @param {DataType} dataType
* @return {AnyObject}
*/
loadDataFile(basePath, dataType) {
try {
const filePath = basePath + DATA_FILES[dataType];
@ -1246,6 +1323,9 @@ class ModdedDex {
return {};
}
/**
* @return {ModdedDex}
*/
includeMods() {
if (!this.isBase) throw new Error(`This must be called on the base Dex`);
if (this.modsLoaded) return this;
@ -1259,14 +1339,28 @@ class ModdedDex {
return this;
}
/**
* @return {ModdedDex}
*/
includeModData() {
for (const mod in this.dexes) {
dexes[mod].includeData();
}
return this;
}
/**
* @return {ModdedDex}
*/
includeData() {
if (this.dataCache) return this;
this.loadData();
return this;
}
/**
* @return {DexData}
*/
loadData() {
if (this.dataCache) return this.dataCache;
dexes['base'].includeMods();
this.dataCache = {};
@ -1336,9 +1430,12 @@ class ModdedDex {
// Execute initialization script.
if (BattleScripts.init) BattleScripts.init.call(this);
return this;
return this.dataCache;
}
/**
* @return {ModdedDex}
*/
includeFormats() {
if (!this.isBase) throw new Error(`This should only be run on the base mod`);
this.includeMods();
@ -1379,23 +1476,20 @@ class ModdedDex {
return this;
}
/**
* @param {string} id - Format ID
* @param {object} format - Format
*/
installFormat(id, format) {
dexes['base'].includeFormats();
// @ts-ignore
dexes['base'].formatsCache[id] = format;
if (this.dataCache) this.dataCache.Formats[id] = format;
if (!this.isBase) {
// @ts-ignore
if (dexes['base'].dataCache) dexes['base'].dataCache.Formats[id] = format;
}
}
/**
* Install our Dex functions into the battle object
*/
install(battle) {
for (let i in this.data.Scripts) {
battle[i] = this.data.Scripts[i];
}
}
}
dexes['base'] = new ModdedDex();

View File

@ -27,7 +27,11 @@ exports.construct = function (format, rated, send, prng) {
const dex = Dex.mod(mod);
const proto = Object.create(Battle.prototype);
Object.assign(proto, dex);
dex.install(proto);
for (let i in dex.data.Scripts) {
proto[i] = dex.data.Scripts[i];
}
battleProtoCache.set(mod, proto);
}
const battle = Object.create(battleProtoCache.get(mod));

View File

@ -21,9 +21,10 @@ class PRNG {
constructor(seed = PRNG.generateSeed()) {
// We slice() the seed so we get a copy of it instead of the original seed.
/** @type {PRNGSeed} */
// @ts-ignore
// @ts-ignore TypeScript bug
this.initialSeed = seed.slice();
/** @type {PRNGSeed} */
// @ts-ignore TypeScript bug
this.seed = seed.slice();
}
@ -127,6 +128,7 @@ class PRNG {
This is all ignoring overflow/carry because that cannot be shown in a pseudo-mathematical equation.
The below code implements a optimised version of that equation while also checking for overflow/carry.
@param {PRNGSeed} initialSeed
@param {number} [framesToAdvance = 1]
@return {PRNGSeed} the new seed
*/

View File

@ -45,7 +45,7 @@ class Side {
let sideScripts = battle.data.Scripts.side;
if (sideScripts) Object.assign(this, sideScripts);
this.getChoice = (this.getChoice || Side.getChoice).bind(this);
this.getChoice = (side => this.getChoiceInner(side));
this.battle = battle;
this.n = n;
@ -106,7 +106,7 @@ class Side {
}
}
static getChoice(side) {
getChoiceInner(side) {
if (side !== this && side !== true) return '';
return this.choice.actions.map(action => {
switch (action.choice) {

View File

@ -3,7 +3,7 @@
"lib": ["es2017"],
"noEmit": true,
"target": "ESNext",
"strictNullChecks": true,
"strict": true,
"allowJs": true,
"checkJs": true
},