UNPKG

isaacscript-common

Version:

Helper functions and features for IsaacScript mods.

482 lines (481 loc) • 24.3 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); 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; }; var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.CustomStages = void 0; const isaac_typescript_definitions_1 = require("isaac-typescript-definitions"); const cachedClasses_1 = require("../../../core/cachedClasses"); const metadataJSON = __importStar(require("../../../customStageMetadata.json")); // This will correspond to "customStageMetadata.lua" at run-time. const decorators_1 = require("../../../decorators"); const ISCFeature_1 = require("../../../enums/ISCFeature"); const ModCallbackCustom_1 = require("../../../enums/ModCallbackCustom"); const array_1 = require("../../../functions/array"); const doors_1 = require("../../../functions/doors"); const flag_1 = require("../../../functions/flag"); const log_1 = require("../../../functions/log"); const rng_1 = require("../../../functions/rng"); const rockAlt_1 = require("../../../functions/rockAlt"); const rooms_1 = require("../../../functions/rooms"); const sound_1 = require("../../../functions/sound"); const stage_1 = require("../../../functions/stage"); const utils_1 = require("../../../functions/utils"); const Feature_1 = require("../../private/Feature"); const backdrop_1 = require("./customStages/backdrop"); const constants_1 = require("./customStages/constants"); const gridEntities_1 = require("./customStages/gridEntities"); const shadows_1 = require("./customStages/shadows"); const streakText_1 = require("./customStages/streakText"); const utils_2 = require("./customStages/utils"); const v_1 = require("./customStages/v"); const versusScreen_1 = require("./customStages/versusScreen"); /** * 60 does not work correctly (the music kicking in from stage -1 will mute it), so we use 70 to be * safe. */ const MUSIC_DELAY_RENDER_FRAMES = 70; class CustomStages extends Feature_1.Feature { /** @internal */ v = v_1.v; /** Indexed by custom stage name. */ customStagesMap = new Map(); /** Indexed by room variant. */ customStageCachedRoomData = new Map(); usingRedKey = false; customGridEntities; customTrapdoors; disableAllSound; gameReorderedCallbacks; pause; runInNFrames; /** @internal */ constructor(customGridEntities, customTrapdoors, disableAllSound, gameReorderedCallbacks, pause, runInNFrames) { super(); this.featuresUsed = [ ISCFeature_1.ISCFeature.CUSTOM_GRID_ENTITIES, ISCFeature_1.ISCFeature.CUSTOM_TRAPDOORS, ISCFeature_1.ISCFeature.DISABLE_ALL_SOUND, ISCFeature_1.ISCFeature.GAME_REORDERED_CALLBACKS, ISCFeature_1.ISCFeature.PAUSE, ISCFeature_1.ISCFeature.RUN_IN_N_FRAMES, ]; this.callbacksUsed = [ // 2 [isaac_typescript_definitions_1.ModCallback.POST_RENDER, this.postRender], // 3 [ isaac_typescript_definitions_1.ModCallback.POST_USE_ITEM, this.postUseItemRedKey, [isaac_typescript_definitions_1.CollectibleType.RED_KEY], ], // 12 [isaac_typescript_definitions_1.ModCallback.POST_CURSE_EVAL, this.postCurseEval], // 21 [isaac_typescript_definitions_1.ModCallback.GET_SHADER_PARAMS, this.getShaderParams], // 23 [ isaac_typescript_definitions_1.ModCallback.PRE_USE_ITEM, this.preUseItemRedKey, [isaac_typescript_definitions_1.CollectibleType.RED_KEY], ], ]; this.customCallbacksUsed = [ [ ModCallbackCustom_1.ModCallbackCustom.POST_GRID_ENTITY_BROKEN, this.postGridEntityBrokenRockAlt, [isaac_typescript_definitions_1.GridEntityType.ROCK_ALT], ], [ModCallbackCustom_1.ModCallbackCustom.POST_GRID_ENTITY_INIT, this.postGridEntityInit], [ModCallbackCustom_1.ModCallbackCustom.POST_NEW_ROOM_REORDERED, this.postNewRoomReordered], ]; this.customGridEntities = customGridEntities; this.customTrapdoors = customTrapdoors; this.disableAllSound = disableAllSound; this.gameReorderedCallbacks = gameReorderedCallbacks; this.pause = pause; this.runInNFrames = runInNFrames; this.initCustomStageMetadata(); } initCustomStageMetadata() { if (!(0, array_1.isArray)(metadataJSON)) { error('The IsaacScript standard library attempted to read the custom stage metadata from the "customStageMetadata.lua" file, but it was not an array.'); } const customStagesLua = metadataJSON; for (const customStageLua of customStagesLua) { this.initRoomTypeMap(customStageLua); this.initCustomTrapdoorDestination(customStageLua); } } initRoomTypeMap(customStageLua) { const roomTypeMap = getRoomTypeMap(customStageLua); const customStage = { ...customStageLua, roomTypeMap, }; this.customStagesMap.set(customStage.name, customStage); } initCustomTrapdoorDestination(customStageLua) { this.customTrapdoors.registerCustomTrapdoorDestination(customStageLua.name, this.goToCustomStage); } goToCustomStage = (destinationName, destinationStage, _destinationStageType) => { (0, utils_1.assertDefined)(destinationName, "Failed to go to a custom stage since the custom trapdoors feature did not pass a destination name to the logic function."); const firstFloor = destinationStage === isaac_typescript_definitions_1.LevelStage.BASEMENT_1; this.setCustomStage(destinationName, firstFloor); }; // ModCallback.POST_RENDER (2) postRender = () => { const customStage = v_1.v.run.currentCustomStage; if (customStage === null) { return; } (0, streakText_1.streakTextPostRender)(); (0, versusScreen_1.versusScreenPostRender)(this.pause, this.disableAllSound); // Fix the bug where the music will stop after loading a new room. (This does not work if placed // in the `POST_NEW_ROOM_REORDERED` callback or the `POST_UPDATE` callback.) if (customStage.music !== undefined) { const currentMusic = cachedClasses_1.musicManager.GetCurrentMusicID(); const music = Isaac.GetMusicIdByName(customStage.music); if (currentMusic === music) { cachedClasses_1.musicManager.Resume(); cachedClasses_1.musicManager.UpdateVolume(); } } }; /** * Fix the bug where Red Key will not work on custom floors (due to the stage being a bugged * value). */ // ModCallback.POST_USE_ITEM (3) postUseItemRedKey = () => { const customStage = v_1.v.run.currentCustomStage; if (customStage === null) { return undefined; } if (!this.usingRedKey) { return undefined; } this.usingRedKey = false; const level = cachedClasses_1.game.GetLevel(); level.SetStage(constants_1.CUSTOM_FLOOR_STAGE, constants_1.CUSTOM_FLOOR_STAGE_TYPE); return undefined; }; // ModCallback.POST_CURSE_EVAL (12) postCurseEval = (curses) => { const customStage = v_1.v.run.currentCustomStage; if (customStage === null) { return undefined; } // Prevent XL floors on custom stages, since the streak text will not work properly. if ((0, flag_1.hasFlag)(curses, isaac_typescript_definitions_1.LevelCurse.LABYRINTH)) { return (0, flag_1.removeFlag)(curses, isaac_typescript_definitions_1.LevelCurse.LABYRINTH); } return undefined; }; // ModCallback.GET_SHADER_PARAMS (22) getShaderParams = (shaderName) => { const customStage = v_1.v.run.currentCustomStage; if (customStage === null) { return undefined; } (0, streakText_1.streakTextGetShaderParams)(customStage, shaderName); return undefined; }; /** * Fix the bug where Red Key will not work on custom floors (due to the stage being a bugged * value). */ // ModCallback.PRE_USE_ITEM (23) preUseItemRedKey = () => { const customStage = v_1.v.run.currentCustomStage; if (customStage === null) { return undefined; } this.usingRedKey = true; const level = cachedClasses_1.game.GetLevel(); const stage = customStage.baseStage ?? constants_1.DEFAULT_BASE_STAGE; const stageType = customStage.baseStageType ?? constants_1.DEFAULT_BASE_STAGE_TYPE; level.SetStage(stage, stageType); // eslint-disable-line complete/strict-enums return undefined; }; // ModCallbackCustom.POST_GRID_ENTITY_BROKEN // GridEntityType.ROCK_ALT postGridEntityBrokenRockAlt = (gridEntity) => { const customStage = v_1.v.run.currentCustomStage; if (customStage === null) { return; } // Assume that if the end-user does not have custom rock graphics specified, they want to keep // the vanilla urn reward functionality. if (customStage.rocksPNGPath === undefined) { return; } // On the bugged stage of -1, only urns will spawn, so we do not have to handle the case of // mushroom rewards, skull rewards, and so on. (0, rockAlt_1.removeUrnRewards)(gridEntity); }; // ModCallbackCustom.POST_GRID_ENTITY_INIT postGridEntityInit = (gridEntity) => { const customStage = v_1.v.run.currentCustomStage; if (customStage === null) { return; } // We only want to modify vanilla grid entities. if (this.customGridEntities.isCustomGridEntity(gridEntity)) { return; } (0, gridEntities_1.setCustomDecorationGraphics)(customStage, gridEntity); (0, gridEntities_1.setCustomRockGraphics)(customStage, gridEntity); (0, gridEntities_1.setCustomPitGraphics)(customStage, gridEntity); (0, gridEntities_1.setCustomDoorGraphics)(customStage, gridEntity); (0, gridEntities_1.convertVanillaTrapdoors)(customStage, gridEntity, v_1.v.run.firstFloor, this.customTrapdoors); }; // ModCallbackCustom.POST_NEW_ROOM_REORDERED postNewRoomReordered = () => { const customStage = v_1.v.run.currentCustomStage; if (customStage === null) { return; } (0, backdrop_1.setCustomStageBackdrop)(customStage); (0, shadows_1.setShadows)(customStage); (0, versusScreen_1.playVersusScreenAnimation)(customStage, this.disableAllSound, this.pause, this.runInNFrames); // Fix the bug where music from special rooms (like the "Boss Over" music) will persist for the // rest of the floor. if (customStage.music !== undefined && (0, rooms_1.inRoomType)(isaac_typescript_definitions_1.RoomType.DEFAULT)) { const music = Isaac.GetMusicIdByName(customStage.music); const currentMusic = cachedClasses_1.musicManager.GetCurrentMusicID(); if (currentMusic !== music) { cachedClasses_1.musicManager.Fadein(music); } } }; /** Pick a custom room for each vanilla room. */ setStageRoomsData(customStage, rng, verbose) { const level = cachedClasses_1.game.GetLevel(); const startingRoomGridIndex = level.GetStartingRoomIndex(); for (const room of (0, rooms_1.getRoomsInsideGrid)()) { // The starting floor of each room should stay empty. if (room.SafeGridIndex === startingRoomGridIndex) { continue; } if (room.Data === undefined) { continue; } const roomType = room.Data.Type; const roomShapeMap = customStage.roomTypeMap.get(roomType); if (roomShapeMap === undefined) { // Only show errors for non-default room types. (We do not require that end-users provide // custom rooms for shops and other special rooms inside of their XML file.) if (roomType === isaac_typescript_definitions_1.RoomType.DEFAULT) { (0, log_1.logError)(`Failed to find any custom rooms for RoomType.${isaac_typescript_definitions_1.RoomType[roomType]} (${roomType}) for custom stage: ${customStage.name}`); } continue; } const roomShape = room.Data.Shape; const roomDoorSlotFlagMap = roomShapeMap.get(roomShape); if (roomDoorSlotFlagMap === undefined) { (0, log_1.logError)(`Failed to find any custom rooms for RoomType.${isaac_typescript_definitions_1.RoomType[roomType]} (${roomType}) + RoomShape.${isaac_typescript_definitions_1.RoomShape[roomShape]} (${roomShape}) for custom stage: ${customStage.name}`); continue; } const doorSlotFlags = room.Data.Doors; let roomsMetadata = roomDoorSlotFlagMap.get(doorSlotFlags); if (roomsMetadata === undefined) { // The custom stage does not have any rooms for the specific room type + room shape + door // slot combination. As a fallback, check to see if the custom stage has one or more rooms // for this specific room type + room shape + all doors. const allDoorSlots = (0, doors_1.getDoorSlotsForRoomShape)(roomShape); const allDoorSlotFlags = (0, doors_1.doorSlotsToDoorSlotFlags)(allDoorSlots); roomsMetadata = roomDoorSlotFlagMap.get(allDoorSlotFlags); if (roomsMetadata === undefined) { (0, log_1.logError)(`Failed to find any custom rooms for RoomType.${isaac_typescript_definitions_1.RoomType[roomType]} (${roomType}) + RoomShape.${isaac_typescript_definitions_1.RoomShape[roomShape]} (${roomShape}) + all doors enabled for custom stage: ${customStage.name}`); continue; } } let randomRoom; if (roomType === isaac_typescript_definitions_1.RoomType.BOSS) { if (customStage.bossPool === undefined) { continue; } randomRoom = (0, utils_2.getRandomBossRoomFromPool)(roomsMetadata, customStage.bossPool, rng, verbose); } else { randomRoom = (0, utils_2.getRandomCustomStageRoom)(roomsMetadata, rng, verbose); } let newRoomData = this.customStageCachedRoomData.get(randomRoom.variant); if (newRoomData === undefined) { // We do not already have the room data for this room cached. newRoomData = (0, rooms_1.getRoomDataForTypeVariant)(roomType, randomRoom.variant, false, // Since we are going to multiple rooms, we cancel the transition. true); if (newRoomData === undefined) { (0, log_1.logError)(`Failed to get the room data for room variant ${randomRoom.variant} for custom stage: ${customStage.name}`); continue; } this.customStageCachedRoomData.set(randomRoom.variant, newRoomData); } room.Data = newRoomData; } } /** * Helper function to warp to a custom stage/level. * * Custom stages/levels must first be defined in the "tsconfig.json" file. See the documentation * for more details: https://isaacscript.github.io/main/custom-stages/ * * In order to use this function, you must upgrade your mod with `ISCFeature.CUSTOM_STAGES`. * * @param name The name of the custom stage, corresponding to what is in the "tsconfig.json" file. * @param firstFloor Optional. Whether to go to the first floor or the second floor. For example, * if you have a custom stage emulating Caves, then the first floor would be * Caves 1, and the second floor would be Caves 2. Default is true. * @param streakText Optional. Whether to show the streak text at the top of the screen that * announces the name of the level. Default is true. * @param verbose Optional. Whether to log additional information about the rooms that are chosen. * Default is false. * @public */ setCustomStage(name, firstFloor = true, streakText = true, verbose = false) { const customStage = this.customStagesMap.get(name); (0, utils_1.assertDefined)(customStage, `Failed to set the custom stage of "${name}" because it was not found in the custom stages map. (Try restarting IsaacScript / recompiling the mod / restarting the game, and try again. If that does not work, you probably forgot to define it in your "tsconfig.json" file.) See the website for more details on how to set up custom stages.`); const level = cachedClasses_1.game.GetLevel(); const stage = level.GetStage(); const seeds = cachedClasses_1.game.GetSeeds(); const startSeed = seeds.GetStartSeed(); const rng = (0, rng_1.newRNG)(startSeed); v_1.v.run.currentCustomStage = customStage; v_1.v.run.firstFloor = firstFloor; // Before changing the stage, we have to revert the bugged stage, if necessary. This prevents // the bug where the backdrop will not spawn. if (stage === constants_1.CUSTOM_FLOOR_STAGE) { level.SetStage(isaac_typescript_definitions_1.LevelStage.BASEMENT_1, isaac_typescript_definitions_1.StageType.ORIGINAL); } let baseStage = customStage.baseStage === undefined ? constants_1.DEFAULT_BASE_STAGE : customStage.baseStage; if (!firstFloor) { baseStage++; // eslint-disable-line complete/strict-enums } const baseStageType = customStage.baseStageType === undefined ? constants_1.DEFAULT_BASE_STAGE_TYPE : customStage.baseStageType; const reseed = stage >= baseStage; (0, stage_1.setStage)(baseStage, baseStageType, reseed); // As soon as we warp to the base stage, the base stage music will begin to play. Thus, we // temporarily mute all music. cachedClasses_1.musicManager.Disable(); this.setStageRoomsData(customStage, rng, verbose); // Set the stage to an invalid value, which will prevent the walls and floors from loading. const targetStage = constants_1.CUSTOM_FLOOR_STAGE; const targetStageType = constants_1.CUSTOM_FLOOR_STAGE_TYPE; level.SetStage(targetStage, targetStageType); this.gameReorderedCallbacks.reorderedCallbacksSetStage(targetStage, targetStageType); // In vanilla, the streak text appears about when the pixelation has faded and while Isaac is // still laying on the ground. Unfortunately, we cannot exactly replicate the vanilla timing, // because the level text will bug out and smear the background if we play it from a // `POST_RENDER` callback. Thus, we run it on the next game frame as a workaround. if (streakText) { this.runInNFrames.runNextGameFrame(() => { (0, streakText_1.topStreakTextStart)(); }); } // The bugged stage will not have any music associated with it, so we must manually start to // play a track. First, prefer the music that is explicitly assigned to this custom floor. let customStageMusic; if (customStage.music !== undefined) { customStageMusic = Isaac.GetMusicIdByName(customStage.music); if (customStageMusic === -1) { (0, log_1.logError)(`Failed to get the music ID associated with the name of: ${customStage.music}`); } } const music = customStageMusic === undefined || customStageMusic === -1 ? (0, sound_1.getMusicForStage)(baseStage, baseStageType) : customStageMusic; this.runInNFrames.runInNRenderFrames(() => { cachedClasses_1.musicManager.Enable(); cachedClasses_1.musicManager.Play(music); cachedClasses_1.musicManager.UpdateVolume(); }, MUSIC_DELAY_RENDER_FRAMES); // We must reload the current room in order for the `Level.SetStage` method to take effect. // Furthermore, we need to cancel the queued warp to the `GridRoom.DEBUG` room. We can // accomplish both of these things by initiating a room transition to an arbitrary room. // However, we rely on the parent function to do this, since for normal purposes, we need to // initiate a room transition for the pixelation effect. } /** * Helper function to disable the custom stage. This is typically called before taking the player * to a vanilla floor. * * In order to use this function, you must upgrade your mod with `ISCFeature.CUSTOM_STAGES`. * * @public */ disableCustomStage() { v_1.v.run.currentCustomStage = null; } } exports.CustomStages = CustomStages; __decorate([ decorators_1.Exported ], CustomStages.prototype, "setCustomStage", null); __decorate([ decorators_1.Exported ], CustomStages.prototype, "disableCustomStage", null); function getRoomTypeMap(customStageLua) { const roomTypeMap = new Map(); for (const roomMetadata of customStageLua.roomsMetadata) { const roomType = roomMetadata.type; let roomShapeMap = roomTypeMap.get(roomType); if (roomShapeMap === undefined) { roomShapeMap = new Map(); roomTypeMap.set(roomType, roomShapeMap); } const roomShape = roomMetadata.shape; let roomDoorSlotFlagMap = roomShapeMap.get(roomShape); if (roomDoorSlotFlagMap === undefined) { roomDoorSlotFlagMap = new Map(); roomShapeMap.set(roomShape, roomDoorSlotFlagMap); } const doorSlotFlags = roomMetadata.doorSlotFlags; let rooms = roomDoorSlotFlagMap.get(doorSlotFlags); if (rooms === undefined) { rooms = []; roomDoorSlotFlagMap.set(doorSlotFlags, rooms); } rooms.push(roomMetadata); } return roomTypeMap; }