UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

304 lines (303 loc) • 13.7 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.DeployJSONRoom = void 0; const isaac_typescript_definitions_1 = require("isaac-typescript-definitions"); const cachedEnumValues_1 = require("../../../cachedEnumValues"); const cachedClasses_1 = require("../../../core/cachedClasses"); const decorators_1 = require("../../../decorators"); const ISCFeature_1 = require("../../../enums/ISCFeature"); const emptyRoom_1 = require("../../../functions/emptyRoom"); const entities_1 = require("../../../functions/entities"); const gridEntities_1 = require("../../../functions/gridEntities"); const jsonRoom_1 = require("../../../functions/jsonRoom"); const log_1 = require("../../../functions/log"); const rng_1 = require("../../../functions/rng"); const roomGrid_1 = require("../../../functions/roomGrid"); const rooms_1 = require("../../../functions/rooms"); const spawnCollectible_1 = require("../../../functions/spawnCollectible"); const types_1 = require("../../../functions/types"); const utils_1 = require("../../../functions/utils"); const ReadonlySet_1 = require("../../../types/ReadonlySet"); const Feature_1 = require("../../private/Feature"); const GRID_ENTITY_XML_TYPE_SET = new ReadonlySet_1.ReadonlySet(cachedEnumValues_1.GRID_ENTITY_XML_TYPE_VALUES); class DeployJSONRoom extends Feature_1.Feature { preventGridEntityRespawn; /** @internal */ constructor(preventGridEntityRespawn) { super(); this.featuresUsed = [ISCFeature_1.ISCFeature.PREVENT_GRID_ENTITY_RESPAWN]; this.preventGridEntityRespawn = preventGridEntityRespawn; } spawnAllEntities(jsonRoom, rng, verbose = false) { let shouldUnclearRoom = false; for (const jsonSpawn of jsonRoom.spawn) { const xString = jsonSpawn.$.x; const x = (0, types_1.parseIntSafe)(xString); (0, utils_1.assertDefined)(x, `Failed to convert the following x coordinate to an integer (for a spawn): ${xString}`); const yString = jsonSpawn.$.y; const y = (0, types_1.parseIntSafe)(yString); (0, utils_1.assertDefined)(y, `Failed to convert the following y coordinate to an integer (for a spawn): ${yString}`); const jsonEntity = (0, jsonRoom_1.getRandomJSONEntity)(jsonSpawn.entity, rng); const entityTypeString = jsonEntity.$.type; const entityTypeNumber = (0, types_1.parseIntSafe)(entityTypeString); (0, utils_1.assertDefined)(entityTypeNumber, `Failed to convert the entity type to an integer: ${entityTypeString}`); const variantString = jsonEntity.$.variant; const variant = (0, types_1.parseIntSafe)(variantString); (0, utils_1.assertDefined)(variant, `Failed to convert the entity variant to an integer: ${variant}`); const subTypeString = jsonEntity.$.subtype; const subType = (0, types_1.parseIntSafe)(subTypeString); (0, utils_1.assertDefined)(subType, `Failed to convert the entity sub-type to an integer: ${subType}`); const isGridEntity = GRID_ENTITY_XML_TYPE_SET.has(entityTypeNumber); if (isGridEntity) { const gridEntityXMLType = entityTypeNumber; if (verbose) { (0, log_1.log)(`Spawning grid entity ${gridEntityXMLType}.${variant} at: (${x}, ${y})`); } spawnGridEntityForJSONRoom(gridEntityXMLType, variant, x, y); } else { const entityType = entityTypeNumber; if (verbose) { const entityID = (0, entities_1.getEntityIDFromConstituents)(entityType, variant, subType); (0, log_1.log)(`Spawning normal entity ${entityID} at: (${x}, ${y})`); } const entity = this.spawnNormalEntityForJSONRoom(entityType, variant, subType, x, y, rng); const npc = entity.ToNPC(); if (npc !== undefined && npc.CanShutDoors) { shouldUnclearRoom = true; } } } // After emptying the room, we manually cleared the room. However, if the room layout contains // an battle NPC, then we need to reset the clear state and close the doors again. if (shouldUnclearRoom) { if (verbose) { (0, log_1.log)("Setting the room to be uncleared since there were one or more battle NPCs spawned."); } (0, rooms_1.setRoomUncleared)(); } else if (verbose) { (0, log_1.log)("Leaving the room cleared since there were no battle NPCs spawned."); } } spawnNormalEntityForJSONRoom(entityType, variant, subType, x, y, rng) { const room = cachedClasses_1.game.GetRoom(); const roomType = room.GetType(); const position = (0, roomGrid_1.gridCoordinatesToWorldPosition)(x, y); const seed = rng.Next(); let entity; if (entityType === isaac_typescript_definitions_1.EntityType.PICKUP // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && variant === isaac_typescript_definitions_1.PickupVariant.COLLECTIBLE) { const collectibleType = (0, types_1.asCollectibleType)(subType); const options = roomType === isaac_typescript_definitions_1.RoomType.ANGEL; entity = (0, spawnCollectible_1.spawnCollectible)(collectibleType, position, seed, options); } else { entity = (0, entities_1.spawnWithSeed)(entityType, variant, subType, position, seed); } // For some reason, Pitfalls do not spawn with the correct collision classes. if (entityType === isaac_typescript_definitions_1.EntityType.PITFALL // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison && variant === isaac_typescript_definitions_1.PitfallVariant.PITFALL) { entity.EntityCollisionClass = isaac_typescript_definitions_1.EntityCollisionClass.ENEMIES; entity.GridCollisionClass = isaac_typescript_definitions_1.EntityGridCollisionClass.WALLS; } return entity; } /** * Helper function to deconstruct a vanilla room and set up a custom room in its place. * Specifically, this will clear the current room of all entities and grid entities, and then * spawn all of the entries and grid entities in the provided JSON room. For this reason, you must * be in the actual room in order to use this function. * * A JSON room is simply an XML file converted to JSON. You can create JSON rooms by using the * Basement Renovator room editor to create an XML file, and then convert it to JSON using the * `convert-xml-to-json` tool (e.g. `npx convert-xml-to-json my-rooms.xml`). * * This function is meant to be used in the `POST_NEW_ROOM` callback. * * For example: * * ```ts * * import customRooms from "./customRooms.json"; * * export function postNewRoom(): void { * const firstJSONRoom = customRooms.rooms.room[0]; * deployJSONRoom(firstJSONRoom); * } * ``` * * If you want to deploy an unseeded room, you must explicitly pass `undefined` to the `seedOrRNG` * parameter. * * In order to use this function, you must upgrade your mod with `ISCFeature.DEPLOY_JSON_ROOM`. * * @param jsonRoom The JSON room to deploy. * @param seedOrRNG The `Seed` or `RNG` object to use. If an `RNG` object is provided, the * `RNG.Next` method will be called. If `undefined` is provided, it will default * to a random seed. * @param verbose Optional. If specified, will write entries to the "log.txt" file that describe * what the function is doing. Default is false. * @public */ deployJSONRoom(jsonRoom, seedOrRNG, verbose = false) { const rng = (0, rng_1.isRNG)(seedOrRNG) ? seedOrRNG : (0, rng_1.newRNG)(seedOrRNG); if (verbose) { (0, log_1.log)("Starting to empty the room of entities and grid entities."); } (0, emptyRoom_1.emptyRoom)(); if (verbose) { (0, log_1.log)("Finished emptying the room of entities and grid entities."); } (0, rooms_1.setRoomCleared)(); if (verbose) { (0, log_1.log)("Starting to spawn all of the new entities and grid entities."); } this.spawnAllEntities(jsonRoom, rng, verbose); if (verbose) { (0, log_1.log)("Finished spawning all of the new entities and grid entities."); } fixPitGraphics(); this.preventGridEntityRespawn.preventGridEntityRespawn(); } } exports.DeployJSONRoom = DeployJSONRoom; __decorate([ decorators_1.Exported ], DeployJSONRoom.prototype, "deployJSONRoom", null); function spawnGridEntityForJSONRoom(gridEntityXMLType, gridEntityXMLVariant, x, y) { const room = cachedClasses_1.game.GetRoom(); const gridEntityTuple = (0, gridEntities_1.convertXMLGridEntityType)(gridEntityXMLType, gridEntityXMLVariant); if (gridEntityTuple === undefined) { return undefined; } const [gridEntityType, variant] = gridEntityTuple; const position = (0, roomGrid_1.gridCoordinatesToWorldPosition)(x, y); const gridIndex = room.GetGridIndex(position); const gridEntity = (0, gridEntities_1.spawnGridEntityWithVariant)(gridEntityType, variant, gridIndex); if (gridEntity === undefined) { return gridEntity; } // Prevent poops from playing an appear animation, since that is not supposed to normally happen // when entering a new room. if (gridEntityType === isaac_typescript_definitions_1.GridEntityType.POOP) { const sprite = gridEntity.GetSprite(); sprite.Play("State1", true); sprite.SetLastFrame(); } return gridEntity; } /** * By default, when spawning multiple pits next to each other, the graphics will not "meld" * together. Thus, now that all of the entities in the room are spawned, we must iterate over the * pits in the room and manually fix their sprites, if necessary. */ function fixPitGraphics() { const room = cachedClasses_1.game.GetRoom(); const gridWidth = room.GetGridWidth(); const pitMap = getPitMap(); for (const [gridIndex, gridEntity] of pitMap) { const gridIndexLeft = gridIndex - 1; const L = pitMap.has(gridIndexLeft); const gridIndexRight = gridIndex + 1; const R = pitMap.has(gridIndexRight); const gridIndexUp = gridIndex - gridWidth; const U = pitMap.has(gridIndexUp); const gridIndexDown = gridIndex + gridWidth; const D = pitMap.has(gridIndexDown); const gridIndexUpLeft = gridIndex - gridWidth - 1; const UL = pitMap.has(gridIndexUpLeft); const gridIndexUpRight = gridIndex - gridWidth + 1; const UR = pitMap.has(gridIndexUpRight); const gridIndexDownLeft = gridIndex + gridWidth - 1; const DL = pitMap.has(gridIndexDownLeft); const gridIndexDownRight = gridIndex + gridWidth + 1; const DR = pitMap.has(gridIndexDownRight); const pitFrame = getPitFrame(L, R, U, D, UL, UR, DL, DR); const sprite = gridEntity.GetSprite(); sprite.SetFrame(pitFrame); } } function getPitMap() { const pitMap = new Map(); for (const gridEntity of (0, gridEntities_1.getGridEntities)(isaac_typescript_definitions_1.GridEntityType.PIT)) { const gridIndex = gridEntity.GetGridIndex(); pitMap.set(gridIndex, gridEntity); } return pitMap; } /** The logic in this function is copied from Basement Renovator. */ function getPitFrame(L, R, U, D, UL, UR, DL, DR) { let F = 0; // First, check for bitwise frames. (It works for all combinations of just left/up/right/down.) if (L) { F |= 1; } if (U) { F |= 2; } if (R) { F |= 4; } if (D) { F |= 8; } // Then, check for other combinations. if (U && L && !UL && !R && !D) { F = 17; } if (U && R && !UR && !L && !D) { F = 18; } if (L && D && !DL && !U && !R) { F = 19; } if (R && D && !DR && !L && !U) { F = 20; } if (L && U && R && D && !UL) { F = 21; } if (L && U && R && D && !UR) { F = 22; } if (U && R && D && !L && !UR) { F = 25; } if (L && U && D && !R && !UL) { F = 26; } if (L && U && R && D && !DL && !DR) { F = 24; } if (L && U && R && D && !UR && !UL) { F = 23; } if (L && U && R && UL && !UR && !D) { F = 27; } if (L && U && R && UR && !UL && !D) { F = 28; } if (L && U && R && !D && !UR && !UL) { F = 29; } if (L && R && D && DL && !U && !DR) { F = 30; } if (L && R && D && DR && !U && !DL) { F = 31; } if (L && R && D && !U && !DL && !DR) { F = 32; } return F; }