UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

274 lines (250 loc) • 8.75 kB
import { game } from "../core/cachedClasses"; import { ReadonlySet } from "../types/ReadonlySet"; import { getAllPlayers } from "./playerIndex"; import { isFunction } from "./types"; /** * Helper function to throw an error (using the `error` Lua function) if the provided value is equal * to `undefined`. * * This is useful to have TypeScript narrow a `T | undefined` value to `T` in a concise way. */ export function assertDefined<T>( value: T, ...[msg]: [undefined] extends [T] ? [string] : [ "The assertion is useless because the provided value does not contain undefined.", ] ): asserts value is Exclude<T, undefined> { if (value === undefined) { error(msg); } } /** * Helper function to throw an error (using the `error` Lua function) if the provided value is equal * to `null`. * * This is useful to have TypeScript narrow a `T | null` value to `T` in a concise way. */ export function assertNotNull<T>( value: T, ...[msg]: [null] extends [T] ? [string] : [ "The assertion is useless because the provided value does not contain null.", ] ): asserts value is Exclude<T, null> { if (value === null) { error(msg); } } /** * Helper function to return an array of integers with the specified range, inclusive on the lower * end and exclusive on the high end. (The "e" in the function name stands for exclusive.) Thus, * this function works in a similar way as the built-in `range` function from Python. * * If the end is lower than the start, an empty array will be returned. * * For example: * * - `eRange(2)` returns `[0, 1]`. * - `eRange(3)` returns `[0, 1, 2]`. * - `eRange(-3)` returns `[0, -1, -2]`. * - `eRange(1, 3)` returns `[1, 2]`. * - `eRange(2, 5)` returns `[2, 3, 4]`. * - `eRange(5, 2)` returns `[]`. * - `eRange(3, 3)` returns `[]`. * * @param start The integer to start at. * @param end Optional. The integer to end at. If not specified, then the start will be 0 and the * first argument will be the end. * @param increment Optional. The increment to use. Default is 1. */ export function eRange(start: int, end?: int, increment = 1): readonly int[] { if (end === undefined) { return eRange(0, start, increment); } const array: int[] = []; for (let i = start; i < end; i += increment) { array.push(i); } return array; } /** * Helper function to log what is happening in functions that recursively move through nested data * structures. */ export function getTraversalDescription( key: unknown, traversalDescription: string, ): string { if (traversalDescription !== "") { traversalDescription += " --> "; } traversalDescription += tostring(key); return traversalDescription; } /** * Helper function to return an array of integers with the specified range, inclusive on both ends. * (The "i" in the function name stands for inclusive.) * * If the end is lower than the start, an empty array will be returned. * * For example: * * - `iRange(2)` returns `[0, 1, 2]`. * - `iRange(3)` returns `[0, 1, 2, 3]`. * - `iRange(-3)` returns `[0, -1, -2, -3]`. * - `iRange(1, 3)` returns `[1, 2, 3]`. * - `iRange(2, 5)` returns `[2, 3, 4, 5]`. * - `iRange(5, 2)` returns `[]`. * - `iRange(3, 3)` returns `[3]`. * * @param start The integer to start at. * @param end Optional. The integer to end at. If not specified, then the start will be 0 and the * first argument will be the end. * @param increment Optional. The increment to use. Default is 1. */ export function iRange(start: int, end?: int, increment = 1): readonly int[] { if (end === undefined) { return iRange(0, start, increment); } const exclusiveEnd = end + 1; return eRange(start, exclusiveEnd, increment); } /** * Helper function to check if a variable is within a certain range, inclusive on both ends. * * - For example, `inRange(1, 1, 3)` will return `true`. * - For example, `inRange(0, 1, 3)` will return `false`. * * @param num The number to check. * @param start The start of the range to check. * @param end The end of the range to check. */ export function inRange(num: int, start: int, end: int): boolean { return num >= start && num <= end; } /** * Helper function to detect if there is two or more players currently playing. * * Specifically, this function looks for unique `ControllerIndex` values across all players. * * This function is not safe to use in the `POST_PLAYER_INIT` callback, because the * `ControllerIndex` will not be set properly. As a workaround, you can use it in the * `POST_PLAYER_INIT_FIRST` callback (or some other callback like `POST_UPDATE`). */ export function isMultiplayer(): boolean { const players = getAllPlayers(); const controllerIndexes = players.map((player) => player.ControllerIndex); const controllerIndexesSet = new ReadonlySet(controllerIndexes); return controllerIndexesSet.size > 1; } /** * Helper function to check if the player has the Repentance DLC installed. * * This function should always be used over the `REPENTANCE` constant, since the latter is not safe. * * Specifically, this function checks for the `Sprite.GetAnimation` method: * https://bindingofisaacrebirth.fandom.com/wiki/V1.06.J818#Lua_Changes */ export function isRepentance(): boolean { const metatable = getmetatable(Sprite) as LuaMap<string, unknown> | undefined; assertDefined( metatable, "Failed to get the metatable of the Sprite global table.", ); const classTable = metatable.get("__class") as | LuaMap<string, unknown> | undefined; assertDefined( classTable, 'Failed to get the "__class" key of the Sprite metatable.', ); const getAnimation = classTable.get("GetAnimation"); return isFunction(getAnimation); } /** * Helper function to check if the player has the Repentance+ DLC installed. * * This function should always be used over the `REPENTANCE_PLUS` constant, since the latter is not * safe. * * Specifically, this function checks for `Room:DamageGridWithSource` method: * https://bindingofisaacrebirth.wiki.gg/wiki/The_Binding_of_Isaac:_Repentance%2B#Modding_Changes */ export function isRepentancePlus(): boolean { const room = game.GetRoom(); const metatable = getmetatable(room) as LuaMap<string, unknown> | undefined; assertDefined(metatable, "Failed to get the metatable of the room class."); const damageGridWithSource = metatable.get("DamageGridWithSource"); return isFunction(damageGridWithSource); } /** * Helper function to check if the player is using REPENTOGON, an exe-hack which expands the modding * API. * * Although REPENTOGON has a `REPENTOGON` global to check if it's present, it is not safe to use as * it can be overwritten by other mods. * * Specifically, this function checks for the `Sprite.Continue` method: * https://repentogon.com/Sprite.html#continue */ export function isRepentogon(): boolean { const metatable = getmetatable(Sprite) as LuaMap<string, unknown> | undefined; assertDefined( metatable, "Failed to get the metatable of the Sprite global table.", ); const classTable = metatable.get("__class") as | LuaMap<string, unknown> | undefined; assertDefined( classTable, 'Failed to get the "__class" key of the Sprite metatable.', ); const getAnimation = classTable.get("Continue"); return isFunction(getAnimation); } /** * Helper function to repeat code N times. This is faster to type and cleaner than using a for loop. * * For example: * * ```ts * const player = Isaac.GetPlayer(); * repeat(10, () => { * player.AddCollectible(CollectibleType.STEVEN); * }); * ``` * * The repeated function is passed the index of the iteration, if needed: * * ```ts * repeat(3, (i) => { * print(i); // Prints "0", "1", "2" * }); * ``` */ export function repeat(num: int, func: (i: int) => void): void { for (let i = 0; i < num; i++) { func(i); } } /** * Helper function to signify that the enclosing code block is not yet complete. Using this function * is similar to writing a "TODO" comment, but it has the benefit of preventing ESLint errors due to * unused variables or early returns. * * When you see this function, it simply means that the programmer intends to add in more code to * this spot later. * * This function is variadic, meaning that you can pass as many arguments as you want. (This is * useful as a means to prevent unused variables.) * * This function does not actually do anything. (It is an "empty" function.) * * @allowEmptyVariadic */ // eslint-disable-next-line @typescript-eslint/no-unused-vars, @typescript-eslint/no-empty-function export function todo(...args: readonly unknown[]): void {}