diff options
7 files changed, 190 insertions, 23 deletions
diff --git a/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp b/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp new file mode 100644 index 000000000..1f634a842 --- /dev/null +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/GanonIntros.cpp @@ -0,0 +1,102 @@ +#include <algorithm> + +#include <libultraship/bridge/consolevariablebridge.h> + +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include <objects/object_ganon2/object_ganon2.h> +#include <objects/object_ganon_anime3/object_ganon_anime3.h> +#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 9c92d220c..afaf001c5 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 000000000..ec818241b --- /dev/null +++ b/soh/soh/Enhancements/TimeSavers/SkipCutscene/Story/SkipGanonsTowerCollapse.cpp @@ -0,0 +1,32 @@ +#include <libultraship/bridge/consolevariablebridge.h> + +#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 6167206bc..010c409d9 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 cfaef048a..41f34ca4e 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -989,6 +989,14 @@ typedef enum { // true // ``` // #### `args` + // - `*BossGanon` + VB_GANONDORF_DEATH_SCENE, + + // #### `result` + // ```c + // true + // ``` + // #### `args` // - None VB_GANON_HEAL_BEFORE_FIGHT, @@ -2182,6 +2190,14 @@ typedef enum { // true // ``` // #### `args` + // - `*BossGanon2` + VB_PLAY_ESCAPED_TOWER_CS, + + // #### `result` + // ```c + // true + // ``` + // #### `args` // ##### When called from `z_en_ds.c` // - `*EnDs` // ##### When called from `z_en_mk.c` @@ -2217,6 +2233,23 @@ typedef enum { // true // ``` // #### `args` + // - `*BossGanon` + VB_PLAY_GANONDORF_INTRO_CS, + + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*BossGanon2` + // - `*EnZl3` + VB_PLAY_GANON_INTRO_CS, + + // #### `result` + // ```c + // true + // ``` + // #### `args` // - `*EnHeishi2` // - `bool` (clearCamera - true if the code clears a sub-camera, false otherwise) VB_PLAY_GATE_OPENING_OR_CLOSING_CS, @@ -2439,6 +2472,14 @@ typedef enum { // ```c // true // ``` + // #### `args` + // - `*BossGanon` + VB_PLAY_ZELDA_CRYSTAL_CS, + + // #### `result` + // ```c + // true + // ``` // Whether the left stick aims in first person. Off when it moves Link instead. // #### `args` // - `*Player` 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 11146cf69..6311e0bdb 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 f6ae2d062..fee03b755 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; } |
