summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormckinlee <mckinlee@ymail.com>2026-05-21 14:55:11 -0400
committerGitHub <noreply@github.com>2026-05-21 14:55:11 -0400
commitd14c988635de3760d1461b3ca8ac35c896014032 (patch)
tree5905480b065f23ded8f6a2cf1519d316c77d48e8
parentb754231c3d135e7b3e2f1b18dc0a396317661d1f (diff)
Merge pull request #1681 from mckinlee/extendedAC
[Enhancement] Extended Projectile Interaction Distance
-rw-r--r--mm/2s2h/BenGui/BenMenu.cpp6
-rw-r--r--mm/2s2h/Enhancements/Gameplay/ExtendedProjectileInteractionDistance.cpp399
2 files changed, 405 insertions, 0 deletions
diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp
index 2e6427d15..39910e6e4 100644
--- a/mm/2s2h/BenGui/BenMenu.cpp
+++ b/mm/2s2h/BenGui/BenMenu.cpp
@@ -1137,6 +1137,12 @@ void BenMenu::AddEnhancements() {
.CVar("gEnhancements.Items.ExtraPowderKegs")
.Options(CheckboxOptions().Tooltip(
"Allows carrying up to 3 Powder Kegs at once instead of the vanilla limit of 1."));
+ AddWidget(path, "Extended Projectile Interaction Distance", WIDGET_CVAR_CHECKBOX)
+ .CVar("gEnhancements.Gameplay.ExtendedProjectileInteractionDistance")
+ .Options(CheckboxOptions().Tooltip(
+ "Allows projectiles and explosions to hit breakable objects at a distance matching your "
+ "Increase Actor Draw Distance setting.\n\n"
+ "Does not affect pickup ranges, talk prompts, or physical body collision."));
AddWidget(path, "Curiosity Shop Refills", WIDGET_CVAR_CHECKBOX)
.CVar("gEnhancements.Shops.CuriosityShopRefills")
.Options(CheckboxOptions().Tooltip(
diff --git a/mm/2s2h/Enhancements/Gameplay/ExtendedProjectileInteractionDistance.cpp b/mm/2s2h/Enhancements/Gameplay/ExtendedProjectileInteractionDistance.cpp
new file mode 100644
index 000000000..81f256f89
--- /dev/null
+++ b/mm/2s2h/Enhancements/Gameplay/ExtendedProjectileInteractionDistance.cpp
@@ -0,0 +1,399 @@
+#include "2s2h/ShipInit.hpp"
+
+extern "C" {
+#include "overlays/actors/ovl_Obj_Bombiwa/z_obj_bombiwa.h"
+#include "overlays/actors/ovl_Obj_Hamishi/z_obj_hamishi.h"
+#include "overlays/actors/ovl_Obj_Tsubo/z_obj_tsubo.h"
+#include "overlays/actors/ovl_En_Ishi/z_en_ishi.h"
+#include "overlays/actors/ovl_Obj_Kibako/z_obj_kibako.h"
+#include "overlays/actors/ovl_Obj_Flowerpot/z_obj_flowerpot.h"
+#include "overlays/actors/ovl_Obj_Taru/z_obj_taru.h"
+#include "overlays/actors/ovl_Obj_Snowball2/z_obj_snowball2.h"
+#include "overlays/actors/ovl_En_Kusa/z_en_kusa.h"
+#include "overlays/actors/ovl_En_Kusa2/z_en_kusa2.h"
+#include "overlays/actors/ovl_Obj_Grass/z_obj_grass.h"
+#include "overlays/actors/ovl_Obj_Kibako2/z_obj_kibako2.h"
+#include "overlays/actors/ovl_En_Bom/z_en_bom.h"
+
+void ObjGrass_SpawnFragments(Vec3f* basePos, PlayState* play);
+void ObjGrass_DropCollectible(ObjGrassElement* grassElem, PlayState* play);
+}
+
+#define CVAR_NAME "gEnhancements.Gameplay.ExtendedProjectileInteractionDistance"
+#define CVAR CVarGetInteger(CVAR_NAME, 0)
+
+#define REMOTE_GRASS_COLLIDER_COUNT 16
+
+typedef struct {
+ Vec3f pos;
+ f32 radius;
+} GrassThreat;
+
+static ColliderCylinderInit sGrassCylinderInit = {
+ {
+ COL_MATERIAL_NONE,
+ AT_NONE,
+ AC_ON | AC_TYPE_PLAYER,
+ OC1_ON | OC1_TYPE_PLAYER | OC1_TYPE_2,
+ OC2_TYPE_2,
+ COLSHAPE_CYLINDER,
+ },
+ {
+ ELEM_MATERIAL_UNK0,
+ { 0x00000000, 0x00, 0x00 },
+ { 0x0580C71C, 0x00, 0x00 },
+ ATELEM_NONE | ATELEM_SFX_NORMAL,
+ ACELEM_ON,
+ OCELEM_ON,
+ },
+ { 6, 44, 0, { 0, 0, 0 } },
+};
+
+static std::unordered_map<Actor*, std::vector<ObjGrassCollider>> sRemoteGrassCollidersMap;
+
+static void DestroyRemoteGrassColliders(Actor* actor) {
+ auto it = sRemoteGrassCollidersMap.find(actor);
+ if (it != sRemoteGrassCollidersMap.end()) {
+ if (gPlayState != nullptr) {
+ for (auto& slot : it->second) {
+ Collider_DestroyCylinder(gPlayState, &slot.collider);
+ }
+ }
+ sRemoteGrassCollidersMap.erase(it);
+ }
+}
+
+static void DestroyAllRemoteGrassColliders() {
+ if (gPlayState == nullptr) {
+ sRemoteGrassCollidersMap.clear();
+ return;
+ }
+ for (auto& pair : sRemoteGrassCollidersMap) {
+ for (auto& slot : pair.second) {
+ Collider_DestroyCylinder(gPlayState, &slot.collider);
+ }
+ }
+ sRemoteGrassCollidersMap.clear();
+}
+
+static std::vector<ObjGrassCollider>& GetOrInitRemoteGrassColliders(Actor* actor) {
+ auto& remoteColliders = sRemoteGrassCollidersMap[actor];
+ if (remoteColliders.empty()) {
+ remoteColliders.resize(REMOTE_GRASS_COLLIDER_COUNT);
+ for (auto& slot : remoteColliders) {
+ Collider_InitCylinder(gPlayState, &slot.collider);
+ Collider_SetCylinder(gPlayState, &slot.collider, actor, &sGrassCylinderInit);
+ slot.entity = nullptr;
+ }
+ }
+ return remoteColliders;
+}
+
+static bool IsWithinExtendedRange(Actor* actor, f32 vanillaRange) {
+ if (gPlayState == NULL) {
+ return false;
+ }
+ s32 multiplier = CVarGetInteger("gEnhancements.Graphics.IncreaseActorDrawDistance", 1);
+ if (multiplier <= 1) {
+ return false;
+ }
+ f32 xzDist = actor->xzDistToPlayer;
+ return xzDist >= vanillaRange && xzDist < (vanillaRange * multiplier);
+}
+
+static void RegisterObjGrassNearThreat(ObjGrass* grass, const GrassThreat* threat) {
+ if (gPlayState == nullptr)
+ return;
+
+ auto it = sRemoteGrassCollidersMap.find((Actor*)grass);
+ if (it == sRemoteGrassCollidersMap.end() || it->second.size() != REMOTE_GRASS_COLLIDER_COUNT) {
+ return;
+ }
+ auto& remoteColliders = it->second;
+
+ s32 multiplier = CVarGetInteger("gEnhancements.Graphics.IncreaseActorDrawDistance", 1);
+ f32 vanillaRangeSq = SQ(650.0f);
+ f32 extendedRangeSq = SQ(650.0f * multiplier);
+ f32 threatRadiusSq = SQ(threat->radius);
+
+ Vec3f& playerPos = GET_PLAYER(gPlayState)->actor.world.pos;
+
+ // Find first available slot
+ s32 slotIdx = -1;
+ for (s32 i = 0; i < REMOTE_GRASS_COLLIDER_COUNT; i++) {
+ if (remoteColliders[i].entity == nullptr) {
+ slotIdx = i;
+ break;
+ }
+ }
+ if (slotIdx == -1)
+ return;
+
+ for (s32 i = 0; i < grass->activeGrassGroups; i++) {
+ ObjGrassGroup* group = &grass->grassGroups[i];
+
+ if (Math3D_Vec3fDistSq(&group->homePos, (Vec3f*)&threat->pos) > SQ(threat->radius + 250.0f))
+ continue;
+
+ for (s32 j = 0; j < group->count; j++) {
+ ObjGrassElement* elem = &group->elements[j];
+
+ if (!(elem->flags & OBJ_GRASS_ELEM_REMOVED) && !(elem->flags & OBJ_GRASS_ELEM_ANIM)) {
+ f32 distToThreatSq = Math3D_Vec3fDistSq(&elem->pos, (Vec3f*)&threat->pos);
+
+ if (distToThreatSq < threatRadiusSq) {
+ f32 distToPlayerSq = Math3D_Vec3fDistSq(&elem->pos, &playerPos);
+
+ if (distToPlayerSq >= vanillaRangeSq && distToPlayerSq < extendedRangeSq) {
+ ObjGrassCollider* slot = &remoteColliders[slotIdx];
+
+ slot->collider.dim.pos.x = elem->pos.x;
+ slot->collider.dim.pos.y = elem->pos.y;
+ slot->collider.dim.pos.z = elem->pos.z;
+ slot->entity = elem;
+ elem->flags |= OBJ_GRASS_ELEM_ANIM;
+
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &slot->collider.base);
+
+ // Find next available slot
+ slotIdx = -1;
+ for (s32 k = 0; k < REMOTE_GRASS_COLLIDER_COUNT; k++) {
+ if (remoteColliders[k].entity == nullptr) {
+ slotIdx = k;
+ break;
+ }
+ }
+ if (slotIdx == -1)
+ return;
+ }
+ }
+ }
+ }
+ }
+}
+
+void RegisterExtendedProjectileInteractionDistance() {
+ if (!CVAR) {
+ DestroyAllRemoteGrassColliders();
+ }
+
+ static HOOK_ID remoteGrassDestroyHookId = 0;
+ if (remoteGrassDestroyHookId == 0) {
+ remoteGrassDestroyHookId = GameInteractor::Instance->RegisterGameHookForID<GameInteractor::OnActorDestroy>(
+ ACTOR_OBJ_GRASS, [](Actor* actor) { DestroyRemoteGrassColliders(actor); });
+ }
+
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_BOMBIWA, CVAR, [](Actor* actor, bool* should) {
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 1400.0f)) {
+ *should = true;
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_BOMBIWA, CVAR, [](Actor* actor) {
+ ObjBombiwa* thisx = (ObjBombiwa*)actor;
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 1400.0f)) {
+ if (thisx->unk_202 > 0) {
+ thisx->unk_202--;
+ if (thisx->unk_202 == 0) {
+ thisx->collider.base.colMaterial = COL_MATERIAL_HARD;
+ } else {
+ thisx->collider.base.colMaterial = COL_MATERIAL_NONE;
+ }
+ }
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_HAMISHI, CVAR, [](Actor* actor) {
+ ObjHamishi* thisx = (ObjHamishi*)actor;
+ if (IsWithinExtendedRange(actor, 1000.0f)) {
+ if (thisx->unk_1A0 <= 0) {
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ }
+ });
+
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_TSUBO, CVAR, [](Actor* actor, bool* should) {
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 800.0f)) {
+ *should = true;
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_TSUBO, CVAR, [](Actor* actor) {
+ ObjTsubo* thisx = (ObjTsubo*)actor;
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 800.0f) &&
+ gSaveContext.save.entrance != ENTRANCE(GORON_RACETRACK, 1)) {
+ Collider_UpdateCylinder(&thisx->actor, &thisx->cylinderCollider);
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->cylinderCollider.base);
+ }
+ });
+
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_ISHI, CVAR, [](Actor* actor, bool* should) {
+ EnIshi* thisx = (EnIshi*)actor;
+ if ((actor->draw != NULL || (thisx->flags & ISHI_FLAG_CUTSCENE_ROCK)) && IsWithinExtendedRange(actor, 600.0f)) {
+ *should = true;
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_EN_ISHI, CVAR, [](Actor* actor) {
+ EnIshi* thisx = (EnIshi*)actor;
+ s32 cutsceneRock = thisx->flags & ISHI_FLAG_CUTSCENE_ROCK;
+ if (IsWithinExtendedRange(actor, 600.0f) && !cutsceneRock && actor->draw != NULL) {
+ if (thisx->hitTimer > 0) {
+ thisx->hitTimer--;
+ if (thisx->hitTimer == 0) {
+ thisx->collider.base.colMaterial = COL_MATERIAL_HARD;
+ } else {
+ thisx->collider.base.colMaterial = COL_MATERIAL_NONE;
+ }
+ }
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ });
+
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_KIBAKO, CVAR, [](Actor* actor, bool* should) {
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 800.0f)) {
+ *should = true;
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_KIBAKO, CVAR, [](Actor* actor) {
+ ObjKibako* thisx = (ObjKibako*)actor;
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 800.0f)) {
+ if (!KIBAKO_BOMBER_CAN_HIDE_IN_BOX(actor)) {
+ Collider_UpdateCylinder(&thisx->actor, &thisx->collider);
+ }
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ });
+
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_FLOWERPOT, CVAR, [](Actor* actor, bool* should) {
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 600.0f)) {
+ *should = true;
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_FLOWERPOT, CVAR, [](Actor* actor) {
+ ObjFlowerpot* thisx = (ObjFlowerpot*)actor;
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 600.0f)) {
+ Matrix_SetTranslateRotateYXZ(thisx->actor.world.pos.x, thisx->actor.world.pos.y, thisx->actor.world.pos.z,
+ &thisx->actor.shape.rot);
+ Matrix_Scale(thisx->actor.scale.x, thisx->actor.scale.y, thisx->actor.scale.z, MTXMODE_APPLY);
+ Collider_UpdateSpheres(0, &thisx->collider);
+ Collider_UpdateSpheres(1, &thisx->collider);
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ });
+
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_SNOWBALL2, CVAR, [](Actor* actor, bool* should) {
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 800.0f)) {
+ *should = true;
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_SNOWBALL2, CVAR, [](Actor* actor) {
+ ObjSnowball2* thisx = (ObjSnowball2*)actor;
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 800.0f)) {
+ Matrix_SetTranslateRotateYXZ(thisx->actor.world.pos.x,
+ thisx->actor.world.pos.y + (thisx->actor.shape.yOffset * thisx->actor.scale.y),
+ thisx->actor.world.pos.z, &thisx->actor.shape.rot);
+ Matrix_Scale(thisx->actor.scale.x, thisx->actor.scale.y, thisx->actor.scale.z, MTXMODE_APPLY);
+ Collider_UpdateSpheres(0, &thisx->collider);
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ });
+
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_TARU, CVAR, [](Actor* actor, bool* should) {
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 600.0f)) {
+ *should = true;
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_TARU, CVAR, [](Actor* actor) {
+ ObjTaru* thisx = (ObjTaru*)actor;
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 600.0f)) {
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ });
+
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_KUSA, CVAR, [](Actor* actor, bool* should) {
+ EnKusa* thisx = (EnKusa*)actor;
+ if (actor->draw != NULL && !thisx->isCut && IsWithinExtendedRange(actor, 600.0f)) {
+ *should = true;
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_EN_KUSA, CVAR, [](Actor* actor) {
+ EnKusa* thisx = (EnKusa*)actor;
+ if (actor->draw != NULL && !thisx->isCut && IsWithinExtendedRange(actor, 600.0f)) {
+ Collider_UpdateCylinder(&thisx->actor, &thisx->collider);
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_EN_KUSA2, CVAR, [](Actor* actor) {
+ EnKusa2* thisx = (EnKusa2*)actor;
+ if (ENKUSA2_GET_1(actor) && IsWithinExtendedRange(actor, 600.0f)) {
+ Collider_UpdateCylinder(&thisx->actor, &thisx->collider);
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ });
+
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_OBJ_KIBAKO2, CVAR, [](Actor* actor, bool* should) {
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 600.0f)) {
+ *should = true;
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_KIBAKO2, CVAR, [](Actor* actor) {
+ ObjKibako2* thisx = (ObjKibako2*)actor;
+ if (actor->draw != NULL && IsWithinExtendedRange(actor, 600.0f)) {
+ CollisionCheck_SetAC(gPlayState, &gPlayState->colChkCtx, &thisx->collider.base);
+ }
+ });
+
+ COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_GRASS, CVAR, [](Actor* actor) {
+ if (gPlayState == nullptr)
+ return;
+
+ // Handle remote colliders lifecycle and hit processing
+ auto& remoteColliders = GetOrInitRemoteGrassColliders(actor);
+
+ for (auto& slot : remoteColliders) {
+ if (slot.entity != nullptr && (slot.collider.base.acFlags & AC_HIT)) {
+ ObjGrass_SpawnFragments(&slot.entity->pos, gPlayState);
+ ObjGrass_DropCollectible(slot.entity, gPlayState);
+ slot.entity->flags |= OBJ_GRASS_ELEM_REMOVED;
+ SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &slot.entity->pos, 20, NA_SE_EV_PLANT_BROKEN);
+ }
+ slot.entity = nullptr;
+ slot.collider.base.acFlags &= ~AC_HIT;
+ slot.collider.base.ocFlags1 &= ~OC1_HIT;
+ }
+
+ GrassThreat threats[16];
+ s32 threatCount = 0;
+
+ // Collect Explosions (En_Bom)
+ // These update BEFORE grass, so their positions are current.
+ Actor* explosive = gPlayState->actorCtx.actorLists[ACTORCAT_EXPLOSIVES].first;
+ while (explosive != nullptr && threatCount < 16) {
+ if (explosive->id == ACTOR_EN_BOM && explosive->params == BOMB_TYPE_EXPLOSION) {
+ threats[threatCount].pos = explosive->world.pos;
+ threats[threatCount].radius = 150.0f;
+ threatCount++;
+ }
+ explosive = explosive->next;
+ }
+
+ if (threatCount == 0)
+ return;
+
+ // Process each threat
+ for (s32 i = 0; i < threatCount; i++) {
+ RegisterObjGrassNearThreat((ObjGrass*)actor, &threats[i]);
+ }
+ });
+}
+
+static RegisterShipInitFunc initFunc(RegisterExtendedProjectileInteractionDistance, { CVAR_NAME });