UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

939 lines (842 loc) • 33.5 kB
import type { BackdropType, BossID, ItemPoolType, MinibossID, RoomShape, } from "isaac-typescript-definitions"; import { AngelRoomSubType, Dimension, DoorSlot, DoorSlotFlag, DownpourRoomSubType, DungeonSubType, GridRoom, HomeRoomSubType, RoomDescriptorFlag, RoomType, SoundEffect, StageID, } from "isaac-typescript-definitions"; import { game, sfxManager } from "../core/cachedClasses"; import { DIMENSIONS, MAX_LEVEL_GRID_INDEX } from "../core/constants"; import { ROOM_TYPE_NAMES } from "../objects/roomTypeNames"; import { MINE_SHAFT_ROOM_SUB_TYPE_SET } from "../sets/mineShaftRoomSubTypesSet"; import { ReadonlySet } from "../types/ReadonlySet"; import { inDimension } from "./dimensions"; import { closeAllDoors, getDoors, isHiddenSecretRoomDoor, openDoorFast, } from "./doors"; import { getEntities } from "./entities"; import { hasFlag } from "./flag"; import { getEntityPositions, getEntityVelocities, setEntityPositions, setEntityVelocities, } from "./positionVelocity"; import { getRoomData, getRoomDescriptor, getRoomDescriptorReadOnly, getRoomGridIndex, } from "./roomData"; import { is2x1RoomShape, isBigRoomShape, isLRoomShape } from "./roomShape"; import { reloadRoom } from "./roomTransition"; import { getGotoCommand } from "./stage"; import { assertDefined, iRange } from "./utils"; const SECRET_ROOM_TYPES = new ReadonlySet([ RoomType.SECRET, RoomType.SUPER_SECRET, RoomType.ULTRA_SECRET, ]); /** * Helper function for quickly switching to a new room without playing a particular animation. Use * this helper function over invoking the `Game.ChangeRoom` method directly to ensure that you do * not forget to set the `LeaveDoor` field and to prevent crashing on invalid room grid indexes. */ export function changeRoom(roomGridIndex: int): void { const level = game.GetLevel(); const roomData = getRoomData(roomGridIndex); assertDefined( roomData, `Failed to change the room to grid index ${roomGridIndex} because that room does not exist.`, ); // LeaveDoor must be set before every `Game.ChangeRoom` invocation or else the function can send // you to the wrong room. level.LeaveDoor = DoorSlot.NO_DOOR_SLOT; game.ChangeRoom(roomGridIndex); } /** * Helper function to get the number of rooms that are currently on the floor layout. This does not * include off-grid rooms, like the Devil Room. */ export function getNumRooms(): int { const roomsInsideGrid = getRoomsInsideGrid(); return roomsInsideGrid.length; } /** * Helper function to get a read-only copy of the room descriptor for every room on the level. This * includes off-grid rooms, such as the Devil Room, and extra-dimensional rooms, if they are * generated and exist. * * Room descriptors without any data are assumed to be non-existent and are not included. * * Under the hood, this is performed by iterating over the `RoomList` from the `Level.GetRooms` * method. This is the best way to see if off-grid rooms have been initialized, since it is possible * for mods to insert room data at non-official negative room grid indexes. */ export function getReadOnlyRooms(): ReadonlyArray<Readonly<RoomDescriptor>> { const level = game.GetLevel(); const roomList = level.GetRooms(); const readOnlyRoomDescriptors: Array<Readonly<RoomDescriptor>> = []; for (let i = 0; i < roomList.Size; i++) { const readOnlyRoomDescriptor = roomList.Get(i); if ( readOnlyRoomDescriptor !== undefined && readOnlyRoomDescriptor.Data !== undefined ) { readOnlyRoomDescriptors.push(readOnlyRoomDescriptor); } } return readOnlyRoomDescriptors; } /** * Helper function to get the room data for a specific room type and variant combination. This is * accomplished by using the "goto" console command to load the specified room into the * `GridRoom.DEBUG` slot. * * Returns undefined if the provided room type and variant combination were not found. (A warning * message will also appear on the console, since the "goto" command will fail.) * * Note that the side effect of using the "goto" console command is that it will trigger a room * transition after a short delay. By default, this function cancels the incoming room transition by * using the `Game.StartRoomTransition` method to travel to the same room. * * @param roomType The type of room to retrieve. * @param roomVariant The room variant to retrieve. (The room variant is the "ID" of the room in * Basement Renovator.) * @param cancelRoomTransition Optional. Whether to cancel the room transition by using the * `Game.StartRoomTransition` method to travel to the same room. Default * is true. Set this to false if you are getting the data for many rooms * at the same time, and then use the `teleport` helper function when * you are finished. * @param useSpecialRoomsForRoomTypeDefault Optional. Whether to use `s.default` as the prefix for * the `goto` command (instead of `d`) if the room type is * `RoomType.DEFAULT` (1). False by default. */ export function getRoomDataForTypeVariant( roomType: RoomType, roomVariant: int, cancelRoomTransition = true, useSpecialRoomsForRoomTypeDefault = false, ): Readonly<RoomConfig> | undefined { const command = getGotoCommand( roomType, roomVariant, useSpecialRoomsForRoomTypeDefault, ); // We do not want to log the command execution, because this function will potentially be called // many times. Isaac.ExecuteCommand(command); const newRoomData = getRoomData(GridRoom.DEBUG); if (cancelRoomTransition) { reloadRoom(); } return newRoomData; } /** * Helper function to get the item pool type for the current room. For example, this returns * `ItemPoolType.ItemPoolType.POOL_ANGEL` if you are in an Angel Room. */ export function getRoomItemPoolType(): ItemPoolType { const itemPool = game.GetItemPool(); const room = game.GetRoom(); const roomType = room.GetType(); const roomSeed = room.GetSpawnSeed(); return itemPool.GetPoolForRoom(roomType, roomSeed); } /** * Helper function to get the proper name of a room type. * * For example, `RoomType.TREASURE` will return "Treasure Room". */ export function getRoomTypeName(roomType: RoomType): string { return ROOM_TYPE_NAMES[roomType]; } /** * Helper function to get the room descriptor for every room on the level. This includes off-grid * rooms, such as the Devil Room. * * Room without any data are assumed to be non-existent and are not included. * * - If you want just the rooms inside of the grid, use the `getRoomsInsideGrid` helper function. * - If you want just the rooms outside of the grid, use the `getRoomsOutsideGrid` helper function. * * @param includeExtraDimensionalRooms Optional. On some floors (e.g. Downpour 2, Mines 2), * extra-dimensional rooms are automatically generated. Default is * false. */ export function getRooms( includeExtraDimensionalRooms = false, ): readonly RoomDescriptor[] { // The naive way to get all of the rooms would be to iterate over the `RoomList` from the // `Level.GetRooms` method. However, this results in read-only data, and we want to return a // writable object. Instead, we let the heavy lifting be handled by other functions. const roomsInGrid = getRoomsInsideGrid(includeExtraDimensionalRooms); const roomsOutsideGrid = getRoomsOutsideGrid(); return [...roomsInGrid, ...roomsOutsideGrid]; } /** * Helper function to get the room descriptor for every room on the level that is on the grid. (For * example, Devil Rooms are excluded.) * * Room descriptors without any data are assumed to be non-existent and are not included. * * @param includeExtraDimensionalRooms Optional. On some floors (e.g. Downpour 2, Mines 2), * extra-dimensional rooms will be generated. Default is false. */ export function getRoomsInsideGrid( includeExtraDimensionalRooms = false, ): readonly RoomDescriptor[] { const level = game.GetLevel(); const dimensions = includeExtraDimensionalRooms ? DIMENSIONS : [Dimension.CURRENT]; /** We use a map instead of an array because room shapes occupy more than one room grid index. */ const roomDescriptorMap = new Map<PtrHash, RoomDescriptor>(); for (const dimension of dimensions) { for (const roomGridIndex of iRange(MAX_LEVEL_GRID_INDEX)) { const roomDescriptor = level.GetRoomByIdx(roomGridIndex, dimension); if (roomDescriptor.Data !== undefined) { const ptrHash = GetPtrHash(roomDescriptor); roomDescriptorMap.set(ptrHash, roomDescriptor); } } } return [...roomDescriptorMap.values()]; } /** * Helper function to get the room descriptor for every room on the level in a specific dimension. * This will not include any off-grid rooms, such as the Devil Room. * * Room descriptors without any data are assumed to be non-existent and are not included. */ export function getRoomsOfDimension( dimension: Dimension, ): readonly RoomDescriptor[] { const level = game.GetLevel(); /** We use a map instead of an array because room shapes occupy more than one room grid index. */ const roomsMap = new Map<PtrHash, RoomDescriptor>(); for (const roomGridIndex of iRange(MAX_LEVEL_GRID_INDEX)) { const roomDescriptor = level.GetRoomByIdx(roomGridIndex, dimension); if (roomDescriptor.Data !== undefined) { const ptrHash = GetPtrHash(roomDescriptor); roomsMap.set(ptrHash, roomDescriptor); } } return [...roomsMap.values()]; } /** * Helper function to get the room descriptor for every room on the level that is outside of the * grid (like a Devil Room). * * Room descriptors without any data are assumed to be non-existent and are not included. */ export function getRoomsOutsideGrid(): readonly RoomDescriptor[] { // We filter an array of all rooms instead of iterating over the `GridRoom` enum because it is // possible for mods to insert data at arbitrary negative room grid indexes. const readOnlyRooms = getReadOnlyRooms(); const readOnlyRoomsOffGrid = readOnlyRooms.filter( (readOnlyRoomDescriptor) => readOnlyRoomDescriptor.SafeGridIndex < 0, ); return readOnlyRoomsOffGrid.map((readOnlyRoomDescriptor) => getRoomDescriptor(readOnlyRoomDescriptor.SafeGridIndex), ); } /** * Helper function to determine if the current room shape is equal to `RoomShape.1x2` or * `RoomShape.2x1`. */ export function in2x1Room(): boolean { const roomData = getRoomData(); return is2x1Room(roomData); } /** * Helper function to check to see if the current room is an angel shop. * * Under the hood, this checks the room type being equal to `RoomType.ANGEL` (15) and the sub-type * being equal to `AngelRoomSubType.SHOP` (1). */ export function inAngelShop(): boolean { const roomData = getRoomData(); return isAngelShop(roomData); } /** * Helper function to check to see if the current room is the Boss Room for The Beast. * * This function is useful because the `Room.GetBossID` method returns 0 for The Beast room. * * Under the hood, this checks the room type being equal to `RoomType.DUNGEON` (16) and the sub-type * being equal to `DungeonSubType.BEAST_ROOM` (4). */ export function inBeastRoom(): boolean { const roomData = getRoomData(); return isBeastRoom(roomData); } /** * Helper function to detect if the current room is big. Specifically, this is all 1x2 rooms, 2x2 * rooms, and L rooms. */ export function inBigRoom(): boolean { const roomData = getRoomData(); return isBigRoom(roomData); } /** * Helper function to check if the current room is the Boss Room for a particular boss. This will * only work for bosses that have dedicated boss rooms in the "00.special rooms.stb" file. */ export function inBossRoomOf(bossID: BossID): boolean { const roomData = getRoomData(); return isBossRoomOf(roomData, bossID); } /** * Helper function for determining whether the current room is a crawl space. Use this function over * comparing to `RoomType.DUNGEON` or `GridRoom.DUNGEON_IDX` since there is a special case of the * player being in a boss fight that takes place in a dungeon. */ export function inCrawlSpace(): boolean { const roomData = getRoomData(); return isCrawlSpace(roomData); } /** * Helper function for checking whether the current room is a crawl space with a door corresponding * to `DoorSlotFlag.RIGHT_0` (1 << 2). */ export function inCrawlSpaceWithBlackMarketEntrance(): boolean { const roomData = getRoomData(); return isCrawlSpaceWithBlackMarketEntrance(roomData); } /** * Helper function to detect if the current room is one of the rooms in the Death Certificate area. */ export function inDeathCertificateArea(): boolean { const roomData = getRoomData(); return isDeathCertificateArea(roomData); } /** * Helper function to detect if the current room is a Treasure Room created when entering with a * Devil's Crown trinket. * * Under the hood, this checks for `RoomDescriptorFlag.DEVIL_TREASURE`. */ export function inDevilsCrownTreasureRoom(): boolean { const roomDescriptor = getRoomDescriptorReadOnly(); return isDevilsCrownTreasureRoom(roomDescriptor); } /** * Helper function to check to see if the current room is the Boss Room for Dogma. * * This function is useful because the `Room.GetBossID` method returns 0 for the Dogma room. * * Note that the "living room" on the Home floor with the TV at the top of the room is not the Dogma * Boss Room, as the player is teleported to a different room after watching the TV cutscene. * * Under the hood, this checks the stage ID being equal to `StageID.HOME` (35) and the room type * being equal to `RoomType.DEFAULT` (1) and the variant being equal to 1000 (which is the only * Dogma Boss Room that exists in vanilla) and the sub-type being equal to * `HomeRoomSubType.LIVING_ROOM` (3). */ export function inDogmaRoom(): boolean { const roomData = getRoomData(); return isDogmaRoom(roomData); } /** * Helper function to detect if the current room is a Double Trouble Boss Room. * * This is performed by checking for the string "Double Trouble" inside of the room name. The * vanilla game uses this convention for every Double Trouble Boss Room. Note that this method might * fail for mods that add extra Double Trouble rooms but do not follow the convention. * * Internally, the game is coded to detect Double Trouble Boss Rooms by checking for the variant * range of 3700 through 3850. We intentionally do not use this method since it may not work as well * with modded rooms. */ export function inDoubleTrouble(): boolean { const roomData = getRoomData(); return isDoubleTrouble(roomData); } /** Helper function to determine if the current room index is equal to `GridRoom.GENESIS`. */ export function inGenesisRoom(): boolean { const roomGridIndex = getRoomGridIndex(); return isGenesisRoom(roomGridIndex); } /** * Helper function to check if the current room is either the left Home closet (behind the red door) * or the right Home closet (with one random pickup). * * Home closets have a unique shape that is different from any other room in the game. */ export function inHomeCloset(): boolean { const roomData = getRoomData(); return isHomeCloset(roomData); } /** Helper function to determine if the current room shape is one of the four L room shapes. */ export function inLRoom(): boolean { const roomData = getRoomData(); return isLRoom(roomData); } /** Helper function to determine if the current room index is equal to `GridRoom.MEGA_SATAN`. */ export function inMegaSatanRoom(): boolean { const roomGridIndex = getRoomGridIndex(); return isMegaSatanRoom(roomGridIndex); } /** * Helper function to determine if the current room is part of the Repentance "escape sequence" in * the Mines/Ashpit. */ export function inMineShaft(): boolean { const roomData = getRoomData(); return isMineShaft(roomData); } /** * Helper function to check if the current room is a miniboss room for a particular miniboss. This * will only work for mini-bosses that have dedicated boss rooms in the "00.special rooms.stb" file. */ export function inMinibossRoomOf(minibossID: MinibossID): boolean { const roomData = getRoomData(); return isMinibossRoomOf(roomData, minibossID); } /** * Helper function to check if the current room is a "mirror room" in Downpour or Dross. (These * rooms are marked with a specific sub-type.) */ export function inMirrorRoom(): boolean { const roomData = getRoomData(); return isMirrorRoom(roomData); } /** * Helper function to check if the current room shape matches one of the given room shapes. * * This function is variadic, which means you can pass as many room shapes as you want to match for. */ export function inRoomShape(...roomShapes: readonly RoomShape[]): boolean { const roomData = getRoomData(); return isRoomShape(roomData, ...roomShapes); } /** * Helper function to check if the current room matches one of the given room types. * * This function is variadic, which means you can pass as many room types as you want to match for. */ export function inRoomType(...roomTypes: readonly RoomType[]): boolean { const roomData = getRoomData(); return isRoomType(roomData, ...roomTypes); } /** * Helper function for checking if the current room is a secret exit that leads to a Repentance * floor. */ export function inSecretExit(): boolean { const roomGridIndex = getRoomGridIndex(); return isSecretExit(roomGridIndex); } /** * Helper function for checking if the current room is a secret shop (from the Member Card * collectible). * * Secret shops are simply copies of normal shops, but with the backdrop of a secret room. In other * words, they will have the same room type, room variant, and room sub-type of a normal shop. Thus, * the only way to detect them is by using the grid index. */ export function inSecretShop(): boolean { const roomGridIndex = getRoomGridIndex(); return isSecretShop(roomGridIndex); } /** * Helper function to determine whether the current room is the starting room of a floor. It only * returns true for the starting room of the primary dimension (meaning that being in the starting * room of the mirror world does not count). */ export function inStartingRoom(): boolean { const level = game.GetLevel(); const startingRoomGridIndex = level.GetStartingRoomIndex(); const roomGridIndex = getRoomGridIndex(); return roomGridIndex === startingRoomGridIndex && inDimension(Dimension.MAIN); } /** * Helper function to determine if the provided room is equal to `RoomShape.1x2` or `RoomShape.2x1`. */ export function is2x1Room(roomData: RoomConfig): boolean { return is2x1RoomShape(roomData.Shape); } /** * Helper function to loop through every room on the floor and see if it has been cleared. * * This function will only check rooms inside the grid and inside the current dimension. * * @param onlyCheckRoomTypes Optional. A whitelist of room types. If specified, room types not in * the array will be ignored. If not specified, then all rooms will be * checked. Undefined by default. * @param includeSecretRoom Optional. Whether to include the Secret Room. Default is false. * @param includeSuperSecretRoom Optional. Whether to include the Super Secret Room. Default is * false. * @param includeUltraSecretRoom Optional. Whether to include the Ultra Secret Room. Default is * false. * @allowEmptyVariadic */ export function isAllRoomsClear( onlyCheckRoomTypes?: readonly RoomType[], includeSecretRoom = false, includeSuperSecretRoom = false, includeUltraSecretRoom = false, ): boolean { const roomsInsideGrid = getRoomsInsideGrid(); let matchingRooms: readonly RoomDescriptor[]; if (onlyCheckRoomTypes === undefined) { matchingRooms = roomsInsideGrid; } else { const roomTypeWhitelist = new ReadonlySet(onlyCheckRoomTypes); matchingRooms = roomsInsideGrid.filter( (roomDescriptor) => roomDescriptor.Data !== undefined && roomTypeWhitelist.has(roomDescriptor.Data.Type), ); } if (!includeSecretRoom) { matchingRooms = matchingRooms.filter( (roomDescriptor) => roomDescriptor.Data !== undefined && roomDescriptor.Data.Type !== RoomType.SECRET, ); } if (!includeSuperSecretRoom) { matchingRooms = matchingRooms.filter( (roomDescriptor) => roomDescriptor.Data !== undefined && roomDescriptor.Data.Type !== RoomType.SUPER_SECRET, ); } if (!includeUltraSecretRoom) { matchingRooms = matchingRooms.filter( (roomDescriptor) => roomDescriptor.Data !== undefined && roomDescriptor.Data.Type !== RoomType.ULTRA_SECRET, ); } return matchingRooms.every((roomDescriptor) => roomDescriptor.Clear); } /** * Helper function to check to see if the current room is an angel shop. * * Under the hood, this checks the room type being equal to `RoomType.ANGEL` (15) and the sub-type * being equal to `AngelRoomSubType.SHOP` (1). */ export function isAngelShop(roomData: RoomConfig): boolean { return ( roomData.Type === RoomType.ANGEL // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && roomData.Subtype === AngelRoomSubType.SHOP ); } /** * Helper function to check to see if the provided room is the Boss Room for The Beast. * * This function is useful because the `Room.GetBossID` method returns 0 for The Beast room. * * Under the hood, this checks the room type being equal to `RoomType.DUNGEON` (16) and the sub-type * being equal to `DungeonSubType.BEAST_ROOM` (4). */ export function isBeastRoom(roomData: RoomConfig): boolean { return ( roomData.Type === RoomType.DUNGEON // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && roomData.Subtype === DungeonSubType.BEAST_ROOM ); } /** * Helper function to detect if the provided room is big. Specifically, this is all 1x2 rooms, 2x2 * rooms, and L rooms. */ export function isBigRoom(roomData: RoomConfig): boolean { return isBigRoomShape(roomData.Shape); } /** * Helper function to check if the provided room is the Boss Room for a particular boss. This will * only work for bosses that have dedicated boss rooms in the "00.special rooms.stb" file. */ export function isBossRoomOf(roomData: RoomConfig, bossID: BossID): boolean { return ( roomData.Type === RoomType.BOSS && roomData.StageID === StageID.SPECIAL_ROOMS // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && roomData.Subtype === bossID ); } /** * Helper function for determining whether the provided room is a crawl space. Use this function * over comparing to `RoomType.DUNGEON` or `GridRoom.DUNGEON_IDX` since there is a special case of * the player being in a boss fight that takes place in a dungeon. */ export function isCrawlSpace(roomData: RoomConfig): boolean { return ( roomData.Type === RoomType.DUNGEON // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && roomData.Subtype === DungeonSubType.NORMAL ); } /** * Helper function for checking whether the provided room is a crawl space with a door corresponding * to `DoorSlotFlag.RIGHT_0` (1 << 2). */ export function isCrawlSpaceWithBlackMarketEntrance( roomData: RoomConfig, ): boolean { return ( isCrawlSpace(roomData) && hasFlag(roomData.Doors, DoorSlotFlag.RIGHT_0) ); } /** * Helper function to detect if the provided room is one of the rooms in the Death Certificate area. */ export function isDeathCertificateArea(roomData: RoomConfig): boolean { return ( roomData.StageID === StageID.HOME // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && (roomData.Subtype === HomeRoomSubType.DEATH_CERTIFICATE_ENTRANCE // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison || roomData.Subtype === HomeRoomSubType.DEATH_CERTIFICATE_ITEMS) ); } /** * Helper function to detect if the provided room is a Treasure Room created when entering with a * Devil's Crown trinket. * * Under the hood, this checks for `RoomDescriptorFlag.DEVIL_TREASURE`. */ export function isDevilsCrownTreasureRoom( roomDescriptor: RoomDescriptor, ): boolean { return hasFlag(roomDescriptor.Flags, RoomDescriptorFlag.DEVIL_TREASURE); } /** * Helper function to check to see if the provided room is the Boss Room for Dogma. * * This function is useful because the `Room.GetBossID` method returns 0 for the Dogma room. * * Note that the "living room" on the Home floor with the TV at the top of the room is not the Dogma * Boss Room, as the player is teleported to a different room after watching the TV cutscene. * * Under the hood, this checks the stage ID being equal to `StageID.HOME` (35) and the room type * being equal to `RoomType.DEFAULT` (1) and the variant being equal to 1000 (which is the only * Dogma Boss Room that exists in vanilla) and the sub-type being equal to * `HomeRoomSubType.LIVING_ROOM` (3). */ export function isDogmaRoom(roomData: RoomConfig): boolean { return ( roomData.StageID === StageID.HOME && roomData.Type === RoomType.DEFAULT && roomData.Variant === 1000 // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && roomData.Subtype === HomeRoomSubType.LIVING_ROOM ); } /** * Helper function to detect if the provided room is a Double Trouble Boss Room. * * This is performed by checking for the string "Double Trouble" inside of the room name. The * vanilla game uses this convention for every Double Trouble Boss Room. Note that this method might * fail for mods that add extra Double Trouble rooms but do not follow the convention. * * Internally, the game is coded to detect Double Trouble Boss Rooms by checking for the variant * range of 3700 through 3850. We intentionally do not use this method since it may not work as well * with modded rooms. */ export function isDoubleTrouble(roomData: RoomConfig): boolean { return ( roomData.Type === RoomType.BOSS && roomData.Name.includes("Double Trouble") ); } /** * Helper function to determine if the index of the provided room is equal to `GridRoom.GENESIS`. */ export function isGenesisRoom(roomGridIndex: int): boolean { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison return roomGridIndex === GridRoom.GENESIS; } /** * Helper function to check if the provided room is either the left Home closet (behind the red * door) or the right Home closet (with one random pickup). * * Home closets have a unique shape that is different from any other room in the game. */ export function isHomeCloset(roomData: RoomConfig): boolean { return ( roomData.StageID === StageID.HOME // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && (roomData.Subtype === HomeRoomSubType.CLOSET_LEFT // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison || roomData.Subtype === HomeRoomSubType.CLOSET_RIGHT) ); } /** Helper function to determine if the provided room is one of the four L room shapes. */ export function isLRoom(roomData: RoomConfig): boolean { return isLRoomShape(roomData.Shape); } /** * Helper function to determine if the index of the provided room is equal to `GridRoom.MEGA_SATAN`. */ export function isMegaSatanRoom(roomGridIndex: int): boolean { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison return roomGridIndex === GridRoom.MEGA_SATAN; } /** * Helper function to determine if the provided room is part of the Repentance "escape sequence" in * the Mines/Ashpit. */ export function isMineShaft(roomData: RoomConfig): boolean { return ( (roomData.StageID === StageID.MINES || roomData.StageID === StageID.ASHPIT) // eslint-disable-next-line complete/strict-enums && MINE_SHAFT_ROOM_SUB_TYPE_SET.has(roomData.Subtype) ); } /** * Helper function to check if the provided room is a miniboss room for a particular miniboss. This * will only work for mini-bosses that have dedicated boss rooms in the "00.special rooms.stb" file. */ export function isMinibossRoomOf( roomData: RoomConfig, minibossID: MinibossID, ): boolean { return ( roomData.Type === RoomType.MINI_BOSS && roomData.StageID === StageID.SPECIAL_ROOMS // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && roomData.Subtype === minibossID ); } /** * Helper function to check if the provided room is a "mirror room" in Downpour or Dross. (These * rooms are marked with a specific sub-type.) */ export function isMirrorRoom(roomData: RoomConfig): boolean { return ( roomData.Type === RoomType.DEFAULT && (roomData.StageID === StageID.DOWNPOUR || roomData.StageID === StageID.DROSS) // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && roomData.Subtype === DownpourRoomSubType.MIRROR ); } /** * Helper function to check if the provided room matches one of the given room shapes. * * This function is variadic, which means you can pass as many room shapes as you want to match for. */ export function isRoomShape( roomData: RoomConfig, ...roomShapes: readonly RoomShape[] ): boolean { return roomShapes.includes(roomData.Shape); } /** * Helper function to check if the provided room matches one of the given room types. * * This function is variadic, which means you can pass as many room types as you want to match for. */ export function isRoomType( roomData: RoomConfig, ...roomTypes: readonly RoomType[] ): boolean { return roomTypes.includes(roomData.Type); } /** * Helper function for checking if the provided room is a secret exit that leads to a Repentance * floor. */ export function isSecretExit(roomGridIndex: int): boolean { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison return roomGridIndex === GridRoom.SECRET_EXIT; } /** * Helper function to detect if a room type is a Secret Room, a Super Secret Room, or an Ultra * Secret Room. */ export function isSecretRoomType(roomType: RoomType): boolean { return SECRET_ROOM_TYPES.has(roomType); } /** * Helper function for checking if the provided room is a secret shop (from the Member Card * collectible). * * Secret shops are simply copies of normal shops, but with the backdrop of a secret room. In other * words, they will have the same room type, room variant, and room sub-type of a normal shop. Thus, * the only way to detect them is by using the grid index. */ export function isSecretShop(roomGridIndex: int): boolean { // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison return roomGridIndex === GridRoom.SECRET_SHOP; } /** * If the `Room.Update` method is called in a `POST_NEW_ROOM` callback, then some entities will * slide around (such as the player). Since those entity velocities are already at zero, setting * them to zero will have no effect. Thus, a generic solution is to record all of the entity * positions/velocities before updating the room, and then restore those positions/velocities. */ export function roomUpdateSafe(): void { const room = game.GetRoom(); const entities = getEntities(); const entityPositions = getEntityPositions(entities); const entityVelocities = getEntityVelocities(entities); room.Update(); setEntityPositions(entityPositions, entities); setEntityVelocities(entityVelocities, entities); } /** Helper function to set the backdrop (i.e. background) of the current room. */ export function setBackdrop(backdropType: BackdropType): void { game.ShowHallucination(0, backdropType); sfxManager.Stop(SoundEffect.DEATH_CARD); } /** * Helper function to convert an uncleared room to a cleared room in the `POST_NEW_ROOM` callback. * This is useful because if enemies are removed in this callback, a room drop will be awarded and * the doors will start closed and then open. */ export function setRoomCleared(): void { const room = game.GetRoom(); const roomClear = room.IsClear(); // If the room is already cleared, we don't have to do anything. if (roomClear) { return; } room.SetClear(true); for (const door of getDoors()) { if (isHiddenSecretRoomDoor(door)) { continue; } // We don't use the `EntityDoor.Open` method since that will cause the door to play an // animation. openDoorFast(door); // If this is a mini-boss room, then the door would be barred in addition to being closed. // Ensure that the bar is not visible. door.ExtraVisible = false; } sfxManager.Stop(SoundEffect.DOOR_HEAVY_OPEN); // If the room contained Mom's Hands, then a screen shake will be queued. Override it with a 0 // frame shake. game.ShakeScreen(0); } /** * Helper function to emulate what happens when you bomb an Angel Statue or push a Reward Plate that * spawns an NPC. */ export function setRoomUncleared(): void { const room = game.GetRoom(); room.SetClear(false); closeAllDoors(); }