UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

529 lines (528 loc) • 24 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.countEntities = countEntities; exports.doesAnyEntityExist = doesAnyEntityExist; exports.doesEntityExist = doesEntityExist; exports.getClosestEntityTo = getClosestEntityTo; exports.getConstituentsFromEntityID = getConstituentsFromEntityID; exports.getEntities = getEntities; exports.getEntityFields = getEntityFields; exports.getEntityFromPtrHash = getEntityFromPtrHash; exports.getEntityID = getEntityID; exports.getEntityIDFromConstituents = getEntityIDFromConstituents; exports.getFilteredNewEntities = getFilteredNewEntities; exports.hasArmor = hasArmor; exports.isActiveEnemy = isActiveEnemy; exports.isEntityMoving = isEntityMoving; exports.parseEntityID = parseEntityID; exports.parseEntityTypeVariantString = parseEntityTypeVariantString; exports.removeAllMatchingEntities = removeAllMatchingEntities; exports.removeEntities = removeEntities; exports.rerollEnemy = rerollEnemy; exports.setEntityDamageFlash = setEntityDamageFlash; exports.setEntityOpacity = setEntityOpacity; exports.setEntityRandomColor = setEntityRandomColor; exports.spawn = spawn; exports.spawnEntityID = spawnEntityID; exports.spawnWithSeed = spawnWithSeed; const isaac_typescript_definitions_1 = require("isaac-typescript-definitions"); const cachedClasses_1 = require("../core/cachedClasses"); const constants_1 = require("../core/constants"); const entitiesWithArmorSet_1 = require("../sets/entitiesWithArmorSet"); const isaacAPIClass_1 = require("./isaacAPIClass"); const random_1 = require("./random"); const readOnly_1 = require("./readOnly"); const rng_1 = require("./rng"); const sprites_1 = require("./sprites"); const tstlClass_1 = require("./tstlClass"); const types_1 = require("./types"); const utils_1 = require("./utils"); const vector_1 = require("./vector"); /** From DeadInfinity. */ const DAMAGE_FLASH_COLOR = (0, readOnly_1.newReadonlyColor)(0.5, 0.5, 0.5, 1, 200 / 255, 0 / 255, 0 / 255); /** * Helper function to count the number of entities in room. Use this over the vanilla * `Isaac.CountEntities` method to avoid having to specify a spawner and to handle ignoring charmed * enemies. * * @param entityType Optional. Default is -1, which matches every entity type. * @param variant Optional. Default is -1, which matches every variant. * @param subType Optional. Default is -1, which matches every sub-type. * @param ignoreFriendly Optional. Default is false. Will throw a runtime error if set to true and * the `entityType` is equal to -1. */ function countEntities(entityType = -1, variant = -1, subType = -1, ignoreFriendly = false) { if (entityType === -1) { // The `Isaac.CountEntities` method requires an argument of `EntityType`, so we must revert to // using the `Isaac.GetRoomEntities` method, which is slower. const entities = Isaac.GetRoomEntities(); if (!ignoreFriendly) { return entities.length; } const nonFriendlyEntities = entities.filter((entity) => !entity.HasEntityFlags(isaac_typescript_definitions_1.EntityFlag.FRIENDLY)); return nonFriendlyEntities.length; } if (!ignoreFriendly) { return Isaac.CountEntities(undefined, entityType, variant, subType); } const entities = Isaac.FindByType(entityType, variant, subType, false, ignoreFriendly); return entities.length; } /** * Helper function to check if one or more matching entities exist in the current room. It uses the * `doesEntityExist` helper function to determine this. * * @param entityTypes An array or set of the entity types that you want to check for. Will return * true if any of the provided entity types exist. * @param ignoreFriendly Optional. Default is false. */ function doesAnyEntityExist(entityTypes, ignoreFriendly = false) { const entityTypesMutable = entityTypes; const entityTypesArray = (0, tstlClass_1.isTSTLSet)(entityTypesMutable) ? [...entityTypesMutable.values()] : entityTypesMutable; return entityTypesArray.some((entityType) => doesEntityExist(entityType, -1, -1, ignoreFriendly)); } /** * Helper function to check if one or more of a specific kind of entity is present in the current * room. It uses the `countEntities` helper function to determine this. * * @param entityType Optional. Default is -1, which matches every entity type. * @param variant Optional. Default is -1, which matches every variant. * @param subType Optional. Default is -1, which matches every sub-type. * @param ignoreFriendly Optional. Default is false. */ function doesEntityExist(entityType = -1, variant = -1, subType = -1, ignoreFriendly = false) { const count = countEntities(entityType, variant, subType, ignoreFriendly); return count > 0; } /** * Given an array of entities, this helper function returns the closest one to a provided reference * entity. * * For example: * * ```ts * const player = Isaac.GetPlayer(); * const gapers = getEntities(EntityType.GAPER); * const closestGaper = getClosestEntityTo(player, gapers); * ``` * * @param referenceEntity The entity that is close by. * @param entities The array of entities to look through. * @param filterFunc Optional. A function to filter for a specific type of entity, like e.g. an * enemy with a certain amount of HP left. */ function getClosestEntityTo(referenceEntity, entities, filterFunc) { let closestEntity; let closestDistance = Number.POSITIVE_INFINITY; for (const entity of entities) { const distance = referenceEntity.Position.Distance(entity.Position); if (distance < closestDistance && (filterFunc === undefined || filterFunc(entity))) { closestEntity = entity; closestDistance = distance; } } return closestEntity; } /** Helper function to get the entity type, variant, and sub-type from an `EntityID`. */ function getConstituentsFromEntityID(entityID) { const parts = entityID.split("."); if (parts.length !== 3) { error(`Failed to get the constituents from entity ID: ${entityID}`); } const [entityTypeString, variantString, subTypeString] = parts; (0, utils_1.assertDefined)(entityTypeString, `Failed to get the first constituent from an entity ID: ${entityID}`); (0, utils_1.assertDefined)(variantString, `Failed to get the second constituent from an entity ID: ${entityID}`); (0, utils_1.assertDefined)(subTypeString, `Failed to get the third constituent from an entity ID: ${entityID}`); const entityType = (0, types_1.parseIntSafe)(entityTypeString); (0, utils_1.assertDefined)(entityType, `Failed to convert the entity type to an integer: ${entityTypeString}`); const variant = (0, types_1.parseIntSafe)(variantString); (0, utils_1.assertDefined)(variant, `Failed to convert the entity variant to an integer: ${variantString}`); const subType = (0, types_1.parseIntSafe)(subTypeString); (0, utils_1.assertDefined)(subType, `Failed to convert the entity sub-type to an integer: ${subTypeString}`); return [entityType, variant, subType]; } /** * Helper function to get all of the entities in the room or all of the entities that match a * specific entity type / variant / sub-type. * * Due to bugs with `Isaac.FindInRadius`, this function uses `Isaac.GetRoomEntities`, which is more * expensive but also more robust. (If a matching entity type is provided, then `Isaac.FindByType` * will be used instead.) * * For example: * * ```ts * // Make all of the entities in the room invisible. * for (const entity of getEntities()) { * entity.Visible = false; * } * ``` * * @param entityType Optional. If specified, will only get the entities that match the type. Default * is -1, which matches every type. * @param variant Optional. If specified, will only get the entities that match the variant. Default * is -1, which matches every variant. * @param subType Optional. If specified, will only get the entities that match the sub-type. * Default is -1, which matches every sub-type. * @param ignoreFriendly Optional. If set to true, it will exclude friendly NPCs from being * returned. Default is false. Will only be taken into account if the * `entityType` is specified. */ function getEntities(entityType = -1, variant = -1, subType = -1, ignoreFriendly = false) { if (entityType === -1) { return Isaac.GetRoomEntities(); } return Isaac.FindByType(entityType, variant, subType, ignoreFriendly); } /** * Helper function to get all the fields on an entity. For example, this is useful for comparing it * to another entity later. (One option is to use the `logTableDifferences` function for this.) * * This function will only get fields that are equal to booleans, numbers, or strings, or Vectors, * as comparing other types is non-trivial. */ function getEntityFields(entity) { const entityFields = new LuaMap(); const metatable = getmetatable(entity); (0, utils_1.assertDefined)(metatable, "Failed to get the metatable for an entity."); setPrimitiveEntityFields(entity, metatable, entityFields); // If this is a class that inherits from `Entity` (like `EntityPlayer`), the "__propget" table // will not contain the attributes for `Entity`. Thus, if this is not an `Entity`, we have // additional fields to add. const className = (0, isaacAPIClass_1.getIsaacAPIClassName)(entity); if (className === "Entity") { return entityFields; } const parentTable = metatable.get("__parent"); (0, utils_1.assertDefined)(parentTable, 'Failed to get the "__parent" table for an entity.'); setPrimitiveEntityFields(entity, parentTable, entityFields); return entityFields; } function setPrimitiveEntityFields(entity, metatable, entityFields) { const propGetTable = metatable.get("__propget"); (0, utils_1.assertDefined)(propGetTable, 'Failed to get the "__propget" table for an entity.'); for (const [key] of propGetTable) { // The values of this table are functions. Thus, we use the key to index the original entity. const indexKey = key; const value = entity[indexKey]; if ((0, types_1.isPrimitive)(value)) { entityFields.set(indexKey, value); } else if ((0, vector_1.isVector)(value)) { entityFields.set(indexKey, (0, vector_1.vectorToString)(value)); } } } /** * Helper function to get an entity from a `PtrHash`. Note that doing this is very expensive, so you * should only use this function when debugging. (Normally, if you need to work backwards from a * reference, you would use an `EntityPtr` instead of a `PtrHash`. */ function getEntityFromPtrHash(ptrHash) { const entities = getEntities(); return entities.find((entity) => GetPtrHash(entity) === ptrHash); } /** Helper function to get a string containing the entity's type, variant, and sub-type. */ function getEntityID(entity) { return `${entity.Type}.${entity.Variant}.${entity.SubType}`; } /** * Helper function to get a formatted string in the format returned by the `getEntityID` function. */ function getEntityIDFromConstituents(entityType, variant, subType) { return `${entityType}.${variant}.${subType}`; } /** * Helper function to compare two different arrays of entities. Returns the entities that are in the * second array but not in the first array. */ function getFilteredNewEntities(oldEntities, newEntities) { const oldEntitiesSet = new Set(); for (const entity of oldEntities) { const ptrHash = GetPtrHash(entity); oldEntitiesSet.add(ptrHash); } return newEntities.filter((entity) => { const ptrHash = GetPtrHash(entity); return !oldEntitiesSet.has(ptrHash); }); } /** * Helper function to see if a particular entity has armor. In this context, armor refers to the * damage scaling mechanic. For example, Ultra Greed has armor, but a Gaper does not. * * For more on armor, see the wiki: https://bindingofisaacrebirth.fandom.com/wiki/Damage_Scaling */ function hasArmor(entity) { const typeVariantString = `${entity.Type}.${entity.Variant}`; return entitiesWithArmorSet_1.ENTITIES_WITH_ARMOR_SET.has(typeVariantString); } /** * Helper function to detect if a particular entity is an active enemy. Use this over the * `Entity.IsActiveEnemy` method since it is bugged with friendly enemies, Grimaces, Ultra Greed, * and Mother. */ function isActiveEnemy(entity) { if (entity.HasEntityFlags(isaac_typescript_definitions_1.EntityFlag.FRIENDLY)) { return false; } const room = cachedClasses_1.game.GetRoom(); const isClear = room.IsClear(); // Some entities count as being "active" enemies, but they deactivate when the room is cleared. if (isClear) { switch (entity.Type) { // 42 case isaac_typescript_definitions_1.EntityType.GRIMACE: { return false; } // 294 case isaac_typescript_definitions_1.EntityType.ULTRA_DOOR: { return false; } // 406 case isaac_typescript_definitions_1.EntityType.ULTRA_GREED: { const npc = entity.ToNPC(); if (npc !== undefined) { const ultraGreedVariant = npc.Variant; switch (ultraGreedVariant) { case isaac_typescript_definitions_1.UltraGreedVariant.ULTRA_GREED: { if (npc.State === (0, types_1.asNPCState)(isaac_typescript_definitions_1.UltraGreedState.GOLD_STATUE)) { return false; } break; } case isaac_typescript_definitions_1.UltraGreedVariant.ULTRA_GREEDIER: { if (npc.State === (0, types_1.asNPCState)(isaac_typescript_definitions_1.UltraGreedierState.POST_EXPLOSION)) { return false; } break; } } } break; } // 912 case isaac_typescript_definitions_1.EntityType.MOTHER: { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison if (entity.Variant === isaac_typescript_definitions_1.MotherVariant.MOTHER_1) { const npc = entity.ToNPC(); if (npc !== undefined && npc.State === isaac_typescript_definitions_1.NPCState.SPECIAL) { return false; } } break; } default: { break; } } } // eslint-disable-next-line @typescript-eslint/no-deprecated return entity.IsActiveEnemy(false); } /** * Helper function to measure an entity's velocity to see if it is moving. * * Use this helper function over checking if the velocity length is equal to 0 because entities can * look like they are completely immobile but yet still have a non zero velocity. Thus, using a * threshold is needed. * * @param entity The entity whose velocity to measure. * @param threshold Optional. The threshold from 0 to consider to be moving. Default is 0.01. */ function isEntityMoving(entity, threshold = 0.01) { return (0, vector_1.doesVectorHaveLength)(entity.Velocity, threshold); } /** * Helper function to parse a string that contains an entity type, a variant, and a sub-type, * separated by periods. * * For example, passing "45.0.1" would return an array of [45, 0, 1]. * * Returns undefined if the string cannot be parsed. */ function parseEntityID(entityID) { const entityIDArray = entityID.split("."); if (entityIDArray.length !== 3) { return undefined; } const [entityTypeString, variantString, subTypeString] = entityIDArray; if (entityTypeString === undefined || variantString === undefined || subTypeString === undefined) { return undefined; } const entityType = (0, types_1.parseIntSafe)(entityTypeString); const variant = (0, types_1.parseIntSafe)(variantString); const subType = (0, types_1.parseIntSafe)(subTypeString); if (entityType === undefined || variant === undefined || subType === undefined) { return undefined; } return [entityType, variant, subType]; } /** * Helper function to parse a string that contains an entity type and a variant separated by a * period. * * For example, passing "45.0" would return an array of [45, 0]. * * Returns undefined if the string cannot be parsed. */ function parseEntityTypeVariantString(entityTypeVariantString) { const entityTypeVariantArray = entityTypeVariantString.split("."); if (entityTypeVariantArray.length !== 2) { return undefined; } const [entityTypeString, variantString] = entityTypeVariantArray; if (entityTypeString === undefined || variantString === undefined) { return undefined; } const entityType = (0, types_1.parseIntSafe)(entityTypeString); const variant = (0, types_1.parseIntSafe)(variantString); if (entityType === undefined || variant === undefined) { return undefined; } return [entityType, variant]; } /** * Helper function to remove all of the matching entities in the room. * * @param entityType The entity type to match. * @param entityVariant Optional. The variant to match. Default is -1, which matches every variant. * @param entitySubType Optional. The sub-type to match. Default is -1, which matches every * sub-type. * @param cap Optional. If specified, will only remove the given amount of collectibles. * @returns An array of the entities that were removed. */ function removeAllMatchingEntities(entityType, entityVariant = -1, entitySubType = -1, cap = undefined) { const entities = getEntities(entityType, entityVariant, entitySubType); return removeEntities(entities, cap); } /** * Helper function to remove all of the entities in the supplied array. * * @param entities The array of entities to remove. * @param cap Optional. If specified, will only remove the given amount of entities. * @returns An array of the entities that were removed. */ function removeEntities(entities, cap) { if (entities.length === 0) { return []; } const entitiesRemoved = []; for (const entity of entities) { entity.Remove(); entitiesRemoved.push(entity); if (cap !== undefined && entitiesRemoved.length >= cap) { break; } } return entitiesRemoved; } /** * Helper function to reroll an enemy. Use this instead of the vanilla "Game.RerollEnemy" function * if you want the rerolled enemy to be returned. * * @param entity The entity to reroll. * @returns If the game failed to reroll the enemy, returns undefined. Otherwise, returns the * rerolled entity. */ function rerollEnemy(entity) { const oldEntities = getEntities(); const wasRerolled = cachedClasses_1.game.RerollEnemy(entity); if (!wasRerolled) { return undefined; } const newEntities = getEntities(); const filteredNewEntities = getFilteredNewEntities(oldEntities, newEntities); if (filteredNewEntities.length === 0) { error('Failed to find the new entity generated by the "Game.RerollEnemy" method.'); } return filteredNewEntities[0]; } /** * Helper function to make an entity flash red like it is taking damage. This is useful when you * want to make it appear as if an entity is taking damage without actually dealing any damage to * it. */ function setEntityDamageFlash(entity) { entity.SetColor(DAMAGE_FLASH_COLOR, 2, 0); } /** * Helper function to keep an entity's color the same values as it already is but set the opacity to * a specific value. * * @param entity The entity to set. * @param alpha A value between 0 and 1 that represents the fade amount. */ function setEntityOpacity(entity, alpha) { const sprite = entity.GetSprite(); (0, sprites_1.setSpriteOpacity)(sprite, alpha); } function setEntityRandomColor(entity) { const seed = entity.InitSeed === 0 ? (0, rng_1.getRandomSeed)() : entity.InitSeed; const rng = (0, rng_1.newRNG)(seed); const r = (0, random_1.getRandom)(rng); const g = (0, random_1.getRandom)(rng); const b = (0, random_1.getRandom)(rng); const color = Color(r, g, b); entity.SetColor(color, 100_000, 100_000, false, false); } /** * Helper function to spawn an entity. Always use this instead of the `Isaac.Spawn` method, since * using that method can crash the game. * * Also see the `spawnWithSeed` helper function. * * @param entityType The `EntityType` of the entity to spawn. * @param variant The variant of the entity to spawn. * @param subType The sub-type of the entity to spawn. * @param positionOrGridIndex The position or grid index of the entity to spawn. * @param velocity Optional. The velocity of the entity to spawn. Default is `VectorZero`. * @param spawner Optional. The entity that will be the `SpawnerEntity`. Default is undefined. * @param seedOrRNG Optional. The seed or RNG object to use to generate the `InitSeed` of the * entity. Default is undefined, which will make the entity spawn with a random * seed. */ function spawn(entityType, variant, subType, positionOrGridIndex, velocity = constants_1.VectorZero, spawner = undefined, seedOrRNG = undefined) { const room = cachedClasses_1.game.GetRoom(); // We do an explicit check to prevent run-time errors in Lua environments. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (positionOrGridIndex === undefined) { const entityID = getEntityIDFromConstituents(entityType, variant, subType); error(`Failed to spawn entity ${entityID} since an undefined position was passed to the "spawn" function.`); } const position = (0, vector_1.isVector)(positionOrGridIndex) ? positionOrGridIndex : room.GetGridPosition(positionOrGridIndex); seedOrRNG ??= (0, rng_1.newRNG)(); const seed = (0, rng_1.isRNG)(seedOrRNG) ? seedOrRNG.Next() : seedOrRNG; return cachedClasses_1.game.Spawn(entityType, variant, position, velocity, spawner, subType, seed); } /** * Helper function to spawn the entity corresponding to an `EntityID`. * * @param entityID The `EntityID` of the entity to spawn. * @param positionOrGridIndex The position or grid index of the entity to spawn. * @param velocity Optional. The velocity of the entity to spawn. Default is `VectorZero`. * @param spawner Optional. The entity that will be the `SpawnerEntity`. Default is undefined. * @param seedOrRNG Optional. The seed or RNG object to use to generate the `InitSeed` of the * entity. Default is undefined, which will make the entity spawn with a random * seed using the `Isaac.Spawn` method. */ function spawnEntityID(entityID, positionOrGridIndex, velocity = constants_1.VectorZero, spawner = undefined, seedOrRNG = undefined) { const [entityType, variant, subType] = getConstituentsFromEntityID(entityID); return spawn(entityType, variant, subType, positionOrGridIndex, velocity, spawner, seedOrRNG); } /** * Helper function to spawn an entity. Use this instead of the `Game.Spawn` method if you do not * need to specify the velocity or spawner. */ function spawnWithSeed(entityType, variant, subType, positionOrGridIndex, seedOrRNG, velocity = constants_1.VectorZero, spawner = undefined) { return spawn(entityType, variant, subType, positionOrGridIndex, velocity, spawner, seedOrRNG); }