UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

195 lines (194 loc) • 9.68 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.PlayerCollectibleDetection = void 0; const isaac_typescript_definitions_1 = require("isaac-typescript-definitions"); const cachedEnumValues_1 = require("../../../cachedEnumValues"); const ISCFeature_1 = require("../../../enums/ISCFeature"); const ModCallbackCustom_1 = require("../../../enums/ModCallbackCustom"); const array_1 = require("../../../functions/array"); const flag_1 = require("../../../functions/flag"); const playerDataStructures_1 = require("../../../functions/playerDataStructures"); const players_1 = require("../../../functions/players"); const sort_1 = require("../../../functions/sort"); const utils_1 = require("../../../functions/utils"); const DefaultMap_1 = require("../../DefaultMap"); const Feature_1 = require("../../private/Feature"); const v = { run: { playersCollectibleCount: new DefaultMap_1.DefaultMap(0), playersCollectibleMap: new DefaultMap_1.DefaultMap(() => new Map()), playersActiveItemMap: new DefaultMap_1.DefaultMap(() => new Map()), }, }; class PlayerCollectibleDetection extends Feature_1.Feature { v = v; postPlayerCollectibleAdded; postPlayerCollectibleRemoved; moddedElementSets; runInNFrames; constructor(postPlayerCollectibleAdded, postPlayerCollectibleRemoved, moddedElementSets, runInNFrames) { super(); this.featuresUsed = [ ISCFeature_1.ISCFeature.MODDED_ELEMENT_SETS, ISCFeature_1.ISCFeature.RUN_IN_N_FRAMES, ]; this.callbacksUsed = [ // 3 [isaac_typescript_definitions_1.ModCallback.POST_USE_ITEM, this.postUseItemD4, [isaac_typescript_definitions_1.CollectibleType.D4]], ]; this.customCallbacksUsed = [ [ModCallbackCustom_1.ModCallbackCustom.ENTITY_TAKE_DMG_PLAYER, this.entityTakeDmgPlayer], [ModCallbackCustom_1.ModCallbackCustom.POST_ITEM_PICKUP, this.postItemPickup], [ ModCallbackCustom_1.ModCallbackCustom.POST_PEFFECT_UPDATE_REORDERED, this.postPEffectUpdateReordered, ], ]; this.postPlayerCollectibleAdded = postPlayerCollectibleAdded; this.postPlayerCollectibleRemoved = postPlayerCollectibleRemoved; this.moddedElementSets = moddedElementSets; this.runInNFrames = runInNFrames; } /** * This is called when the collectible count changes and in situations where the entire build is * rerolled. * * Since getting a new player collectible map is expensive, we want to only run this function when * necessary, and not on e.g. every frame. Unfortunately, this has the side effect of missing out * on collectible changes from mods that add and remove a collectible on the same frame. * * @param player The player to update. * @param numCollectiblesChanged Pass undefined for situations where the entire build was * rerolled. */ updateCollectibleMapAndFire(player, numCollectiblesChanged) { const oldCollectibleMap = (0, playerDataStructures_1.defaultMapGetPlayer)(v.run.playersCollectibleMap, player); const newCollectibleMap = this.moddedElementSets.getPlayerCollectibleMap(player); (0, playerDataStructures_1.mapSetPlayer)(v.run.playersCollectibleMap, player, newCollectibleMap); const collectibleTypesSet = new Set([ ...oldCollectibleMap.keys(), ...newCollectibleMap.keys(), ]); let numFired = 0; for (const collectibleType of collectibleTypesSet) { const oldNum = oldCollectibleMap.get(collectibleType) ?? 0; const newNum = newCollectibleMap.get(collectibleType) ?? 0; const difference = newNum - oldNum; const increased = difference > 0; const absoluteDifference = Math.abs(difference); (0, utils_1.repeat)(absoluteDifference, () => { if (increased) { this.postPlayerCollectibleAdded.fire(player, collectibleType); } else { this.postPlayerCollectibleRemoved.fire(player, collectibleType); } numFired++; }); if (numFired === numCollectiblesChanged) { return; } } } // ModCallback.POST_USE_ITEM (3) // CollectibleType.D4 (284) postUseItemD4 = (_collectibleType, _rng, player) => { // This function is also triggered for: // - D100 // - D Infinity copying D4 or D100 // - 1-pip dice room // - 6-pip dice room // - Reverse Wheel of Fortune copying 1-pip or 6-pip dice room // - First getting Missing No. // - Arriving on a new floor with Missing No. // This function is not triggered for: // - Tainted Eden getting hit (this is explicitly handled elsewhere) // - Genesis (which is automatically handled by the collectibles being removed in the normal // `POST_PLAYER_COLLECTIBLE_REMOVED` callback) this.updateCollectibleMapAndFire(player, undefined); return undefined; }; /** We need to handle the case of Tainted Eden taking damage. */ // ModCallbackCustom.ENTITY_TAKE_DMG_PLAYER entityTakeDmgPlayer = (player, _amount, damageFlags, _source, _countdownFrames) => { // Tainted Eden's mechanic does not apply if she e.g. uses Dull Razor. if ((0, flag_1.hasFlag)(damageFlags, isaac_typescript_definitions_1.DamageFlag.FAKE)) { return undefined; } const character = player.GetPlayerType(); if (character !== isaac_typescript_definitions_1.PlayerType.EDEN_B) { return undefined; } // The items will only be rerolled after the damage is successfully applied. const entityPtr = EntityPtr(player); this.runInNFrames.runNextGameFrame(() => { const futurePlayer = (0, players_1.getPlayerFromPtr)(entityPtr); if (futurePlayer !== undefined) { this.updateCollectibleMapAndFire(player, undefined); } }); return undefined; }; /** * We need to handle TMTRAINER collectibles, since they do not cause the player's collectible * count to change. */ // ModCallbackCustom.POST_ITEM_PICKUP postItemPickup = (player, pickingUpItem) => { if (pickingUpItem.itemType === isaac_typescript_definitions_1.ItemType.TRINKET || pickingUpItem.itemType === isaac_typescript_definitions_1.ItemType.NULL) { return; } const newCollectibleCount = player.GetCollectibleCount(); (0, playerDataStructures_1.mapSetPlayer)(v.run.playersCollectibleCount, player, newCollectibleCount); this.updateCollectibleMapAndFire(player, 1); }; // ModCallbackCustom.POST_PEFFECT_UPDATE_REORDERED postPEffectUpdateReordered = (player) => { const oldCollectibleCount = (0, playerDataStructures_1.defaultMapGetPlayer)(v.run.playersCollectibleCount, player); const newCollectibleCount = player.GetCollectibleCount(); (0, playerDataStructures_1.mapSetPlayer)(v.run.playersCollectibleCount, player, newCollectibleCount); const difference = newCollectibleCount - oldCollectibleCount; if (difference > 0) { this.updateCollectibleMapAndFire(player, difference); } else if (difference < 0) { this.updateCollectibleMapAndFire(player, difference * -1); } else if (difference === 0) { this.checkActiveItemsChanged(player); } }; /** * Checking for collectible count will work to detect when a player swaps their active item for * another active item. This is because the collectible count will decrement by 1 when the item is * swapped onto the pedestal and the hold animation begins, and increment by 1 when the item is * dequeued and the hold animation ends. * * However, we also want to explicitly check for the case where a mod swaps in a custom active * collectible on the same frame, since doing so is cheap. */ checkActiveItemsChanged(player) { const activeItemMap = (0, playerDataStructures_1.defaultMapGetPlayer)(v.run.playersActiveItemMap, player); const oldCollectibleTypes = []; const newCollectibleTypes = []; for (const activeSlot of cachedEnumValues_1.ACTIVE_SLOT_VALUES) { const oldCollectibleType = activeItemMap.get(activeSlot) ?? isaac_typescript_definitions_1.CollectibleType.NULL; const newCollectibleType = player.GetActiveItem(activeSlot); activeItemMap.set(activeSlot, newCollectibleType); oldCollectibleTypes.push(oldCollectibleType); newCollectibleTypes.push(newCollectibleType); } // For example, it is possible for the player to switch Schoolbag items, which will cause the // collectibles in the array to be the same, but in a different order. Thus, we sort both arrays // before comparing them. oldCollectibleTypes.sort(sort_1.sortNormal); newCollectibleTypes.sort(sort_1.sortNormal); if (!(0, array_1.arrayEquals)(oldCollectibleTypes, newCollectibleTypes)) { // One or more active items have changed (with the player's total collectible count remaining // the same). this.updateCollectibleMapAndFire(player, undefined); } } } exports.PlayerCollectibleDetection = PlayerCollectibleDetection;