isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
229 lines (228 loc) • 10.1 kB
JavaScript
;
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.PickupIndexCreation = void 0;
const isaac_typescript_definitions_1 = require("isaac-typescript-definitions");
const cachedClasses_1 = require("../../../core/cachedClasses");
const decorators_1 = require("../../../decorators");
const ISCFeature_1 = require("../../../enums/ISCFeature");
const entities_1 = require("../../../functions/entities");
const frames_1 = require("../../../functions/frames");
const roomData_1 = require("../../../functions/roomData");
const stage_1 = require("../../../functions/stage");
const vector_1 = require("../../../functions/vector");
const DefaultMap_1 = require("../../DefaultMap");
const Feature_1 = require("../../private/Feature");
const v = {
run: {
/** Is incremented before assignment. Thus, the first pickup will have an index of 1. */
pickupCounter: 0,
pickupDataTreasureRooms: new Map(),
pickupDataBossRooms: new Map(),
},
level: {
/** Indexed by room list index. */
pickupData: new DefaultMap_1.DefaultMap(() => new Map()),
},
room: {
pickupIndexes: new Map(),
},
};
class PickupIndexCreation extends Feature_1.Feature {
/** @internal */
v = v;
roomHistory;
saveDataManager;
/** @internal */
constructor(roomHistory, saveDataManager) {
super();
this.featuresUsed = [ISCFeature_1.ISCFeature.ROOM_HISTORY, ISCFeature_1.ISCFeature.SAVE_DATA_MANAGER];
this.callbacksUsed = [
// 34
[isaac_typescript_definitions_1.ModCallback.POST_PICKUP_INIT, this.postPickupInit],
// 67
[
isaac_typescript_definitions_1.ModCallback.POST_ENTITY_REMOVE,
this.postEntityRemovePickup,
[isaac_typescript_definitions_1.EntityType.PICKUP],
],
];
this.roomHistory = roomHistory;
this.saveDataManager = saveDataManager;
}
// ModCallback.POST_PICKUP_INIT (34)
postPickupInit = (pickup) => {
this.setPickupIndex(pickup);
};
setPickupIndex(pickup) {
const ptrHash = GetPtrHash(pickup);
// In certain situations, pickups can be morphed, which will trigger the `POST_PICKUP_INIT`
// callback but should not incur a new pickup counter. (For example, the collectible rotation
// with Tainted Isaac.) For these situations, we will already be tracking an index for this
// pointer hash.
if (v.room.pickupIndexes.has(ptrHash)) {
return;
}
// First, handle the special case of re-entering a room with a previously tracked pickup. If we
// find a match in the level pickup data, we will use the pickup index from the match.
const pickupIndexFromLevelData = this.getPickupIndexFromPreviousData(pickup);
const room = cachedClasses_1.game.GetRoom();
const isFirstVisit = room.IsFirstVisit();
if (pickupIndexFromLevelData !== undefined
&& !isFirstVisit
&& (0, frames_1.onOrBeforeRoomFrame)(0)) {
v.room.pickupIndexes.set(ptrHash, pickupIndexFromLevelData);
return;
}
// This is a brand new pickup that we have not previously seen on this run.
v.run.pickupCounter++;
v.room.pickupIndexes.set(ptrHash, v.run.pickupCounter);
}
getPickupIndexFromPreviousData(pickup) {
const roomListIndex = (0, roomData_1.getRoomListIndex)();
const pickupDescriptions = v.level.pickupData.getAndSetDefault(roomListIndex);
let pickupIndex = getStoredPickupIndex(pickup, pickupDescriptions);
pickupIndex ??= this.getPostAscentPickupIndex(pickup);
return pickupIndex;
}
// ModCallback.POST_ENTITY_REMOVE (67)
// EntityType.PICKUP (5)
postEntityRemovePickup = (entity) => {
this.checkDespawningFromPlayerLeavingRoom(entity);
};
checkDespawningFromPlayerLeavingRoom(entity) {
const ptrHash = GetPtrHash(entity);
const pickupIndex = v.room.pickupIndexes.get(ptrHash);
if (pickupIndex === undefined) {
return;
}
if (!this.roomHistory.isLeavingRoom()) {
return;
}
this.trackDespawningPickupMetadata(entity, pickupIndex);
}
/**
* This is a pickup that is despawning because the player is in the process of leaving the room.
* Keep track of the metadata for later.
*/
trackDespawningPickupMetadata(entity, pickupIndex) {
// The "latest" room description is really the previous room, because the `POST_NEW_ROOM`
// callback has not fired yet.
const previousRoomDescription = this.roomHistory.getLatestRoomDescription();
if (previousRoomDescription === undefined) {
return;
}
const previousRoomListIndex = previousRoomDescription.roomListIndex;
const pickupDescriptions = v.level.pickupData.getAndSetDefault(previousRoomListIndex);
const pickupDescription = {
position: entity.Position,
initSeed: entity.InitSeed,
};
pickupDescriptions.set(pickupIndex, pickupDescription);
const pickupDataMapForCurrentRoom = this.getPickupDataMapForCurrentRoom();
if (pickupDataMapForCurrentRoom !== undefined) {
pickupDataMapForCurrentRoom.set(pickupIndex, pickupDescription);
}
// Since the `POST_ENTITY_REMOVE` callback fires after the `PRE_GAME_EXIT` callback, we need to
// explicitly save data again if the player is in the process of saving and quitting the run.
if (this.saveDataManager.saveDataManagerInMenu()) {
this.saveDataManager.saveDataManagerSave();
}
}
/**
* If the despawning pickup was in a Treasure Room or Boss Room, then it is possible that the
* pickup could re-appear during The Ascent. If this is the case, we store the metadata on a
* separate map to reference later.
*/
// eslint-disable-next-line complete/no-mutable-return
getPickupDataMapForCurrentRoom() {
if ((0, stage_1.onAscent)()) {
return undefined;
}
const room = cachedClasses_1.game.GetRoom();
const roomType = room.GetType();
switch (roomType) {
case isaac_typescript_definitions_1.RoomType.TREASURE: {
return v.run.pickupDataTreasureRooms;
}
case isaac_typescript_definitions_1.RoomType.BOSS: {
return v.run.pickupDataBossRooms;
}
default: {
return undefined;
}
}
}
getPostAscentPickupIndex(pickup) {
// If we have not found the pickup index yet, we might be re-entering a post-Ascent Treasure
// Room or Boss Room.
if (!(0, stage_1.onAscent)()) {
return undefined;
}
const room = cachedClasses_1.game.GetRoom();
const roomType = room.GetType();
switch (roomType) {
case isaac_typescript_definitions_1.RoomType.TREASURE: {
return getStoredPickupIndex(pickup, v.run.pickupDataTreasureRooms);
}
case isaac_typescript_definitions_1.RoomType.BOSS: {
return getStoredPickupIndex(pickup, v.run.pickupDataBossRooms);
}
default: {
return undefined;
}
}
}
/**
* Mods often have to track variables relating to a pickups. Finding an index for these kinds of
* data structures is difficult, since pickups are respawned every time a player re-enters a room,
* so the `PtrHash` will change.
*
* Use this function to get a unique index for a pickup to use in these data structures.
*
* Specifically, `PickupIndex` is a number that represents the spawn order of the pickup on the
* current run. For example, the first pickup spawned will have an index of 1, the second one will
* have an index of 2, and so on.
*
* Tracking pickups requires stateful tracking, so using pickup indexes requires an upgraded mod.
*
* Note that the pickup index will not change:
* - When a pickup is rolled with e.g. D6 or D20.
* - When an item is "rotated" via e.g. Tainted Isaac's mechanic.
*
* In order to use this function, you must upgrade your mod with
* `ISCFeature.PICKUP_INDEX_CREATION`.
*/
getPickupIndex(pickup) {
const ptrHash = GetPtrHash(pickup);
const pickupIndexInitial = v.room.pickupIndexes.get(ptrHash);
if (pickupIndexInitial !== undefined) {
return pickupIndexInitial;
}
this.setPickupIndex(pickup);
const pickupIndex = v.room.pickupIndexes.get(ptrHash);
if (pickupIndex !== undefined) {
return pickupIndex;
}
const entityID = (0, entities_1.getEntityID)(pickup);
error(`Failed to generate a new pickup index for pickup: ${entityID}`);
}
}
exports.PickupIndexCreation = PickupIndexCreation;
__decorate([
decorators_1.Exported
], PickupIndexCreation.prototype, "getPickupIndex", null);
function getStoredPickupIndex(pickup, pickupDescriptions) {
for (const [pickupIndex, pickupDescription] of pickupDescriptions) {
if ((0, vector_1.vectorEquals)(pickupDescription.position, pickup.Position)
&& pickupDescription.initSeed === pickup.InitSeed) {
return pickupIndex;
}
}
return undefined;
}