diff --git a/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp b/soh/soh/Enhancements/ExtraModes/EnemyRandomizer.cpp index e3d311535b..f518106c7c 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 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; - - // Bari - Spawns 3 more enemies, potentially extremely difficult in timed rooms. - bool enemiesToExcludeTimedRooms = enemiesToExcludeClearRooms || enemy.id == ACTOR_EN_VALI; - - 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 - case SCENE_DODONGOS_CAVERN: - return (!(!isMQ && enemiesToExcludeClearRooms && roomNum == 15) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 5 || roomNum == 13 || roomNum == 14))); - // Jabu Jabu - case SCENE_JABU_JABU: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 8 || roomNum == 9)) && - !(!isMQ && enemiesToExcludeTimedRooms && roomNum == 12) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 11 || roomNum == 14))); - // Forest Temple - 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 - case SCENE_FIRE_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && roomNum == 15) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 15 || roomNum == 17 || roomNum == 18))); - // Water Temple - case SCENE_WATER_TEMPLE: - return (!(!isMQ && enemiesToExcludeClearRooms && (roomNum == 13 || roomNum == 18 || roomNum == 19)) && - !(isMQ && enemiesToExcludeClearRooms && (roomNum == 13 || roomNum == 18))); - // Spirit Temple - 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 - 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 - 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 - 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. - 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. - 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. - case SCENE_GROTTOS: - return (!(enemiesToExcludeClearRooms && (roomNum == 2 || roomNum == 7))); - // Royal Grave. - 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); +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 1; + return false; } } +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) { + case SCENE_DEKU_TREE: + if (mq) { + return roomNum == 4 || roomNum == 6 || roomNum == 9 || roomNum == 10; + } else { + return roomNum == 1 || roomNum == 9; + } + case SCENE_DODONGOS_CAVERN: + if (mq) { + return roomNum == 5 || roomNum == 6 || roomNum == 13 || roomNum == 14; + } else { + return roomNum == 15; + } + case SCENE_JABU_JABU: + if (mq) { + return roomNum == 11 || roomNum == 13 || roomNum == 14; + } else { + return roomNum == 8 || roomNum == 9; + } + case SCENE_FOREST_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: + if (mq) { + return roomNum == 15 || roomNum == 17 || roomNum == 18; + } else { + return roomNum == 15; + } + case SCENE_WATER_TEMPLE: + if (mq) { + return roomNum == 13 || roomNum == 18; + } else { + return roomNum == 13 || roomNum == 18 || roomNum == 19; + } + case SCENE_SPIRIT_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: + 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: + if (mq) { + return roomNum == 0 || roomNum == 2 || roomNum == 5 || roomNum == 9; + } else { + return roomNum == 2 || roomNum == 5 || roomNum == 9; + } + case SCENE_ICE_CAVERN: + if (mq) { + return roomNum == 3 || roomNum == 7; + } else { + return roomNum == 1 || roomNum == 7; + } + case SCENE_GERUDO_TRAINING_GROUND: + if (mq) { + return roomNum == 10; + } else { + return roomNum == 3 || roomNum == 5 || roomNum == 10; + } + case SCENE_GANONS_TOWER: + return true; + case SCENE_GROTTOS: + return roomNum == 2 || roomNum == 7; + case SCENE_ROYAL_FAMILYS_TOMB: + 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 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 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 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); } + + return ShipUtils::RandomElement(filteredEnemyList, false); } -bool IsEnemyFoundToRandomize(int16_t sceneNum, int8_t roomNum, int16_t actorId, int16_t params, float posX) { - - 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(thisx); } +struct CustomPeehatLarvaData { + EnPeehat* peehat = nullptr; + ActorFunc originalDestroy = nullptr; +}; + +static ObjectExtension::Register CustomPeehatLarvaDataRegister; + +void CustomPeehatLarvaDestroy(Actor* thisx, PlayState* play) { + assert(ObjectExtension::GetInstance().Has(thisx)); + + CustomPeehatLarvaData* customPeehatLarvaData = ObjectExtension::GetInstance().Get(thisx); + + customPeehatLarvaData->peehat->unk_2FA -= 1; + + customPeehatLarvaData->originalDestroy(thisx, play); + + ObjectExtension::GetInstance().Remove(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, ¶ms)) { + 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, + ¶ms, 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, ¶ms, + 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( + enemy, CustomPeehatLarvaData{ .peehat = peehat, .originalDestroy = enemy->destroy }); + enemy->destroy = CustomPeehatLarvaDestroy; + } + } + }); } -static const std::map enemyRandomizerModes = { +static const std::map 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 09ce04580a..956711edc9 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 0957398887..d48b611ea5 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 (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++; + 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;