From d6692653c9f66fb21a012fe23b438b3a2b4367b9 Mon Sep 17 00:00:00 2001 From: Unreference <87878910+unreference@users.noreply.github.com> Date: Thu, 14 May 2026 11:42:44 -0700 Subject: [PATCH 1/5] feat(enhancement): Richard's Rage dog carry and throw system --- soh/soh/SohGui/SohMenuEnhancements.cpp | 3 + soh/soh/config/ConfigUpdaters.cpp | 1 + soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c | 115 +++++++++++++++++- .../actors/ovl_player_actor/z_player.c | 54 ++++++-- 4 files changed, 160 insertions(+), 13 deletions(-) diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp index fc4f9f95c80..c86c6fe4d98 100644 --- a/soh/soh/SohGui/SohMenuEnhancements.cpp +++ b/soh/soh/SohGui/SohMenuEnhancements.cpp @@ -1635,6 +1635,9 @@ void SohMenu::AddMenuEnhancements() { AddWidget(path, "Dogs Follow You Everywhere", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("DogFollowsEverywhere")) .Options(CheckboxOptions().Tooltip("Allows dogs to follow you anywhere you go, even if you leave the Market.")); + AddWidget(path, "Richard's Rage", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("RichardsRage")) + .Options(CheckboxOptions().Tooltip("Allows Link to carry and throw dogs around like Ruto.")); AddWidget(path, "Rupee Dash Mode", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("RupeeDash")) .Options(CheckboxOptions().Tooltip("Rupees reduce over time, Link suffers damage when the count hits 0.")); diff --git a/soh/soh/config/ConfigUpdaters.cpp b/soh/soh/config/ConfigUpdaters.cpp index 8249bb48d31..da99609034d 100644 --- a/soh/soh/config/ConfigUpdaters.cpp +++ b/soh/soh/config/ConfigUpdaters.cpp @@ -181,6 +181,7 @@ static const Migration version3Migrations[] = { { "gDisableNaviCallAudio", "gAudioEditor.DisableNaviCallAudio" }, { "gDisableTunicWarningText", "gEnhancements.DisableTunicWarningText" }, { "gDogFollowsEverywhere", "gEnhancements.DogFollowsEverywhere" }, + { "gRichardsRage", "gEnhancements.RichardsRage" }, { "gDpadNoDropOcarinaInput", "gEnhancements.DpadNoDropOcarinaInput" }, { "gDrawLineupTick", "gEnhancements.DrawLineupTick" }, { "gDynamicWalletIcon", "gEnhancements.DynamicWalletIcon" }, diff --git a/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c b/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c index c8cb4bd971d..51e95b9786e 100644 --- a/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c +++ b/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c @@ -22,6 +22,11 @@ void EnDog_RunAway(EnDog* this, PlayState* play); void EnDog_FaceLink(EnDog* this, PlayState* play); void EnDog_Wait(EnDog* this, PlayState* play); +// #region SOH [Enhancement] - Richard's Rage +void EnDog_Carried(EnDog* this, PlayState* play); +void EnDog_Thrown(EnDog* this, PlayState* play); +// #endregion + const ActorInit En_Dog_InitVars = { ACTOR_EN_DOG, ACTORCAT_NPC, @@ -478,17 +483,117 @@ void EnDog_Wait(EnDog* this, PlayState* play) { } } +// #region SOH [Enhancement] - Richard's Rage +void EnDog_Carried(EnDog* this, PlayState* play) { + // Link dropped or threw the Richard. + if (Actor_HasNoParent(&this->actor, play)) { + this->actor.gravity = -1.0f; + this->actor.minVelocityY = -10.0f; + this->actor.bgCheckFlags = 0; // Clear stale flags from before carry + + // Enable AT collider so the thrown Richard can hit enemies. + this->collider.base.atFlags = AT_ON | AT_TYPE_PLAYER; + this->collider.info.toucher.dmgFlags = DMG_HAMMER; + this->collider.info.toucher.damage = 4; + this->collider.info.toucherFlags = TOUCH_ON; + + // Clear carry flag -- Richard is no longer held. + gSaveContext.dogParams &= ~0x4000; + + this->nextBehavior = DOG_BARK; + this->actionFunc = EnDog_Thrown; + return; + } + + // Signal carry state for scene transitions. + gSaveContext.dogParams |= 0x4000; + + // While carried, just sit still. + this->nextBehavior = DOG_SIT; + this->actor.speedXZ = 0.0f; + this->actor.shape.shadowAlpha = 0; +} + +void EnDog_Thrown(EnDog* this, PlayState* play) { + // Megaton Hammer impact effects when the Richard hits something mid-flight. + if (this->collider.base.atFlags & AT_HIT) { + this->collider.base.atFlags &= ~AT_HIT; + + const s16 quakeIndex = Quake_Add(Play_GetCamera(play, 0), 3); + Quake_SetSpeed(quakeIndex, 27767); + Quake_SetQuakeValues(quakeIndex, 7, 0, 0, 0); + Quake_SetCountdown(quakeIndex, 20); + + play->actorCtx.unk_02 = 4; + func_800AA000(0, 255, 20, 150); + Audio_PlayActorSound2(&this->actor, NA_SE_IT_HAMMER_HIT); + } + + // Landed on the ground. + if (this->actor.bgCheckFlags & 1) { + // Disable AT collider. + this->collider.base.atFlags = AT_NONE; + this->collider.info.toucherFlags = TOUCH_NONE; + + this->actor.speedXZ = 0.0f; + this->actor.velocity.y = 0.0f; + this->nextBehavior = DOG_BARK; + this->actor.shape.shadowAlpha = 255; + + this->actionFunc = EnDog_FollowPlayer; + Audio_PlayActorSound2(&this->actor, NA_SE_EV_SMALL_DOG_BARK); + } +} + void EnDog_Update(Actor* thisx, PlayState* play) { EnDog* this = (EnDog*)thisx; s32 pad; + // #region SOH [Enhancement] - Richard's Rage + // Offer carry and detect pickup. + if (CVarGetInteger(CVAR_ENHANCEMENT("RichardsRage"), 0)) { + if (Actor_HasParent(&this->actor, play)) { + // Link just picked up the Richard. Set dogParams so it respawns in the next scene. + if (gSaveContext.dogParams == 0) { + gSaveContext.dogParams = this->actor.params & 0x7FFF; + } + + this->nextBehavior = DOG_SIT; + this->actor.speedXZ = 0.0f; + this->actionFunc = EnDog_Carried; + } else if (this->actionFunc != EnDog_Carried && this->actionFunc != EnDog_Thrown) { + // Offer carry when not already held or airborne. + Actor_OfferCarry(&this->actor, play); + } + } + // #endregion + EnDog_PlayAnimAndSFX(this); SkelAnime_Update(&this->skelAnime); - Actor_UpdateBgCheckInfo(play, &this->actor, this->collider.dim.radius, this->collider.dim.height * 0.5f, 0.0f, 5); - Actor_MoveXZGravity(&this->actor); + + // #region SOH [Enhancement] - Richard's Rage + // Skip physics and collision while the Richard is being carried -- Link controls the Richard's position. + // Without this, the Richard's OC collider pushes Link around and movement code fights the carry position. + if (this->actionFunc != EnDog_Carried) { + Actor_UpdateBgCheckInfo(play, &this->actor, this->collider.dim.radius, + this->collider.dim.height * 0.5f, 0.0f, 5); + Actor_MoveXZGravity(&this->actor); + } + // #endregion + this->actionFunc(this, play); - Collider_UpdateCylinder(&this->actor, &this->collider); - CollisionCheck_SetOC(play, &play->colChkCtx, &this->collider.base); + + // #region SOH [Enhancement] - Richard's Rage + if (this->actionFunc != EnDog_Carried) { + Collider_UpdateCylinder(&this->actor, &this->collider); + CollisionCheck_SetOC(play, &play->colChkCtx, &this->collider.base); + } + + // Register AT collider while the Richard is airborne so it can hit enemies. + if (this->actionFunc == EnDog_Thrown) { + CollisionCheck_SetAT(play, &play->colChkCtx, &this->collider.base); + } + // #endregion } s32 EnDog_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f* pos, Vec3s* rot, void* thisx) { @@ -520,4 +625,4 @@ void EnDog_Draw(Actor* thisx, PlayState* play) { SkelAnime_DrawSkeletonOpa(play, &this->skelAnime, EnDog_OverrideLimbDraw, EnDog_PostLimbDraw, this); CLOSE_DISPS(play->state.gfxCtx); -} +} \ No newline at end of file diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 7ffe8af7f43..fc04f641d2a 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -3161,6 +3161,38 @@ s32 func_80835644(PlayState* play, Player* this, Actor* arg2) { return 0; } +// #region SOH [Enhancement] - Richard's Rage +// Returns true if the held actor should be treated like Ruto for carry interactions (door opening, fall sounds, +// action text). +static s32 Player_HeldActorAllowsInteraction(Player* this) { + if (this->heldActor == NULL) { + return false; + } + + if (this->heldActor->id == ACTOR_EN_RU1) { + return true; + } + + if (this->heldActor->id == ACTOR_EN_DOG && CVarGetInteger(CVAR_ENHANCEMENT("RichardsRage"), 0)) { + return true; + } + + return false; +} + +static void Player_HeldActorFallSound(Player* this) { + if (this->heldActor == NULL) { + return; + } + + if (this->heldActor->id == ACTOR_EN_RU1) { + Audio_PlayActorSound2(this->heldActor, NA_SE_VO_RT_FALL); + } else if (this->heldActor->id == ACTOR_EN_DOG && CVarGetInteger(CVAR_ENHANCEMENT("RichardsRage"), 0)) { + Audio_PlayActorSound2(this->heldActor, NA_SE_EV_SMALL_DOG_BARK); + } +} +// #endregion + void func_80835688(Player* this, PlayState* play) { if (!func_80835644(play, this, this->heldActor)) { Player_SetUpperActionFunc(this, Player_UpperAction_CarryActor); @@ -5333,8 +5365,7 @@ s32 Player_ActionHandler_1(Player* this, PlayState* play) { Vec3f checkPos; if ((this->doorType != PLAYER_DOORTYPE_NONE) && - (!(this->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) || - ((this->heldActor != NULL) && (this->heldActor->id == ACTOR_EN_RU1)))) { + (!(this->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) || Player_HeldActorAllowsInteraction(this))) { if ((CHECK_BTN_ALL(sControlInput->press.button, BTN_A) || (Player_Action_8084F9A0 == this->actionFunc)) && GameInteractor_Should(VB_BE_ABLE_TO_OPEN_DOORS, true)) { doorActor = this->doorActor; @@ -9574,10 +9605,7 @@ void Player_Action_80843CEC(Player* this, PlayState* play) { void func_80843E14(Player* this, u16 sfxId) { Player_PlayVoiceSfx(this, sfxId); - - if ((this->heldActor != NULL) && (this->heldActor->id == ACTOR_EN_RU1)) { - Audio_PlayActorSound2(this->heldActor, NA_SE_VO_RT_FALL); - } + Player_HeldActorFallSound(this); } static FallImpactInfo D_80854600[] = { @@ -11016,8 +11044,7 @@ void Player_UpdateInterface(PlayState* play, Player* this) { } } else if ((Player_Action_8084E3C4 != this->actionFunc) && !(this->stateFlags2 & PLAYER_STATE2_CRAWLING)) { if ((this->doorType != PLAYER_DOORTYPE_NONE) && - (!(this->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) || - ((heldActor != NULL) && (heldActor->id == ACTOR_EN_RU1)))) { + (!(this->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) || Player_HeldActorAllowsInteraction(this))) { doAction = DO_ACTION_OPEN; } else if ((!(this->stateFlags1 & PLAYER_STATE1_CARRYING_ACTOR) || (heldActor == NULL)) && (interactRangeActor != NULL) && @@ -12288,6 +12315,17 @@ void Player_Update(Actor* thisx, PlayState* play) { if (dog != NULL) { // Room -1 allows actor to cross between rooms, similar to Navi dog->room = CVarGetInteger(CVAR_ENHANCEMENT("DogFollowsEverywhere"), 0) ? -1 : 0; + + // #region SOH [Enhancement] - Richard's Rage + if (CVarGetInteger(CVAR_ENHANCEMENT("RichardsRage"), 1) && gSaveContext.dogParams & 0x4000) { + this->heldActor = dog; + this->interactRangeActor = dog; + dog->parent = &this->actor; + this->actor.child = dog; + this->stateFlags1 |= PLAYER_STATE1_CARRYING_ACTOR; + func_80835688(this, play); // Upper body action (carry pose) + gSaveContext.dogParams &= ~0x4000; + } } } } From cdb5ca5d4d33bd54f03fe1c0a93e87d26b680209 Mon Sep 17 00:00:00 2001 From: Unreference <87878910+unreference@users.noreply.github.com> Date: Thu, 14 May 2026 14:14:41 -0700 Subject: [PATCH 2/5] chore: Comment describing func_800AA000 --- soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c b/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c index 51e95b9786e..9fb6ee2557d 100644 --- a/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c +++ b/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c @@ -525,7 +525,7 @@ void EnDog_Thrown(EnDog* this, PlayState* play) { Quake_SetCountdown(quakeIndex, 20); play->actorCtx.unk_02 = 4; - func_800AA000(0, 255, 20, 150); + func_800AA000(0, 255, 20, 150); // Screen shake Audio_PlayActorSound2(&this->actor, NA_SE_IT_HAMMER_HIT); } From 898e4b63f7507dea835e54f4ab4a9aadb632e493 Mon Sep 17 00:00:00 2001 From: Unreference <87878910+unreference@users.noreply.github.com> Date: Thu, 14 May 2026 14:22:50 -0700 Subject: [PATCH 3/5] chore: Prefer bool over s32 --- soh/src/overlays/actors/ovl_player_actor/z_player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index fc04f641d2a..890b351f5de 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -3164,7 +3164,7 @@ s32 func_80835644(PlayState* play, Player* this, Actor* arg2) { // #region SOH [Enhancement] - Richard's Rage // Returns true if the held actor should be treated like Ruto for carry interactions (door opening, fall sounds, // action text). -static s32 Player_HeldActorAllowsInteraction(Player* this) { +static bool Player_HeldActorAllowsInteraction(Player* this) { if (this->heldActor == NULL) { return false; } From e1831ce6e58f6ec91f1fbff5ce75d9075fd5be32 Mon Sep 17 00:00:00 2001 From: Unreference <87878910+unreference@users.noreply.github.com> Date: Thu, 14 May 2026 16:32:24 -0700 Subject: [PATCH 4/5] chore: Remove from config updater --- soh/soh/config/ConfigUpdaters.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/soh/soh/config/ConfigUpdaters.cpp b/soh/soh/config/ConfigUpdaters.cpp index da99609034d..8249bb48d31 100644 --- a/soh/soh/config/ConfigUpdaters.cpp +++ b/soh/soh/config/ConfigUpdaters.cpp @@ -181,7 +181,6 @@ static const Migration version3Migrations[] = { { "gDisableNaviCallAudio", "gAudioEditor.DisableNaviCallAudio" }, { "gDisableTunicWarningText", "gEnhancements.DisableTunicWarningText" }, { "gDogFollowsEverywhere", "gEnhancements.DogFollowsEverywhere" }, - { "gRichardsRage", "gEnhancements.RichardsRage" }, { "gDpadNoDropOcarinaInput", "gEnhancements.DpadNoDropOcarinaInput" }, { "gDrawLineupTick", "gEnhancements.DrawLineupTick" }, { "gDynamicWalletIcon", "gEnhancements.DynamicWalletIcon" }, From af7b44b02e67b43d57a4eaa9566c30524d9ee687 Mon Sep 17 00:00:00 2001 From: Unreference <87878910+unreference@users.noreply.github.com> Date: Fri, 15 May 2026 10:02:28 -0700 Subject: [PATCH 5/5] Revert "chore: Prefer bool over s32" This reverts commit 898e4b63f7507dea835e54f4ab4a9aadb632e493. --- soh/src/overlays/actors/ovl_player_actor/z_player.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 890b351f5de..fc04f641d2a 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -3164,7 +3164,7 @@ s32 func_80835644(PlayState* play, Player* this, Actor* arg2) { // #region SOH [Enhancement] - Richard's Rage // Returns true if the held actor should be treated like Ruto for carry interactions (door opening, fall sounds, // action text). -static bool Player_HeldActorAllowsInteraction(Player* this) { +static s32 Player_HeldActorAllowsInteraction(Player* this) { if (this->heldActor == NULL) { return false; }