isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
195 lines (194 loc) • 9.68 kB
JavaScript
;
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;