isaacscript-common
Version:
Helper functions and features for IsaacScript mods.
227 lines (226 loc) • 11.5 kB
JavaScript
"use strict";
// This emulates the vanilla versus screen that shows up when you enter a boss room.
Object.defineProperty(exports, "__esModule", { value: true });
exports.playVersusScreenAnimation = playVersusScreenAnimation;
exports.versusScreenPostRender = versusScreenPostRender;
const isaac_typescript_definitions_1 = require("isaac-typescript-definitions");
const cachedClasses_1 = require("../../../../core/cachedClasses");
const array_1 = require("../../../../functions/array");
const bosses_1 = require("../../../../functions/bosses");
const roomData_1 = require("../../../../functions/roomData");
const string_1 = require("../../../../functions/string");
const ui_1 = require("../../../../functions/ui");
const utils_1 = require("../../../../functions/utils");
const versusScreen_1 = require("../../../../functions/versusScreen");
const versusScreenBackgroundColors_1 = require("../../../../objects/versusScreenBackgroundColors");
const versusScreenDirtSpotColors_1 = require("../../../../objects/versusScreenDirtSpotColors");
const constants_1 = require("./constants");
const v_1 = require("./v");
const DEFAULT_STAGE_ID = isaac_typescript_definitions_1.StageID.BASEMENT;
const VERSUS_SCREEN_ANIMATION_NAME = "Scene";
/** The layers range from 0 to 13. */
const NUM_VERSUS_SCREEN_ANM2_LAYERS = 14;
/** Corresponds to "resources/gfx/ui/boss/versusscreen.anm2". */
var VersusScreenLayer;
(function (VersusScreenLayer) {
VersusScreenLayer[VersusScreenLayer["BACKGROUND"] = 0] = "BACKGROUND";
VersusScreenLayer[VersusScreenLayer["FRAME"] = 1] = "FRAME";
/** The boss dirt spot. */
VersusScreenLayer[VersusScreenLayer["BOSS_SPOT"] = 2] = "BOSS_SPOT";
/** The player dirt spot. */
VersusScreenLayer[VersusScreenLayer["PLAYER_SPOT"] = 3] = "PLAYER_SPOT";
VersusScreenLayer[VersusScreenLayer["BOSS_PORTRAIT"] = 4] = "BOSS_PORTRAIT";
VersusScreenLayer[VersusScreenLayer["PLAYER_PORTRAIT"] = 5] = "PLAYER_PORTRAIT";
VersusScreenLayer[VersusScreenLayer["PLAYER_NAME"] = 6] = "PLAYER_NAME";
VersusScreenLayer[VersusScreenLayer["BOSS_NAME"] = 7] = "BOSS_NAME";
VersusScreenLayer[VersusScreenLayer["VS_TEXT"] = 8] = "VS_TEXT";
VersusScreenLayer[VersusScreenLayer["BOSS_DOUBLE"] = 9] = "BOSS_DOUBLE";
VersusScreenLayer[VersusScreenLayer["DT_TEXT"] = 10] = "DT_TEXT";
VersusScreenLayer[VersusScreenLayer["OVERLAY"] = 11] = "OVERLAY";
/**
* We only need to render either the normal player portrait layer or the alternate player portrait
* layer. Rendering both will cause the player not to shake.
*/
VersusScreenLayer[VersusScreenLayer["PLAYER_PORTRAIT_ALT"] = 12] = "PLAYER_PORTRAIT_ALT";
VersusScreenLayer[VersusScreenLayer["BOSS_PORTRAIT_GROUND"] = 13] = "BOSS_PORTRAIT_GROUND";
VersusScreenLayer[VersusScreenLayer["BOSS_PORTRAIT_2_GROUND"] = 14] = "BOSS_PORTRAIT_2_GROUND";
})(VersusScreenLayer || (VersusScreenLayer = {}));
/** These are the non-special layers that we will render last. */
const OTHER_ANM2_LAYERS = (0, array_1.arrayRemove)((0, utils_1.eRange)(NUM_VERSUS_SCREEN_ANM2_LAYERS), VersusScreenLayer.BACKGROUND, VersusScreenLayer.BOSS_SPOT, VersusScreenLayer.PLAYER_SPOT, VersusScreenLayer.OVERLAY, VersusScreenLayer.PLAYER_PORTRAIT_ALT);
const VANILLA_VERSUS_PLAYBACK_SPEED = 0.5;
/** We lazy load the sprite when first needed. */
const versusScreenSprite = Sprite();
/**
* We lazy load the sprite when first needed.
*
* Unfortunately, we must split the background layer into an entirely different sprite so that we
* can color it with the `Color` field.
*/
const versusScreenBackgroundSprite = Sprite();
/**
* We lazy load the sprite when first needed.
*
* Unfortunately, we must split the dirt layer into an entirely different sprite so that we can
* color it with the `Color` field.
*/
const versusScreenDirtSpotSprite = Sprite();
function playVersusScreenAnimation(customStage, disableAllSound, pause, runInNFrames) {
const room = cachedClasses_1.game.GetRoom();
const roomType = room.GetType();
const roomCleared = room.IsClear();
const hud = cachedClasses_1.game.GetHUD();
if (roomType !== isaac_typescript_definitions_1.RoomType.BOSS) {
return;
}
if (roomCleared) {
return;
}
if (willVanillaVersusScreenPlay()) {
// Since we are on an invalid stage, the versus screen will have a completely black background.
// Revert to using the background from the default stage.
const level = cachedClasses_1.game.GetLevel();
level.SetStage(constants_1.DEFAULT_BASE_STAGE, constants_1.DEFAULT_BASE_STAGE_TYPE);
runInNFrames.runNextGameFrame(() => {
const futureLevel = cachedClasses_1.game.GetLevel();
futureLevel.SetStage(constants_1.CUSTOM_FLOOR_STAGE, constants_1.CUSTOM_FLOOR_STAGE_TYPE);
});
return;
}
v_1.v.run.showingBossVersusScreen = true;
pause.pause();
hud.SetVisible(false);
disableAllSound.disableAllSound(constants_1.CUSTOM_STAGE_FEATURE_NAME);
// In vanilla, the "overlay.png" file has a white background. We must convert it to a PNG that
// uses a transparent background in order for the background behind it to be visible. We use the
// same "overlay.png" file as StageAPI uses for this purpose.
if (!versusScreenSprite.IsLoaded()) {
versusScreenSprite.Load("gfx/ui/boss/versusscreen.anm2", false);
versusScreenSprite.ReplaceSpritesheet(VersusScreenLayer.OVERLAY, `${constants_1.ISAACSCRIPT_CUSTOM_STAGE_GFX_PATH}/overlay.png`);
}
// Player
{
const { namePNGPath, portraitPNGPath } = getPlayerPNGPaths();
versusScreenSprite.ReplaceSpritesheet(VersusScreenLayer.PLAYER_NAME, namePNGPath);
versusScreenSprite.ReplaceSpritesheet(VersusScreenLayer.PLAYER_PORTRAIT, portraitPNGPath);
}
// Boss
{
const { namePNGPath, portraitPNGPath } = getBossPNGPaths(customStage);
const trimmedNamePNGPath = (0, string_1.removeCharactersBefore)(namePNGPath, "gfx/");
versusScreenSprite.ReplaceSpritesheet(VersusScreenLayer.BOSS_NAME, trimmedNamePNGPath);
const trimmedPortraitPNGPath = (0, string_1.removeCharactersBefore)(portraitPNGPath, "gfx/");
versusScreenSprite.ReplaceSpritesheet(VersusScreenLayer.BOSS_PORTRAIT, trimmedPortraitPNGPath);
}
versusScreenSprite.LoadGraphics();
if (!versusScreenBackgroundSprite.IsLoaded()) {
versusScreenBackgroundSprite.Load("gfx/ui/boss/versusscreen.anm2", true);
}
let backgroundColor = versusScreenBackgroundColors_1.VERSUS_SCREEN_BACKGROUND_COLORS[DEFAULT_STAGE_ID];
if (customStage.versusScreen?.backgroundColor !== undefined) {
const { r, g, b, a } = customStage.versusScreen.backgroundColor;
backgroundColor = Color(r, g, b, a);
}
versusScreenBackgroundSprite.Color = backgroundColor;
if (!versusScreenDirtSpotSprite.IsLoaded()) {
versusScreenDirtSpotSprite.Load("gfx/ui/boss/versusscreen.anm2", true);
}
let dirtSpotColor = versusScreenDirtSpotColors_1.VERSUS_SCREEN_DIRT_SPOT_COLORS[DEFAULT_STAGE_ID];
if (customStage.versusScreen?.dirtSpotColor !== undefined) {
const { r, g, b } = customStage.versusScreen.dirtSpotColor;
dirtSpotColor = Color(r, g, b);
}
versusScreenDirtSpotSprite.Color = dirtSpotColor;
for (const sprite of [
versusScreenBackgroundSprite,
versusScreenDirtSpotSprite,
versusScreenSprite,
]) {
sprite.Play(VERSUS_SCREEN_ANIMATION_NAME, true);
sprite.PlaybackSpeed = VANILLA_VERSUS_PLAYBACK_SPEED;
}
}
function willVanillaVersusScreenPlay() {
const bosses = (0, bosses_1.getBosses)();
return bosses.some((boss) => boss.GetBossID() !== 0);
}
/** Use the character of the 0th player. */
function getPlayerPNGPaths() {
const player = Isaac.GetPlayer();
const character = player.GetPlayerType();
if (character === isaac_typescript_definitions_1.PlayerType.POSSESSOR) {
error("Failed to get the player PNG paths since they are a possessor.");
}
const namePNGPath = (0, versusScreen_1.getCharacterNamePNGFilePath)(character);
const portraitPNGPath = (0, versusScreen_1.getCharacterPortraitPNGFilePath)(character);
return { namePNGPath, portraitPNGPath };
}
/** Use the boss of the first boss found. */
function getBossPNGPaths(customStage) {
// Prefer the PNG paths specified by the end-user, if any.
const paths = getBossPNGPathsCustom(customStage);
if (paths !== undefined) {
return paths;
}
// If this is not a vanilla boss, default to showing question marks.
const bosses = (0, bosses_1.getBosses)();
const firstBoss = bosses[0];
const bossID = firstBoss === undefined ? 0 : firstBoss.GetBossID();
if (bossID === 0) {
const questionMarkPath = (0, versusScreen_1.getBossNamePNGFilePath)(isaac_typescript_definitions_1.BossID.BLUE_BABY);
const namePNGPath = questionMarkPath;
const portraitPNGPath = questionMarkPath;
return { namePNGPath, portraitPNGPath };
}
// If this is a vanilla boss, it will have a boss ID, and we can use the corresponding vanilla
// files.
const namePNGPath = (0, versusScreen_1.getBossNamePNGFilePath)(bossID);
const portraitPNGPath = (0, versusScreen_1.getBossPortraitPNGFilePath)(bossID);
return { namePNGPath, portraitPNGPath };
}
function getBossPNGPathsCustom(customStage) {
if (customStage.bossPool === undefined) {
return undefined;
}
const roomSubType = (0, roomData_1.getRoomSubType)();
const matchingBossEntry = customStage.bossPool.find((bossEntry) => bossEntry.subType === roomSubType);
if (matchingBossEntry === undefined) {
return undefined;
}
return matchingBossEntry.versusScreen;
}
function finishVersusScreenAnimation(pause, disableAllSound) {
const hud = cachedClasses_1.game.GetHUD();
v_1.v.run.showingBossVersusScreen = false;
pause.unpause();
hud.SetVisible(true);
disableAllSound.enableAllSound(constants_1.CUSTOM_STAGE_FEATURE_NAME);
// The sound effect only plays once the versus cutscene is over.
cachedClasses_1.sfxManager.Play(isaac_typescript_definitions_1.SoundEffect.CASTLE_PORTCULLIS);
}
// ModCallback.POST_RENDER (2)
function versusScreenPostRender(pause, disableAllSound) {
if (!v_1.v.run.showingBossVersusScreen) {
return;
}
// We do not want to early return when the game is paused because we need to start displaying the
// black screen as soon as the slide animation starts.
if (versusScreenSprite.IsFinished(VERSUS_SCREEN_ANIMATION_NAME)) {
finishVersusScreenAnimation(pause, disableAllSound);
return;
}
const position = (0, ui_1.getScreenCenterPos)();
// First, we render the background.
versusScreenBackgroundSprite.RenderLayer(VersusScreenLayer.BACKGROUND, position);
versusScreenBackgroundSprite.Update();
// Second, we render the overlay.
versusScreenSprite.RenderLayer(VersusScreenLayer.OVERLAY, position);
// Third, we render the dirt.
versusScreenDirtSpotSprite.RenderLayer(VersusScreenLayer.BOSS_SPOT, position);
versusScreenDirtSpotSprite.RenderLayer(VersusScreenLayer.PLAYER_SPOT, position);
versusScreenDirtSpotSprite.Update();
// Lastly, we render everything else.
for (const layerID of OTHER_ANM2_LAYERS) {
versusScreenSprite.RenderLayer(layerID, position);
}
versusScreenSprite.Update();
}