"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("../../../arrays/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 &&
            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 &&
            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;
}
