summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPepe20129 <72659707+Pepe20129@users.noreply.github.com>2026-06-09 21:52:25 +0200
committerGitHub <noreply@github.com>2026-06-09 19:52:25 +0000
commite93ea5b9192b0f3b55e5a7b333018e4976676a5c (patch)
tree86ebe7adb6c6088142dadd006fb880d6afd6817a
parentb61db770207664c17fb5c54f20b8547e56fde2dd (diff)
Enemy rando cleanup 3 (#6518)
* Fix bari's biris not respecting the seeded option * Randomize Peehat Larvas * Refactor `IsEnemyAllowedToSpawn` * Fix the issue where some enemies spawn above the ceiling * Partially fix twisted hallway issue * Prevent Baris from spawning Baris
-rw-r--r--soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp553
-rw-r--r--soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h11
-rw-r--r--soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c24
3 files changed, 378 insertions, 210 deletions
diff --git a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp
index e3d311535..f518106c7 100644
--- a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp
+++ b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp
@@ -1,6 +1,5 @@
#include "functions.h"
#include "macros.h"
-#include "soh/Enhancements/randomizer/3drando/random.hpp"
#include "soh/Enhancements/randomizer/SeedContext.h"
#include "soh/Enhancements/enhancementTypes.h"
#include "soh/ObjectExtension/ObjectExtension.h"
@@ -19,6 +18,7 @@ extern "C" {
#include "src/overlays/actors/ovl_En_Blkobj/z_en_blkobj.h"
#include "src/overlays/actors/ovl_En_Encount1/z_en_encount1.h"
#include "src/overlays/actors/ovl_En_GeldB/z_en_geldb.h"
+#include "src/overlays/actors/ovl_En_Peehat/z_en_peehat.h"
#include "src/overlays/actors/ovl_En_Rr/z_en_rr.h"
#include "src/overlays/actors/ovl_En_Vali/z_en_vali.h"
@@ -37,8 +37,8 @@ extern std::shared_ptr<SohMenu> mSohMenu;
typedef struct EnemyEntry {
const char* cvar;
const char* name;
- int16_t id;
- int16_t params;
+ s16 id;
+ s16 params;
} EnemyEntry;
// clang-format off
@@ -60,7 +60,7 @@ static EnemyEntry randomizedEnemySpawnTable[] = {
{ CVAR_ENHANCEMENT("RandomizedEnemyList.Dinolfos"), "Dinolfos", ACTOR_EN_ZF, -2 }, // Dinolfos
{ CVAR_ENHANCEMENT("RandomizedEnemyList.Dodongo"), "Dodongo", ACTOR_EN_DODONGO, -1 }, // Dodongo
{ CVAR_ENHANCEMENT("RandomizedEnemyList.FireKeese"), "Fire Keese", ACTOR_EN_FIREFLY, 1 }, // Fire Keese
- // { CVAR_ENHANCEMENT("RandomizedEnemyList.FlareDancer"), "Flare Dancer", ACTOR_EN_FD, 0 }, // Flare Dancer (possible cause of crashes because of spawning flame actors on sloped ground)
+ // { CVAR_ENHANCEMENT("RandomizedEnemyList.FlareDancer"), "Flare Dancer", ACTOR_EN_FD, 0 }, // Flare Dancer (possible cause of crashes because of spawning flame actors on sloped ground or overloading)
{ CVAR_ENHANCEMENT("RandomizedEnemyList.FloorTile"), "Floor Tile", ACTOR_EN_YUKABYUN, 0 }, // Flying Floor Tile
{ CVAR_ENHANCEMENT("RandomizedEnemyList.Floormaster"), "Floormaster", ACTOR_EN_FLOORMAS, 0 }, // Floormaster
{ CVAR_ENHANCEMENT("RandomizedEnemyList.FlyingPeahat"), "Flying Peahat", ACTOR_EN_PEEHAT, -1 }, // Flying Peahat (big grounded, doesn't spawn larva)
@@ -80,16 +80,16 @@ static EnemyEntry randomizedEnemySpawnTable[] = {
{ CVAR_ENHANCEMENT("RandomizedEnemyList.InvisStalfos"), "Invisible Stalfos", ACTOR_EN_TEST, 0 }, // Stalfos (invisible)
{ CVAR_ENHANCEMENT("RandomizedEnemyList.Keese"), "Keese", ACTOR_EN_FIREFLY, 2 }, // Regular Keese
{ CVAR_ENHANCEMENT("RandomizedEnemyList.LargeBaba"), "Large Deku Baba", ACTOR_EN_DEKUBABA, 1 }, // Deku Baba (large)
- // { CVAR_ENHANCEMENT("RandomizedEnemyList.Leever"), "Leever", ACTOR_EN_REEBA, 0 }, // Leever Doesn't work (reliant on surface, without a spawner it kills itself too quickly)
+ // { CVAR_ENHANCEMENT("RandomizedEnemyList.Leever"), "Leever", ACTOR_EN_REEBA, 0 }, // Leever Doesn't work (reliant on surface, without a spawner it kills itself too quickly)
{ CVAR_ENHANCEMENT("RandomizedEnemyList.LikeLike"), "Like-Like", ACTOR_EN_RR, 0 }, // Like-Like
{ CVAR_ENHANCEMENT("RandomizedEnemyList.Lizalfos"), "Lizalfos", ACTOR_EN_ZF, -1 }, // Lizalfos
{ CVAR_ENHANCEMENT("RandomizedEnemyList.MadScrub"), "Mad Scrub", ACTOR_EN_DEKUNUTS, 768 }, // Mad Scrub (triple attack) (projectiles don't work)
{ CVAR_ENHANCEMENT("RandomizedEnemyList.NormalWolfos"), "Wolfos (Normal)", ACTOR_EN_WF, 0 }, // Wolfos (normal)
- // { CVAR_ENHANCEMENT("RandomizedEnemyList.Octorok"), "Octorok", ACTOR_EN_OKUTA, 0 }, // Octorok Doesn't work (actor directly uses water box collision to handle hiding/popping up)
+ // { CVAR_ENHANCEMENT("RandomizedEnemyList.Octorok"), "Octorok", ACTOR_EN_OKUTA, 0 }, // Octorok Doesn't work (actor directly uses water box collision to handle hiding/popping up)
{ CVAR_ENHANCEMENT("RandomizedEnemyList.PeahatLarva"), "Peahat Larva", ACTOR_EN_PEEHAT, 1 }, // Flying Peahat Larva
- // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 0 }, // Poe Doesn't work (Seems to rely on other objects?)
- // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 2 }, // Poe (composer Sharp) Doesn't work (Seems to rely on other objects?)
- // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 3 }, // Poe (composer Flat) Doesn't work (Seems to rely on other objects?)
+ // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe"), "Poe", ACTOR_EN_POH, 0 }, // Poe Doesn't work (Seems to rely on other objects?)
+ // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe.Sharp"), "Poe (Sharp)", ACTOR_EN_POH, 2 }, // Poe (composer Sharp) Doesn't work (Seems to rely on other objects?)
+ // { CVAR_ENHANCEMENT("RandomizedEnemyList.Poe.Flat"), "Poe (Flat)", ACTOR_EN_POH, 3 }, // Poe (composer Flat) Doesn't work (Seems to rely on other objects?)
{ CVAR_ENHANCEMENT("RandomizedEnemyList.Redead"), "Redead", ACTOR_EN_RD, 1 }, // Redead (standing)
{ CVAR_ENHANCEMENT("RandomizedEnemyList.RedTektite"), "Red Tektite", ACTOR_EN_TITE, -1 }, // Tektite (red)
{ CVAR_ENHANCEMENT("RandomizedEnemyList.Shabom"), "Shabom", ACTOR_EN_BUBBLE, 0 }, // Shabom (bubble)
@@ -159,125 +159,205 @@ static int enemiesToRandomize[] = {
// ACTOR_EN_REEBA, // Leever (reliant on spawner (z_en_encount1.c))
};
-bool IsEnemyAllowedToSpawn(int16_t sceneNum, int8_t roomNum, EnemyEntry enemy) {
- uint32_t isMQ = ResourceMgr_IsSceneMasterQuest(sceneNum);
-
- // Freezard - Child Link can only kill this with Deku Stick jumpslash or other equipment like bombs.
- // Beamos - Needs bombs.
- // Anubis - Needs fire.
- // Shell Blade & Spike - Child Link can't kill these with sword or Deku Stick.
- // Flare dancer, Arwing & Dark Link - Both go out of bounds way too easily, softlocking the player.
- // Wallmaster - Not easily visible, often makes players think they're softlocked and that there's no enemies left.
- // Club Moblin - Many issues with them falling or placing out of bounds. Maybe fixable in the future?
- bool enemiesToExcludeClearRooms =
- enemy.id == ACTOR_EN_FZ || enemy.id == ACTOR_EN_VM || enemy.id == ACTOR_EN_SB || enemy.id == ACTOR_EN_NY ||
- enemy.id == ACTOR_EN_CLEAR_TAG || enemy.id == ACTOR_EN_WALLMAS || enemy.id == ACTOR_EN_TORCH2 ||
- (enemy.id == ACTOR_EN_MB && enemy.params == 0) || enemy.id == ACTOR_EN_FD || enemy.id == ACTOR_EN_ANUBICE_TAG;
+static bool IsExcludedFromClearRooms(s16 enemyId, s16 enemyParams) {
+ switch (enemyId) {
+ // Freezard - Child Link can only kill this with Deku Stick jumpslash or other equipment like bombs
+ case ACTOR_EN_FZ:
+ // Beamos - Needs bombs
+ case ACTOR_EN_VM:
+ // Shell Blade - It's annoying to kill these as Child Link with sword or Deku Stick
+ case ACTOR_EN_SB:
+ // Spike - Child Link can't kill these with sword or Deku Stick
+ case ACTOR_EN_NY:
+ // Arwing - Goes out of bounds way too easily, softlocking the player
+ case ACTOR_EN_CLEAR_TAG:
+ // Wallmaster - Not easily visible, often makes players think they're softlocked and that there's no enemies
+ // left
+ case ACTOR_EN_WALLMAS:
+ // Dark Link - Goes out of bounds way too easily, softlocking the player
+ case ACTOR_EN_TORCH2:
+ // Flare dancer - Goes out of bounds way too easily, softlocking the player
+ case ACTOR_EN_FD:
+ // Anubis - Needs fire
+ case ACTOR_EN_ANUBICE_TAG:
+ return true;
+ case ACTOR_EN_MB:
+ return enemyParams == 0;
+ default:
+ return false;
+ }
+}
- // Bari - Spawns 3 more enemies, potentially extremely difficult in timed rooms.
- bool enemiesToExcludeTimedRooms = enemiesToExcludeClearRooms || enemy.id == ACTOR_EN_VALI;
+static bool IsExcludedFromTimedRooms(s16 enemyId, s16 enemyParams) {
+ switch (enemyId) {
+ // Bari - Spawns 3 more enemies, potentially extremely difficult in timed rooms
+ case ACTOR_EN_VALI:
+ return true;
+ default:
+ return IsExcludedFromClearRooms(enemyId, enemyParams);
+ }
+}
+static bool IsClearRoom(bool mq, s16 sceneNum, s8 roomNum) {
switch (sceneNum) {
- // Deku Tree
case SCENE_DEKU_TREE:
- return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 1 || roomNum == 9)) &&
- !(isMQ && enemiesToExcludeClearRooms &&
- (roomNum == 4 || roomNum == 6 || roomNum == 9 || roomNum == 10)));
- // Dodongo's Cavern
+ if (mq) {
+ return roomNum == 4 || roomNum == 6 || roomNum == 9 || roomNum == 10;
+ } else {
+ return roomNum == 1 || roomNum == 9;
+ }
case SCENE_DODONGOS_CAVERN:
- return (!(!isMQ && enemiesToExcludeClearRooms && roomNum == 15) &&
- !(isMQ && enemiesToExcludeClearRooms && (roomNum == 5 || roomNum == 13 || roomNum == 14)));
- // Jabu Jabu
+ if (mq) {
+ return roomNum == 5 || roomNum == 6 || roomNum == 13 || roomNum == 14;
+ } else {
+ return roomNum == 15;
+ }
case SCENE_JABU_JABU:
- return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 8 || roomNum == 9)) &&
- !(!isMQ && enemiesToExcludeTimedRooms && roomNum == 12) &&
- !(isMQ && enemiesToExcludeClearRooms && (roomNum == 11 || roomNum == 14)));
- // Forest Temple
+ if (mq) {
+ return roomNum == 11 || roomNum == 13 || roomNum == 14;
+ } else {
+ return roomNum == 8 || roomNum == 9;
+ }
case SCENE_FOREST_TEMPLE:
- return (!(!isMQ && enemiesToExcludeClearRooms &&
- (roomNum == 6 || roomNum == 10 || roomNum == 18 || roomNum == 21)) &&
- !(isMQ && enemiesToExcludeClearRooms &&
- (roomNum == 5 || roomNum == 6 || roomNum == 18 || roomNum == 21)));
- // Fire Temple
+ if (mq) {
+ return roomNum == 5 || roomNum == 6 || roomNum == 18 || roomNum == 21;
+ } else {
+ return roomNum == 6 || roomNum == 10 || roomNum == 18 || roomNum == 21;
+ }
case SCENE_FIRE_TEMPLE:
- return (!(!isMQ && enemiesToExcludeClearRooms && roomNum == 15) &&
- !(isMQ && enemiesToExcludeClearRooms && (roomNum == 15 || roomNum == 17 || roomNum == 18)));
- // Water Temple
+ if (mq) {
+ return roomNum == 15 || roomNum == 17 || roomNum == 18;
+ } else {
+ return roomNum == 15;
+ }
case SCENE_WATER_TEMPLE:
- return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 13 || roomNum == 18 || roomNum == 19)) &&
- !(isMQ && enemiesToExcludeClearRooms && (roomNum == 13 || roomNum == 18)));
- // Spirit Temple
+ if (mq) {
+ return roomNum == 13 || roomNum == 18;
+ } else {
+ return roomNum == 13 || roomNum == 18 || roomNum == 19;
+ }
case SCENE_SPIRIT_TEMPLE:
- return (!(!isMQ && enemiesToExcludeClearRooms &&
- (roomNum == 1 || roomNum == 10 || roomNum == 17 || roomNum == 20)) &&
- !(isMQ && enemiesToExcludeClearRooms &&
- (roomNum == 1 || roomNum == 2 || roomNum == 4 || roomNum == 10 || roomNum == 15 ||
- roomNum == 19 || roomNum == 20)));
- // Shadow Temple
+ if (mq) {
+ return roomNum == 1 || roomNum == 2 || roomNum == 4 || roomNum == 10 || roomNum == 15 ||
+ roomNum == 19 || roomNum == 20;
+ } else {
+ return roomNum == 1 || roomNum == 10 || roomNum == 17 || roomNum == 20 || roomNum == 27;
+ }
case SCENE_SHADOW_TEMPLE:
- return (
- !(!isMQ && enemiesToExcludeClearRooms &&
- (roomNum == 1 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 16 || roomNum == 17 ||
- roomNum == 19 || roomNum == 20)) &&
- !(isMQ && enemiesToExcludeClearRooms &&
- (roomNum == 1 || roomNum == 6 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 20)));
- // Ganon's Castle Trials
+ if (mq) {
+ return roomNum == 1 || roomNum == 6 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 20;
+ } else {
+ return roomNum == 1 || roomNum == 7 || roomNum == 11 || roomNum == 14 || roomNum == 16 ||
+ roomNum == 17 || roomNum == 19 || roomNum == 20;
+ }
case SCENE_INSIDE_GANONS_CASTLE:
- return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 2 || roomNum == 5 || roomNum == 9)) &&
- !(isMQ && enemiesToExcludeClearRooms &&
- (roomNum == 0 || roomNum == 2 || roomNum == 5 || roomNum == 9)));
- // Ice Caverns
+ if (mq) {
+ return roomNum == 0 || roomNum == 2 || roomNum == 5 || roomNum == 9;
+ } else {
+ return roomNum == 2 || roomNum == 5 || roomNum == 9;
+ }
case SCENE_ICE_CAVERN:
- return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 1 || roomNum == 7)) &&
- !(isMQ && enemiesToExcludeClearRooms && (roomNum == 3 || roomNum == 7)));
- // Bottom of the Well
- // Exclude Dark Link from room with holes in the floor because it can pull you in a like-like making the player
- // fall down.
- case SCENE_BOTTOM_OF_THE_WELL:
- return (!(!isMQ && enemy.id == ACTOR_EN_TORCH2 && roomNum == 3));
- // Don't allow Dark Link in areas with lava void out zones as it voids out the player as well.
- // Gerudo Training Ground.
+ if (mq) {
+ return roomNum == 3 || roomNum == 7;
+ } else {
+ return roomNum == 1 || roomNum == 7;
+ }
case SCENE_GERUDO_TRAINING_GROUND:
- return (!(enemy.id == ACTOR_EN_TORCH2 && roomNum == 6) &&
- !(!isMQ && enemiesToExcludeTimedRooms && (roomNum == 1 || roomNum == 7)) &&
- !(!isMQ && enemiesToExcludeClearRooms && (roomNum == 3 || roomNum == 5 || roomNum == 10)) &&
- !(isMQ && enemiesToExcludeTimedRooms &&
- (roomNum == 1 || roomNum == 3 || roomNum == 5 || roomNum == 7)) &&
- !(isMQ && enemiesToExcludeClearRooms && roomNum == 10));
- // Don't allow certain enemies in Ganon's Tower because they would spawn up on the ceiling,
- // becoming impossible to kill.
- // Ganon's Tower.
+ if (mq) {
+ return roomNum == 10;
+ } else {
+ return roomNum == 3 || roomNum == 5 || roomNum == 10;
+ }
case SCENE_GANONS_TOWER:
- return (!(enemiesToExcludeClearRooms || enemy.id == ACTOR_EN_VALI ||
- (enemy.id == ACTOR_EN_ZF && enemy.params == -1)));
- // Ganon's Tower Escape.
- case SCENE_GANONS_TOWER_COLLAPSE_INTERIOR:
- return (!((enemiesToExcludeTimedRooms || (enemy.id == ACTOR_EN_ZF && enemy.params == -1)) && roomNum == 1));
- // Don't allow big Stalchildren, big Peahats and the large Bari (jellyfish) during the Gohma fight because they
- // can clip into Gohma and it crashes the game. Likely because Gohma on the ceiling can't handle collision with
- // other enemies.
- case SCENE_DEKU_TREE_BOSS:
- return (!enemiesToExcludeTimedRooms && !(enemy.id == ACTOR_EN_SKB && enemy.params == 20) &&
- !(enemy.id == ACTOR_EN_PEEHAT && enemy.params == -1));
- // Grottos.
+ return true;
case SCENE_GROTTOS:
- return (!(enemiesToExcludeClearRooms && (roomNum == 2 || roomNum == 7)));
- // Royal Grave.
+ return roomNum == 2 || roomNum == 7;
case SCENE_ROYAL_FAMILYS_TOMB:
- return (!(enemiesToExcludeClearRooms && roomNum == 0));
- // Don't allow Dark Link in areas with lava void out zones as it voids out the player as well.
- // Death Mountain Crater.
- case SCENE_DEATH_MOUNTAIN_CRATER:
- return (enemy.id != ACTOR_EN_TORCH2);
+ return roomNum == 0;
+ default:
+ return false;
+ }
+}
+
+static bool IsTimedRoom(bool mq, s16 sceneNum, s8 roomNum) {
+ switch (sceneNum) {
+ case SCENE_JABU_JABU:
+ return !mq && roomNum == 12;
+ case SCENE_GERUDO_TRAINING_GROUND:
+ if (mq) {
+ return roomNum == 1 || roomNum == 3 || roomNum == 5 || roomNum == 7;
+ } else {
+ return roomNum == 1 || roomNum == 7;
+ }
+ case SCENE_GANONS_TOWER_COLLAPSE_INTERIOR:
+ return roomNum == 1;
default:
- return 1;
+ return false;
}
}
+static bool IsEnemyAllowedToSpawn(s16 sceneNum, s8 roomNum, EnemyEntry enemy, s16 posY, bool fromBari) {
+ bool mq = ResourceMgr_IsSceneMasterQuest(sceneNum);
+
+ if (IsExcludedFromClearRooms(enemy.id, enemy.params) && IsClearRoom(mq, sceneNum, roomNum)) {
+ return false;
+ }
+
+ if (IsExcludedFromTimedRooms(enemy.id, enemy.params) && IsTimedRoom(mq, sceneNum, roomNum)) {
+ return false;
+ }
+
+ // Don't allow Lizalfos or Baris in Ganon's Tower because they would spawn up on the ceiling, becoming impossible to
+ // kill.
+ if (sceneNum == SCENE_GANONS_TOWER &&
+ (enemy.id == ACTOR_EN_VALI || (enemy.id == ACTOR_EN_ZF && enemy.params == -1))) {
+ return false;
+ }
+
+ // Don't allow Lizalfos in the first room of the interior of the castle collapse
+ if (sceneNum == SCENE_GANONS_TOWER_COLLAPSE_INTERIOR && roomNum == 1 && enemy.id == ACTOR_EN_ZF &&
+ enemy.params == -1) {
+ return false;
+ }
+
+ // Don't allow big Stalchildren, big Peahats and Baris (big jellyfish) during the Gohma fight because they can clip
+ // into Gohma and it crashes the game. Likely because Gohma on the ceiling can't handle collision with other
+ // enemies.
+ if (sceneNum == SCENE_DEKU_TREE_BOSS &&
+ ((enemy.id == ACTOR_EN_SKB && enemy.params == 20) || (enemy.id == ACTOR_EN_PEEHAT && enemy.params == -1) ||
+ (enemy.id == ACTOR_EN_VALI))) {
+ return false;
+ }
+
+ // Don't allow the following enemies in the first spawn of the first room in MQ Fire Temple loop as when spawned and
+ // they get stuck in the room above
+ // - Lizalfos/Dinolfos, Bari: they drop in
+ // - Skulltulla: they appear above
+ // - Flying Peehat: they rise above the ceiling
+ if (mq && sceneNum == SCENE_FIRE_TEMPLE && roomNum == 15 && posY == 64 &&
+ (enemy.id == ACTOR_EN_ZF || enemy.id == ACTOR_EN_VALI || enemy.id == ACTOR_EN_ST ||
+ enemy.id == ACTOR_EN_PEEHAT)) {
+ return false;
+ }
+
+ // Don't allow Stalfos in the child spirit clear room as they jump out of bounds frequently
+ if (sceneNum == SCENE_SPIRIT_TEMPLE && roomNum == 1 && enemy.id == ACTOR_EN_TEST) {
+ return false;
+ }
+
+ // Don't allow baris to spawn another bari
+ if (fromBari && enemy.id == ACTOR_EN_VALI) {
+ return false;
+ }
+
+ return true;
+}
+
static std::vector<EnemyEntry> selectedEnemyList;
-void GetSelectedEnemies() {
+static void UpdateSelectedEnemies() {
selectedEnemyList.clear();
+
for (int i = 0; i < ARRAY_COUNT(randomizedEnemySpawnTable); i++) {
if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemyList.All"), 0)) {
selectedEnemyList.push_back(randomizedEnemySpawnTable[i]);
@@ -285,108 +365,119 @@ void GetSelectedEnemies() {
selectedEnemyList.push_back(randomizedEnemySpawnTable[i]);
}
}
+
if (selectedEnemyList.size() == 0) {
selectedEnemyList.push_back(randomizedEnemySpawnTable[0]);
}
}
-EnemyEntry GetRandomizedEnemyEntry(uint32_t seed, PlayState* play) {
+static EnemyEntry GetRandomizedEnemyEntry(u32 seed, PlayState* play, s16 posY, bool fromBari) {
std::vector<EnemyEntry> filteredEnemyList = {};
+
if (selectedEnemyList.size() == 0) {
- GetSelectedEnemies();
+ UpdateSelectedEnemies();
}
+
for (EnemyEntry enemy : selectedEnemyList) {
- if (IsEnemyAllowedToSpawn(play->sceneNum, play->roomCtx.curRoom.num, enemy)) {
+ if (IsEnemyAllowedToSpawn(play->sceneNum, play->roomCtx.curRoom.num, enemy, posY, fromBari)) {
filteredEnemyList.push_back(enemy);
}
}
+
if (filteredEnemyList.size() == 0) {
filteredEnemyList = selectedEnemyList;
}
+
if (CVAR_ENEMY_RANDOMIZER_VALUE == ENEMY_RANDOMIZER_RANDOM_SEEDED) {
- uint32_t finalSeed =
- seed + (IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : gSaveContext.ship.stats.fileCreatedAt);
- Random_Init(finalSeed);
- uint32_t randomNumber = Random(0, filteredEnemyList.size());
- return filteredEnemyList[randomNumber];
- } else {
- uint32_t randomSelectedEnemy = Random(0, filteredEnemyList.size());
- return filteredEnemyList[randomSelectedEnemy];
+ uint64_t randomState = 0;
+
+ ShipUtils::RandInit(
+ seed + (IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : gSaveContext.ship.stats.fileCreatedAt),
+ &randomState);
+
+ return ShipUtils::RandomElement(filteredEnemyList, false, &randomState);
}
-}
-bool IsEnemyFoundToRandomize(int16_t sceneNum, int8_t roomNum, int16_t actorId, int16_t params, float posX) {
+ return ShipUtils::RandomElement(filteredEnemyList, false);
+}
- uint32_t isMQ = ResourceMgr_IsSceneMasterQuest(sceneNum);
+static bool IsEnemyFoundToRandomize(s16 sceneNum, s8 roomNum, s16 actorId, s16 params, f32 posX) {
+ u32 isMQ = ResourceMgr_IsSceneMasterQuest(sceneNum);
for (int i = 0; i < ARRAY_COUNT(enemiesToRandomize); i++) {
- if (actorId == enemiesToRandomize[i]) {
- switch (actorId) {
- // Only randomize the main component of Electric Tailparasans, not the tail segments they spawn.
- case ACTOR_EN_TP:
- return (params == -1);
- // Only randomize the initial Deku Scrub actor (single and triple attack), not the flower they spawn.
- case ACTOR_EN_DEKUNUTS:
- return (params == -256 || params == 768);
- // Don't randomize the OoB wallmaster in the Silver Rupee room because it's only there to
- // not trigger unlocking the door after killing the other wallmaster in authentic gameplay.
- case ACTOR_EN_WALLMAS:
- return (!(!isMQ && sceneNum == SCENE_GERUDO_TRAINING_GROUND && roomNum == 2 && posX == -2345));
- // Only randomize initial Floormaster actor (it can split and does some spawning on init).
- case ACTOR_EN_FLOORMAS:
- return (params == 0 || params == -32768);
- // Only randomize the initial eggs, not the enemies that spawn from them.
- case ACTOR_EN_GOMA:
- return (params >= 0 && params <= 9);
- // Only randomize Skullwalltulas, not Golden Skulltulas.
- case ACTOR_EN_SW:
- return (params == 0);
- // Don't randomize Nabooru because it'll break the cutscene and the door.
- // Don't randomize Iron Knuckle in MQ Spirit Trial because it's needed to
- // break the thrones in the room to access a button.
- case ACTOR_EN_IK:
- return (params != 1280 && !(isMQ && sceneNum == SCENE_INSIDE_GANONS_CASTLE && roomNum == 17));
- // Only randomize the initial spawn of the huge jellyfish. It spawns another copy when hit with a sword.
- case ACTOR_EN_VALI:
- return (params == -1);
- // Don't randomize Lizalfos in Dodongo's Cavern because the gates won't work correctly otherwise.
- case ACTOR_EN_ZF:
- return (params != 1280 && params != 1281 && params != 1536 && params != 1537);
- // Don't randomize the Wolfos in SFM because it's needed to open the gate.
- case ACTOR_EN_WF:
- return (params != 7936);
- // Don't randomize the Stalfos in Forest Temple because other enemies fall through the hole and don't
- // trigger the platform. Don't randomize the Stalfos spawning on the boat in Shadow Temple, as
- // randomizing them places the new enemies down in the river.
- case ACTOR_EN_TEST:
- return (params != 1 && !(sceneNum == SCENE_SHADOW_TEMPLE && roomNum == 21));
- // Only randomize the enemy variant of Armos Statue.
- // Leave one Armos unrandomized in the Spirit Temple room where an armos is needed to push down a
- // button.
- case ACTOR_EN_AM:
- return ((params == -1 || params == 255) && !(sceneNum == SCENE_SPIRIT_TEMPLE && posX == 2141));
- // Don't randomize Shell Blades and Spikes in the underwater portion in Water Temple as it's impossible
- // to kill most other enemies underwater with just hookshot and they're required to be killed for a
- // grate to open.
- case ACTOR_EN_SB:
- case ACTOR_EN_NY:
- return (!(!isMQ && sceneNum == SCENE_WATER_TEMPLE && roomNum == 2));
- case ACTOR_EN_SKJ:
- return !(sceneNum == SCENE_LOST_WOODS && LINK_IS_CHILD);
- default:
- return 1;
- }
+ if (actorId != enemiesToRandomize[i]) {
+ continue;
+ }
+
+ switch (actorId) {
+ // Only randomize the main component of Electric Tailparasans, not the tail segments they spawn.
+ case ACTOR_EN_TP:
+ return params == -1;
+ // Only randomize the initial Deku Scrub actor (single and triple attack), not the flower they spawn.
+ case ACTOR_EN_DEKUNUTS:
+ return params == -256 || params == 768;
+ // Don't randomize the OoB wallmaster in the Silver Rupee room because it's only there to
+ // not trigger unlocking the door after killing the other wallmaster in authentic gameplay.
+ case ACTOR_EN_WALLMAS:
+ return !(!isMQ && sceneNum == SCENE_GERUDO_TRAINING_GROUND && roomNum == 2 && posX == -2345);
+ // Only randomize initial Floormaster actor (it can split and does some spawning on init).
+ case ACTOR_EN_FLOORMAS:
+ return params == 0 || params == -32768;
+ // Only randomize the initial eggs, not the enemies that spawn from them.
+ case ACTOR_EN_GOMA:
+ return params >= 0 && params <= 9;
+ // Only randomize Skullwalltulas, not Golden Skulltulas.
+ case ACTOR_EN_SW:
+ return params == 0;
+ // Don't randomize Nabooru because it'll break the cutscene and the door.
+ // Don't randomize Iron Knuckle in MQ Spirit Trial because it's needed to
+ // break the thrones in the room to access a button.
+ case ACTOR_EN_IK:
+ return params != 1280 && !(isMQ && sceneNum == SCENE_INSIDE_GANONS_CASTLE && roomNum == 17);
+ // Only randomize the initial spawn of the huge jellyfish. It spawns another copy when hit with a sword.
+ case ACTOR_EN_VALI:
+ return params == -1;
+ // Don't randomize Lizalfos in Dodongo's Cavern because the gates won't work correctly otherwise.
+ case ACTOR_EN_ZF:
+ return params != 1280 && params != 1281 && params != 1536 && params != 1537;
+ // Don't randomize the right baby dodongo on the first tunnel in Dodongo's Cavern as in vanilla you use them
+ // isntead of bombs to blow up a wall
+ case ACTOR_EN_DODOJR:
+ return !(sceneNum == SCENE_DODONGOS_CAVERN && roomNum == 1 && posX == 1972);
+ // Don't randomize the Wolfos in SFM because it's needed to open the gate.
+ case ACTOR_EN_WF:
+ return params != 7936;
+ // Don't randomize the Stalfos in Forest Temple because other enemies fall through the hole and don't
+ // trigger the platform. Don't randomize the Stalfos spawning on the boat in Shadow Temple, as
+ // randomizing them places the new enemies down in the river.
+ case ACTOR_EN_TEST:
+ return params != 1 && !(sceneNum == SCENE_SHADOW_TEMPLE && roomNum == 21);
+ // Only randomize the enemy variant of Armos Statue.
+ // Leave one Armos unrandomized in the Spirit Temple room where an armos is needed to push down a
+ // button.
+ case ACTOR_EN_AM:
+ return (params == -1 || params == 255) && !(sceneNum == SCENE_SPIRIT_TEMPLE && posX == 2141);
+ // Don't randomize Shell Blades and Spikes in the underwater portion in Water Temple as it's impossible
+ // to kill most other enemies underwater with just hookshot and they're required to be killed for a
+ // grate to open.
+ case ACTOR_EN_SB:
+ case ACTOR_EN_NY:
+ return !(!isMQ && sceneNum == SCENE_WATER_TEMPLE && roomNum == 2);
+ // Don't randomize Skull Kids in Lost Woods as child as they're not enemies
+ case ACTOR_EN_SKJ:
+ return !(sceneNum == SCENE_LOST_WOODS && LINK_IS_CHILD);
+ default:
+ return true;
}
}
// If no enemy is found, don't randomize the actor.
- return 0;
+ return false;
}
-uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* posY, s16* posZ, int16_t* rotX,
- int16_t* rotY, int16_t* rotZ, int16_t* params) {
-
- uint32_t isMQ = ResourceMgr_IsSceneMasterQuest(play->sceneNum);
+static u8 GetRandomizedEnemy(PlayState* play, s16* actorId, s16* posX, s16* posY, s16* posZ, s16* rotX, s16* rotY,
+ s16* rotZ, s16* params, s16 offset = 0, bool fromBari = false) {
+ u32 isMQ = ResourceMgr_IsSceneMasterQuest(play->sceneNum);
// Hack to remove enemies that wrongfully spawn because of bypassing object dependency with enemy randomizer on.
// This should probably be handled on OTR generation in the future when object dependency is fully removed.
@@ -412,7 +503,6 @@ uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* po
}
if (IsEnemyFoundToRandomize(play->sceneNum, play->roomCtx.curRoom.num, *actorId, *params, *posX)) {
-
// When replacing Iron Knuckles in Spirit Temple, move them away from the throne because
// some enemies can get stuck on the throne.
if (*actorId == ACTOR_EN_IK && play->sceneNum == SCENE_SPIRIT_TEMPLE) {
@@ -443,17 +533,27 @@ uint8_t GetRandomizedEnemy(PlayState* play, int16_t* actorId, s16* posX, s16* po
pos.x = *posX;
pos.y = *posY + 50;
pos.z = *posZ;
- raycastResult = BgCheck_AnyRaycastFloor1(&play->colCtx, &poly, &pos);
- // If ground is found below actor, move actor to that height.
- if (raycastResult > BGCHECK_Y_MIN) {
- *posY = raycastResult;
+ // the forest temple second twisted hallway spawns after the enemies so we need to "find the floor" manually
+ if (play->sceneNum == SCENE_FOREST_TEMPLE && play->roomCtx.curRoom.num == 20 && *posZ > -3000) {
+ // when hallway is twisted (play->actorCtx.flags.tempSwch & 1), one spawn has the floor at 1235.165 &
+ // the other at 1239.094 but that changes based on the player position
+ // when not twisted, the whole floor is at 1228
+
+ *posY = 1228.0;
+ } else {
+ raycastResult = BgCheck_AnyRaycastFloor1(&play->colCtx, &poly, &pos);
+
+ // If ground is found below actor, move actor to that height.
+ if (raycastResult > BGCHECK_Y_MIN) {
+ *posY = raycastResult;
+ }
}
// Get randomized enemy ID and parameter.
- uint32_t seed =
- play->sceneNum + *actorId + (int)*posX + (int)*posY + (int)*posZ + *rotX + *rotY + *rotZ + *params;
- EnemyEntry randomEnemy = GetRandomizedEnemyEntry(seed, play);
+ u32 seed =
+ play->sceneNum + *actorId + (int)*posX + (int)*posY + (int)*posZ + *rotX + *rotY + *rotZ + *params + offset;
+ EnemyEntry randomEnemy = GetRandomizedEnemyEntry(seed, play, *posY, fromBari);
*actorId = randomEnemy.id;
*params = randomEnemy.params;
@@ -528,6 +628,25 @@ void CustomStalfosPairFightDestroy(Actor* thisx, PlayState* play) {
ObjectExtension::GetInstance().Remove<CustomStalfosPairFightData>(thisx);
}
+struct CustomPeehatLarvaData {
+ EnPeehat* peehat = nullptr;
+ ActorFunc originalDestroy = nullptr;
+};
+
+static ObjectExtension::Register<CustomPeehatLarvaData> CustomPeehatLarvaDataRegister;
+
+void CustomPeehatLarvaDestroy(Actor* thisx, PlayState* play) {
+ assert(ObjectExtension::GetInstance().Has<CustomPeehatLarvaData>(thisx));
+
+ CustomPeehatLarvaData* customPeehatLarvaData = ObjectExtension::GetInstance().Get<CustomPeehatLarvaData>(thisx);
+
+ customPeehatLarvaData->peehat->unk_2FA -= 1;
+
+ customPeehatLarvaData->originalDestroy(thisx, play);
+
+ ObjectExtension::GetInstance().Remove<CustomPeehatLarvaData>(thisx);
+}
+
void RegisterEnemyRandomizer() {
COND_ID_HOOK(OnActorInit, ACTOR_EN_MB, ENEMY_RANDOMIZER_ENABLED, FixClubMoblinScale);
@@ -756,14 +875,18 @@ void RegisterEnemyRandomizer() {
s16 rotZ = 0;
s16 params = 0;
- for (s32 i = 0; i < 3; i++) {
- // Offset small jellyfish with Enemy Randomizer, otherwise it gets
- // stuck in a loop spawning more big jellyfish with seeded spawns.
- if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0)) {
- rotY += rand() % 50;
- }
+ s16 homePosX = vali->actor.home.pos.x;
+ s16 homePosY = vali->actor.home.pos.y;
+ s16 homePosZ = vali->actor.home.pos.z;
- if (!GetRandomizedEnemy(play, &actorId, &posX, &posY, &posZ, &rotX, &rotY, &rotZ, &params)) {
+ s16 homeRotX = vali->actor.home.rot.x;
+ s16 homeRotY = vali->actor.home.rot.y;
+ s16 homeRotZ = vali->actor.home.rot.z;
+
+ for (s32 i = 0; i < 3; i++) {
+ // use the home pos & rot to make it consistent
+ if (!GetRandomizedEnemy(play, &actorId, &homePosX, &homePosY, &homePosZ, &homeRotX, &homeRotY, &homeRotZ,
+ &params, i * 1000, true)) {
assert(false);
}
@@ -919,9 +1042,43 @@ void RegisterEnemyRandomizer() {
*should = false;
});
+
+ COND_VB_SHOULD(VB_PEEHAT_SPAWN_LARVAS, ENEMY_RANDOMIZER_ENABLED, {
+ EnPeehat* peehat = va_arg(args, EnPeehat*);
+ PlayState* play = va_arg(args, PlayState*);
+
+ s16 actorId = ACTOR_EN_PEEHAT;
+ s16 homePosX = peehat->actor.home.pos.x;
+ s16 homePosY = peehat->actor.home.pos.y + 50.0f;
+ s16 homePosZ = peehat->actor.home.pos.z;
+ s16 rotX = 0;
+ s16 rotY = 0;
+ s16 rotZ = 0;
+ s16 params = PEAHAT_TYPE_LARVA;
+
+ // 3 is MAX_LARVA
+ for (s32 i = 3 - peehat->unk_2FA; i > 0; i--) {
+ if (!GetRandomizedEnemy(play, &actorId, &homePosX, &homePosY, &homePosZ, &rotX, &rotY, &rotZ, &params,
+ i * 1000)) {
+ assert(false);
+ }
+
+ Actor* enemy =
+ Actor_Spawn(&play->actorCtx, play, actorId, homePosX, homePosY, homePosZ, rotX, rotY, rotZ, params);
+
+ if (enemy == NULL) {
+ assert(false);
+ } else {
+ peehat->unk_2FA++;
+ ObjectExtension::GetInstance().Set<CustomPeehatLarvaData>(
+ enemy, CustomPeehatLarvaData{ .peehat = peehat, .originalDestroy = enemy->destroy });
+ enemy->destroy = CustomPeehatLarvaDestroy;
+ }
+ }
+ });
}
-static const std::map<int32_t, const char*> enemyRandomizerModes = {
+static const std::map<s32, const char*> enemyRandomizerModes = {
{ ENEMY_RANDOMIZER_OFF, "Disabled" },
{ ENEMY_RANDOMIZER_RANDOM, "Random" },
{ ENEMY_RANDOMIZER_RANDOM_SEEDED, "Random (Seeded)" },
@@ -932,7 +1089,7 @@ void RegisterEnemyRandomizerWidgets() {
SohGui::mSohMenu->AddWidget(path, "Enemy Randomizer", WIDGET_CVAR_COMBOBOX)
.CVar(CVAR_ENHANCEMENT("RandomizedEnemies"))
- .Callback([](WidgetInfo& info) { GetSelectedEnemies(); })
+ .Callback([](WidgetInfo& info) { UpdateSelectedEnemies(); })
.Options(
UIWidgets::ComboboxOptions()
.DefaultIndex(ENEMY_RANDOMIZER_OFF)
@@ -963,7 +1120,7 @@ void RegisterEnemyRandomizerWidgets() {
SohGui::mSohMenu->AddWidget(path, "Select all Enemies", WIDGET_CVAR_CHECKBOX)
.CVar(CVAR_ENHANCEMENT("RandomizedEnemyList.All"))
.PreFunc([](WidgetInfo& info) { info.isHidden = !CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0); })
- .Callback([](WidgetInfo& info) { GetSelectedEnemies(); });
+ .Callback([](WidgetInfo& info) { UpdateSelectedEnemies(); });
SohGui::mSohMenu->AddWidget(path, "Enemy List", WIDGET_SEPARATOR).PreFunc([](WidgetInfo& info) {
info.isHidden = !CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0);
@@ -978,7 +1135,7 @@ void RegisterEnemyRandomizerWidgets() {
info.options->disabled = CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemyList.All"), 0);
info.options->disabledTooltip = "These options are disabled because \"Select All Enemies\" is enabled.";
})
- .Callback([](WidgetInfo& info) { GetSelectedEnemies(); });
+ .Callback([](WidgetInfo& info) { UpdateSelectedEnemies(); });
}
}
diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h
index 09ce04580..956711edc 100644
--- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h
+++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h
@@ -2784,7 +2784,16 @@ typedef enum {
// ```
// #### `args`
// - `*int32_t (camId)`
- VB_SHOULD_LOAD_BG_IMAGE
+ VB_SHOULD_LOAD_BG_IMAGE,
+
+ // #### `result`
+ // ```c
+ // true
+ // ```
+ // #### `args`
+ // - `*EnPeehat`
+ // - `*PlayState`
+ VB_PEEHAT_SPAWN_LARVAS,
} GIVanillaBehavior;
#endif
diff --git a/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c b/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c
index 095739888..d48b611ea 100644
--- a/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c
+++ b/soh/src/overlays/actors/ovl_En_Peehat/z_en_peehat.c
@@ -298,17 +298,19 @@ void EnPeehat_HitWhenGrounded(EnPeehat* this, PlayState* play) {
s32 i;
this->colCylinder.base.acFlags &= ~AC_HIT;
- for (i = MAX_LARVA - this->unk_2FA; i > 0; i--) {
- Actor* larva =
- Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_PEEHAT,
- Rand_CenteredFloat(25.0f) + this->actor.world.pos.x,
- Rand_CenteredFloat(25.0f) + (this->actor.world.pos.y + 50.0f),
- Rand_CenteredFloat(25.0f) + this->actor.world.pos.z, 0, 0, 0, PEAHAT_TYPE_LARVA);
-
- if (larva != NULL) {
- larva->velocity.y = 6.0f;
- larva->shape.rot.y = larva->world.rot.y = Rand_CenteredFloat(0xFFFF);
- this->unk_2FA++;
+ if (GameInteractor_Should(VB_PEEHAT_SPAWN_LARVAS, true, this, play)) {
+ for (i = MAX_LARVA - this->unk_2FA; i > 0; i--) {
+ Actor* larva =
+ Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_PEEHAT,
+ Rand_CenteredFloat(25.0f) + this->actor.world.pos.x,
+ Rand_CenteredFloat(25.0f) + (this->actor.world.pos.y + 50.0f),
+ Rand_CenteredFloat(25.0f) + this->actor.world.pos.z, 0, 0, 0, PEAHAT_TYPE_LARVA);
+
+ if (larva != NULL) {
+ larva->velocity.y = 6.0f;
+ larva->shape.rot.y = larva->world.rot.y = Rand_CenteredFloat(0xFFFF);
+ this->unk_2FA++;
+ }
}
}
this->unk_2D4 = 8;