diff --git a/soh/soh/Enhancements/RemoteBombchu.cpp b/soh/soh/Enhancements/RemoteBombchu.cpp new file mode 100644 index 000000000..1813db23d --- /dev/null +++ b/soh/soh/Enhancements/RemoteBombchu.cpp @@ -0,0 +1,206 @@ +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "src/overlays/actors/ovl_En_Bom_Chu/z_en_bom_chu.h" + +void EnBomChu_Move(EnBomChu*, PlayState*); +void EnBomChu_Explode(EnBomChu*, PlayState*); +s32 Camera_BGCheck(Camera* camera, Vec3f* from, Vec3f* to); +} + +#define CVAR_REMOTE_BOMBCHU_NAME CVAR_ENHANCEMENT("RemoteBombchu") +#define CVAR_REMOTE_BOMBCHU_DEFAULT 0 +#define CVAR_REMOTE_BOMBCHU_VALUE CVarGetInteger(CVAR_REMOTE_BOMBCHU_NAME, CVAR_REMOTE_BOMBCHU_DEFAULT) + +// Camera constants +#define CAM_DIST 200.0f +#define CAM_HEIGHT 50.0f +#define CAM_OFFSET_UP 15.0f + +// Control constants +#define TURN_RATE 1200.0f +#define STICK_THRESHOLD 10.0f + +typedef struct { + EnBomChu* activeChu; + s16 subCamId; + bool isActive; + Vec3f cameraEye; + Vec3f cameraAt; +} RemoteBombchuState; + +static RemoteBombchuState sState = { nullptr, SUBCAM_NONE, false, { 0.0f, 0.0f, 0.0f }, { 0.0f, 0.0f, 0.0f } }; + +static void RotateVectorAroundAxis(Vec3f* vec, Vec3f* axis, f32 angle) { + Matrix_RotateAxis(angle, axis, MTXMODE_NEW); + Vec3f result; + Matrix_MultVec3f(vec, &result); + *vec = result; +} + +static void StartControl(PlayState* play) { + if (sState.isActive) + return; + + sState.subCamId = Play_CreateSubCamera(play); + if (sState.subCamId == SUBCAM_NONE) + return; + + Play_ChangeCameraStatus(play, MAIN_CAM, CAM_STAT_WAIT); + Play_ChangeCameraStatus(play, sState.subCamId, CAM_STAT_ACTIVE); + + // Initialize camera vectors from main camera for smooth transition + Camera* mainCam = Play_GetCamera(play, MAIN_CAM); + sState.cameraEye = mainCam->eye; + sState.cameraAt = mainCam->at; + + Player* player = GET_PLAYER(play); + player->stateFlags1 |= PLAYER_STATE1_INPUT_DISABLED; + + // Reset first-person/item camera state + player->unk_6AD = 0; + + sState.isActive = true; +} + +static void StopControl(PlayState* play) { + if (!sState.isActive) + return; + + if (sState.subCamId != SUBCAM_NONE) { + Play_ChangeCameraStatus(play, MAIN_CAM, CAM_STAT_ACTIVE); + Play_ClearCamera(play, sState.subCamId); + sState.subCamId = SUBCAM_NONE; + } + + Player* player = GET_PLAYER(play); + player->stateFlags1 &= ~PLAYER_STATE1_INPUT_DISABLED; + + sState.activeChu = nullptr; + sState.isActive = false; +} + +// UpdateCamera function +static void UpdateCamera(EnBomChu* chu, PlayState* play) { + Vec3f targetEye, targetAt; + + // Calculate target "At" point (looking at the bombchu, slightly above) + targetAt.x = chu->actor.world.pos.x + chu->axisUp.x * CAM_OFFSET_UP; + targetAt.y = chu->actor.world.pos.y + chu->axisUp.y * CAM_OFFSET_UP; + targetAt.z = chu->actor.world.pos.z + chu->axisUp.z * CAM_OFFSET_UP; + + // Calculate target "Eye" position (behind and above the bombchu) + targetEye.x = targetAt.x - chu->axisForwards.x * CAM_DIST + chu->axisUp.x * CAM_HEIGHT; + targetEye.y = targetAt.y - chu->axisForwards.y * CAM_DIST + chu->axisUp.y * CAM_HEIGHT; + targetEye.z = targetAt.z - chu->axisForwards.z * CAM_DIST + chu->axisUp.z * CAM_HEIGHT; + + // Smoothly approach "At" position + Math_ApproachF(&sState.cameraAt.x, targetAt.x, 0.2f, 30.0f); + Math_ApproachF(&sState.cameraAt.y, targetAt.y, 0.2f, 30.0f); + Math_ApproachF(&sState.cameraAt.z, targetAt.z, 0.2f, 30.0f); + + Camera* camera = Play_GetCamera(play, sState.subCamId); + + // Check if the ideal target eye position collides with walls + Vec3f safeEye = targetEye; + Camera_BGCheck(camera, &sState.cameraAt, &safeEye); + + // Smoothly approach the *safe* eye position + Math_ApproachF(&sState.cameraEye.x, safeEye.x, 0.2f, 50.0f); + Math_ApproachF(&sState.cameraEye.y, safeEye.y, 0.2f, 50.0f); + Math_ApproachF(&sState.cameraEye.z, safeEye.z, 0.2f, 50.0f); + + Play_CameraSetAtEye(play, sState.subCamId, &sState.cameraAt, &sState.cameraEye); +} + +static void HandleSteering(EnBomChu* chu, Input* input) { + f32 stickX = input->cur.stick_x; + if (fabsf(stickX) <= STICK_THRESHOLD) + return; + + // Calculate turn angle based on stick input + f32 turnAngle = BINANG_TO_RAD((s16)(TURN_RATE * (stickX / 85.0f))); + + // Rotate forward and left vectors around the up axis + RotateVectorAroundAxis(&chu->axisForwards, &chu->axisUp, -turnAngle); + RotateVectorAroundAxis(&chu->axisLeft, &chu->axisUp, -turnAngle); + + // Update actor rotation from the new orientation + MtxF mf; + mf.xx = chu->axisLeft.x; + mf.yx = chu->axisLeft.y; + mf.zx = chu->axisLeft.z; + mf.xy = chu->axisUp.x; + mf.yy = chu->axisUp.y; + mf.zy = chu->axisUp.z; + mf.xz = chu->axisForwards.x; + mf.yz = chu->axisForwards.y; + mf.zz = chu->axisForwards.z; + + Matrix_MtxFToYXZRotS(&mf, &chu->actor.world.rot, 0); + chu->actor.world.rot.x = -chu->actor.world.rot.x; + chu->actor.shape.rot = chu->actor.world.rot; + chu->actor.shape.rot.x = -chu->actor.shape.rot.x; +} + +static void HandleInput(EnBomChu* chu, Input* input, PlayState* play) { + HandleSteering(chu, input); + + if (input->press.button & BTN_B) { + EnBomChu_Explode(chu, play); + } + + if (input->press.button & BTN_A) { + StopControl(play); + } +} + +// Track the most recently spawned bombchu +static void OnActorInit(void* refActor) { + sState.activeChu = (EnBomChu*)refActor; +} + +// Clean up if active bombchu is destroyed +static void OnActorDestroy(void* refActor) { + if (refActor == sState.activeChu) { + if (sState.isActive) { + StopControl(gPlayState); + } + sState.activeChu = nullptr; + } +} + +// Main update logic +static void OnActorUpdate(void* refActor) { + if (refActor != sState.activeChu) + return; + + if (sState.activeChu->actionFunc != EnBomChu_Move) + return; + + // Start control when bombchu begins moving + if (!sState.isActive) { + StartControl(gPlayState); + } + + // Exit early if control was not established or was stopped + if (!sState.isActive) + return; + + HandleInput(sState.activeChu, &gPlayState->state.input[0], gPlayState); + + // Check again after input handling (user might have pressed A to stop) + if (!sState.isActive) + return; + + UpdateCamera(sState.activeChu, gPlayState); +} + +void RegisterRemoteBombchu() { + COND_ID_HOOK(OnActorInit, ACTOR_EN_BOM_CHU, CVAR_REMOTE_BOMBCHU_VALUE, OnActorInit); + COND_ID_HOOK(OnActorDestroy, ACTOR_EN_BOM_CHU, CVAR_REMOTE_BOMBCHU_VALUE, OnActorDestroy); + COND_ID_HOOK(OnActorUpdate, ACTOR_EN_BOM_CHU, CVAR_REMOTE_BOMBCHU_VALUE, OnActorUpdate); +} + +static RegisterShipInitFunc initFunc(RegisterRemoteBombchu, { CVAR_REMOTE_BOMBCHU_NAME }); diff --git a/soh/soh/SohGui/SohMenuEnhancements.cpp b/soh/soh/SohGui/SohMenuEnhancements.cpp index e913bf8ef..8ae21a2aa 100644 --- a/soh/soh/SohGui/SohMenuEnhancements.cpp +++ b/soh/soh/SohGui/SohMenuEnhancements.cpp @@ -846,6 +846,12 @@ void SohMenu::AddMenuEnhancements() { path.column = SECTION_COLUMN_2; AddWidget(path, "Explosives", WIDGET_SEPARATOR_TEXT); + AddWidget(path, "Remote Bombchu", WIDGET_CVAR_CHECKBOX) + .CVar(CVAR_ENHANCEMENT("RemoteBombchu")) + .Options(CheckboxOptions().Tooltip("Allows you to control a Bombchu after dropping it.\n" + "Control Stick: Steer\n" + "B: Detonate\n" + "A: Quit Control")); AddWidget(path, "Deku Nuts Explode Bombs", WIDGET_CVAR_CHECKBOX) .CVar(CVAR_ENHANCEMENT("NutsExplodeBombs")) .Options(CheckboxOptions().Tooltip("Make Deku Nuts explode Bombs, similar to how they interact with Bombchus. "