"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.spawnWithSeed = exports.spawnEntityID = exports.spawn = exports.setEntityRandomColor = exports.setEntityOpacity = exports.setEntityDamageFlash = exports.rerollEnemy = exports.removeEntities = exports.removeAllMatchingEntities = exports.parseEntityTypeVariantString = exports.parseEntityID = exports.isEntityMoving = exports.isActiveEnemy = exports.hasArmor = exports.getFilteredNewEntities = exports.getEntityIDFromConstituents = exports.getEntityID = exports.getEntityFromPtrHash = exports.getEntityFields = exports.getEntities = exports.getConstituentsFromEntityID = exports.getClosestEntityTo = exports.doesEntityExist = exports.doesAnyEntityExist = exports.countEntities = void 0;
const isaac_typescript_definitions_1 = require("isaac-typescript-definitions");
const cachedClasses_1 = require("../core/cachedClasses");
const constants_1 = require("../core/constants");
const entitiesWithArmorSet_1 = require("../sets/entitiesWithArmorSet");
const isaacAPIClass_1 = require("./isaacAPIClass");
const random_1 = require("./random");
const readOnly_1 = require("./readOnly");
const rng_1 = require("./rng");
const sprites_1 = require("./sprites");
const tstlClass_1 = require("./tstlClass");
const types_1 = require("./types");
const utils_1 = require("./utils");
const vector_1 = require("./vector");
/** From DeadInfinity. */
const DAMAGE_FLASH_COLOR = (0, readOnly_1.newReadonlyColor)(0.5, 0.5, 0.5, 1, 200 / 255, 0 / 255, 0 / 255);
/**
 * Helper function to count the number of entities in room. Use this over the vanilla
 * `Isaac.CountEntities` method to avoid having to specify a spawner and to handle ignoring charmed
 * enemies.
 *
 * @param entityType Optional. Default is -1, which matches every entity type.
 * @param variant Optional. Default is -1, which matches every variant.
 * @param subType Optional. Default is -1, which matches every sub-type.
 * @param ignoreFriendly Optional. Default is false. Will throw a runtime error if set to true and
 *                       the `entityType` is equal to -1.
 */
function countEntities(entityType = -1, variant = -1, subType = -1, ignoreFriendly = false) {
    if (entityType === -1) {
        // The `Isaac.CountEntities` method requires an argument of `EntityType`, so we must revert to
        // using the `Isaac.GetRoomEntities` method, which is slower.
        const entities = Isaac.GetRoomEntities();
        if (!ignoreFriendly) {
            return entities.length;
        }
        const nonFriendlyEntities = entities.filter((entity) => !entity.HasEntityFlags(isaac_typescript_definitions_1.EntityFlag.FRIENDLY));
        return nonFriendlyEntities.length;
    }
    if (!ignoreFriendly) {
        return Isaac.CountEntities(undefined, entityType, variant, subType);
    }
    const entities = Isaac.FindByType(entityType, variant, subType, false, ignoreFriendly);
    return entities.length;
}
exports.countEntities = countEntities;
/**
 * Helper function to check if one or more matching entities exist in the current room. It uses the
 * `doesEntityExist` helper function to determine this.
 *
 * @param entityTypes An array or set of the entity types that you want to check for. Will return
 *                    true if any of the provided entity types exist.
 * @param ignoreFriendly Optional. Default is false.
 */
function doesAnyEntityExist(entityTypes, ignoreFriendly = false) {
    const entityTypesMutable = entityTypes;
    const entityTypesArray = (0, tstlClass_1.isTSTLSet)(entityTypesMutable)
        ? [...entityTypesMutable.values()]
        : entityTypesMutable;
    return entityTypesArray.some((entityType) => doesEntityExist(entityType, -1, -1, ignoreFriendly));
}
exports.doesAnyEntityExist = doesAnyEntityExist;
/**
 * Helper function to check if one or more of a specific kind of entity is present in the current
 * room. It uses the `countEntities` helper function to determine this.
 *
 * @param entityType Optional. Default is -1, which matches every entity type.
 * @param variant Optional. Default is -1, which matches every variant.
 * @param subType Optional. Default is -1, which matches every sub-type.
 * @param ignoreFriendly Optional. Default is false.
 */
function doesEntityExist(entityType = -1, variant = -1, subType = -1, ignoreFriendly = false) {
    const count = countEntities(entityType, variant, subType, ignoreFriendly);
    return count > 0;
}
exports.doesEntityExist = doesEntityExist;
/**
 * Given an array of entities, this helper function returns the closest one to a provided reference
 * entity.
 *
 * For example:
 *
 * ```ts
 * const player = Isaac.GetPlayer();
 * const gapers = getEntities(EntityType.GAPER);
 * const closestGaper = getClosestEntityTo(player, gapers);
 * ```
 *
 * @param referenceEntity The entity that is close by.
 * @param entities The array of entities to look through.
 * @param filterFunc Optional. A function to filter for a specific type of entity, like e.g. an
 *                   enemy with a certain amount of HP left.
 */
function getClosestEntityTo(referenceEntity, entities, filterFunc) {
    let closestEntity;
    let closestDistance = Number.POSITIVE_INFINITY;
    for (const entity of entities) {
        const distance = referenceEntity.Position.Distance(entity.Position);
        if (distance < closestDistance &&
            (filterFunc === undefined || filterFunc(entity))) {
            closestEntity = entity;
            closestDistance = distance;
        }
    }
    return closestEntity;
}
exports.getClosestEntityTo = getClosestEntityTo;
/** Helper function to get the entity type, variant, and sub-type from an `EntityID`. */
function getConstituentsFromEntityID(entityID) {
    const parts = entityID.split(".");
    if (parts.length !== 3) {
        error(`Failed to get the constituents from entity ID: ${entityID}`);
    }
    const [entityTypeString, variantString, subTypeString] = parts;
    (0, utils_1.assertDefined)(entityTypeString, `Failed to get the first constituent from an entity ID: ${entityID}`);
    (0, utils_1.assertDefined)(variantString, `Failed to get the second constituent from an entity ID: ${entityID}`);
    (0, utils_1.assertDefined)(subTypeString, `Failed to get the third constituent from an entity ID: ${entityID}`);
    const entityType = (0, types_1.parseIntSafe)(entityTypeString);
    (0, utils_1.assertDefined)(entityType, `Failed to convert the entity type to an integer: ${entityTypeString}`);
    const variant = (0, types_1.parseIntSafe)(variantString);
    (0, utils_1.assertDefined)(variant, `Failed to convert the entity variant to an integer: ${variantString}`);
    const subType = (0, types_1.parseIntSafe)(subTypeString);
    (0, utils_1.assertDefined)(subType, `Failed to convert the entity sub-type to an integer: ${subTypeString}`);
    return [entityType, variant, subType];
}
exports.getConstituentsFromEntityID = getConstituentsFromEntityID;
/**
 * Helper function to get all of the entities in the room or all of the entities that match a
 * specific entity type / variant / sub-type.
 *
 * Due to bugs with `Isaac.FindInRadius`, this function uses `Isaac.GetRoomEntities`, which is more
 * expensive but also more robust. (If a matching entity type is provided, then `Isaac.FindByType`
 * will be used instead.)
 *
 * For example:
 *
 * ```ts
 * // Make all of the entities in the room invisible.
 * for (const entity of getEntities()) {
 *   entity.Visible = false;
 * }
 * ```
 *
 * @param entityType Optional. If specified, will only get the entities that match the type. Default
 *                   is -1, which matches every type.
 * @param variant Optional. If specified, will only get the entities that match the variant. Default
 *                is -1, which matches every variant.
 * @param subType Optional. If specified, will only get the entities that match the sub-type.
 *                Default is -1, which matches every sub-type.
 * @param ignoreFriendly Optional. If set to true, it will exclude friendly NPCs from being
 *                       returned. Default is false. Will only be taken into account if the
 *                       `entityType` is specified.
 */
function getEntities(entityType = -1, variant = -1, subType = -1, ignoreFriendly = false) {
    if (entityType === -1) {
        return Isaac.GetRoomEntities();
    }
    return Isaac.FindByType(entityType, variant, subType, ignoreFriendly);
}
exports.getEntities = getEntities;
/**
 * Helper function to get all the fields on an entity. For example, this is useful for comparing it
 * to another entity later. (One option is to use the `logTableDifferences` function for this.)
 *
 * This function will only get fields that are equal to booleans, numbers, or strings, or Vectors,
 * as comparing other types is non-trivial.
 */
function getEntityFields(entity) {
    const entityFields = new LuaMap();
    const metatable = getmetatable(entity);
    (0, utils_1.assertDefined)(metatable, "Failed to get the metatable for an entity.");
    setPrimitiveEntityFields(entity, metatable, entityFields);
    // If this is a class that inherits from `Entity` (like `EntityPlayer`), the "__propget" table
    // will not contain the attributes for `Entity`. Thus, if this is not an `Entity`, we have
    // additional fields to add.
    const className = (0, isaacAPIClass_1.getIsaacAPIClassName)(entity);
    if (className === "Entity") {
        return entityFields;
    }
    const parentTable = metatable.get("__parent");
    (0, utils_1.assertDefined)(parentTable, 'Failed to get the "__parent" table for an entity.');
    setPrimitiveEntityFields(entity, parentTable, entityFields);
    return entityFields;
}
exports.getEntityFields = getEntityFields;
function setPrimitiveEntityFields(entity, metatable, entityFields) {
    const propGetTable = metatable.get("__propget");
    (0, utils_1.assertDefined)(propGetTable, 'Failed to get the "__propget" table for an entity.');
    for (const [key] of propGetTable) {
        // The values of this table are functions. Thus, we use the key to index the original entity.
        const indexKey = key;
        const value = entity[indexKey];
        if ((0, types_1.isPrimitive)(value)) {
            entityFields.set(indexKey, value);
        }
        else if ((0, vector_1.isVector)(value)) {
            entityFields.set(indexKey, (0, vector_1.vectorToString)(value));
        }
    }
}
/**
 * Helper function to get an entity from a `PtrHash`. Note that doing this is very expensive, so you
 * should only use this function when debugging. (Normally, if you need to work backwards from a
 * reference, you would use an `EntityPtr` instead of a `PtrHash`.
 */
function getEntityFromPtrHash(ptrHash) {
    const entities = getEntities();
    return entities.find((entity) => GetPtrHash(entity) === ptrHash);
}
exports.getEntityFromPtrHash = getEntityFromPtrHash;
/** Helper function to get a string containing the entity's type, variant, and sub-type. */
function getEntityID(entity) {
    return `${entity.Type}.${entity.Variant}.${entity.SubType}`;
}
exports.getEntityID = getEntityID;
/**
 * Helper function to get a formatted string in the format returned by the `getEntityID` function.
 */
function getEntityIDFromConstituents(entityType, variant, subType) {
    return `${entityType}.${variant}.${subType}`;
}
exports.getEntityIDFromConstituents = getEntityIDFromConstituents;
/**
 * Helper function to compare two different arrays of entities. Returns the entities that are in the
 * second array but not in the first array.
 */
function getFilteredNewEntities(oldEntities, newEntities) {
    const oldEntitiesSet = new Set();
    for (const entity of oldEntities) {
        const ptrHash = GetPtrHash(entity);
        oldEntitiesSet.add(ptrHash);
    }
    return newEntities.filter((entity) => {
        const ptrHash = GetPtrHash(entity);
        return !oldEntitiesSet.has(ptrHash);
    });
}
exports.getFilteredNewEntities = getFilteredNewEntities;
/**
 * Helper function to see if a particular entity has armor. In this context, armor refers to the
 * damage scaling mechanic. For example, Ultra Greed has armor, but a Gaper does not.
 *
 * For more on armor, see the wiki: https://bindingofisaacrebirth.fandom.com/wiki/Damage_Scaling
 */
function hasArmor(entity) {
    const typeVariantString = `${entity.Type}.${entity.Variant}`;
    return entitiesWithArmorSet_1.ENTITIES_WITH_ARMOR_SET.has(typeVariantString);
}
exports.hasArmor = hasArmor;
/**
 * Helper function to detect if a particular entity is an active enemy. Use this over the
 * `Entity.IsActiveEnemy` method since it is bugged with friendly enemies, Grimaces, Ultra Greed,
 * and Mother.
 */
function isActiveEnemy(entity) {
    if (entity.HasEntityFlags(isaac_typescript_definitions_1.EntityFlag.FRIENDLY)) {
        return false;
    }
    const room = cachedClasses_1.game.GetRoom();
    const isClear = room.IsClear();
    // Some entities count as being "active" enemies, but they deactivate when the room is cleared.
    if (isClear) {
        switch (entity.Type) {
            // 42
            case isaac_typescript_definitions_1.EntityType.GRIMACE: {
                return false;
            }
            // 294
            case isaac_typescript_definitions_1.EntityType.ULTRA_DOOR: {
                return false;
            }
            // 406
            case isaac_typescript_definitions_1.EntityType.ULTRA_GREED: {
                const npc = entity.ToNPC();
                if (npc !== undefined) {
                    const ultraGreedVariant = npc.Variant;
                    switch (ultraGreedVariant) {
                        case isaac_typescript_definitions_1.UltraGreedVariant.ULTRA_GREED: {
                            if (npc.State === (0, types_1.asNPCState)(isaac_typescript_definitions_1.UltraGreedState.GOLD_STATUE)) {
                                return false;
                            }
                            break;
                        }
                        case isaac_typescript_definitions_1.UltraGreedVariant.ULTRA_GREEDIER: {
                            if (npc.State === (0, types_1.asNPCState)(isaac_typescript_definitions_1.UltraGreedierState.POST_EXPLOSION)) {
                                return false;
                            }
                            break;
                        }
                    }
                }
                break;
            }
            // 912
            case isaac_typescript_definitions_1.EntityType.MOTHER: {
                if (entity.Variant === isaac_typescript_definitions_1.MotherVariant.MOTHER_1) {
                    const npc = entity.ToNPC();
                    if (npc !== undefined && npc.State === isaac_typescript_definitions_1.NPCState.SPECIAL) {
                        return false;
                    }
                }
                break;
            }
            default: {
                break;
            }
        }
    }
    // eslint-disable-next-line deprecation/deprecation
    return entity.IsActiveEnemy(false);
}
exports.isActiveEnemy = isActiveEnemy;
/**
 * Helper function to measure an entity's velocity to see if it is moving.
 *
 * Use this helper function over checking if the velocity length is equal to 0 because entities can
 * look like they are completely immobile but yet still have a non zero velocity. Thus, using a
 * threshold is needed.
 *
 * @param entity The entity whose velocity to measure.
 * @param threshold Optional. The threshold from 0 to consider to be moving. Default is 0.01.
 */
function isEntityMoving(entity, threshold = 0.01) {
    return (0, vector_1.doesVectorHaveLength)(entity.Velocity, threshold);
}
exports.isEntityMoving = isEntityMoving;
/**
 * Helper function to parse a string that contains an entity type, a variant, and a sub-type,
 * separated by periods.
 *
 * For example, passing "45.0.1" would return an array of [45, 0, 1].
 *
 * Returns undefined if the string cannot be parsed.
 */
function parseEntityID(entityID) {
    const entityIDArray = entityID.split(".");
    if (entityIDArray.length !== 3) {
        return undefined;
    }
    const [entityTypeString, variantString, subTypeString] = entityIDArray;
    if (entityTypeString === undefined ||
        variantString === undefined ||
        subTypeString === undefined) {
        return undefined;
    }
    const entityType = (0, types_1.parseIntSafe)(entityTypeString);
    const variant = (0, types_1.parseIntSafe)(variantString);
    const subType = (0, types_1.parseIntSafe)(subTypeString);
    if (entityType === undefined ||
        variant === undefined ||
        subType === undefined) {
        return undefined;
    }
    return [entityType, variant, subType];
}
exports.parseEntityID = parseEntityID;
/**
 * Helper function to parse a string that contains an entity type and a variant separated by a
 * period.
 *
 * For example, passing "45.0" would return an array of [45, 0].
 *
 * Returns undefined if the string cannot be parsed.
 */
function parseEntityTypeVariantString(entityTypeVariantString) {
    const entityTypeVariantArray = entityTypeVariantString.split(".");
    if (entityTypeVariantArray.length !== 2) {
        return undefined;
    }
    const [entityTypeString, variantString] = entityTypeVariantArray;
    if (entityTypeString === undefined || variantString === undefined) {
        return undefined;
    }
    const entityType = (0, types_1.parseIntSafe)(entityTypeString);
    const variant = (0, types_1.parseIntSafe)(variantString);
    if (entityType === undefined || variant === undefined) {
        return undefined;
    }
    return [entityType, variant];
}
exports.parseEntityTypeVariantString = parseEntityTypeVariantString;
/**
 * Helper function to remove all of the matching entities in the room.
 *
 * @param entityType The entity type to match.
 * @param entityVariant Optional. The variant to match. Default is -1, which matches every variant.
 * @param entitySubType Optional. The sub-type to match. Default is -1, which matches every
 *                      sub-type.
 * @param cap Optional. If specified, will only remove the given amount of collectibles.
 * @returns An array of the entities that were removed.
 */
function removeAllMatchingEntities(entityType, entityVariant = -1, entitySubType = -1, cap = undefined) {
    const entities = getEntities(entityType, entityVariant, entitySubType);
    return removeEntities(entities, cap);
}
exports.removeAllMatchingEntities = removeAllMatchingEntities;
/**
 * Helper function to remove all of the entities in the supplied array.
 *
 * @param entities The array of entities to remove.
 * @param cap Optional. If specified, will only remove the given amount of entities.
 * @returns An array of the entities that were removed.
 */
function removeEntities(entities, cap) {
    if (entities.length === 0) {
        return [];
    }
    const entitiesRemoved = [];
    for (const entity of entities) {
        entity.Remove();
        entitiesRemoved.push(entity);
        if (cap !== undefined && entitiesRemoved.length >= cap) {
            break;
        }
    }
    return entitiesRemoved;
}
exports.removeEntities = removeEntities;
/**
 * Helper function to reroll an enemy. Use this instead of the vanilla "Game.RerollEnemy" function
 * if you want the rerolled enemy to be returned.
 *
 * @param entity The entity to reroll.
 * @returns If the game failed to reroll the enemy, returns undefined. Otherwise, returns the
 *          rerolled entity.
 */
function rerollEnemy(entity) {
    const oldEntities = getEntities();
    const wasRerolled = cachedClasses_1.game.RerollEnemy(entity);
    if (!wasRerolled) {
        return undefined;
    }
    const newEntities = getEntities();
    const filteredNewEntities = getFilteredNewEntities(oldEntities, newEntities);
    if (filteredNewEntities.length === 0) {
        error('Failed to find the new entity generated by the "Game.RerollEnemy" method.');
    }
    return filteredNewEntities[0];
}
exports.rerollEnemy = rerollEnemy;
/**
 * Helper function to make an entity flash red like it is taking damage. This is useful when you
 * want to make it appear as if an entity is taking damage without actually dealing any damage to
 * it.
 */
function setEntityDamageFlash(entity) {
    entity.SetColor(DAMAGE_FLASH_COLOR, 2, 0);
}
exports.setEntityDamageFlash = setEntityDamageFlash;
/**
 * Helper function to keep an entity's color the same values as it already is but set the opacity to
 * a specific value.
 *
 * @param entity The entity to set.
 * @param alpha A value between 0 and 1 that represents the fade amount.
 */
function setEntityOpacity(entity, alpha) {
    const sprite = entity.GetSprite();
    (0, sprites_1.setSpriteOpacity)(sprite, alpha);
}
exports.setEntityOpacity = setEntityOpacity;
function setEntityRandomColor(entity) {
    const seed = entity.InitSeed === 0 ? (0, rng_1.getRandomSeed)() : entity.InitSeed;
    const rng = (0, rng_1.newRNG)(seed);
    const r = (0, random_1.getRandom)(rng);
    const g = (0, random_1.getRandom)(rng);
    const b = (0, random_1.getRandom)(rng);
    const color = Color(r, g, b);
    entity.SetColor(color, 100_000, 100_000, false, false);
}
exports.setEntityRandomColor = setEntityRandomColor;
/**
 * Helper function to spawn an entity. Use this instead of the `Isaac.Spawn` method if you do not
 * need to specify the velocity or spawner.
 *
 * Also see the `spawnWithSeed` helper function.
 *
 * @param entityType The `EntityType` of the entity to spawn.
 * @param variant The variant of the entity to spawn.
 * @param subType The sub-type of the entity to spawn.
 * @param positionOrGridIndex The position or grid index of the entity to spawn.
 * @param velocity Optional. The velocity of the entity to spawn. Default is `VectorZero`.
 * @param spawner Optional. The entity that will be the `SpawnerEntity`. Default is undefined.
 * @param seedOrRNG Optional. The seed or RNG object to use to generate the `InitSeed` of the
 *                  entity. Default is undefined, which will make the entity spawn with a random
 *                  seed using the `Isaac.Spawn` method.
 */
function spawn(entityType, variant, subType, positionOrGridIndex, velocity = constants_1.VectorZero, spawner = undefined, seedOrRNG = undefined) {
    const room = cachedClasses_1.game.GetRoom();
    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
    if (positionOrGridIndex === undefined) {
        const entityID = getEntityIDFromConstituents(entityType, variant, subType);
        error(`Failed to spawn entity ${entityID} since an undefined position was passed to the "spawn" function.`);
    }
    const position = (0, vector_1.isVector)(positionOrGridIndex)
        ? positionOrGridIndex
        : room.GetGridPosition(positionOrGridIndex);
    if (seedOrRNG === undefined) {
        return Isaac.Spawn(entityType, variant, subType, position, velocity, spawner);
    }
    const seed = (0, rng_1.isRNG)(seedOrRNG) ? seedOrRNG.Next() : seedOrRNG;
    return cachedClasses_1.game.Spawn(entityType, variant, position, velocity, spawner, subType, seed);
}
exports.spawn = spawn;
/**
 * Helper function to spawn the entity corresponding to an `EntityID`.
 *
 * @param entityID The `EntityID` of the entity to spawn.
 * @param positionOrGridIndex The position or grid index of the entity to spawn.
 * @param velocity Optional. The velocity of the entity to spawn. Default is `VectorZero`.
 * @param spawner Optional. The entity that will be the `SpawnerEntity`. Default is undefined.
 * @param seedOrRNG Optional. The seed or RNG object to use to generate the `InitSeed` of the
 *                  entity. Default is undefined, which will make the entity spawn with a random
 *                  seed using the `Isaac.Spawn` method.
 */
function spawnEntityID(entityID, positionOrGridIndex, velocity = constants_1.VectorZero, spawner = undefined, seedOrRNG = undefined) {
    const [entityType, variant, subType] = getConstituentsFromEntityID(entityID);
    return spawn(entityType, variant, subType, positionOrGridIndex, velocity, spawner, seedOrRNG);
}
exports.spawnEntityID = spawnEntityID;
/**
 * Helper function to spawn an entity. Use this instead of the `Game.Spawn` method if you do not
 * need to specify the velocity or spawner.
 */
function spawnWithSeed(entityType, variant, subType, positionOrGridIndex, seedOrRNG, velocity = constants_1.VectorZero, spawner = undefined) {
    return spawn(entityType, variant, subType, positionOrGridIndex, velocity, spawner, seedOrRNG);
}
exports.spawnWithSeed = spawnWithSeed;
