UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

360 lines (359 loc) • 17.5 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.CustomGridEntities = void 0; const isaac_typescript_definitions_1 = require("isaac-typescript-definitions"); const cachedClasses_1 = require("../../../core/cachedClasses"); const decorators_1 = require("../../../decorators"); const ISCFeature_1 = require("../../../enums/ISCFeature"); const ModCallbackCustom_1 = require("../../../enums/ModCallbackCustom"); const gridEntities_1 = require("../../../functions/gridEntities"); const players_1 = require("../../../functions/players"); const roomData_1 = require("../../../functions/roomData"); const types_1 = require("../../../functions/types"); const utils_1 = require("../../../functions/utils"); const vector_1 = require("../../../functions/vector"); const DefaultMap_1 = require("../../DefaultMap"); const Feature_1 = require("../../private/Feature"); const v = { level: { /** Indexed by room list index and grid index. */ customGridEntities: new DefaultMap_1.DefaultMap(() => new Map()), }, room: { genericPropPtrHashes: new Set(), manuallyUsingShovel: false, }, }; class CustomGridEntities extends Feature_1.Feature { /** @internal */ v = v; runInNFrames; /** @internal */ constructor(runInNFrames) { super(); this.featuresUsed = [ISCFeature_1.ISCFeature.RUN_IN_N_FRAMES]; this.callbacksUsed = [ // 23 [ isaac_typescript_definitions_1.ModCallback.PRE_USE_ITEM, this.preUseItemWeNeedToGoDeeper, [isaac_typescript_definitions_1.CollectibleType.WE_NEED_TO_GO_DEEPER], ], ]; this.customCallbacksUsed = [ [ModCallbackCustom_1.ModCallbackCustom.POST_NEW_ROOM_REORDERED, this.postNewRoomReordered], ]; this.runInNFrames = runInNFrames; } // ModCallback.PRE_USE_ITEM (23) // CollectibleType.WE_NEED_TO_GO_DEEPER (84) preUseItemWeNeedToGoDeeper = (_collectibleType, _rng, player, _useFlags, _activeSlot, _customVarData) => { // If a player uses We Need to Go Deeper on top of a custom grid entity, then they will always // get a crawlspace, due to how custom grids are implemented with decorations. Thus, remove the // custom grid entity to prevent this from happening if needed. const room = cachedClasses_1.game.GetRoom(); const roomListIndex = (0, roomData_1.getRoomListIndex)(); const roomCustomGridEntities = v.level.customGridEntities.get(roomListIndex); if (roomCustomGridEntities === undefined) { return undefined; } const gridIndex = room.GetGridIndex(player.Position); const customGridEntity = roomCustomGridEntities.get(gridIndex); if (customGridEntity === undefined) { return undefined; } // If the custom grid entity has collision, then the player should not be able to be standing on // top of it. if (customGridEntity.gridCollisionClass !== isaac_typescript_definitions_1.GridCollisionClass.NONE) { return undefined; } (0, gridEntities_1.removeGridEntity)(customGridEntity.gridIndex, false); const entityPtr = EntityPtr(player); this.runInNFrames.runNextGameFrame(() => { const futurePlayer = (0, players_1.getPlayerFromPtr)(entityPtr); if (futurePlayer === undefined) { return; } v.room.manuallyUsingShovel = true; futurePlayer.UseActiveItem(isaac_typescript_definitions_1.CollectibleType.WE_NEED_TO_GO_DEEPER); v.room.manuallyUsingShovel = false; }); // Cancel the original effect. return true; }; // ModCallbackCustom.POST_NEW_ROOM_REORDERED postNewRoomReordered = () => { // When we re-enter a room, the graphics for any custom entities will be reverted back to that // of a normal decoration. Thus, we must re-apply the anm2. const roomListIndex = (0, roomData_1.getRoomListIndex)(); const roomCustomGridEntities = v.level.customGridEntities.get(roomListIndex); if (roomCustomGridEntities === undefined) { return; } const room = cachedClasses_1.game.GetRoom(); for (const [gridIndex, data] of roomCustomGridEntities) { const decoration = room.GetGridEntity(gridIndex); if (decoration === undefined) { roomCustomGridEntities.delete(gridIndex); continue; } if (data.anm2Path !== undefined) { const sprite = decoration.GetSprite(); sprite.Load(data.anm2Path, true); const animationToPlay = data.defaultAnimation ?? sprite.GetDefaultAnimation(); sprite.Play(animationToPlay, true); } } }; /** * Helper function to spawn a custom grid entity. Custom grid entities are persistent in that they * will reappear if the player leaves and re-enters the room. (It will be manually respawned in * the `POST_NEW_ROOM` callback.) * * In order to use this function, you must upgrade your mod with * `ISCFeature.CUSTOM_GRID_ENTITIES`. * * Custom grid entities are built on top of real grid entities. You can use any existing grid * entity type as a base. For example, if you want to create a custom rock that would be breakable * like a normal rock, then you should specify `GridEntityType.ROCK` as the base grid entity type. * * Once a custom grid entity is spawned, you can take advantage of the custom grid callbacks such * as `POST_GRID_ENTITY_CUSTOM_UPDATE`. Note that the "normal" grid entities callbacks will not * fire for custom entities. For example, if you had a custom grid entity based on * `GridEntityType.ROCK`, and you also had a subscription to the `POST_GRID_ENTITY_UPDATE` * callback, the callback would only fire for normal rocks and not the custom entity. * * Custom grid entities are an IsaacScript feature because the vanilla game does not support any * custom grid entities. * * For example, this would be code to create a custom rock called a "Silver Rock" that produces a * dime when destroyed: * * ```ts * // This is local to the mod and can safely overlap with the values of `GridEntityType` (or * // values chosen by other mods). * const GridEntityTypeCustom = { * SILVER_ROCK: 0 as GridEntityType, * } as const; * * // This is copied from "gfx/grid/grid_rock.anm2" with some tweaks to make it look special. * const SILVER_ROCK_ANM2_PATH = "gfx/grid/grid_rock_silver.anm2"; * * export function silverRockInit(mod: ModUpgraded): void { * mod.AddCallbackCustom( * ModCallbackCustom.POST_GRID_ENTITY_CUSTOM_BROKEN, * postGridEntityCustomBrokenSilverRock, * GridEntityTypeCustom.SILVER_ROCK, * ); * } * * function postGridEntityCustomBrokenSilverRock(gridEntity: GridEntity) { * spawnCoin(CoinSubType.DIME, gridEntity.Position); * } * * export function spawnSilverRock(mod: ModUpgraded, gridIndex: int): GridEntity { * return mod.spawnCustomGridEntity( * GridEntityTypeCustom.SILVER_ROCK, * gridIndex, * undefined, * SILVER_ROCK_ANM2_PATH, * undefined, * GridEntityType.ROCK, * ); * } * ``` * * @param gridEntityTypeCustom An integer that identifies what kind of grid entity you are * creating. It should correspond to a local enum value created in * your mod. The integer can be any unique value and will not * correspond to the actual grid entity type used. (This integer is * used in the various custom grid entity callbacks.) * @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 gridCollisionClass Optional. The collision class that you want the custom grid entity to * have. If not specified, the grid collision class from the base grid * entity will be used. * @param anm2Path Optional. The path to the ANM2 file to use for the sprite. If not specified, * the normal sprite from the base grid entity will be used. * @param defaultAnimation Optional. The name of the animation to play after the sprite is * initialized and after the player re-enters a room with this grid entity * in it. If not specified, the default animation in the anm2 will be * used. * @param baseGridEntityType Optional. The type of the grid entity to use as a "base" for this * custom grid entity. Default is `GridEntityType.DECORATION`. * @param baseGridEntityVariant Optional. The variant of the grid entity to use as a "base" for * this custom grid entity. Default is 0. * @public */ spawnCustomGridEntity(gridEntityTypeCustom, gridIndexOrPosition, gridCollisionClass, anm2Path, defaultAnimation, baseGridEntityType = isaac_typescript_definitions_1.GridEntityType.DECORATION, baseGridEntityVariant = 0) { const room = cachedClasses_1.game.GetRoom(); const roomListIndex = (0, roomData_1.getRoomListIndex)(); const gridIndex = (0, vector_1.isVector)(gridIndexOrPosition) ? room.GetGridIndex(gridIndexOrPosition) : gridIndexOrPosition; const customGridEntity = (0, gridEntities_1.spawnGridEntityWithVariant)(baseGridEntityType, baseGridEntityVariant, gridIndexOrPosition); (0, utils_1.assertDefined)(customGridEntity, "Failed to spawn a custom grid entity."); if (gridCollisionClass !== undefined) { customGridEntity.CollisionClass = gridCollisionClass; } if (anm2Path !== undefined) { const sprite = customGridEntity.GetSprite(); sprite.Load(anm2Path, true); const animationToPlay = defaultAnimation ?? sprite.GetDefaultAnimation(); sprite.Play(animationToPlay, true); } const customGridEntityData = { gridEntityTypeCustom, roomListIndex, gridIndex, anm2Path, defaultAnimation, gridCollisionClass, }; const roomCustomGridEntities = v.level.customGridEntities.getAndSetDefault(roomListIndex); roomCustomGridEntities.set(gridIndex, customGridEntityData); return customGridEntity; } /** * Helper function to remove a custom grid entity created by the `spawnCustomGrid` function. * * In order to use this function, you must upgrade your mod with * `ISCFeature.CUSTOM_GRID_ENTITIES`. * * @param gridIndexOrPositionOrGridEntity You can specify the custom grid entity to remove by * providing the grid index, the room position, or the grid entity * itself. * @param updateRoom Optional. Whether to update the room after the grid entity is removed. * Default is true. 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 it to false if you need to run this * function multiple times. * @returns The grid entity that was removed. Returns undefined if no grid entity was found at the * given location or if the given grid entity was not a custom grid entity. * @public */ removeCustomGridEntity(gridIndexOrPositionOrGridEntity, updateRoom = true) { const room = cachedClasses_1.game.GetRoom(); const roomListIndex = (0, roomData_1.getRoomListIndex)(); let decoration; if (typeof gridIndexOrPositionOrGridEntity === "number") { const gridIndex = gridIndexOrPositionOrGridEntity; const gridEntity = room.GetGridEntity(gridIndex); if (gridEntity === undefined) { return undefined; } decoration = gridEntity; } else if ((0, vector_1.isVector)(gridIndexOrPositionOrGridEntity)) { const position = gridIndexOrPositionOrGridEntity; const gridEntity = room.GetGridEntityFromPos(position); if (gridEntity === undefined) { return undefined; } decoration = gridEntity; } else { decoration = gridIndexOrPositionOrGridEntity; } const gridIndex = decoration.GetGridIndex(); const roomCustomGridEntities = v.level.customGridEntities.getAndSetDefault(roomListIndex); const exists = roomCustomGridEntities.has(gridIndex); if (!exists) { return undefined; } roomCustomGridEntities.delete(gridIndex); (0, gridEntities_1.removeGridEntity)(decoration, updateRoom); return decoration; } /** * Helper function to get the custom grid entities in the current room. Returns an array of tuples * containing the raw decoration grid entity and the associated entity data. * * In order to use this function, you must upgrade your mod with * `ISCFeature.CUSTOM_GRID_ENTITIES`. * * @public */ getCustomGridEntities() { const roomListIndex = (0, roomData_1.getRoomListIndex)(); const roomCustomGridEntities = v.level.customGridEntities.get(roomListIndex); if (roomCustomGridEntities === undefined) { return []; } const room = cachedClasses_1.game.GetRoom(); const customGridEntities = []; for (const [gridIndex, data] of roomCustomGridEntities) { const gridEntity = room.GetGridEntity(gridIndex); if (gridEntity !== undefined) { customGridEntities.push({ gridEntity, data }); } } return customGridEntities; } /** * Helper function to get the custom `GridEntityType` from a `GridEntity` or grid index. Returns * undefined if the provided `GridEntity` is not a custom grid entity, or if there was not a grid * entity on the provided grid index. * * In order to use this function, you must upgrade your mod with * `ISCFeature.CUSTOM_GRID_ENTITIES`. * * @public */ getCustomGridEntityType(gridEntityOrGridIndex) { if (!this.initialized) { return undefined; } const gridIndex = (0, types_1.isInteger)(gridEntityOrGridIndex) ? gridEntityOrGridIndex : gridEntityOrGridIndex.GetGridIndex(); const roomListIndex = (0, roomData_1.getRoomListIndex)(); const roomCustomGridEntities = v.level.customGridEntities.get(roomListIndex); if (roomCustomGridEntities === undefined) { return undefined; } for (const [_gridIndex, data] of roomCustomGridEntities) { if (data.gridIndex === gridIndex) { return data.gridEntityTypeCustom; } } return undefined; } /** * Helper function to check if a `GridEntity` is a custom grid entity or if a grid index has a * custom grid entity. * * In order to use this function, you must upgrade your mod with * `ISCFeature.CUSTOM_GRID_ENTITIES`. * * @public */ isCustomGridEntity(gridEntityOrGridIndex) { const gridEntityTypeCustom = this.getCustomGridEntityType(gridEntityOrGridIndex); return gridEntityTypeCustom !== undefined; } } exports.CustomGridEntities = CustomGridEntities; __decorate([ decorators_1.Exported ], CustomGridEntities.prototype, "spawnCustomGridEntity", null); __decorate([ decorators_1.Exported ], CustomGridEntities.prototype, "removeCustomGridEntity", null); __decorate([ decorators_1.Exported ], CustomGridEntities.prototype, "getCustomGridEntities", null); __decorate([ decorators_1.Exported ], CustomGridEntities.prototype, "getCustomGridEntityType", null); __decorate([ decorators_1.Exported ], CustomGridEntities.prototype, "isCustomGridEntity", null);