isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
1,298 lines (1,145 loc) • 39.8 kB
text/typescript
import type { GridEntityXMLType } from "isaac-typescript-definitions";
import {
BackdropType,
EffectVariant,
GridCollisionClass,
GridEntityType,
PoopGridEntityVariant,
StatueVariant,
TrapdoorVariant,
} from "isaac-typescript-definitions";
import { GRID_ENTITY_XML_TYPE_VALUES } from "../cachedEnumValues";
import { game } from "../core/cachedClasses";
import { DISTANCE_OF_GRID_TILE, VectorOne } from "../core/constants";
import { GRID_ENTITY_TYPE_TO_BROKEN_STATE_MAP } from "../maps/gridEntityTypeToBrokenStateMap";
import { GRID_ENTITY_XML_MAP } from "../maps/gridEntityXMLMap";
import {
DEFAULT_TOP_LEFT_WALL_GRID_INDEX,
ROOM_SHAPE_TO_TOP_LEFT_WALL_GRID_INDEX_MAP,
} from "../maps/roomShapeToTopLeftWallGridIndexMap";
import { GRID_ENTITY_TYPE_TO_ANM2_NAME } from "../objects/gridEntityTypeToANM2Name";
import { POOP_GRID_ENTITY_XML_TYPES_SET } from "../sets/poopGridEntityXMLTypesSet";
import type { AnyGridEntity } from "../types/AnyGridEntity";
import type { GridEntityID } from "../types/GridEntityID";
import { ReadonlySet } from "../types/ReadonlySet";
import { removeEntities } from "./entities";
import { getEffects } from "./entitiesSpecific";
import { isCircleIntersectingRectangle } from "./math";
import { roomUpdateSafe } from "./rooms";
import { isInteger, parseIntSafe } from "./types";
import { assertDefined, eRange, iRange } from "./utils";
import { isVector, vectorEquals } from "./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([
GridEntityType.SPIKES_ON_OFF, // 9
GridEntityType.PRESSURE_PLATE, // 20
GridEntityType.TELEPORTER, // 23
]);
const BREAKABLE_GRID_ENTITY_TYPES_BY_EXPLOSIONS =
new ReadonlySet<GridEntityType>([
GridEntityType.ROCK, // 2
GridEntityType.ROCK_TINTED, // 4
GridEntityType.ROCK_BOMB, // 5
GridEntityType.ROCK_ALT, // 6
GridEntityType.SPIDER_WEB, // 10
GridEntityType.TNT, // 12
// GridEntityType.FIREPLACE (13) does not count since it is turned into a non-grid entity upon
// spawning.
GridEntityType.POOP, // 14
GridEntityType.ROCK_SUPER_SPECIAL, // 22
GridEntityType.ROCK_SPIKED, // 25
GridEntityType.ROCK_ALT_2, // 26
GridEntityType.ROCK_GOLD, // 27
]);
const BREAKABLE_GRID_ENTITY_TYPES_VARIANTS_BY_EXPLOSIONS =
new ReadonlySet<string>([`${GridEntityType.STATUE}.${StatueVariant.ANGEL}`]);
const GRID_ENTITY_XML_TYPES_SET = new ReadonlySet(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).
*/
export function convertXMLGridEntityType(
gridEntityXMLType: GridEntityXMLType,
gridEntityXMLVariant: int,
): [GridEntityType, int] | undefined {
const gridEntityArray = GRID_ENTITY_XML_MAP.get(gridEntityXMLType);
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.
*/
export function doesGridEntityExist(
gridEntityType: GridEntityType,
variant = -1,
): boolean {
const room = 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.
*/
export function getAllGridIndexes(): readonly int[] {
const room = game.GetRoom();
const gridSize = room.GetGridSize();
return 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.
*/
export function getCollidingEntitiesWithGridEntity(
gridEntity: GridEntity,
): readonly Entity[] {
const { topLeft, bottomRight } = getGridEntityCollisionPoints(gridEntity);
const closeEntities = Isaac.FindInRadius(
gridEntity.Position,
DISTANCE_OF_GRID_TILE * 2,
);
return closeEntities.filter(
(entity) =>
entity.CollidesWithGrid()
&& 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`. */
export function getConstituentsFromGridEntityID(
gridEntityID: GridEntityID,
): [gridEntityType: GridEntityType, variant: int] {
const parts = gridEntityID.split(".");
if (parts.length !== 2) {
error(
`Failed to get the constituents from a grid entity ID: ${gridEntityID}`,
);
}
const [gridEntityTypeString, variantString] = parts;
assertDefined(
gridEntityTypeString,
`Failed to get the first constituent from a grid entity ID: ${gridEntityID}`,
);
assertDefined(
variantString,
`Failed to get the second constituent from a grid entity ID: ${gridEntityID}`,
);
const gridEntityType = parseIntSafe(gridEntityTypeString);
assertDefined(
gridEntityType,
`Failed to convert the grid entity type to a number: ${gridEntityTypeString}`,
);
const variant = parseIntSafe(variantString);
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
*/
export function getGridEntities(
...gridEntityTypes: readonly GridEntityType[]
): readonly GridEntity[] {
const gridEntities = getAllGridEntities();
if (gridEntityTypes.length === 0) {
return gridEntities;
}
const gridEntityTypesSet = new 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.
*/
export function getGridEntitiesExcept(
...gridEntityTypes: readonly GridEntityType[]
): readonly GridEntity[] {
const gridEntities = getAllGridEntities();
if (gridEntityTypes.length === 0) {
return gridEntities;
}
const gridEntityTypesSet = new ReadonlySet(gridEntityTypes);
return gridEntities.filter((gridEntity) => {
const gridEntityType = gridEntity.GetType();
return !gridEntityTypesSet.has(gridEntityType);
});
}
function getAllGridEntities(): readonly GridEntity[] {
const room = game.GetRoom();
const gridEntities: GridEntity[] = [];
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. */
export function getGridEntitiesInRadius(
targetPosition: Vector,
radius: number,
): readonly GridEntity[] {
radius = Math.abs(radius);
const topLeftOffset = VectorOne.mul(-radius);
const mostTopLeftPosition = targetPosition.add(topLeftOffset);
const room = game.GetRoom();
const diameter = radius * 2;
const iterations = Math.ceil(diameter / DISTANCE_OF_GRID_TILE);
const separation = diameter / iterations;
const gridEntities: GridEntity[] = [];
const registeredGridIndexes = new Set<number>();
for (const x of iRange(iterations)) {
for (const y of 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 (
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
*/
export function getGridEntitiesMap(
...gridEntityTypes: readonly GridEntityType[]
): ReadonlyMap<int, GridEntity> {
const gridEntities = getGridEntities(...gridEntityTypes);
const gridEntityMap = new Map<int, GridEntity>();
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. */
export function getGridEntityANM2Path(
gridEntityType: GridEntityType,
): string | undefined {
const gridEntityANM2Name = getGridEntityANM2Name(gridEntityType);
return `gfx/grid/${gridEntityANM2Name}`;
}
function getGridEntityANM2Name(
gridEntityType: GridEntityType,
): string | undefined {
switch (gridEntityType) {
// 1
case GridEntityType.DECORATION: {
return getGridEntityANM2NameDecoration();
}
default: {
return 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(): string {
const room = game.GetRoom();
const backdropType = room.GetBackdropType();
switch (backdropType) {
// 1, 2, 3, 36, 49, 52
case BackdropType.BASEMENT:
case BackdropType.CELLAR:
case BackdropType.BURNING_BASEMENT:
case BackdropType.DOWNPOUR_ENTRANCE:
case BackdropType.ISAACS_BEDROOM:
case BackdropType.CLOSET: {
return "Props_01_Basement.anm2";
}
// 4, 5, 6, 37
case BackdropType.CAVES:
case BackdropType.CATACOMBS:
case BackdropType.FLOODED_CAVES:
case BackdropType.MINES_ENTRANCE: {
return "Props_03_Caves.anm2";
}
// 7, 8, 9, 30, 33, 38, 39, 40, 41, 42, 53, 60
case BackdropType.DEPTHS:
case BackdropType.NECROPOLIS:
case BackdropType.DANK_DEPTHS:
case BackdropType.SACRIFICE:
case BackdropType.MAUSOLEUM:
case BackdropType.MAUSOLEUM_ENTRANCE:
case BackdropType.CORPSE_ENTRANCE:
case BackdropType.MAUSOLEUM_2:
case BackdropType.MAUSOLEUM_3:
case BackdropType.MAUSOLEUM_4:
case BackdropType.CLOSET_B:
case BackdropType.DARK_CLOSET: {
return "Props_05_Depths.anm2";
}
// 10, 12
case BackdropType.WOMB:
case BackdropType.SCARRED_WOMB: {
return "Props_07_The Womb.anm2";
}
// 11
case BackdropType.UTERO: {
return "Props_07_Utero.anm2";
}
// 13, 27
case BackdropType.BLUE_WOMB:
case BackdropType.BLUE_WOMB_PASS: {
return "Props_07_The Womb_blue.anm2";
}
// 14, 47
case BackdropType.SHEOL:
case BackdropType.GEHENNA: {
return "Props_09_Sheol.anm2";
}
// 15
case BackdropType.CATHEDRAL: {
return "Props_10_Cathedral.anm2";
}
// 17
case BackdropType.CHEST: {
return "Props_11_The Chest.anm2";
}
// 28
case BackdropType.GREED_SHOP: {
return "Props_12_Greed.anm2";
}
// 31
case BackdropType.DOWNPOUR: {
return "props_01x_downpour.anm2";
}
// 32, 46, 58, 59
case BackdropType.MINES:
case BackdropType.ASHPIT:
case BackdropType.MINES_SHAFT:
case BackdropType.ASHPIT_SHAFT: {
return "props_03x_mines.anm2";
}
// 34, 43, 44, 48
case BackdropType.CORPSE:
case BackdropType.CORPSE_2:
case BackdropType.CORPSE_3:
case BackdropType.MORTIS: {
return "props_07_the corpse.anm2";
}
// 45
case 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. */
export function getGridEntityCollisionPoints(gridEntity: GridEntity): {
topLeft: Vector;
bottomRight: Vector;
} {
const topLeft = Vector(
gridEntity.Position.X - DISTANCE_OF_GRID_TILE / 2,
gridEntity.Position.Y - DISTANCE_OF_GRID_TILE / 2,
);
const bottomRight = Vector(
gridEntity.Position.X + DISTANCE_OF_GRID_TILE / 2,
gridEntity.Position.Y + DISTANCE_OF_GRID_TILE / 2,
);
return { topLeft, bottomRight };
}
/** Helper function to get a string containing the grid entity's type and variant. */
export function getGridEntityID(gridEntity: GridEntity): GridEntityID {
const gridEntityType = gridEntity.GetType();
const variant = gridEntity.GetVariant();
return `${gridEntityType}.${variant}` as GridEntityID;
}
/**
* Helper function to get a formatted string in the format returned by the `getGridEntityID`
* function.
*/
export function getGridEntityIDFromConstituents(
gridEntityType: GridEntityType,
variant: int,
): GridEntityID {
return `${gridEntityType}.${variant}` as GridEntityID;
}
/**
* 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.
*/
export function getMatchingGridEntities(
gridEntityType: GridEntityType,
variant: int,
): readonly GridEntity[] {
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.
*/
export function getRockPNGPath(): string {
const rockPNGName = getRockPNGName();
return `gfx/grid/${rockPNGName}`;
}
function getRockPNGName(): string {
const room = game.GetRoom();
const backdropType = room.GetBackdropType();
switch (backdropType) {
// 1, 17
case BackdropType.BASEMENT:
case BackdropType.CHEST: {
return "rocks_basement.png";
}
// 2
case BackdropType.CELLAR: {
return "rocks_cellar.png";
}
// 3
case BackdropType.BURNING_BASEMENT: {
return "rocks_burningbasement.png"; // cspell:ignore burningbasement
}
// 4
case BackdropType.CAVES: {
return "rocks_caves.png";
}
// 5
case BackdropType.CATACOMBS: {
return "rocks_catacombs.png";
}
// 6
case BackdropType.FLOODED_CAVES: {
return "rocks_drownedcaves.png"; // cspell:ignore drownedcaves
}
// 7, 8, 9, 30, 60
case BackdropType.DEPTHS:
case BackdropType.NECROPOLIS:
case BackdropType.DANK_DEPTHS:
case BackdropType.SACRIFICE:
case BackdropType.DARK_CLOSET: {
return "rocks_depths.png";
}
// 10
case BackdropType.WOMB: {
return "rocks_womb.png";
}
// 11
case BackdropType.UTERO: {
return "rocks_utero.png";
}
// 12
case BackdropType.SCARRED_WOMB: {
return "rocks_scarredwomb.png"; // cspell:ignore scarredwomb
}
// 13, 27
case BackdropType.BLUE_WOMB:
case BackdropType.BLUE_WOMB_PASS: {
return "rocks_bluewomb.png"; // cspell:ignore bluewomb
}
// 14, 16
case BackdropType.SHEOL:
case BackdropType.DARK_ROOM: {
return "rocks_sheol.png";
}
// 15, 35
case BackdropType.CATHEDRAL:
case BackdropType.PLANETARIUM: {
return "rocks_cathedral.png";
}
// 23, 32, 37, 58
case BackdropType.SECRET:
case BackdropType.MINES:
case BackdropType.MINES_ENTRANCE:
case BackdropType.MINES_SHAFT: {
return "rocks_secretroom.png"; // cspell:ignore secretroom
}
// 31, 36
case BackdropType.DOWNPOUR:
case BackdropType.DOWNPOUR_ENTRANCE: {
return "rocks_downpour.png";
}
// 33, 38, 40, 41, 42
case BackdropType.MAUSOLEUM:
case BackdropType.MAUSOLEUM_ENTRANCE:
case BackdropType.MAUSOLEUM_2:
case BackdropType.MAUSOLEUM_3:
case BackdropType.MAUSOLEUM_4: {
return "rocks_mausoleum.png";
}
// 34, 48
case BackdropType.CORPSE:
case BackdropType.MORTIS: {
return "rocks_corpse.png";
}
// 39
case BackdropType.CORPSE_ENTRANCE: {
return "rocks_corpseentrance.png"; // cspell:ignore corpseentrance
}
// 43
case BackdropType.CORPSE_2: {
return "rocks_corpse2.png";
}
// 44
case BackdropType.CORPSE_3: {
return "rocks_corpse3.png";
}
// 45
case BackdropType.DROSS: {
return "rocks_dross.png";
}
// 46, 59
case BackdropType.ASHPIT:
case BackdropType.ASHPIT_SHAFT: {
return "rocks_ashpit.png";
}
// 47
case 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).
*/
export function getSurroundingGridEntities(
gridEntity: GridEntity,
): readonly GridEntity[] {
const room = game.GetRoom();
const gridIndex = gridEntity.GetGridIndex();
const surroundingGridIndexes = getSurroundingGridIndexes(gridIndex);
const surroundingGridEntities: GridEntity[] = [];
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.
*/
export function getSurroundingGridIndexes(
gridIndex: int,
): [
topLeft: int,
top: int,
topRight: int,
left: int,
right: int,
bottomLeft: int,
bottom: int,
bottomRight: int,
] {
const room = 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.
*/
export function getTopLeftWall(): GridEntity | undefined {
const room = 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.
*/
export function getTopLeftWallGridIndex(): int {
const room = game.GetRoom();
const roomShape = room.GetRoomShape();
const topLeftWallGridIndex =
ROOM_SHAPE_TO_TOP_LEFT_WALL_GRID_INDEX_MAP.get(roomShape);
return topLeftWallGridIndex ?? 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.
*/
export function isGridEntityBreakableByExplosion(
gridEntity: GridEntity,
): boolean {
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.
*/
export function isGridEntityBroken(gridEntity: GridEntity): boolean {
const gridEntityType = gridEntity.GetType();
const brokenState = 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.
*/
export function isGridEntityXMLType(num: number): num is GridEntityXMLType {
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.
*/
export function isGridIndexAdjacentToDoor(gridIndex: int): boolean {
const room = 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. */
export function isPoopGridEntityXMLType(
gridEntityXMLType: GridEntityXMLType,
): boolean {
return 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.
*/
export function isPostBossVoidPortal(gridEntity: GridEntity): boolean {
const saveState = gridEntity.GetSaveState();
return (
saveState.Type === GridEntityType.TRAPDOOR
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
&& saveState.Variant === 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.
*/
export function removeAllGridEntitiesExcept(
...gridEntityTypes: readonly GridEntityType[]
): readonly GridEntity[] {
const gridEntityTypeExceptions = new ReadonlySet(gridEntityTypes);
const gridEntities = getGridEntities();
const removedGridEntities: GridEntity[] = [];
for (const gridEntity of gridEntities) {
const gridEntityType = gridEntity.GetType();
if (!gridEntityTypeExceptions.has(gridEntityType)) {
removeGridEntity(gridEntity, false);
removedGridEntities.push(gridEntity);
}
}
if (removedGridEntities.length > 0) {
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.
*/
export function removeAllMatchingGridEntities(
...gridEntityType: readonly GridEntityType[]
): readonly GridEntity[] {
const gridEntities = getGridEntities(...gridEntityType);
if (gridEntities.length === 0) {
return [];
}
for (const gridEntity of gridEntities) {
removeGridEntity(gridEntity, false);
}
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.
*/
export function removeEntitiesSpawnedFromGridEntity(
entities: readonly Entity[],
gridEntity: GridEntity,
): void {
const entitiesFromGridEntity = entities.filter(
(entity) =>
entity.FrameCount === 0
&& vectorEquals(entity.Position, gridEntity.Position),
);
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.
*/
export function removeGridEntities<T extends AnyGridEntity>(
gridEntities: readonly T[],
updateRoom: boolean,
cap?: int,
): readonly T[] {
if (gridEntities.length === 0) {
return [];
}
const gridEntitiesRemoved: T[] = [];
for (const gridEntity of gridEntities) {
removeGridEntity(gridEntity, false);
gridEntitiesRemoved.push(gridEntity);
if (cap !== undefined && gridEntitiesRemoved.length >= cap) {
break;
}
}
if (updateRoom) {
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.
*/
export function removeGridEntity(
gridEntityOrGridIndex: GridEntity | int,
updateRoom: boolean,
): void {
const room = game.GetRoom();
const gridEntity = 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 = isInteger(gridEntityOrGridIndex)
? gridEntityOrGridIndex
: gridEntityOrGridIndex.GetGridIndex();
room.RemoveGridEntity(gridIndex, 0, false);
if (updateRoom) {
roomUpdateSafe();
}
// In the special case of removing a Devil Statue or Angel Statue, we also need to delete the
// corresponding effect.
if (gridEntityType === GridEntityType.STATUE) {
const effectVariant =
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
variant === StatueVariant.DEVIL
? EffectVariant.DEVIL
: EffectVariant.ANGEL;
const effects = getEffects(effectVariant);
const effectsOnTile = effects.filter((effect) =>
vectorEquals(effect.Position, position),
);
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.)
*/
export function setGridEntityInvisible(gridEntity: GridEntity): void {
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.
*/
export function setGridEntityType(
gridEntity: GridEntity,
gridEntityType: GridEntityType,
): void {
gridEntity.SetType(gridEntityType);
const sprite = gridEntity.GetSprite();
const anm2Path = getGridEntityANM2Path(gridEntityType);
if (anm2Path === undefined) {
return;
}
sprite.Load(anm2Path, false);
if (gridEntityType === 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.
*/
export function spawnGiantPoop(topLeftGridIndex: int): boolean {
const room = 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(
GridEntityType.POOP,
PoopGridEntityVariant.GIANT_TOP_LEFT,
topLeftGridIndex,
);
const topRight = spawnGridEntityWithVariant(
GridEntityType.POOP,
PoopGridEntityVariant.GIANT_TOP_RIGHT,
topRightGridIndex,
);
const bottomLeft = spawnGridEntityWithVariant(
GridEntityType.POOP,
PoopGridEntityVariant.GIANT_BOTTOM_LEFT,
bottomLeftGridIndex,
);
const bottomRight = spawnGridEntityWithVariant(
GridEntityType.POOP,
PoopGridEntityVariant.GIANT_BOTTOM_RIGHT,
bottomRightGridIndex,
);
return (
topLeft !== undefined
&& topLeft.GetType() === GridEntityType.POOP
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
&& topLeft.GetVariant() === PoopGridEntityVariant.GIANT_TOP_LEFT
&& topRight !== undefined
&& topRight.GetType() === GridEntityType.POOP
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
&& topRight.GetVariant() === PoopGridEntityVariant.GIANT_TOP_RIGHT
&& bottomLeft !== undefined
&& bottomLeft.GetType() === GridEntityType.POOP
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
&& bottomLeft.GetVariant() === PoopGridEntityVariant.GIANT_BOTTOM_LEFT
&& bottomRight !== undefined
&& bottomRight.GetType() === GridEntityType.POOP
// eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison
&& bottomRight.GetVariant() === 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.
*/
export function spawnGridEntity(
gridEntityType: GridEntityType,
gridIndexOrPosition: int | Vector,
removeExistingGridEntity = true,
): GridEntity | undefined {
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.
*/
export function spawnGridEntityWithVariant(
gridEntityType: GridEntityType,
variant: int,
gridIndexOrPosition: int | Vector,
removeExistingGridEntity = true,
): GridEntity | undefined {
const room = 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 = isVector(gridIndexOrPosition)
? room.GetGridEntityFromPos(gridIndexOrPosition)
: room.GetGridEntity(gridIndexOrPosition);
if (existingGridEntity !== undefined) {
if (removeExistingGridEntity) {
removeGridEntity(existingGridEntity, true);
} else {
return undefined;
}
}
const position = isVector(gridIndexOrPosition)
? gridIndexOrPosition
: room.GetGridPosition(gridIndexOrPosition);
const gridEntity = Isaac.GridSpawn(gridEntityType, variant, position);
if (gridEntity === undefined) {
return gridEntity;
}
if (gridEntityType === 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 === GridEntityType.WALL) {
// For some reason, spawned walls start with a collision class of `NONE`, so we have to manually
// set it.
gridEntity.CollisionClass = 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.
*/
export function spawnVoidPortal(gridIndex: int): GridEntity | undefined {
const voidPortal = spawnGridEntityWithVariant(
GridEntityType.TRAPDOOR,
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;
}