UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

1,003 lines (1,002 loc) • 45.7 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.convertXMLGridEntityType = convertXMLGridEntityType; exports.doesGridEntityExist = doesGridEntityExist; exports.getAllGridIndexes = getAllGridIndexes; exports.getCollidingEntitiesWithGridEntity = getCollidingEntitiesWithGridEntity; exports.getConstituentsFromGridEntityID = getConstituentsFromGridEntityID; exports.getGridEntities = getGridEntities; exports.getGridEntitiesExcept = getGridEntitiesExcept; exports.getGridEntitiesInRadius = getGridEntitiesInRadius; exports.getGridEntitiesMap = getGridEntitiesMap; exports.getGridEntityANM2Path = getGridEntityANM2Path; exports.getGridEntityCollisionPoints = getGridEntityCollisionPoints; exports.getGridEntityID = getGridEntityID; exports.getGridEntityIDFromConstituents = getGridEntityIDFromConstituents; exports.getMatchingGridEntities = getMatchingGridEntities; exports.getRockPNGPath = getRockPNGPath; exports.getSurroundingGridEntities = getSurroundingGridEntities; exports.getSurroundingGridIndexes = getSurroundingGridIndexes; exports.getTopLeftWall = getTopLeftWall; exports.getTopLeftWallGridIndex = getTopLeftWallGridIndex; exports.isGridEntityBreakableByExplosion = isGridEntityBreakableByExplosion; exports.isGridEntityBroken = isGridEntityBroken; exports.isGridEntityXMLType = isGridEntityXMLType; exports.isGridIndexAdjacentToDoor = isGridIndexAdjacentToDoor; exports.isPoopGridEntityXMLType = isPoopGridEntityXMLType; exports.isPostBossVoidPortal = isPostBossVoidPortal; exports.removeAllGridEntitiesExcept = removeAllGridEntitiesExcept; exports.removeAllMatchingGridEntities = removeAllMatchingGridEntities; exports.removeEntitiesSpawnedFromGridEntity = removeEntitiesSpawnedFromGridEntity; exports.removeGridEntities = removeGridEntities; exports.removeGridEntity = removeGridEntity; exports.setGridEntityInvisible = setGridEntityInvisible; exports.setGridEntityType = setGridEntityType; exports.spawnGiantPoop = spawnGiantPoop; exports.spawnGridEntity = spawnGridEntity; exports.spawnGridEntityWithVariant = spawnGridEntityWithVariant; exports.spawnVoidPortal = spawnVoidPortal; const isaac_typescript_definitions_1 = require("isaac-typescript-definitions"); const cachedEnumValues_1 = require("../cachedEnumValues"); const cachedClasses_1 = require("../core/cachedClasses"); const constants_1 = require("../core/constants"); const gridEntityTypeToBrokenStateMap_1 = require("../maps/gridEntityTypeToBrokenStateMap"); const gridEntityXMLMap_1 = require("../maps/gridEntityXMLMap"); const roomShapeToTopLeftWallGridIndexMap_1 = require("../maps/roomShapeToTopLeftWallGridIndexMap"); const gridEntityTypeToANM2Name_1 = require("../objects/gridEntityTypeToANM2Name"); const poopGridEntityXMLTypesSet_1 = require("../sets/poopGridEntityXMLTypesSet"); const ReadonlySet_1 = require("../types/ReadonlySet"); const entities_1 = require("./entities"); const entitiesSpecific_1 = require("./entitiesSpecific"); const math_1 = require("./math"); const rooms_1 = require("./rooms"); const types_1 = require("./types"); const utils_1 = require("./utils"); const vector_1 = require("./vector"); /** * For some specific grid entities, the variant defined in the XML is what is used by the actual * game (which is not the case for e.g. poops). */ const GRID_ENTITY_TYPES_THAT_KEEP_GRID_ENTITY_XML_VARIANT = new ReadonlySet_1.ReadonlySet([ isaac_typescript_definitions_1.GridEntityType.SPIKES_ON_OFF, // 9 isaac_typescript_definitions_1.GridEntityType.PRESSURE_PLATE, // 20 isaac_typescript_definitions_1.GridEntityType.TELEPORTER, // 23 ]); const BREAKABLE_GRID_ENTITY_TYPES_BY_EXPLOSIONS = new ReadonlySet_1.ReadonlySet([ isaac_typescript_definitions_1.GridEntityType.ROCK, // 2 isaac_typescript_definitions_1.GridEntityType.ROCK_TINTED, // 4 isaac_typescript_definitions_1.GridEntityType.ROCK_BOMB, // 5 isaac_typescript_definitions_1.GridEntityType.ROCK_ALT, // 6 isaac_typescript_definitions_1.GridEntityType.SPIDER_WEB, // 10 isaac_typescript_definitions_1.GridEntityType.TNT, // 12 // GridEntityType.FIREPLACE (13) does not count since it is turned into a non-grid entity upon // spawning. isaac_typescript_definitions_1.GridEntityType.POOP, // 14 isaac_typescript_definitions_1.GridEntityType.ROCK_SUPER_SPECIAL, // 22 isaac_typescript_definitions_1.GridEntityType.ROCK_SPIKED, // 25 isaac_typescript_definitions_1.GridEntityType.ROCK_ALT_2, // 26 isaac_typescript_definitions_1.GridEntityType.ROCK_GOLD, // 27 ]); const BREAKABLE_GRID_ENTITY_TYPES_VARIANTS_BY_EXPLOSIONS = new ReadonlySet_1.ReadonlySet([`${isaac_typescript_definitions_1.GridEntityType.STATUE}.${isaac_typescript_definitions_1.StatueVariant.ANGEL}`]); const GRID_ENTITY_XML_TYPES_SET = new ReadonlySet_1.ReadonlySet(cachedEnumValues_1.GRID_ENTITY_XML_TYPE_VALUES); /** * Helper function to convert the grid entity type found in a room XML file to the corresponding * grid entity type and variant normally used by the game. For example, `GridEntityXMLType.ROCK` is * 1000 (in a room XML file), but `GridEntityType.ROCK` is equal to 2 (in-game). */ function convertXMLGridEntityType(gridEntityXMLType, gridEntityXMLVariant) { const gridEntityArray = gridEntityXMLMap_1.GRID_ENTITY_XML_MAP.get(gridEntityXMLType); (0, utils_1.assertDefined)(gridEntityArray, `Failed to find an entry in the grid entity map for XML entity type: ${gridEntityXMLType}`); const gridEntityType = gridEntityArray[0]; const variant = GRID_ENTITY_TYPES_THAT_KEEP_GRID_ENTITY_XML_VARIANT.has(gridEntityType) ? gridEntityXMLVariant : gridEntityArray[1]; return [gridEntityType, variant]; } /** * Helper function to check if one or more of a specific kind of grid entity is present in the * current room. * * @param gridEntityType The grid entity type to match. * @param variant Optional. Default is -1, which matches every variant. */ function doesGridEntityExist(gridEntityType, variant = -1) { const room = cachedClasses_1.game.GetRoom(); const gridIndexes = getAllGridIndexes(); return gridIndexes.some((gridIndex) => { const gridEntity = room.GetGridEntity(gridIndex); if (gridEntity === undefined) { return false; } const thisGridEntityType = gridEntity.GetType(); const thisVariant = gridEntity.GetVariant(); return (gridEntityType === thisGridEntityType && (variant === -1 || variant === thisVariant)); }); } /** * Helper function to get every legal grid index for the current room. * * Under the hood, this uses the `Room.GetGridSize` method. */ function getAllGridIndexes() { const room = cachedClasses_1.game.GetRoom(); const gridSize = room.GetGridSize(); return (0, utils_1.eRange)(gridSize); } /** * Gets the entities that have a hitbox that overlaps with any part of the square that the grid * entity is on. * * This function is useful because the vanilla collision callbacks do not work with grid entities. * This is used by `POST_GRID_ENTITY_COLLISION` custom callback. * * Note that this function will not work properly in the `POST_NEW_ROOM` callback since entities do * not have collision yet in that callback. */ function getCollidingEntitiesWithGridEntity(gridEntity) { const { topLeft, bottomRight } = getGridEntityCollisionPoints(gridEntity); const closeEntities = Isaac.FindInRadius(gridEntity.Position, constants_1.DISTANCE_OF_GRID_TILE * 2); return closeEntities.filter((entity) => entity.CollidesWithGrid() && (0, math_1.isCircleIntersectingRectangle)(entity.Position, // We arbitrarily add 0.1 to account for entities that are already pushed back by the time // the `POST_UPDATE` callback fires. entity.Size + 0.1, topLeft, bottomRight)); } /** Helper function to get the grid entity type and variant from a `GridEntityID`. */ function getConstituentsFromGridEntityID(gridEntityID) { const parts = gridEntityID.split("."); if (parts.length !== 2) { error(`Failed to get the constituents from a grid entity ID: ${gridEntityID}`); } const [gridEntityTypeString, variantString] = parts; (0, utils_1.assertDefined)(gridEntityTypeString, `Failed to get the first constituent from a grid entity ID: ${gridEntityID}`); (0, utils_1.assertDefined)(variantString, `Failed to get the second constituent from a grid entity ID: ${gridEntityID}`); const gridEntityType = (0, types_1.parseIntSafe)(gridEntityTypeString); (0, utils_1.assertDefined)(gridEntityType, `Failed to convert the grid entity type to a number: ${gridEntityTypeString}`); const variant = (0, types_1.parseIntSafe)(variantString); (0, utils_1.assertDefined)(variant, `Failed to convert the grid entity variant to an integer: ${variantString}`); return [gridEntityType, variant]; } /** * Helper function to get every grid entity in the current room. * * Use this function with no arguments to get every grid entity, or specify a variadic amount of * arguments to match specific grid entity types. * * For example: * * ```ts * for (const gridEntity of getGridEntities()) { * print(gridEntity.GetType()) * } * ``` * * For example: * * ```ts * const rocks = getGridEntities( * GridEntityType.ROCK, * GridEntityType.BLOCK, * GridEntityType.ROCK_TINTED, * ); * ``` * * @allowEmptyVariadic */ function getGridEntities(...gridEntityTypes) { const gridEntities = getAllGridEntities(); if (gridEntityTypes.length === 0) { return gridEntities; } const gridEntityTypesSet = new ReadonlySet_1.ReadonlySet(gridEntityTypes); return gridEntities.filter((gridEntity) => { const gridEntityType = gridEntity.GetType(); return gridEntityTypesSet.has(gridEntityType); }); } /** * Helper function to get every grid entity in the current room except for certain specific types. * * This function is variadic, meaning that you can specify as many grid entity types as you want to * exclude. */ function getGridEntitiesExcept(...gridEntityTypes) { const gridEntities = getAllGridEntities(); if (gridEntityTypes.length === 0) { return gridEntities; } const gridEntityTypesSet = new ReadonlySet_1.ReadonlySet(gridEntityTypes); return gridEntities.filter((gridEntity) => { const gridEntityType = gridEntity.GetType(); return !gridEntityTypesSet.has(gridEntityType); }); } function getAllGridEntities() { const room = cachedClasses_1.game.GetRoom(); const gridEntities = []; for (const gridIndex of getAllGridIndexes()) { const gridEntity = room.GetGridEntity(gridIndex); if (gridEntity !== undefined) { gridEntities.push(gridEntity); } } return gridEntities; } /** Helper function to get all grid entities in a given radius around a given point. */ function getGridEntitiesInRadius(targetPosition, radius) { radius = Math.abs(radius); const topLeftOffset = constants_1.VectorOne.mul(-radius); const mostTopLeftPosition = targetPosition.add(topLeftOffset); const room = cachedClasses_1.game.GetRoom(); const diameter = radius * 2; const iterations = Math.ceil(diameter / constants_1.DISTANCE_OF_GRID_TILE); const separation = diameter / iterations; const gridEntities = []; const registeredGridIndexes = new Set(); for (const x of (0, utils_1.iRange)(iterations)) { for (const y of (0, utils_1.iRange)(iterations)) { const position = mostTopLeftPosition.add(Vector(x * separation, y * separation)); const gridIndex = room.GetGridIndex(position); const gridEntity = room.GetGridEntityFromPos(position); if (gridEntity === undefined || registeredGridIndexes.has(gridIndex)) { continue; } registeredGridIndexes.add(gridIndex); const { topLeft, bottomRight } = getGridEntityCollisionPoints(gridEntity); if ((0, math_1.isCircleIntersectingRectangle)(targetPosition, radius, topLeft, bottomRight)) { gridEntities.push(gridEntity); } } } return gridEntities; } /** * Helper function to get a map of every grid entity in the current room. The indexes of the map are * equal to the grid index. The values of the map are equal to the grid entities. * * Use this function with no arguments to get every grid entity, or specify a variadic amount of * arguments to match specific grid entity types. * * @allowEmptyVariadic */ function getGridEntitiesMap(...gridEntityTypes) { const gridEntities = getGridEntities(...gridEntityTypes); const gridEntityMap = new Map(); for (const gridEntity of gridEntities) { const gridIndex = gridEntity.GetGridIndex(); gridEntityMap.set(gridIndex, gridEntity); } return gridEntityMap; } /** Helper function to get the ANM2 path for a grid entity type. */ function getGridEntityANM2Path(gridEntityType) { const gridEntityANM2Name = getGridEntityANM2Name(gridEntityType); return `gfx/grid/${gridEntityANM2Name}`; } function getGridEntityANM2Name(gridEntityType) { switch (gridEntityType) { // 1 case isaac_typescript_definitions_1.GridEntityType.DECORATION: { return getGridEntityANM2NameDecoration(); } default: { return gridEntityTypeToANM2Name_1.GRID_ENTITY_TYPE_TO_ANM2_NAME[gridEntityType]; } } } /** * Helper function to get the ANM2 path for a decoration. This depends on the current room's * backdrop. The values are taken from the "backdrops.xml" file. */ function getGridEntityANM2NameDecoration() { const room = cachedClasses_1.game.GetRoom(); const backdropType = room.GetBackdropType(); switch (backdropType) { // 1, 2, 3, 36, 49, 52 case isaac_typescript_definitions_1.BackdropType.BASEMENT: case isaac_typescript_definitions_1.BackdropType.CELLAR: case isaac_typescript_definitions_1.BackdropType.BURNING_BASEMENT: case isaac_typescript_definitions_1.BackdropType.DOWNPOUR_ENTRANCE: case isaac_typescript_definitions_1.BackdropType.ISAACS_BEDROOM: case isaac_typescript_definitions_1.BackdropType.CLOSET: { return "Props_01_Basement.anm2"; } // 4, 5, 6, 37 case isaac_typescript_definitions_1.BackdropType.CAVES: case isaac_typescript_definitions_1.BackdropType.CATACOMBS: case isaac_typescript_definitions_1.BackdropType.FLOODED_CAVES: case isaac_typescript_definitions_1.BackdropType.MINES_ENTRANCE: { return "Props_03_Caves.anm2"; } // 7, 8, 9, 30, 33, 38, 39, 40, 41, 42, 53, 60 case isaac_typescript_definitions_1.BackdropType.DEPTHS: case isaac_typescript_definitions_1.BackdropType.NECROPOLIS: case isaac_typescript_definitions_1.BackdropType.DANK_DEPTHS: case isaac_typescript_definitions_1.BackdropType.SACRIFICE: case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM: case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM_ENTRANCE: case isaac_typescript_definitions_1.BackdropType.CORPSE_ENTRANCE: case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM_2: case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM_3: case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM_4: case isaac_typescript_definitions_1.BackdropType.CLOSET_B: case isaac_typescript_definitions_1.BackdropType.DARK_CLOSET: { return "Props_05_Depths.anm2"; } // 10, 12 case isaac_typescript_definitions_1.BackdropType.WOMB: case isaac_typescript_definitions_1.BackdropType.SCARRED_WOMB: { return "Props_07_The Womb.anm2"; } // 11 case isaac_typescript_definitions_1.BackdropType.UTERO: { return "Props_07_Utero.anm2"; } // 13, 27 case isaac_typescript_definitions_1.BackdropType.BLUE_WOMB: case isaac_typescript_definitions_1.BackdropType.BLUE_WOMB_PASS: { return "Props_07_The Womb_blue.anm2"; } // 14, 47 case isaac_typescript_definitions_1.BackdropType.SHEOL: case isaac_typescript_definitions_1.BackdropType.GEHENNA: { return "Props_09_Sheol.anm2"; } // 15 case isaac_typescript_definitions_1.BackdropType.CATHEDRAL: { return "Props_10_Cathedral.anm2"; } // 17 case isaac_typescript_definitions_1.BackdropType.CHEST: { return "Props_11_The Chest.anm2"; } // 28 case isaac_typescript_definitions_1.BackdropType.GREED_SHOP: { return "Props_12_Greed.anm2"; } // 31 case isaac_typescript_definitions_1.BackdropType.DOWNPOUR: { return "props_01x_downpour.anm2"; } // 32, 46, 58, 59 case isaac_typescript_definitions_1.BackdropType.MINES: case isaac_typescript_definitions_1.BackdropType.ASHPIT: case isaac_typescript_definitions_1.BackdropType.MINES_SHAFT: case isaac_typescript_definitions_1.BackdropType.ASHPIT_SHAFT: { return "props_03x_mines.anm2"; } // 34, 43, 44, 48 case isaac_typescript_definitions_1.BackdropType.CORPSE: case isaac_typescript_definitions_1.BackdropType.CORPSE_2: case isaac_typescript_definitions_1.BackdropType.CORPSE_3: case isaac_typescript_definitions_1.BackdropType.MORTIS: { return "props_07_the corpse.anm2"; } // 45 case isaac_typescript_definitions_1.BackdropType.DROSS: { return "props_02x_dross.anm2"; } default: { return "Props_01_Basement.anm2"; } } } /** Helper function to get the top left and bottom right corners of a given grid entity. */ function getGridEntityCollisionPoints(gridEntity) { const topLeft = Vector(gridEntity.Position.X - constants_1.DISTANCE_OF_GRID_TILE / 2, gridEntity.Position.Y - constants_1.DISTANCE_OF_GRID_TILE / 2); const bottomRight = Vector(gridEntity.Position.X + constants_1.DISTANCE_OF_GRID_TILE / 2, gridEntity.Position.Y + constants_1.DISTANCE_OF_GRID_TILE / 2); return { topLeft, bottomRight }; } /** Helper function to get a string containing the grid entity's type and variant. */ function getGridEntityID(gridEntity) { const gridEntityType = gridEntity.GetType(); const variant = gridEntity.GetVariant(); return `${gridEntityType}.${variant}`; } /** * Helper function to get a formatted string in the format returned by the `getGridEntityID` * function. */ function getGridEntityIDFromConstituents(gridEntityType, variant) { return `${gridEntityType}.${variant}`; } /** * Helper function to get all of the grid entities in the room that specifically match the type and * variant provided. * * If you want to match every variant, use the `getGridEntities` function instead. */ function getMatchingGridEntities(gridEntityType, variant) { const gridEntities = getGridEntities(gridEntityType); return gridEntities.filter((gridEntity) => gridEntity.GetVariant() === variant); } /** * Helper function to get the PNG path for a rock. This depends on the current room's backdrop. The * values are taken from the "backdrops.xml" file. * * All of the rock PNGs are in the "gfx/grid" directory. */ function getRockPNGPath() { const rockPNGName = getRockPNGName(); return `gfx/grid/${rockPNGName}`; } function getRockPNGName() { const room = cachedClasses_1.game.GetRoom(); const backdropType = room.GetBackdropType(); switch (backdropType) { // 1, 17 case isaac_typescript_definitions_1.BackdropType.BASEMENT: case isaac_typescript_definitions_1.BackdropType.CHEST: { return "rocks_basement.png"; } // 2 case isaac_typescript_definitions_1.BackdropType.CELLAR: { return "rocks_cellar.png"; } // 3 case isaac_typescript_definitions_1.BackdropType.BURNING_BASEMENT: { return "rocks_burningbasement.png"; // cspell:ignore burningbasement } // 4 case isaac_typescript_definitions_1.BackdropType.CAVES: { return "rocks_caves.png"; } // 5 case isaac_typescript_definitions_1.BackdropType.CATACOMBS: { return "rocks_catacombs.png"; } // 6 case isaac_typescript_definitions_1.BackdropType.FLOODED_CAVES: { return "rocks_drownedcaves.png"; // cspell:ignore drownedcaves } // 7, 8, 9, 30, 60 case isaac_typescript_definitions_1.BackdropType.DEPTHS: case isaac_typescript_definitions_1.BackdropType.NECROPOLIS: case isaac_typescript_definitions_1.BackdropType.DANK_DEPTHS: case isaac_typescript_definitions_1.BackdropType.SACRIFICE: case isaac_typescript_definitions_1.BackdropType.DARK_CLOSET: { return "rocks_depths.png"; } // 10 case isaac_typescript_definitions_1.BackdropType.WOMB: { return "rocks_womb.png"; } // 11 case isaac_typescript_definitions_1.BackdropType.UTERO: { return "rocks_utero.png"; } // 12 case isaac_typescript_definitions_1.BackdropType.SCARRED_WOMB: { return "rocks_scarredwomb.png"; // cspell:ignore scarredwomb } // 13, 27 case isaac_typescript_definitions_1.BackdropType.BLUE_WOMB: case isaac_typescript_definitions_1.BackdropType.BLUE_WOMB_PASS: { return "rocks_bluewomb.png"; // cspell:ignore bluewomb } // 14, 16 case isaac_typescript_definitions_1.BackdropType.SHEOL: case isaac_typescript_definitions_1.BackdropType.DARK_ROOM: { return "rocks_sheol.png"; } // 15, 35 case isaac_typescript_definitions_1.BackdropType.CATHEDRAL: case isaac_typescript_definitions_1.BackdropType.PLANETARIUM: { return "rocks_cathedral.png"; } // 23, 32, 37, 58 case isaac_typescript_definitions_1.BackdropType.SECRET: case isaac_typescript_definitions_1.BackdropType.MINES: case isaac_typescript_definitions_1.BackdropType.MINES_ENTRANCE: case isaac_typescript_definitions_1.BackdropType.MINES_SHAFT: { return "rocks_secretroom.png"; // cspell:ignore secretroom } // 31, 36 case isaac_typescript_definitions_1.BackdropType.DOWNPOUR: case isaac_typescript_definitions_1.BackdropType.DOWNPOUR_ENTRANCE: { return "rocks_downpour.png"; } // 33, 38, 40, 41, 42 case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM: case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM_ENTRANCE: case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM_2: case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM_3: case isaac_typescript_definitions_1.BackdropType.MAUSOLEUM_4: { return "rocks_mausoleum.png"; } // 34, 48 case isaac_typescript_definitions_1.BackdropType.CORPSE: case isaac_typescript_definitions_1.BackdropType.MORTIS: { return "rocks_corpse.png"; } // 39 case isaac_typescript_definitions_1.BackdropType.CORPSE_ENTRANCE: { return "rocks_corpseentrance.png"; // cspell:ignore corpseentrance } // 43 case isaac_typescript_definitions_1.BackdropType.CORPSE_2: { return "rocks_corpse2.png"; } // 44 case isaac_typescript_definitions_1.BackdropType.CORPSE_3: { return "rocks_corpse3.png"; } // 45 case isaac_typescript_definitions_1.BackdropType.DROSS: { return "rocks_dross.png"; } // 46, 59 case isaac_typescript_definitions_1.BackdropType.ASHPIT: case isaac_typescript_definitions_1.BackdropType.ASHPIT_SHAFT: { return "rocks_ashpit.png"; } // 47 case isaac_typescript_definitions_1.BackdropType.GEHENNA: { return "rocks_gehenna.png"; } default: { return "rocks_basement.png"; } } } /** * Helper function to get the grid entities on the surrounding tiles from the provided grid entity. * * For example, if a rock was surrounded by rocks on all sides, this would return an array of 8 * rocks (e.g. top-left + top + top-right + left + right + bottom-left + bottom + right). */ function getSurroundingGridEntities(gridEntity) { const room = cachedClasses_1.game.GetRoom(); const gridIndex = gridEntity.GetGridIndex(); const surroundingGridIndexes = getSurroundingGridIndexes(gridIndex); const surroundingGridEntities = []; for (const surroundingGridIndex of surroundingGridIndexes) { const surroundingGridEntity = room.GetGridEntity(surroundingGridIndex); if (surroundingGridEntity !== undefined) { surroundingGridEntities.push(surroundingGridEntity); } } return surroundingGridEntities; } /** * Helper function to get the grid indexes on the surrounding tiles from the provided grid index. * * There are always 8 grid indexes returned (e.g. top-left + top + top-right + left + right + * bottom-left + bottom + right), even if the computed values would be negative or otherwise * invalid. */ function getSurroundingGridIndexes(gridIndex) { const room = cachedClasses_1.game.GetRoom(); const gridWidth = room.GetGridWidth(); return [ gridIndex - gridWidth - 1, // Top-left gridIndex - gridWidth, // Top gridIndex - gridWidth + 1, // Top-right gridIndex - 1, // Left gridIndex + 1, // Right gridIndex + gridWidth - 1, // Bottom-left gridIndex + gridWidth, // Bottom gridIndex + gridWidth + 1, // Bottom-right ]; } /** * Helper function to get the top left wall in the current room. * * This function can be useful in certain situations to determine if the room is currently loaded. */ function getTopLeftWall() { const room = cachedClasses_1.game.GetRoom(); const topLeftWallGridIndex = getTopLeftWallGridIndex(); return room.GetGridEntity(topLeftWallGridIndex); } /** * Helper function to get the grid index of the top left wall. (This will depend on what the current * room shape is.) * * This function can be useful in certain situations to determine if the room is currently loaded. */ function getTopLeftWallGridIndex() { const room = cachedClasses_1.game.GetRoom(); const roomShape = room.GetRoomShape(); const topLeftWallGridIndex = roomShapeToTopLeftWallGridIndexMap_1.ROOM_SHAPE_TO_TOP_LEFT_WALL_GRID_INDEX_MAP.get(roomShape); return topLeftWallGridIndex ?? roomShapeToTopLeftWallGridIndexMap_1.DEFAULT_TOP_LEFT_WALL_GRID_INDEX; } /** * Helper function to detect if a particular grid entity would "break" if it was touched by an * explosion. * * For example, rocks and pots are breakable by explosions, but blocks are not. */ function isGridEntityBreakableByExplosion(gridEntity) { const gridEntityType = gridEntity.GetType(); const variant = gridEntity.GetVariant(); const gridEntityTypeVariant = `${gridEntityType}.${variant}`; return (BREAKABLE_GRID_ENTITY_TYPES_BY_EXPLOSIONS.has(gridEntityType) || BREAKABLE_GRID_ENTITY_TYPES_VARIANTS_BY_EXPLOSIONS.has(gridEntityTypeVariant)); } /** * Helper function to see if the provided grid entity is in its respective broken state. See the * `GRID_ENTITY_TYPE_TO_BROKEN_STATE_MAP` constant for more details. * * Note that in the case of `GridEntityType.LOCK` (11), the state will turn to being broken before * the actual collision for the entity is removed. */ function isGridEntityBroken(gridEntity) { const gridEntityType = gridEntity.GetType(); const brokenState = gridEntityTypeToBrokenStateMap_1.GRID_ENTITY_TYPE_TO_BROKEN_STATE_MAP.get(gridEntityType); return gridEntity.State === brokenState; } /** * Helper function to see if an arbitrary number is a valid `GridEntityXMLType`. This is useful in * the `PRE_ROOM_ENTITY_SPAWN` callback for narrowing the type of the first argument. */ function isGridEntityXMLType(num) { return GRID_ENTITY_XML_TYPES_SET.has(num); // eslint-disable-line complete/strict-enums } /** * Helper function to check if the provided grid index has a door on it or if the surrounding 8 grid * indexes have a door on it. */ function isGridIndexAdjacentToDoor(gridIndex) { const room = cachedClasses_1.game.GetRoom(); const surroundingGridIndexes = getSurroundingGridIndexes(gridIndex); const gridIndexes = [gridIndex, ...surroundingGridIndexes]; for (const gridIndexToInspect of gridIndexes) { const gridEntity = room.GetGridEntity(gridIndexToInspect); if (gridEntity !== undefined) { const door = gridEntity.ToDoor(); if (door !== undefined) { return true; } } } return false; } /** Helper function to see if a `GridEntityXMLType` is some kind of poop. */ function isPoopGridEntityXMLType(gridEntityXMLType) { return poopGridEntityXMLTypesSet_1.POOP_GRID_ENTITY_XML_TYPES_SET.has(gridEntityXMLType); } /** * Helper function to detect whether a given Void Portal is one that randomly spawns after a boss is * defeated or is one that naturally spawns in the room after Hush. * * Under the hood, this is determined by looking at the `VarData` of the entity: * - The `VarData` of Void Portals that are spawned after bosses will be equal to 1. * - The `VarData` of the Void Portal in the room after Hush is equal to 0. */ function isPostBossVoidPortal(gridEntity) { const saveState = gridEntity.GetSaveState(); return (saveState.Type === isaac_typescript_definitions_1.GridEntityType.TRAPDOOR // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && saveState.Variant === isaac_typescript_definitions_1.TrapdoorVariant.VOID_PORTAL && saveState.VarData === 1); } /** * Helper function to all grid entities in the room except for ones matching the grid entity types * provided. * * Note that this function will automatically update the room. (This means that you can spawn new * grid entities on the same tile on the same frame, if needed.) * * For example: * * ```ts * removeAllGridEntitiesExcept( * GridEntityType.WALL, * GridEntityType.DOOR, * ); * ``` * * @returns The grid entities that were removed. */ function removeAllGridEntitiesExcept(...gridEntityTypes) { const gridEntityTypeExceptions = new ReadonlySet_1.ReadonlySet(gridEntityTypes); const gridEntities = getGridEntities(); const removedGridEntities = []; for (const gridEntity of gridEntities) { const gridEntityType = gridEntity.GetType(); if (!gridEntityTypeExceptions.has(gridEntityType)) { removeGridEntity(gridEntity, false); removedGridEntities.push(gridEntity); } } if (removedGridEntities.length > 0) { (0, rooms_1.roomUpdateSafe)(); } return removedGridEntities; } /** * Helper function to remove all of the grid entities in the room that match the grid entity types * provided. * * Note that this function will automatically update the room. (This means that you can spawn new * grid entities on the same tile on the same frame, if needed.) * * For example: * * ```ts * removeAllMatchingGridEntities( * GridEntityType.ROCK, * GridEntityType.BLOCK, * GridEntityType.ROCK_TINTED, * ); * ``` * * @returns An array of the grid entities removed. */ function removeAllMatchingGridEntities(...gridEntityType) { const gridEntities = getGridEntities(...gridEntityType); if (gridEntities.length === 0) { return []; } for (const gridEntity of gridEntities) { removeGridEntity(gridEntity, false); } (0, rooms_1.roomUpdateSafe)(); return gridEntities; } /** * Helper function to remove all entities that just spawned from a grid entity breaking. * Specifically, this is any entities that overlap with the position of a grid entity and are on * frame 0. * * You must specify an array of entities to look through. */ function removeEntitiesSpawnedFromGridEntity(entities, gridEntity) { const entitiesFromGridEntity = entities.filter((entity) => entity.FrameCount === 0 && (0, vector_1.vectorEquals)(entity.Position, gridEntity.Position)); (0, entities_1.removeEntities)(entitiesFromGridEntity); } /** * Helper function to remove all of the grid entities in the supplied array. * * @param gridEntities The array of grid entities to remove. * @param updateRoom Whether to update the room after the grid entities are removed. This is * generally a good idea because if the room is not updated, you will be unable to * spawn another grid entity on the same tile until a frame has passed. However, * doing this is expensive, since it involves a call to `Isaac.GetRoomEntities`, * so set this to false if you need to run this function multiple times. * @param cap Optional. If specified, will only remove the given amount of entities. * @returns An array of the entities that were removed. */ function removeGridEntities(gridEntities, updateRoom, cap) { if (gridEntities.length === 0) { return []; } const gridEntitiesRemoved = []; for (const gridEntity of gridEntities) { removeGridEntity(gridEntity, false); gridEntitiesRemoved.push(gridEntity); if (cap !== undefined && gridEntitiesRemoved.length >= cap) { break; } } if (updateRoom) { (0, rooms_1.roomUpdateSafe)(); } return gridEntitiesRemoved; } /** * Helper function to remove a grid entity by providing the grid entity object or the grid index * inside of the room. * * If removing a Devil Statue or an Angel Statue, this will also remove the associated effect * (`EffectVariant.DEVIL` (6) or `EffectVariant.ANGEL` (9), respectively.) * * @param gridEntityOrGridIndex The grid entity or grid index to remove. * @param updateRoom Whether to update the room after the grid entity is removed. This is generally * a good idea because if the room is not updated, you will be unable to spawn * another grid entity on the same tile until a frame has passed. However, doing * this is expensive, since it involves a call to `Isaac.GetRoomEntities`, so set * this to false if you need to run this function multiple times. */ function removeGridEntity(gridEntityOrGridIndex, updateRoom) { const room = cachedClasses_1.game.GetRoom(); const gridEntity = (0, types_1.isInteger)(gridEntityOrGridIndex) ? room.GetGridEntity(gridEntityOrGridIndex) : gridEntityOrGridIndex; if (gridEntity === undefined) { // There is no grid entity to remove. return; } const gridEntityType = gridEntity.GetType(); const variant = gridEntity.GetVariant(); const position = gridEntity.Position; const gridIndex = (0, types_1.isInteger)(gridEntityOrGridIndex) ? gridEntityOrGridIndex : gridEntityOrGridIndex.GetGridIndex(); room.RemoveGridEntity(gridIndex, 0, false); if (updateRoom) { (0, rooms_1.roomUpdateSafe)(); } // In the special case of removing a Devil Statue or Angel Statue, we also need to delete the // corresponding effect. if (gridEntityType === isaac_typescript_definitions_1.GridEntityType.STATUE) { const effectVariant = // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison variant === isaac_typescript_definitions_1.StatueVariant.DEVIL ? isaac_typescript_definitions_1.EffectVariant.DEVIL : isaac_typescript_definitions_1.EffectVariant.ANGEL; const effects = (0, entitiesSpecific_1.getEffects)(effectVariant); const effectsOnTile = effects.filter((effect) => (0, vector_1.vectorEquals)(effect.Position, position)); (0, entities_1.removeEntities)(effectsOnTile); } } /** * Helper function to make a grid entity invisible. This is accomplished by resetting the sprite. * * Note that this function is destructive such that once you make a grid entity invisible, it can no * longer become visible. (This is because the information about the sprite is lost when it is * reset.) */ function setGridEntityInvisible(gridEntity) { const sprite = gridEntity.GetSprite(); sprite.Reset(); } /** * Helper function to change the type of a grid entity to another type. Use this instead of the * `GridEntity.SetType` method since that does not properly handle updating the sprite of the grid * entity after the type is changed. * * Setting the new type to `GridEntityType.NULL` (0) will have no effect. */ function setGridEntityType(gridEntity, gridEntityType) { gridEntity.SetType(gridEntityType); const sprite = gridEntity.GetSprite(); const anm2Path = getGridEntityANM2Path(gridEntityType); if (anm2Path === undefined) { return; } sprite.Load(anm2Path, false); if (gridEntityType === isaac_typescript_definitions_1.GridEntityType.ROCK) { const pngPath = getRockPNGPath(); sprite.ReplaceSpritesheet(0, pngPath); } sprite.LoadGraphics(); const defaultAnimation = sprite.GetDefaultAnimation(); sprite.Play(defaultAnimation, true); } /** * Helper function to spawn a giant poop. This is performed by spawning each of the four quadrant * grid entities in the appropriate positions. * * @returns Whether spawning the four quadrants was successful. */ function spawnGiantPoop(topLeftGridIndex) { const room = cachedClasses_1.game.GetRoom(); const gridWidth = room.GetGridWidth(); const topRightGridIndex = topLeftGridIndex + 1; const bottomLeftGridIndex = topLeftGridIndex + gridWidth; const bottomRightGridIndex = bottomLeftGridIndex + 1; // First, check to see if all of the tiles are open. for (const gridIndex of [ topLeftGridIndex, topRightGridIndex, bottomLeftGridIndex, bottomRightGridIndex, ]) { const gridEntity = room.GetGridEntity(gridIndex); if (gridEntity !== undefined) { return false; } } const topLeft = spawnGridEntityWithVariant(isaac_typescript_definitions_1.GridEntityType.POOP, isaac_typescript_definitions_1.PoopGridEntityVariant.GIANT_TOP_LEFT, topLeftGridIndex); const topRight = spawnGridEntityWithVariant(isaac_typescript_definitions_1.GridEntityType.POOP, isaac_typescript_definitions_1.PoopGridEntityVariant.GIANT_TOP_RIGHT, topRightGridIndex); const bottomLeft = spawnGridEntityWithVariant(isaac_typescript_definitions_1.GridEntityType.POOP, isaac_typescript_definitions_1.PoopGridEntityVariant.GIANT_BOTTOM_LEFT, bottomLeftGridIndex); const bottomRight = spawnGridEntityWithVariant(isaac_typescript_definitions_1.GridEntityType.POOP, isaac_typescript_definitions_1.PoopGridEntityVariant.GIANT_BOTTOM_RIGHT, bottomRightGridIndex); return (topLeft !== undefined && topLeft.GetType() === isaac_typescript_definitions_1.GridEntityType.POOP // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && topLeft.GetVariant() === isaac_typescript_definitions_1.PoopGridEntityVariant.GIANT_TOP_LEFT && topRight !== undefined && topRight.GetType() === isaac_typescript_definitions_1.GridEntityType.POOP // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && topRight.GetVariant() === isaac_typescript_definitions_1.PoopGridEntityVariant.GIANT_TOP_RIGHT && bottomLeft !== undefined && bottomLeft.GetType() === isaac_typescript_definitions_1.GridEntityType.POOP // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && bottomLeft.GetVariant() === isaac_typescript_definitions_1.PoopGridEntityVariant.GIANT_BOTTOM_LEFT && bottomRight !== undefined && bottomRight.GetType() === isaac_typescript_definitions_1.GridEntityType.POOP // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && bottomRight.GetVariant() === isaac_typescript_definitions_1.PoopGridEntityVariant.GIANT_BOTTOM_RIGHT); } /** * Helper function to spawn a grid entity with a specific type. * * This function assumes you want to give the grid entity a variant of 0. If you want to specify a * variant, use the `spawnGridEntityWithVariant` helper function instead. * * Use this instead of the `Isaac.GridSpawn` method since it: * - handles giving pits collision * - removes existing grid entities on the same tile, if any * - allows you to specify either the grid index or the position * * @param gridEntityType The `GridEntityType` to use. * @param gridIndexOrPosition The grid index or position in the room that you want to spawn the grid * entity at. If a position is specified, the closest grid index will be * used. * @param removeExistingGridEntity Optional. Whether to remove the existing grid entity on the same * tile, if it exists. Defaults to true. If false, this function * will do nothing, since spawning a grid entity on top of another * grid entity will not replace it. */ function spawnGridEntity(gridEntityType, gridIndexOrPosition, removeExistingGridEntity = true) { return spawnGridEntityWithVariant(gridEntityType, 0, gridIndexOrPosition, removeExistingGridEntity); } /** * Helper function to spawn a grid entity with a specific variant. * * Use this instead of the `Isaac.GridSpawn` method since it: * - handles giving pits collision * - removes existing grid entities on the same tile, if any * - allows you to specify the grid index or the position * * @param gridEntityType The `GridEntityType` to use. * @param variant The variant to use. * @param gridIndexOrPosition The grid index or position in the room that you want to spawn the grid * entity at. If a position is specified, the closest grid index will be * used. * @param removeExistingGridEntity Optional. Whether to remove the existing grid entity on the same * tile, if it exists. Defaults to true. If false, this function * will do nothing, since spawning a grid entity on top of another * grid entity will not replace it. */ function spawnGridEntityWithVariant(gridEntityType, variant, gridIndexOrPosition, removeExistingGridEntity = true) { 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 (gridIndexOrPosition === undefined) { const gridEntityID = getGridEntityIDFromConstituents(gridEntityType, variant); error(`Failed to spawn grid entity ${gridEntityID} since an undefined position was passed to the "spawnGridEntityWithVariant" function.`); } const existingGridEntity = (0, vector_1.isVector)(gridIndexOrPosition) ? room.GetGridEntityFromPos(gridIndexOrPosition) : room.GetGridEntity(gridIndexOrPosition); if (existingGridEntity !== undefined) { if (removeExistingGridEntity) { removeGridEntity(existingGridEntity, true); } else { return undefined; } } const position = (0, vector_1.isVector)(gridIndexOrPosition) ? gridIndexOrPosition : room.GetGridPosition(gridIndexOrPosition); const gridEntity = Isaac.GridSpawn(gridEntityType, variant, position); if (gridEntity === undefined) { return gridEntity; } if (gridEntityType === isaac_typescript_definitions_1.GridEntityType.PIT) { // For some reason, spawned pits start with a collision class of `NONE`, so we have to manually // set it. const pit = gridEntity.ToPit(); if (pit !== undefined) { pit.UpdateCollision(); } } else if (gridEntityType === isaac_typescript_definitions_1.GridEntityType.WALL) { // For some reason, spawned walls start with a collision class of `NONE`, so we have to manually // set it. gridEntity.CollisionClass = isaac_typescript_definitions_1.GridCollisionClass.WALL; } return gridEntity; } /** * Helper function to spawn a Void Portal. This is more complicated than simply spawning a trapdoor * with the appropriate variant, as the game does not give it the correct sprite automatically. */ function spawnVoidPortal(gridIndex) { const voidPortal = spawnGridEntityWithVariant(isaac_typescript_definitions_1.GridEntityType.TRAPDOOR, isaac_typescript_definitions_1.TrapdoorVariant.VOID_PORTAL, gridIndex); if (voidPortal === undefined) { return voidPortal; } // If Void Portals are not given a VarData of 1, they will send the player to the next floor // instead of The Void. voidPortal.VarData = 1; const sprite = voidPortal.GetSprite(); sprite.Load("gfx/grid/voidtrapdoor.anm2", true); return voidPortal; }