isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
304 lines (303 loc) • 13.7 kB
JavaScript
"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;
}