diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp new file mode 100644 index 0000000000..1f634a8425 --- /dev/null +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp @@ -0,0 +1,102 @@ +#include + +#include + +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include +#include +#include "src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.h" +#include "src/overlays/actors/ovl_En_Zl3/z_en_zl3.h" +extern PlayState* gPlayState; +void BossGanon2_SetObjectSegment(BossGanon2* ganon, PlayState* play, s32 objectId, u8 setRSPSegment); +} + +// Ganon's intro state where he knocks the sword out of Link's hand +constexpr s16 GANON_KNOCK_SWORD_STATE = 23; +// The timer is not reset going into that state. It has counted since Ganon landed and started flailing. +constexpr u32 GANON_KNOCK_SWORD_TIMER = 215; + +#define CVAR_NAME CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro") +#define CVAR_VALUE CVarGetInteger(CVAR_NAME, IS_RANDO) + +void RegisterGanonIntros() { + COND_HOOK(OnSceneInit, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, [](int16_t sceneNum) { + if (sceneNum == SCENE_GANONDORF_BOSS) { + Flags_SetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE); + } + }); + + COND_VB_SHOULD(VB_PLAY_GANONDORF_INTRO_CS, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, { *should = false; }); + + // Skip Ganon rising from the rubble, transforming and landing. Skip to knocking sword away. + COND_VB_SHOULD(VB_PLAY_GANON_INTRO_CS, CVAR_VALUE, { + BossGanon2* ganon = va_arg(args, BossGanon2*); + EnZl3* zelda = va_arg(args, EnZl3*); + Player* player = GET_PLAYER(gPlayState); + + // The rubble watches unk_314: pieces fly off when Ganondorf rises (1), + // then get removed or become breakable rubble once he floats (2). + // Run their update once for each, with Ganondorf where he bursts out, so they push away from him. + ganon->actor.world.pos.x = ganon->actor.world.pos.z = -200.0f; + for (u8 mode : { 1, 2 }) { + ganon->unk_314 = mode; + for (Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_PROP].head; actor != NULL; + actor = actor->next) { + if (actor->id == ACTOR_DEMO_GJ && actor->update != NULL) { + actor->update(actor, gPlayState); + } + } + } + // Set when he finishes transforming. Stops the smoke. + ganon->unk_314 = 3; + + // Swap Ganondorf's skeleton for Ganon's + BossGanon2_SetObjectSegment(ganon, gPlayState, OBJECT_GANON2, false); + SkelAnime_Free(&ganon->skelAnime, gPlayState); + SkelAnime_InitFlex(gPlayState, &ganon->skelAnime, (FlexSkeletonHeader*)gGanonSkel, NULL, NULL, NULL, 0); + BossGanon2_SetObjectSegment(ganon, gPlayState, OBJECT_GANON_ANIME3, false); + Animation_PlayOnce(&ganon->skelAnime, (AnimationHeader*)gGanonUncurlAndFlailAnim); + ganon->skelAnime.curFrame = std::min((float)GANON_KNOCK_SWORD_TIMER, ganon->skelAnime.endFrame); + + ganon->actor.world.pos.x = 50.0f; + ganon->actor.world.pos.y = 1099.0f; + ganon->actor.world.pos.z = -200.0f; + ganon->actor.world.rot.y = 0x4000; + ganon->actor.velocity.y = 0.0f; + ganon->actor.shape.yOffset = 7000.0f; + + // Values the skipped states leave behind + ganon->unk_337 = 2; + ganon->unk_336 = 2; + ganon->unk_324 = 255.0f; + ganon->unk_228 = 1.0f; + ganon->unk_224 = 0.0f; + ganon->unk_30C = 0.0f; + ganon->unk_394 = 0.0f; + gPlayState->envCtx.unk_D8 = 0.0f; + gPlayState->envCtx.unk_BE = gPlayState->envCtx.unk_BD = 0; + + player->actor.world.pos.x = 250.0f; + player->actor.world.pos.y = 1086.0f; + player->actor.world.pos.z = -266.0f; + player->actor.shape.rot.y = -0x4000; + Player_SetCsActionWithHaltedActors(gPlayState, &ganon->actor, 0x55); + + zelda->actor.world.pos.x = 724.0f; + zelda->actor.world.pos.y = 1086.0f; + zelda->actor.world.pos.z = -186.0f; + zelda->actor.shape.rot.y = -0x5000; + zelda->unk_3C8 = 5; + + Audio_QueueSeqCmd(SEQ_PLAYER_BGM_MAIN << 24 | NA_BGM_GANON_BOSS); + + ganon->csState = GANON_KNOCK_SWORD_STATE; + ganon->csTimer = GANON_KNOCK_SWORD_TIMER; + *should = false; + }); +} + +static RegisterShipInitFunc initFunc(RegisterGanonIntros, { CVAR_NAME, "IS_RANDO", "IS_BOSS_RUSH" }); diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/QuickBossDeaths.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/QuickBossDeaths.cpp index 9c92d220ce..afaf001c5a 100644 --- a/soh/soh/Enhancements/TimeSavers/SkipCutscene/QuickBossDeaths.cpp +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/QuickBossDeaths.cpp @@ -111,6 +111,9 @@ void RegisterQuickBossDeaths() { } }); + // Skip Ganondorf dying, go to the window shattering before the escape + COND_VB_SHOULD(VB_GANONDORF_DEATH_SCENE, CVAR_VALUE || IS_BOSS_RUSH, { *should = false; }); + COND_VB_SHOULD(VB_VOLVAGIA_DEATH_SCENE, CVAR_VALUE, { if (*should) { BossFd2* fd2 = va_arg(args, BossFd2*); diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipGanonsTowerCollapse.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipGanonsTowerCollapse.cpp new file mode 100644 index 0000000000..ec818241bc --- /dev/null +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipGanonsTowerCollapse.cpp @@ -0,0 +1,32 @@ +#include + +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "functions.h" +#include "variables.h" +} + +#define CVAR_NAME CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.Story") +#define CVAR_VALUE CVarGetInteger(CVAR_NAME, IS_RANDO) + +void RegisterSkipGanonsTowerCollapse() { + // Zelda coming down in her crystal after Ganondorf dies + COND_VB_SHOULD(VB_PLAY_ZELDA_CRYSTAL_CS, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, { *should = false; }); + + // The tower falling after the escape, same as what Skip Tower Escape does on arrival + COND_VB_SHOULD(VB_PLAY_TRANSITION_CS, CVAR_VALUE, { + if (gEntranceTable[gSaveContext.entranceIndex].scene == SCENE_GANON_BOSS && + !Flags_GetEventChkInf(EVENTCHKINF_WATCHED_GANONS_CASTLE_COLLAPSE_CAUGHT_BY_GERUDO)) { + Flags_SetEventChkInf(EVENTCHKINF_WATCHED_GANONS_CASTLE_COLLAPSE_CAUGHT_BY_GERUDO); + gSaveContext.entranceIndex = ENTR_GANON_BOSS_0; + *should = false; + } + }); + + // Zelda talking once the tower has fallen + COND_VB_SHOULD(VB_PLAY_ESCAPED_TOWER_CS, CVAR_VALUE || IS_RANDO || IS_BOSS_RUSH, { *should = false; }); +} + +static RegisterShipInitFunc initFunc(RegisterSkipGanonsTowerCollapse, { CVAR_NAME, "IS_RANDO", "IS_BOSS_RUSH" }); diff --git a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp index 6167206bcd..010c409d93 100644 --- a/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp +++ b/soh/soh/Enhancements/TimeSavers/timesaver_hook_handlers.cpp @@ -1225,13 +1225,6 @@ void TimeSaverOnSceneInitHandler(int16_t sceneNum) { } } break; - case SCENE_GANONDORF_BOSS: - if (CVarGetInteger(CVAR_ENHANCEMENT("TimeSavers.SkipCutscene.BossIntro"), IS_RANDO)) { - if (!Flags_GetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE)) { - Flags_SetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE); - } - } - break; } } diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index cfaef048a9..41f34ca4e8 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -984,6 +984,14 @@ typedef enum { // - None VB_FROGS_OCARINA_GAME_TIMER_TICK, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*BossGanon` + VB_GANONDORF_DEATH_SCENE, + // #### `result` // ```c // true @@ -2177,6 +2185,14 @@ typedef enum { // - `void*` (cutscene) VB_PLAY_ENTRANCE_CS, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*BossGanon2` + VB_PLAY_ESCAPED_TOWER_CS, + // #### `result` // ```c // true @@ -2212,6 +2228,23 @@ typedef enum { // - `*EnFr` VB_PLAY_FROG_OCARINA_GAME, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*BossGanon` + VB_PLAY_GANONDORF_INTRO_CS, + + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*BossGanon2` + // - `*EnZl3` + VB_PLAY_GANON_INTRO_CS, + // #### `result` // ```c // true @@ -2435,6 +2468,14 @@ typedef enum { // - `*DemoIm` VB_PLAY_ZELDAS_LULLABY_CS, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*BossGanon` + VB_PLAY_ZELDA_CRYSTAL_CS, + // #### `result` // ```c // true diff --git a/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c b/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c index 11146cf69f..6311e0bdbf 100644 --- a/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c +++ b/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c @@ -585,7 +585,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { Play_ChangeCameraStatus(play, this->csCamIndex, CAM_STAT_ACTIVE); this->csCamFov = 60.0f; - if (Flags_GetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE) || IS_RANDO || IS_BOSS_RUSH) { + if (Flags_GetEventChkInf(EVENTCHKINF_BEGAN_GANONDORF_BATTLE)) { // watched cutscene already, skip most of it this->csState = 17; this->csTimer = 0; @@ -918,7 +918,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { this->csTimer = 0; this->csCamFov = 60.0f; BossGanon_SetIntroCsCamera(this, 12); - if (!IS_RANDO && !IS_BOSS_RUSH) { + if (GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this)) { Message_StartTextbox(play, 0x70CB, NULL); } } @@ -942,7 +942,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { this->csState = 19; this->csTimer = 0; - if (!IS_BOSS_RUSH) { + if (GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this)) { Message_StartTextbox(play, 0x70CC, NULL); } Animation_MorphToPlayOnce(&this->skelAnime, &gGanondorfRaiseHandStartAnim, -5.0f); @@ -985,8 +985,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { } if ((this->csTimer > 80) && (Message_GetState(&play->msgCtx) == TEXT_STATE_NONE)) { - // In rando, skip past dark waves section straight to title card phase of the cutscene. - if (IS_RANDO || IS_BOSS_RUSH) { + if (!GameInteractor_Should(VB_PLAY_GANONDORF_INTRO_CS, true, this)) { this->timers[2] = 30; this->csCamAt.x = this->unk_1FC.x - 10.0f; this->csCamAt.y = this->unk_1FC.y + 30.0f; @@ -1293,10 +1292,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) { this->actor.shape.yOffset = -7000.0f; this->actor.shape.rot.y = 0; - // Skip Ganondorf dying and go straight to next scene. - // The cutscene skip met a mixed reaction, so until we figure out a better way of doing it, - // it will stay not-skipped outside of Boss Rush (originally implemented for randomizer). - if (!IS_BOSS_RUSH) { + if (GameInteractor_Should(VB_GANONDORF_DEATH_SCENE, true, this)) { this->csState = 1; this->csTimer = 0; } else { @@ -1570,7 +1566,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) { sZelda = (EnZl3*)Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_ZL3, 0.0f, 6000.0f, 0.0f, 0, 0, 0, 0x2000); - if (!IS_RANDO && !IS_BOSS_RUSH) { + if (GameInteractor_Should(VB_PLAY_ZELDA_CRYSTAL_CS, true, this)) { this->csState = 101; } else { this->skelAnime.playSpeed = 1.0f; @@ -1697,8 +1693,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) { } // fallthrough case 104: - // In rando, fade out the white here as the earlier part is skipped. - if (IS_RANDO || IS_BOSS_RUSH) { + if (!GameInteractor_Should(VB_PLAY_ZELDA_CRYSTAL_CS, true, this)) { Math_ApproachZeroF(&this->whiteFillAlpha, 1.0f, 10.0f); } @@ -1719,8 +1714,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) { } if (this->csTimer == 50) { - // In rando, skip the rest of the cutscene after the crystal around Zelda dissapears. - if (!IS_RANDO && !IS_BOSS_RUSH) { + if (GameInteractor_Should(VB_PLAY_ZELDA_CRYSTAL_CS, true, this)) { sZelda->unk_3C8 = 4; } else { this->csState = 108; diff --git a/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c b/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c index f6ae2d062c..fee03b7555 100644 --- a/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c +++ b/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c @@ -259,8 +259,7 @@ void func_808FD5F4(BossGanon2* this, PlayState* play) { sZelda->actor.world.pos.z = -214.0f; sZelda->actor.shape.rot.y = -0x7000; - // In rando, skip past the cutscene to the part where the player takes control again. - if (!IS_RANDO && !IS_BOSS_RUSH) { + if (GameInteractor_Should(VB_PLAY_ESCAPED_TOWER_CS, true, this)) { this->csState = 1; this->csTimer = 0; } else { @@ -397,6 +396,9 @@ void func_808FD5F4(BossGanon2* this, PlayState* play) { this->subCamId = Play_CreateSubCamera(play); Play_ChangeCameraStatus(play, CAM_ID_MAIN, CAM_STAT_WAIT); Play_ChangeCameraStatus(play, this->subCamId, CAM_STAT_ACTIVE); + if (!GameInteractor_Should(VB_PLAY_GANON_INTRO_CS, true, this, sZelda)) { + break; + } } else { break; }