mirror of
https://github.com/HarbourMasters/Shipwright
synced 2026-10-13 15:33:53 -04:00
Hookify Ganondorf/Ganon skips (#7321)
These were often only applied in rando or boss rush, vanilla would only skip with entire tower escape skipped
This commit is contained in:
@@ -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" });
|
||||
@@ -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*);
|
||||
|
||||
@@ -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" });
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user