UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

229 lines (228 loc) • 10.1 kB
"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.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; }