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
This commit is contained in:
Pepe20129
2026-06-09 21:52:25 +02:00
committed by GitHub
parent b61db77020
commit e93ea5b919
3 changed files with 394 additions and 226 deletions
@@ -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;
// 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<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);
}
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<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(); });
}
}
@@ -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
@@ -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;