diff --git a/.gitignore b/.gitignore index 180a9fe42b..f0c53b52bf 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ shipofharkinian.json imgui.ini saves/* randomizer/* +2S2HTimeSplitData.json mm/libultraship/extern/Debug/ImGui.lib diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a120c9489..772b89fd15 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -5,13 +5,13 @@ set(CMAKE_CXX_STANDARD 20 CACHE STRING "The C++ standard to use") set(CMAKE_OSX_DEPLOYMENT_TARGET "10.15" CACHE STRING "Minimum OS X deployment version") set(GAME_STR "MM") -project(2s2h VERSION 3.0.1 LANGUAGES C CXX) +project(2s2h VERSION 3.0.2 LANGUAGES C CXX) include(CMake/2ship-cvars.cmake) include(CMake/lus-cvars.cmake) set(SPDLOG_LEVEL_TRACE 0) set(SPDLOG_LEVEL_OFF 6) set(SPDLOG_MIN_CUTOFF SPDLOG_LEVEL_TRACE CACHE STRING "cutoff at trace") -set(PROJECT_BUILD_NAME "Mion Bravo" CACHE STRING "" FORCE) +set(PROJECT_BUILD_NAME "Mion Charlie" CACHE STRING "" FORCE) set(PROJECT_TEAM "github.com/harbourmasters" CACHE STRING "" FORCE) execute_process( diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp index f104e64fb9..5c11a1cbdc 100644 --- a/mm/2s2h/BenGui/BenMenu.cpp +++ b/mm/2s2h/BenGui/BenMenu.cpp @@ -557,6 +557,7 @@ void BenMenu::AddSettings() { .Options(ButtonOptions().Tooltip("Enables the separate Bindings Window.").Size(Sizes::Inline)); path.sidebarName = "Overlay"; + path.column = SECTION_COLUMN_1; AddSidebarEntry("Settings", "Overlay", 2); AddWidget(path, "Notifications", WIDGET_SEPARATOR_TEXT); AddWidget(path, "Position", WIDGET_CVAR_COMBOBOX) @@ -993,6 +994,11 @@ void BenMenu::AddEnhancements() { .CVar("gEnhancements.Equipment.InvertShieldY") .Options(CheckboxOptions().Tooltip( "Invert the Y axis while holding the shield so that it moves up with the left stick.")); + AddWidget(path, "Great Fairy Sword B-Button Attack", WIDGET_CVAR_CHECKBOX) + .CVar("gEnhancements.Equipment.GreatFairySwordBButton") + .Options(CheckboxOptions().Tooltip( + "When the Great Fairy's Sword is held, pressing B attacks with it instead of drawing " + "your equipped sword. The sword can still be put away with A as normal.")); path.column = SECTION_COLUMN_2; AddWidget(path, "Modes", WIDGET_SEPARATOR_TEXT); @@ -1390,6 +1396,12 @@ void BenMenu::AddEnhancements() { .CVar("gEnhancements.Timesavers.SkipBalladOfWindfish") .Options(CheckboxOptions().Tooltip( "Play the complete Ballad after playing in one form if you have all three transformation masks.")); + AddWidget(path, "Auto Bank Deposit", WIDGET_CVAR_CHECKBOX) + .CVar("gEnhancements.Timesavers.AutoBankDeposit") + .Options(CheckboxOptions().Tooltip( + "Automatically deposits excess Rupees into your bank account when your wallet is full. " + "Deposits stop when the bank reaches maximum capacity. " + "Bank rewards are granted automatically. Notifications display deposit amount and new balance.")); // Fixes path = { "Enhancements", "Fixes", SECTION_COLUMN_1 }; @@ -1641,6 +1653,10 @@ void BenMenu::AddEnhancements() { AddWidget(path, "Invincible", WIDGET_CVAR_CHECKBOX) .CVar("gEnhancements.Minigames.BoatArcheryInvincible") .Options(CheckboxOptions().Tooltip("Koume's health does not decrease when hit.")); + AddWidget(path, "Treasure Chest Shop Show Full Maze", WIDGET_CVAR_CHECKBOX) + .CVar("gEnhancements.Minigames.TreasureChestShopShowFullMaze") + .Options(CheckboxOptions().Tooltip("Shows the entire maze layout in the Treasure Chest Shop minigame " + "instead of only revealing tiles near Link.")); path.column = SECTION_COLUMN_3; AddWidget(path, "Other", WIDGET_SEPARATOR_TEXT); diff --git a/mm/2s2h/BenPort.cpp b/mm/2s2h/BenPort.cpp index d913c05920..89ee51a4df 100644 --- a/mm/2s2h/BenPort.cpp +++ b/mm/2s2h/BenPort.cpp @@ -53,6 +53,7 @@ CrowdControl* CrowdControl::Instance; #include "2s2h/GameInteractor/GameInteractor.h" #include "2s2h/Enhancements/Enhancements.h" #include "2s2h/Enhancements/GfxPatcher/AuthenticGfxPatches.h" +#include "2s2h/Enhancements/GfxPatcher/PlayerCustomFlipbooks.h" #include "2s2h/DeveloperTools/DebugConsole.h" #include "2s2h/Rando/Rando.h" #include "2s2h/Rando/Spoiler/Spoiler.h" @@ -719,6 +720,7 @@ extern "C" void InitOTR() { OTRMessage_Init(); OTRAudio_Init(); OTRExtScanner(); + PlayerCustomFlipbooks_Patch(); // Just came up with arbitrary numbers that seemed to work, this is // usually set once(?) in currently stubbed out areas of code. diff --git a/mm/2s2h/DeveloperTools/SaveEditor.cpp b/mm/2s2h/DeveloperTools/SaveEditor.cpp index 8fbdcddca0..a4a111b34f 100644 --- a/mm/2s2h/DeveloperTools/SaveEditor.cpp +++ b/mm/2s2h/DeveloperTools/SaveEditor.cpp @@ -2,6 +2,7 @@ #include "2s2h/BenGui/UIWidgets.hpp" #include "2s2h/GameInteractor/GameInteractor.h" #include "2s2h/Rando/Rando.h" +#include "2s2h/Rando/MiscBehavior/ClockShuffle.h" #include "2s2h/CustomMessage/CustomMessage.h" #include "2s2h/CustomItem/CustomItem.h" #include "2s2h/BenGui/Notification.h" @@ -896,6 +897,72 @@ void DrawItemsAndMasksTab() { UIWidgets::Checkbox("Safe Mode", &safeMode); if (gSaveContext.save.shipSaveInfo.saveType == SAVETYPE_RANDO) { + if (RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE]) { + // Time Items Management Section + ImGui::SeparatorText("Time Items"); + + // Individual time items in 3x2 grid with static positioning + RandoItemId clockItems[] = { RI_TIME_DAY_1, RI_TIME_DAY_2, RI_TIME_DAY_3, + RI_TIME_NIGHT_1, RI_TIME_NIGHT_2, RI_TIME_NIGHT_3 }; + + const char* clockNames[] = { "Day 1", "Day 2", "Day 3", "Night 1", "Night 2", "Night 3" }; + + // Use table for static positioning - 3 columns, 2 rows + if (ImGui::BeginTable("ClockItemsTable", 3, ImGuiTableFlags_None)) { + ImGui::TableSetupColumn("Day 1", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Day 2", ImGuiTableColumnFlags_WidthStretch); + ImGui::TableSetupColumn("Day 3", ImGuiTableColumnFlags_WidthStretch); + + // First row - Day items + ImGui::TableNextRow(); + for (int i = 0; i < 3; i++) { + ImGui::TableNextColumn(); + RandoItemId clockItem = clockItems[i]; + int halfIndex = Rando::ClockItems::GetHalfDayIndexFromClockItem(clockItem); + bool isOwned = Flags_GetRandoInf(static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + halfIndex)); + + std::string buttonText = + isOwned ? ("Remove " + std::string(clockNames[i])) : ("No Item##" + std::to_string(i)); + std::string tooltipText = ""; + if (!isOwned) { + tooltipText = "You don't own " + std::string(clockNames[i]); + } + UIWidgets::ButtonOptions buttonOpts; + buttonOpts.disabled = !isOwned; + buttonOpts.disabledTooltip = !isOwned ? tooltipText.c_str() : ""; + if (UIWidgets::Button(buttonText.c_str(), buttonOpts)) { + Rando::RemoveItem(clockItem); + } + } + + // Second row - Night items + ImGui::TableNextRow(); + for (int i = 3; i < 6; i++) { + ImGui::TableNextColumn(); + RandoItemId clockItem = clockItems[i]; + int halfIndex = Rando::ClockItems::GetHalfDayIndexFromClockItem(clockItem); + bool isOwned = Flags_GetRandoInf(static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + halfIndex)); + + std::string buttonText = + isOwned ? ("Remove " + std::string(clockNames[i])) : ("No Item##" + std::to_string(i)); + std::string tooltipText = ""; + if (!isOwned) { + tooltipText = "You don't own " + std::string(clockNames[i]); + } + UIWidgets::ButtonOptions buttonOpts; + buttonOpts.disabled = !isOwned; + buttonOpts.disabledTooltip = !isOwned ? tooltipText.c_str() : ""; + if (UIWidgets::Button(buttonText.c_str(), buttonOpts)) { + Rando::RemoveItem(clockItem); + } + } + + ImGui::EndTable(); + } + } + + // Queue Randomizer Item Gives section + ImGui::Spacing(); ImGui::SeparatorText("Queue Randomizer Item Gives"); static ImGuiTextFilter riFilter; diff --git a/mm/2s2h/Enhancements/Equipment/GreatFairySwordBButton.cpp b/mm/2s2h/Enhancements/Equipment/GreatFairySwordBButton.cpp new file mode 100644 index 0000000000..e0aedf291b --- /dev/null +++ b/mm/2s2h/Enhancements/Equipment/GreatFairySwordBButton.cpp @@ -0,0 +1,26 @@ +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/ShipInit.hpp" + +extern "C" { +#include "variables.h" +extern Input* sPlayerControlInput; +} + +#define CVAR_NAME "gEnhancements.Equipment.GreatFairySwordBButton" +#define CVAR CVarGetInteger(CVAR_NAME, 0) + +void RegisterGreatFairySwordBButton() { + COND_VB_SHOULD(VB_GET_ITEM_ON_BUTTON, CVAR, { + Player* player = GET_PLAYER(gPlayState); + EquipSlot slot = (EquipSlot)va_arg(args, int); + ItemId* item = va_arg(args, ItemId*); + + if (slot == EQUIP_SLOT_B && player->transformation == PLAYER_FORM_HUMAN && + player->heldItemId == ITEM_SWORD_GREAT_FAIRY) { + *item = ITEM_SWORD_GREAT_FAIRY; + } + }); +} + +static RegisterShipInitFunc initFunc(RegisterGreatFairySwordBButton, { CVAR_NAME }); diff --git a/mm/2s2h/Enhancements/GfxPatcher/PlayerCustomFlipbooks.cpp b/mm/2s2h/Enhancements/GfxPatcher/PlayerCustomFlipbooks.cpp new file mode 100644 index 0000000000..71717d07d4 --- /dev/null +++ b/mm/2s2h/Enhancements/GfxPatcher/PlayerCustomFlipbooks.cpp @@ -0,0 +1,133 @@ +#include "PlayerCustomFlipbooks.h" +#include "2s2h/BenPort.h" + +extern "C" { +#include "z64player.h" +extern TexturePtr sPlayerEyesTextures[PLAYER_FORM_MAX][PLAYER_EYES_MAX]; +extern TexturePtr sPlayerMouthTextures[PLAYER_FORM_MAX][PLAYER_MOUTH_MAX]; +uint8_t ResourceMgr_FileExists(const char* resName); +} + +static const char* sFDEyesTextures[PLAYER_EYES_MAX] = { + "__OTR__objects/object_link_boy/gLinkFierceDeityEyesOpenTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityEyesHalfTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityEyesClosedTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityEyesRightTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityEyesLeftTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityEyesUpTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityEyesDownTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityEyesWincingTex", +}; + +static const char* sFDMouthTextures[PLAYER_MOUTH_MAX] = { + "__OTR__objects/object_link_boy/gLinkFierceDeityMouthClosedTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityMouthHalfTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityMouthOpenTex", + "__OTR__objects/object_link_boy/gLinkFierceDeityMouthSmileTex", +}; + +static const char* sDekuEyesTextures[PLAYER_EYES_MAX] = { + "__OTR__objects/object_link_nuts/gLinkDekuEyesOpenTex", "__OTR__objects/object_link_nuts/gLinkDekuEyesHalfTex", + "__OTR__objects/object_link_nuts/gLinkDekuEyesClosedTex", "__OTR__objects/object_link_nuts/gLinkDekuEyesRightTex", + "__OTR__objects/object_link_nuts/gLinkDekuEyesLeftTex", "__OTR__objects/object_link_nuts/gLinkDekuEyesUpTex", + "__OTR__objects/object_link_nuts/gLinkDekuEyesDownTex", "__OTR__objects/object_link_nuts/gLinkDekuEyesWincingTex", +}; + +static const char* sDekuMouthTextures[PLAYER_MOUTH_MAX] = { + "__OTR__objects/object_link_nuts/gLinkDekuMouthClosedTex", + "__OTR__objects/object_link_nuts/gLinkDekuMouthHalfTex", + "__OTR__objects/object_link_nuts/gLinkDekuMouthOpenTex", + "__OTR__objects/object_link_nuts/gLinkDekuMouthSmileTex", +}; + +static const char* sGoronMouthTextures[PLAYER_MOUTH_MAX] = { + "__OTR__objects/object_link_goron/gLinkGoronMouthClosedTex", + "__OTR__objects/object_link_goron/gLinkGoronMouthHalfTex", + "__OTR__objects/object_link_goron/gLinkGoronMouthOpenTex", + "__OTR__objects/object_link_goron/gLinkGoronMouthSmileTex", +}; + +static s32 sFacePatchState = 0; + +static void PlayerCustomFlipbooks_PatchOnce(void) { + if (sFacePatchState != 0) { + return; + } + + bool EyesPatch = true; + bool MouthPatch = true; + bool DekuEyesPatch = true; + bool DekuMouthPatch = true; + bool GoronMouthPatch = true; + + for (s32 i = 0; i < PLAYER_EYES_MAX; i++) { + if (!ResourceMgr_FileExists(sFDEyesTextures[i])) { + EyesPatch = false; + break; + } + } + + for (s32 i = 0; i < PLAYER_MOUTH_MAX; i++) { + if (!ResourceMgr_FileExists(sFDMouthTextures[i])) { + MouthPatch = false; + break; + } + } + + for (s32 i = 0; i < PLAYER_EYES_MAX; i++) { + if (!ResourceMgr_FileExists(sDekuEyesTextures[i])) { + DekuEyesPatch = false; + break; + } + } + + for (s32 i = 0; i < PLAYER_MOUTH_MAX; i++) { + if (!ResourceMgr_FileExists(sDekuMouthTextures[i])) { + DekuMouthPatch = false; + break; + } + } + + for (s32 i = 0; i < PLAYER_MOUTH_MAX; i++) { + if (!ResourceMgr_FileExists(sGoronMouthTextures[i])) { + GoronMouthPatch = false; + break; + } + } + + if (EyesPatch) { + for (s32 i = 0; i < PLAYER_EYES_MAX; i++) { + sPlayerEyesTextures[PLAYER_FORM_FIERCE_DEITY][i] = (TexturePtr)sFDEyesTextures[i]; + } + } + + if (MouthPatch) { + for (s32 i = 0; i < PLAYER_MOUTH_MAX; i++) { + sPlayerMouthTextures[PLAYER_FORM_FIERCE_DEITY][i] = (TexturePtr)sFDMouthTextures[i]; + } + } + + if (DekuEyesPatch) { + for (s32 i = 0; i < PLAYER_EYES_MAX; i++) { + sPlayerEyesTextures[PLAYER_FORM_DEKU][i] = (TexturePtr)sDekuEyesTextures[i]; + } + } + + if (DekuMouthPatch) { + for (s32 i = 0; i < PLAYER_MOUTH_MAX; i++) { + sPlayerMouthTextures[PLAYER_FORM_DEKU][i] = (TexturePtr)sDekuMouthTextures[i]; + } + } + + if (GoronMouthPatch) { + for (s32 i = 0; i < PLAYER_MOUTH_MAX; i++) { + sPlayerMouthTextures[PLAYER_FORM_GORON][i] = (TexturePtr)sGoronMouthTextures[i]; + } + } + + sFacePatchState = 1; +} + +void PlayerCustomFlipbooks_Patch(void) { + PlayerCustomFlipbooks_PatchOnce(); +} diff --git a/mm/2s2h/Enhancements/GfxPatcher/PlayerCustomFlipbooks.h b/mm/2s2h/Enhancements/GfxPatcher/PlayerCustomFlipbooks.h new file mode 100644 index 0000000000..dc0fc549da --- /dev/null +++ b/mm/2s2h/Enhancements/GfxPatcher/PlayerCustomFlipbooks.h @@ -0,0 +1,3 @@ +#pragma once + +void PlayerCustomFlipbooks_Patch(void); diff --git a/mm/2s2h/Enhancements/Minigames/TreasureChestShopFullMaze.cpp b/mm/2s2h/Enhancements/Minigames/TreasureChestShopFullMaze.cpp new file mode 100644 index 0000000000..485f377059 --- /dev/null +++ b/mm/2s2h/Enhancements/Minigames/TreasureChestShopFullMaze.cpp @@ -0,0 +1,40 @@ +#include +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/ShipInit.hpp" + +// Re-definitions to avoid modifying source headers +#define TAKARAYA_WALL_ROWS 11 +#define TAKARAYA_WALL_COLUMNS 8 + +extern "C" { +extern f32 sTakarayaWallHeights[TAKARAYA_WALL_ROWS][TAKARAYA_WALL_COLUMNS]; +extern u8 sTakarayaWallStates[TAKARAYA_WALL_ROWS][TAKARAYA_WALL_COLUMNS]; +} + +// Re-definition to avoid modifying source headers +typedef enum { TAKARAYA_WALL_INACTIVE, TAKARAYA_WALL_RISING, TAKARAYA_WALL_FALLING } TakarayaWallCellState; + +#define CVAR_NAME "gEnhancements.Minigames.TreasureChestShopShowFullMaze" +#define CVAR CVarGetInteger(CVAR_NAME, 0) + +static void RegisterTreasureChestShopFullMaze() { + COND_ID_HOOK(OnActorUpdate, ACTOR_OBJ_TAKARAYA_WALL, CVAR, [](Actor* actor) { + if (gSaveContext.timerStates[TIMER_ID_MINIGAME_2] == TIMER_STATE_OFF) { + return; + } + + for (int i = 0; i < TAKARAYA_WALL_ROWS; i++) { + for (int j = 0; j < TAKARAYA_WALL_COLUMNS; j++) { + if (sTakarayaWallHeights[i][j] >= 0.0f) { + if (Math_StepToF(&sTakarayaWallHeights[i][j], 120.0f, 15.0f)) { + sTakarayaWallStates[i][j] = TAKARAYA_WALL_INACTIVE; + } else { + sTakarayaWallStates[i][j] = TAKARAYA_WALL_RISING; + } + } + } + } + }); +} + +static RegisterShipInitFunc initFunc(RegisterTreasureChestShopFullMaze, { CVAR_NAME }); diff --git a/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp b/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp index 01d9e07da2..e725da230c 100644 --- a/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp +++ b/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp @@ -30,7 +30,9 @@ extern "C" int SavingEnhancements_GetSaveEntrance() { for (int i = 0; i < RESPAWN_MODE_MAX; i++) { gSaveContext.save.shipSaveInfo.respawn[i] = gSaveContext.respawn[i]; } - return entranceToSave; + // Daytelop on new game, with Time Shuffle, makes it possible for entranceToSave to be -1. Given that the player + // must be at this entrance in that scenario, just use it as a fallback. + return entranceToSave < 0 ? ENTRANCE(SOUTH_CLOCK_TOWN, 0) : entranceToSave; } else { switch (gPlayState->sceneId) { // Woodfall Temple + Odolwa diff --git a/mm/2s2h/Enhancements/Songs/BetterSongOfDoubleTime.cpp b/mm/2s2h/Enhancements/Songs/BetterSongOfDoubleTime.cpp index 6061fa6c30..612b7082d5 100644 --- a/mm/2s2h/Enhancements/Songs/BetterSongOfDoubleTime.cpp +++ b/mm/2s2h/Enhancements/Songs/BetterSongOfDoubleTime.cpp @@ -3,6 +3,8 @@ #include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h" #include "2s2h/GameInteractor/GameInteractor.h" #include "2s2h/ShipInit.hpp" +#include "2s2h/Rando/MiscBehavior/ClockShuffle.h" +#include "2s2h/CustomMessage/CustomMessage.h" extern "C" { #include "variables.h" @@ -113,7 +115,21 @@ void OnPlayerUpdate(Actor* actor) { } // Pressing A should confirm the song - if (CHECK_BTN_ALL(input->press.button, BTN_A)) { + if (CHECK_BTN_ALL(input->press.button, BTN_A) && gPlayState->msgCtx.msgMode == MSGMODE_NONE) { + // Check if the selected time is owned in ClockShuffle mode + if (!Rando::ClockShuffle::IsTimeOwnedForClockShuffle(sSelectedDay, sSelectedTime)) { + // Play error sound + Audio_PlaySfx(NA_SE_SY_OCARINA_ERROR); + + // Get time description for the error message + std::string timeDescription = + Rando::ClockShuffle::GetTimeDescriptionForMessage(sSelectedDay, sSelectedTime); + + // Show message FIRST + CustomMessage::StartTextbox(timeDescription + " is beyond your reach!"); + return; + } + Audio_PlaySfx_MessageDecide(); gPlayState->msgCtx.ocarinaMode = OCARINA_MODE_APPLY_DOUBLE_SOT; sActivelyChangingTime = false; diff --git a/mm/2s2h/Enhancements/Songs/SkipSoTCutscenes.cpp b/mm/2s2h/Enhancements/Songs/SkipSoTCutscenes.cpp index bd5e283ed7..d06287d94f 100644 --- a/mm/2s2h/Enhancements/Songs/SkipSoTCutscenes.cpp +++ b/mm/2s2h/Enhancements/Songs/SkipSoTCutscenes.cpp @@ -1,6 +1,8 @@ #include #include "2s2h/GameInteractor/GameInteractor.h" #include "2s2h/ShipInit.hpp" +#include "2s2h/Rando/MiscBehavior/ClockShuffle.h" +#include "2s2h/Rando/Logic/Logic.h" extern "C" { #include "functions.h" @@ -27,8 +29,28 @@ void RegisterSkipSoTCutscenes() { // Normally set by EnTest6 gSaveContext.save.eventDayCount = 0; - gSaveContext.save.day = 0; - gSaveContext.save.time = CLOCK_TIME(6, 0) - 1; + + // Set time appropriately if clock shuffle is enabled + if (IS_RANDO && RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE]) { + const int earliestOwnedHalfDay = Rando::ClockItems::FindEarliestOwnedHalfDay(false); + if (earliestOwnedHalfDay != -1) { + bool isDayHalf = (earliestOwnedHalfDay % 2 == 0); + int targetDay = (earliestOwnedHalfDay / 2) + 1; + + if (isDayHalf) { + // Set to previous day at 5:59 AM to trigger day transition + // Vanilla will detect the transition and show DayTelop + gSaveContext.save.day = targetDay - 1; + gSaveContext.save.time = CLOCK_TIME(6, 0) - 1; + } else { + // Night halves: set time directly + Rando::ClockShuffle::SetTimeToHalfDayStart(earliestOwnedHalfDay); + } + } + } else { + gSaveContext.save.day = 0; + gSaveContext.save.time = CLOCK_TIME(6, 0) - 1; + } if (gSaveContext.save.entrance == ENTRANCE(CUTSCENE, 1)) { // Loads to flash back montage before going to Dawn of... in clock town diff --git a/mm/2s2h/Enhancements/Timesavers/AutoBankDeposit.cpp b/mm/2s2h/Enhancements/Timesavers/AutoBankDeposit.cpp new file mode 100644 index 0000000000..713e3af181 --- /dev/null +++ b/mm/2s2h/Enhancements/Timesavers/AutoBankDeposit.cpp @@ -0,0 +1,146 @@ +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/ShipInit.hpp" +#include "2s2h/BenGui/Notification.h" +#include "2s2h/Rando/Rando.h" +#include "2s2h/CustomMessage/CustomMessage.h" +#include "2s2h/CustomItem/CustomItem.h" + +#define CVAR_NAME "gEnhancements.Timesavers.AutoBankDeposit" +#define CVAR CVarGetInteger(CVAR_NAME, 0) + +#define BANK_MAX_CAPACITY 5000 + +static void EmitDepositNotification(s16 depositAmount, s16 newBalance) { + Notification::Options notif = {}; + notif.prefix = "Deposit Amount:"; + notif.prefixColor = ImVec4(0.4f, 0.7f, 1.0f, 1.0f); + notif.message = std::to_string(depositAmount); + notif.messageColor = ImVec4(0.9f, 0.9f, 0.9f, 1.0f); + notif.suffix = "New Balance: " + std::to_string(newBalance); + notif.suffixColor = ImVec4(0.3f, 1.0f, 0.3f, 1.0f); + notif.remainingTime = 6.0f; + Notification::Emit(notif); +} + +static void GrantBankFirstReward() { + SET_WEEKEVENTREG(WEEKEVENTREG_RECEIVED_BANK_WALLET_UPGRADE); + + if (IS_RANDO) { + RANDO_SAVE_CHECKS[RC_CLOCK_TOWN_WEST_BANK_ADULTS_WALLET].eligible = true; + } else { + u32 walletLevel = CUR_UPG_VALUE(UPG_WALLET); + s16 itemDrawId = (walletLevel == 0) ? (s16)GID_WALLET_ADULT : (s16)GID_WALLET_GIANT; + + GameInteractor::Instance->events.emplace_back(GIEventGiveItem{ + .showGetItemCutscene = true, .param = itemDrawId, .giveItem = [](Actor* actor, PlayState* play) { + u32 walletLevel = CUR_UPG_VALUE(UPG_WALLET); + ItemId wallet = (walletLevel == 0) ? ITEM_WALLET_ADULT : ITEM_WALLET_GIANT; + const char* walletName = (walletLevel == 0) ? "Adult's Wallet" : "Giant's Wallet"; + + if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_ITEM_CUTSCENE) { + CustomMessage::SetActiveCustomMessage(std::string("You got ") + walletName + "!", + { .textboxType = 2 }); + } else { + CustomMessage::StartTextbox(std::string("You got ") + walletName + "!\x1C\x02\x10", + { .textboxType = 2 }); + } + + Item_Give(play, wallet); + } }); + } +} + +static void GrantBankInterestReward() { + SET_WEEKEVENTREG(WEEKEVENTREG_59_80); + + if (IS_RANDO) { + RANDO_SAVE_CHECKS[RC_CLOCK_TOWN_WEST_BANK_INTEREST].eligible = true; + } else { + GameInteractor::Instance->events.emplace_back(GIEventGiveItem{ + .showGetItemCutscene = true, .param = GID_RUPEE_BLUE, .giveItem = [](Actor* actor, PlayState* play) { + if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_ITEM_CUTSCENE) { + CustomMessage::SetActiveCustomMessage("You got a Blue Rupee!", { .textboxType = 2 }); + } else { + CustomMessage::StartTextbox("You got a Blue Rupee!\x1C\x02\x10", { .textboxType = 2 }); + } + + Item_Give(play, ITEM_RUPEE_BLUE); + } }); + } +} + +static void GrantBankFinalReward() { + SET_WEEKEVENTREG(WEEKEVENTREG_RECEIVED_BANK_HEART_PIECE); + + if (IS_RANDO) { + RANDO_SAVE_CHECKS[RC_CLOCK_TOWN_WEST_BANK_PIECE_OF_HEART].eligible = true; + } else { + GameInteractor::Instance->events.emplace_back(GIEventGiveItem{ + .showGetItemCutscene = true, .param = GID_HEART_PIECE, .giveItem = [](Actor* actor, PlayState* play) { + if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_ITEM_CUTSCENE) { + CustomMessage::SetActiveCustomMessage("You got a Piece of Heart!", { .textboxType = 2 }); + } else { + CustomMessage::StartTextbox("You got a Piece of Heart!\x1C\x02\x10", { .textboxType = 2 }); + } + + Item_Give(play, ITEM_HEART_PIECE); + } }); + } +} + +static void GrantBankerReward(s16 balanceBeforeDeposit, s16 balanceAfterDeposit) { + bool useCustomThresholds = CVarGetInteger("gEnhancements.DifficultyOptions.LowerBankRewardThresholds", 0); + + s16 walletThreshold = useCustomThresholds ? 100 : 200; + s16 interestThreshold = useCustomThresholds ? 500 : 1000; + s16 heartPieceThreshold = useCustomThresholds ? 1000 : 5000; + + if (balanceBeforeDeposit < walletThreshold && balanceAfterDeposit >= walletThreshold && + !CHECK_WEEKEVENTREG(WEEKEVENTREG_RECEIVED_BANK_WALLET_UPGRADE)) { + GrantBankFirstReward(); + } + + if (balanceBeforeDeposit < interestThreshold && balanceAfterDeposit >= interestThreshold && + !CHECK_WEEKEVENTREG(WEEKEVENTREG_59_80)) { + GrantBankInterestReward(); + } + + if (balanceBeforeDeposit < heartPieceThreshold && balanceAfterDeposit >= heartPieceThreshold && + !CHECK_WEEKEVENTREG(WEEKEVENTREG_RECEIVED_BANK_HEART_PIECE)) { + GrantBankFinalReward(); + } +} + +static void HandleWalletOverflow() { + s16 currentBankBalance = HS_GET_BANK_RUPEES(); + + if (currentBankBalance >= BANK_MAX_CAPACITY) { + return; + } + + s16 spaceInBank = BANK_MAX_CAPACITY - currentBankBalance; + s16 depositAmount = MIN(gSaveContext.rupeeAccumulator, spaceInBank); + + if (depositAmount > 0) { + s16 balanceBeforeDeposit = currentBankBalance; + s16 balanceAfterDeposit = currentBankBalance + depositAmount; + + HS_SET_BANK_RUPEES(balanceAfterDeposit); + gSaveContext.rupeeAccumulator -= depositAmount; + + EmitDepositNotification(depositAmount, balanceAfterDeposit); + GrantBankerReward(balanceBeforeDeposit, balanceAfterDeposit); + } +} + +static void RegisterAutoBankDeposit() { + COND_VB_SHOULD(VB_DISCARD_EXCESS_RUPEES, CVAR, { + HandleWalletOverflow(); + + if (gSaveContext.rupeeAccumulator == 0) { + *should = true; + } + }); +} + +static RegisterShipInitFunc initFunc(RegisterAutoBankDeposit, { CVAR_NAME }); diff --git a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp index 05ade479ce..15a2d8ec09 100644 --- a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp +++ b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp @@ -4,6 +4,7 @@ #include "2s2h/BenGui/UIWidgets.hpp" #include "Rando/Rando.h" #include "Rando/ActorBehavior/Souls.h" +#include "Rando/MiscBehavior/ClockShuffle.h" #include "2s2h/ShipUtils.h" #include @@ -89,6 +90,53 @@ extern TrackerImageObject GetTextureObject(int16_t itemId, bool isRandoItem) { case RI_SOUL_BOSS_MAJORA: case RI_SOUL_BOSS_ODOLWA: case RI_SOUL_BOSS_TWINMOLD: + case RI_SOUL_ENEMY_ALIEN: + case RI_SOUL_ENEMY_ARMOS: + case RI_SOUL_ENEMY_BAD_BAT: + case RI_SOUL_ENEMY_BEAMOS: + case RI_SOUL_ENEMY_BOE: + case RI_SOUL_ENEMY_BUBBLE: + case RI_SOUL_ENEMY_CAPTAIN_KEETA: + case RI_SOUL_ENEMY_CHUCHU: + case RI_SOUL_ENEMY_DEATH_ARMOS: + case RI_SOUL_ENEMY_DEEP_PYTHON: + case RI_SOUL_ENEMY_DEKU_BABA: + case RI_SOUL_ENEMY_DEXIHAND: + case RI_SOUL_ENEMY_DINOLFOS: + case RI_SOUL_ENEMY_DODONGO: + case RI_SOUL_ENEMY_DRAGONFLY: + case RI_SOUL_ENEMY_EENO: + case RI_SOUL_ENEMY_EYEGORE: + case RI_SOUL_ENEMY_FREEZARD: + case RI_SOUL_ENEMY_GARO: + case RI_SOUL_ENEMY_GEKKO: + case RI_SOUL_ENEMY_GIANT_BEE: + case RI_SOUL_ENEMY_GOMESS: + case RI_SOUL_ENEMY_GUAY: + case RI_SOUL_ENEMY_HIPLOOP: + case RI_SOUL_ENEMY_IGOS_DU_IKANA: + case RI_SOUL_ENEMY_IRON_KNUCKLE: + case RI_SOUL_ENEMY_KEESE: + case RI_SOUL_ENEMY_LEEVER: + case RI_SOUL_ENEMY_LIKE_LIKE: + case RI_SOUL_ENEMY_MAD_SCRUB: + case RI_SOUL_ENEMY_NEJIRON: + case RI_SOUL_ENEMY_OCTOROK: + case RI_SOUL_ENEMY_PEAHAT: + case RI_SOUL_ENEMY_PIRATE: + case RI_SOUL_ENEMY_POE: + case RI_SOUL_ENEMY_REDEAD: + case RI_SOUL_ENEMY_SHELLBLADE: + case RI_SOUL_ENEMY_SKULLFISH: + case RI_SOUL_ENEMY_SKULLTULA: + case RI_SOUL_ENEMY_SNAPPER: + case RI_SOUL_ENEMY_STALCHILD: + case RI_SOUL_ENEMY_TAKKURI: + case RI_SOUL_ENEMY_TEKTITE: + case RI_SOUL_ENEMY_WALLMASTER: + case RI_SOUL_ENEMY_WART: + case RI_SOUL_ENEMY_WIZROBE: + case RI_SOUL_ENEMY_WOLFOS: itemObtained = Flags_GetRandoInf(SOUL_RI_TO_RANDO_INF(itemId)); break; case RI_TINGLE_MAP_CLOCK_TOWN: @@ -109,6 +157,22 @@ extern TrackerImageObject GetTextureObject(int16_t itemId, bool isRandoItem) { case RI_TINGLE_MAP_STONE_TOWER: itemObtained = CHECK_WEEKEVENTREG(WEEKEVENTREG_TINGLE_MAP_BOUGHT_STONE_TOWER); break; + case RI_TIME_DAY_1: + case RI_TIME_DAY_2: + case RI_TIME_DAY_3: + randoImageObject.textureColor = ImVec4(1.0f, 0.9f, 0.3f, 1.0f); // Yellow/gold for sun + itemObtained = Flags_GetRandoInf( + static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + + Rando::ClockItems::GetHalfDayIndexFromClockItem((RandoItemId)itemId))); + break; + case RI_TIME_NIGHT_1: + case RI_TIME_NIGHT_2: + case RI_TIME_NIGHT_3: + randoImageObject.textureColor = ImVec4(0.5f, 0.7f, 1.0f, 1.0f); // Light blue for moon + itemObtained = Flags_GetRandoInf( + static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + + Rando::ClockItems::GetHalfDayIndexFromClockItem((RandoItemId)itemId))); + break; case RI_TRIFORCE_PIECE: itemObtained = gSaveContext.save.shipSaveInfo.rando.foundTriforcePieces > 0; break; diff --git a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp index 96a35a3a8e..dec6e019dc 100644 --- a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp +++ b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp @@ -27,7 +27,7 @@ std::vector listOrder = { }; std::vector randoListOrder = { - "Frogs", "Boss Souls", "Enemy Souls", "Owl Statues", "Tingle Maps", "Misc", + "Frogs", "Boss Souls", "Enemy Souls", "Owl Statues", "Time", "Tingle Maps", "Misc", }; std::map> defaultItemLists = { @@ -47,6 +47,7 @@ std::map> randoItemLists = { { "Enemy Souls", { RI_SOUL_ENEMY_ALIEN, RI_SOUL_ENEMY_WOLFOS, 6 } }, { "Owl Statues", { RI_OWL_CLOCK_TOWN_SOUTH, RI_OWL_ZORA_CAPE, 5 } }, { "Tingle Maps", { RI_TINGLE_MAP_CLOCK_TOWN, RI_TINGLE_MAP_WOODFALL, 6 } }, + { "Time", { RI_TIME_DAY_1, RI_TIME_NIGHT_3, 6 } }, { "Misc", { RI_TRIFORCE_PIECE, RI_TRIFORCE_PIECE, 1 } }, }; @@ -250,7 +251,7 @@ void DrawItemList(std::string listName, int columns) { std::vector emptyList; if (listName == "Frogs" || listName == "Boss Souls" || listName == "Enemy Souls" || - listName == "Owl Statues" || listName == "Tingle Maps" || listName == "Misc") { + listName == "Owl Statues" || listName == "Tingle Maps" || listName == "Time" || listName == "Misc") { for (int j = std::get<0>(randoItemLists.at(listName)); j <= std::get<1>(randoItemLists.at(listName)); j++) { ImGui::TableNextColumn(); diff --git a/mm/2s2h/GameInteractor/GameInteractor.cpp b/mm/2s2h/GameInteractor/GameInteractor.cpp index 7acfa594be..c50ee54e34 100644 --- a/mm/2s2h/GameInteractor/GameInteractor.cpp +++ b/mm/2s2h/GameInteractor/GameInteractor.cpp @@ -498,7 +498,9 @@ void ProcessEvents(Actor* actor) { enItem00->actor.destroy = [](Actor* actor, PlayState* play) { if (!(CUSTOM_ITEM_FLAGS & CustomItem::CALLED_ACTION)) { // Event was not handled, requeue it - GameInteractor::Instance->events.push_back(GameInteractor::Instance->currentEvent); + auto lostEvent = GameInteractor::Instance->currentEvent; + GameInteractor::Instance->currentEvent = GIEventNone{}; + GameInteractor::Instance->events.push_back(lostEvent); } }; } else if (auto e = std::get_if(&nextEvent)) { diff --git a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h index 067546d754..0d84558fa3 100644 --- a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h +++ b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h @@ -401,6 +401,14 @@ typedef enum { // - None VB_DISABLE_LETTERBOX, + // #### `result` + // ```c + // gSaveContext.save.saveInfo.playerData.rupees >= CUR_CAPACITY(UPG_WALLET) + // ``` + // #### `args` + // - None + VB_DISCARD_EXCESS_RUPEES, + // #### `result` // ```c // true @@ -637,6 +645,16 @@ typedef enum { // - `PlayerItemAction` VB_GET_ITEM_ACTION_FROM_MASK, + // #### `result` + // #### In `Player_GetItemOnButton`: + // ```c + // item + // ``` + // #### `args` + // - `EquipSlot` + // - `*ItemId` + VB_GET_ITEM_ON_BUTTON, + // #### `result` // ```c // false @@ -1643,6 +1661,14 @@ typedef enum { // - None VB_SAVE_ON_B_BUTTON_IN_PAUSE_MENU, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - None + VB_SCARECROW_DANCE_SET_TIME, + // #### `result` // ```c // gSaveContext.save.saveInfo.inventory.items[ITEM_OCARINA_OF_TIME] == ITEM_NONE @@ -1958,6 +1984,14 @@ typedef enum { // - None VB_THIEF_BIRD_STEAL, + // #### `result` + // ```c + // TIME_UNTIL_MOON_CRASH + // ``` + // #### `args` + // - `*u32` (time variable) + VB_TIME_UNTIL_MOON_CRASH_CALCULATION, + // #### `result` // ```c // true diff --git a/mm/2s2h/PresetManager/PresetManager.cpp b/mm/2s2h/PresetManager/PresetManager.cpp index 63462833bb..1ae653b0ca 100644 --- a/mm/2s2h/PresetManager/PresetManager.cpp +++ b/mm/2s2h/PresetManager/PresetManager.cpp @@ -48,6 +48,11 @@ nlohmann::json defaultsPresetJ = R"( nlohmann::json curatedPresetJ = R"( { "CVars": { + "gAudioEditor": { + "ChildGoronCry": 1, + "LowHpAlarm": 1, + "MuteCarpenterSfx": 1 + }, "gCheats": { "EasyFrameAdvance": 1 }, @@ -61,6 +66,7 @@ nlohmann::json curatedPresetJ = R"( "gEnhancements": { "Cutscenes": { "HideTitleCards": 1, + "SkipEnemyCutscenes": 1, "SkipEntranceCutscenes": 1, "SkipFirstCycle": 1, "SkipGetItemCutscenes": 2, @@ -76,13 +82,16 @@ nlohmann::json curatedPresetJ = R"( "DoNotResetRazorSword": 1, "DoNotResetRupees": 1, "DoNotResetTimeSpeed": 1, - "KeepExpressMail": 1 + "KeepExpressMail": 1, + "OceansideWalletAnyDay": 1, + "StopOceansideSpiderHouseSquatter": 1 }, "Dialogue": { "FastBankSelection": 1, "FastText": 1 }, "DifficultyOptions": { + "GoronRace": 1, "LowerBankRewardThresholds": 1 }, "Dpad": { @@ -128,8 +137,10 @@ nlohmann::json curatedPresetJ = R"( "HoneyAndDarlingDay2": 4, "HoneyAndDarlingDay3": 8, "PowderKegCertification": 1, + "RomaniTargetPractice": 5, "SkipBalladOfWindfish": 1, "SkipHorseRace": 1, + "SkipLittleBeaver": 1, "SwampArcheryScore": 1580, "SwordsmanSchoolScore": 6, "TownArcheryScore": 25 @@ -157,11 +168,13 @@ nlohmann::json curatedPresetJ = R"( "InstantRecall": 1 }, "Restorations": { + "BonkCollision": 1, "ConstantFlipsHops": 1, "OoTFasterSwim": 1, "PowerCrouchStab": 1, "SideRoll": 1, - "TatlISG": 1 + "TatlISG": 1, + "WoodfallMountainAppearance": 1 }, "Saving": { "Autosave": 1, @@ -174,12 +187,15 @@ nlohmann::json curatedPresetJ = R"( "EnableSunsSong": 1, "FasterSongPlayback": 1, "PauseOwlWarp": 1, - "SkipSoTCutscenes": 1 + "SkipSoTCutscenes": 1, + "SkipSoaringCutscene": 1 }, "Timesavers": { "DampeDiggingSkip": 1, + "FastChests": 1, "GalleryTwofer": 1, "MarineLabHP": 1, + "SkipBalladOfWindfish": 1, "SwampBoatSpeed": 1 } }, @@ -246,9 +262,6 @@ nlohmann::json curatedPresetJ = R"( "Timers": { "Mode": 3 } - }, - "gModes": { - "PlayAsKafei": 1 } }, "type": "2S2H_PRESET", diff --git a/mm/2s2h/Rando/ActorBehavior/EnAl.cpp b/mm/2s2h/Rando/ActorBehavior/EnAl.cpp index aac179c049..74bf83e46c 100644 --- a/mm/2s2h/Rando/ActorBehavior/EnAl.cpp +++ b/mm/2s2h/Rando/ActorBehavior/EnAl.cpp @@ -17,6 +17,11 @@ void Rando::ActorBehavior::InitEnAlBehavior() { COND_VB_SHOULD(VB_MADAME_AROMA_ASK_FOR_HELP, IS_RANDO, { *should = !RANDO_SAVE_CHECKS[RC_MAYORS_OFFICE_KAFEIS_MASK].cycleObtained; }); + // "I'm counting on you" + COND_ID_HOOK(OnOpenText, 0x2AA2, IS_RANDO, [](u16* textId, bool* loadFromMessageTable) { + Message_BombersNotebookQueueEvent(gPlayState, BOMBERS_NOTEBOOK_EVENT_MET_MADAME_AROMA); + Message_BombersNotebookQueueEvent(gPlayState, BOMBERS_NOTEBOOK_EVENT_RECEIVED_KAFEIS_MASK); + }); COND_VB_SHOULD(VB_EXEC_MSG_EVENT, IS_RANDO, { u32 cmdId = va_arg(args, u32); @@ -31,16 +36,13 @@ void Rando::ActorBehavior::InitEnAlBehavior() { GetItemId getItemId = (GetItemId)SCRIPT_PACK_16(cmd->itemIdH, cmd->itemIdL); skipCmds.clear(); if (getItemId == GI_MASK_KAFEIS_MASK) { // Mayor's Residence - // Prevents the player from moving freely in case a notebook event message pops afterward - Player_SetupWaitForPutAway(gPlayState, player, Player_SetupTalk); // There is no usable flag for this check, so grant it manually RANDO_SAVE_CHECKS[RC_MAYORS_OFFICE_KAFEIS_MASK].eligible = true; } else { // Express Mail reward /* * We do something a little tricky here. We manually open a textbox with the message that normally * plays after the player receives the reward (0x2B20), then also skip the MsgScript commands to - * open that textbox and wait on it. The Player_SetupWaitForPutAway call above does not work for - * this scenario, as it will softlock. More naive attempts at handling this actor case resulted in + * open that textbox and wait on it. More naive attempts at handling this actor case resulted in * softlocks, not appropriately locking textboxes, duplicate textboxes, or Bombers' Notebook * messages being eaten. The method below handles the intended behavior, both with or without * notebook messages, even if it is a little counterintuitive. diff --git a/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp b/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp index 2411b43d4e..8a5c53a9fb 100644 --- a/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp +++ b/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp @@ -145,143 +145,6 @@ bool checkTrackerShouldShowRow(bool obtained, bool skipped) { return showCheck; } -void CheckTrackerDrawLogicalList() { - std::set reachableRegions = {}; - // Get connected entrances from starting & warp points - Rando::Logic::FindReachableRegions(RR_MAX, reachableRegions); - // Get connected regions from current entrance (TODO: Make this optional) - Rando::Logic::FindReachableRegions(Rando::Logic::GetRegionIdFromEntrance(gSaveContext.save.entrance), - reachableRegions); - - std::vector sortedRegionIds; - for (auto& regionId : reachableRegions) { - sortedRegionIds.push_back(regionId); - } - std::sort(sortedRegionIds.begin(), sortedRegionIds.end(), [](RandoRegionId a, RandoRegionId b) { - return betterSceneIndex[Rando::Logic::Regions[a].sceneId] < betterSceneIndex[Rando::Logic::Regions[b].sceneId]; - }); - - for (RandoRegionId regionId : sortedRegionIds) { - if (CVAR_SCROLL_TO_SCENE && sScrollToTargetEntrance != -1 && - Rando::Logic::GetRegionIdFromEntrance(sScrollToTargetEntrance) == regionId) { - ImGui::SetScrollHereY(0.0f); - sScrollToTargetScene = -1; - sScrollToTargetEntrance = -1; - } - auto& randoRegion = Rando::Logic::Regions[regionId]; - std::vector> availableChecks; - std::vector> availableEvents; - uint32_t obtainedCheckSum = 0; - - for (auto& [randoCheckId, accessLogicFunc] : randoRegion.checks) { - auto& randoStaticCheck = Rando::StaticData::Checks[randoCheckId]; - auto& randoSaveCheck = RANDO_SAVE_CHECKS[randoCheckId]; - if (randoSaveCheck.shuffled && accessLogicFunc.first()) { - if (randoSaveCheck.obtained) { - obtainedCheckSum++; - if (CVAR_HIDE_COLLECTED) { - continue; - } - } - - if (randoSaveCheck.skipped && CVAR_HIDE_SKIPPED) { - continue; - } - - if (!sCheckTrackerFilter.PassFilter(Rando::StaticData::CheckNames[randoCheckId].c_str())) { - continue; - } - - availableChecks.push_back({ randoCheckId, accessLogicFunc.second }); - } - } - - for (auto& event : randoRegion.events) { - if (!RANDO_EVENTS[event.first] && event.second()) { - RANDO_EVENTS[event.first]++; - } - } - - if (availableChecks.size() > 0 || availableEvents.size() > 0) { - std::string regionName = Ship_GetSceneName(randoRegion.sceneId); - if (randoRegion.name != "") { - regionName += " - "; - regionName += randoRegion.name; - } - - regionName += " (" + std::to_string(obtainedCheckSum) + "/" + std::to_string(availableChecks.size()) + ")"; - - ImGui::PushID(regionId); - ImGui::Separator(); - ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0)); - if (sExpandedHeadersState != sExpandedHeadersToggle) { - ImGui::SetNextItemOpen(sExpandedHeadersToggle); - } - if (ImGui::CollapsingHeader(regionName.c_str(), ImGuiTreeNodeFlags_DefaultOpen)) { - ImGui::Indent(20.0f); - if (ImGui::BeginTable("Check Tracker", 2)) { - ImGui::TableSetupColumn("Type", ImGuiTableColumnFlags_WidthFixed, 16.0f); - ImGui::TableSetupColumn("Check"); - ImGui::TableNextColumn(); - for (auto& [name, accessLogicString] : availableEvents) { - ImGui::TableNextColumn(); - ImGui::PushStyleColor(ImGuiCol_Text, UIWidgets::ColorValues.at(UIWidgets::Colors::White)); - ImGui::Text("%s (Event)", name.c_str()); - if (accessLogicString != "") { - UIWidgets::Tooltip(accessLogicString.c_str()); - } - ImGui::PopStyleColor(); - ImGui::TableNextColumn(); - } - for (auto& [checkId, accessLogicString] : availableChecks) { - auto& randoStaticCheck = Rando::StaticData::Checks[checkId]; - auto& randoSaveCheck = RANDO_SAVE_CHECKS[checkId]; - ImGui::PushStyleColor(ImGuiCol_Text, randoSaveCheck.obtained - ? UIWidgets::ColorValues.at(UIWidgets::Colors::Green) - : randoSaveCheck.skipped - ? UIWidgets::ColorValues.at(UIWidgets::Colors::Indigo) - : UIWidgets::ColorValues.at(UIWidgets::Colors::White)); - if (checkTrackerShouldShowRow(randoSaveCheck.obtained, randoSaveCheck.skipped)) { - ImGui::BeginGroup(); - float cursorPosY = ImGui::GetCursorPosY(); - if (randoStaticCheck.randoCheckType == RCTYPE_OWL) { - ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 3.5f); - } - DrawCheckTypeIcon(checkId); - ImGui::TableNextColumn(); - ImGui::SetCursorPosY(cursorPosY); - ImGui::Text("%s", Rando::StaticData::CheckNames[checkId].c_str()); - if (accessLogicString != "") { - UIWidgets::Tooltip(accessLogicString.c_str()); - } - if (randoSaveCheck.obtained) { - ImGui::SameLine(0, 25.0f); - ImGui::Text("(%s)", Rando::StaticData::Items[randoSaveCheck.randoItemId].name); - } else if (randoSaveCheck.skipped) { - ImGui::SameLine(0, 25.0f); - ImGui::Text("(Skipped)"); - } - ImGui::EndGroup(); - ImGui::TableSetBgColor(ImGuiTableBgTarget_RowBg0, ImGui::IsItemHovered() - ? IM_COL32(255, 255, 0, 128) - : IM_COL32(255, 255, 255, 0)); - if (ImGui::IsItemClicked()) { - randoSaveCheck.skipped = !randoSaveCheck.skipped; - } - ImGui::TableNextColumn(); - } - ImGui::PopStyleColor(); - } - ImGui::EndTable(); - } - ImGui::Unindent(20.0f); - } - ImGui::PopStyleColor(); - ImGui::PopID(); - } - } -} - std::unordered_map checksInLogic; static u32 lastFrame = 0; @@ -293,18 +156,53 @@ void RefreshChecksInLogic() { lastFrame = gGameState->frames; checksInLogic.clear(); + // Clear all events so they're re-evaluated fresh each refresh + for (int i = 0; i < RE_MAX; i++) { + RANDO_EVENTS[i] = 0; + } + std::set reachableRegions = { RR_MAX, Rando::Logic::GetRegionIdFromEntrance(gSaveContext.save.entrance), }; - // Get connected entrances from starting & warp points - Rando::Logic::FindReachableRegions(RR_MAX, reachableRegions); - // Get connected regions from current entrance (TODO: Make this optional) - Rando::Logic::FindReachableRegions(Rando::Logic::GetRegionIdFromEntrance(gSaveContext.save.entrance), - reachableRegions); + // Initialize time states using shared function + std::unordered_map regionTimeStates = + Rando::Logic::InitializeRegionTimeStates(RR_MAX); + + // Iteratively explore until no new regions/events discovered + bool changed = true; + while (changed) { + changed = false; + auto prevSize = reachableRegions.size(); + + // Explore from all currently reachable regions + std::set regionsToExplore = reachableRegions; + for (RandoRegionId regionId : regionsToExplore) { + Rando::Logic::FindReachableRegions(regionId, reachableRegions, regionTimeStates); + } + + // Trigger events for newly discovered regions + for (RandoRegionId regionId : reachableRegions) { + auto& randoRegion = Rando::Logic::Regions[regionId]; + Rando::Logic::SetCurrentRegionTime(regionTimeStates, regionId); + + for (auto& event : randoRegion.events) { + if (!RANDO_EVENTS[event.first] && event.second()) { + RANDO_EVENTS[event.first]++; + changed = true; + } + } + } + + if (reachableRegions.size() != prevSize) { + changed = true; + } + } + + // Evaluate checks for all reachable regions for (RandoRegionId regionId : reachableRegions) { auto& randoRegion = Rando::Logic::Regions[regionId]; - std::vector> availableChecks; + Rando::Logic::SetCurrentRegionTime(regionTimeStates, regionId); for (auto& [randoCheckId, accessLogicFunc] : randoRegion.checks) { auto& randoStaticCheck = Rando::StaticData::Checks[randoCheckId]; @@ -313,12 +211,6 @@ void RefreshChecksInLogic() { checksInLogic.insert({ randoCheckId, true }); } } - - for (auto& event : randoRegion.events) { - if (!RANDO_EVENTS[event.first] && event.second()) { - RANDO_EVENTS[event.first]++; - } - } } } @@ -512,11 +404,7 @@ void CheckTrackerWindow::Draw() { ImGui::Text("Total: %s", totalChecksFound().c_str()); ImGui::BeginChild("Checks"); - // if (CVAR_SHOW_LOGIC) { - // CheckTrackerDrawLogicalList(); - // } else { CheckTrackerDrawNonLogicalList(); - // } sExpandedHeadersState = sExpandedHeadersToggle; ImGui::EndChild(); diff --git a/mm/2s2h/Rando/ConvertItem.cpp b/mm/2s2h/Rando/ConvertItem.cpp index ea31ded5bb..f6f6404bd4 100644 --- a/mm/2s2h/Rando/ConvertItem.cpp +++ b/mm/2s2h/Rando/ConvertItem.cpp @@ -1,5 +1,6 @@ #include "Rando/Rando.h" #include "Rando/ActorBehavior/Souls.h" +#include "Rando/MiscBehavior/ClockShuffle.h" #include "2s2h/ShipUtils.h" #include @@ -427,7 +428,64 @@ bool Rando::IsItemObtainable(RandoItemId randoItemId, RandoCheckId randoCheckId) case RI_SOUL_BOSS_MAJORA: case RI_SOUL_BOSS_ODOLWA: case RI_SOUL_BOSS_TWINMOLD: + case RI_SOUL_ENEMY_ALIEN: + case RI_SOUL_ENEMY_ARMOS: + case RI_SOUL_ENEMY_BAD_BAT: + case RI_SOUL_ENEMY_BEAMOS: + case RI_SOUL_ENEMY_BOE: + case RI_SOUL_ENEMY_BUBBLE: + case RI_SOUL_ENEMY_CAPTAIN_KEETA: + case RI_SOUL_ENEMY_CHUCHU: + case RI_SOUL_ENEMY_DEATH_ARMOS: + case RI_SOUL_ENEMY_DEEP_PYTHON: + case RI_SOUL_ENEMY_DEKU_BABA: + case RI_SOUL_ENEMY_DEXIHAND: + case RI_SOUL_ENEMY_DINOLFOS: + case RI_SOUL_ENEMY_DODONGO: + case RI_SOUL_ENEMY_DRAGONFLY: + case RI_SOUL_ENEMY_EENO: + case RI_SOUL_ENEMY_EYEGORE: + case RI_SOUL_ENEMY_FREEZARD: + case RI_SOUL_ENEMY_GARO: + case RI_SOUL_ENEMY_GEKKO: + case RI_SOUL_ENEMY_GIANT_BEE: + case RI_SOUL_ENEMY_GOMESS: + case RI_SOUL_ENEMY_GUAY: + case RI_SOUL_ENEMY_HIPLOOP: + case RI_SOUL_ENEMY_IGOS_DU_IKANA: + case RI_SOUL_ENEMY_IRON_KNUCKLE: + case RI_SOUL_ENEMY_KEESE: + case RI_SOUL_ENEMY_LEEVER: + case RI_SOUL_ENEMY_LIKE_LIKE: + case RI_SOUL_ENEMY_MAD_SCRUB: + case RI_SOUL_ENEMY_NEJIRON: + case RI_SOUL_ENEMY_OCTOROK: + case RI_SOUL_ENEMY_PEAHAT: + case RI_SOUL_ENEMY_PIRATE: + case RI_SOUL_ENEMY_POE: + case RI_SOUL_ENEMY_REDEAD: + case RI_SOUL_ENEMY_SHELLBLADE: + case RI_SOUL_ENEMY_SKULLFISH: + case RI_SOUL_ENEMY_SKULLTULA: + case RI_SOUL_ENEMY_SNAPPER: + case RI_SOUL_ENEMY_STALCHILD: + case RI_SOUL_ENEMY_TAKKURI: + case RI_SOUL_ENEMY_TEKTITE: + case RI_SOUL_ENEMY_WALLMASTER: + case RI_SOUL_ENEMY_WART: + case RI_SOUL_ENEMY_WIZROBE: + case RI_SOUL_ENEMY_WOLFOS: return !Flags_GetRandoInf(SOUL_RI_TO_RANDO_INF(randoItemId)); + case RI_TIME_DAY_1: + case RI_TIME_NIGHT_1: + case RI_TIME_DAY_2: + case RI_TIME_NIGHT_2: + case RI_TIME_DAY_3: + case RI_TIME_NIGHT_3: + return !Flags_GetRandoInf(RANDO_INF_OBTAINED_CLOCK_DAY_1 + + Rando::ClockItems::GetHalfDayIndexFromClockItem(randoItemId)); + case RI_TIME_PROGRESSIVE: + return true; // These items are technically fine to receive again because they don't do anything, but we'll convert them to // ensure it's clear to the player something didn't go wrong. We just simply check the inventory state // Masks @@ -472,6 +530,31 @@ bool Rando::IsItemObtainable(RandoItemId randoItemId, RandoCheckId randoCheckId) RandoItemId Rando::ConvertItem(RandoItemId randoItemId, RandoCheckId randoCheckId) { if (IsItemObtainable(randoItemId, randoCheckId)) { switch (randoItemId) { + case RI_TIME_PROGRESSIVE: { + // Choose the next clock according to mode and current owned half-days + int mode = RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE_PROGRESSIVE]; + + if (mode == RO_CLOCK_SHUFFLE_RANDOM) { + // Random mode should never have progressive items + return RI_JUNK; + } + + // Build list in target order + RandoItemId ascending[] = { RI_TIME_DAY_1, RI_TIME_NIGHT_1, RI_TIME_DAY_2, + RI_TIME_NIGHT_2, RI_TIME_DAY_3, RI_TIME_NIGHT_3 }; + RandoItemId descending[] = { RI_TIME_NIGHT_3, RI_TIME_DAY_3, RI_TIME_NIGHT_2, + RI_TIME_DAY_2, RI_TIME_NIGHT_1, RI_TIME_DAY_1 }; + RandoItemId* order = (mode == RO_CLOCK_SHUFFLE_DESCENDING) ? descending : ascending; + for (int i = 0; i < 6; ++i) { + int halfIndex = Rando::ClockItems::GetHalfDayIndexFromClockItem(order[i]); + if (halfIndex >= 0 && + !Flags_GetRandoInf(static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + halfIndex))) { + return order[i]; + } + } + // All owned; degrade to junk + return RI_JUNK; + } case RI_PROGRESSIVE_BOMB_BAG: if (CUR_UPG_VALUE(UPG_BOMB_BAG) == 0) { return RI_BOMB_BAG_20; diff --git a/mm/2s2h/Rando/DrawFuncs.cpp b/mm/2s2h/Rando/DrawFuncs.cpp index 771d115bdd..4188caef07 100644 --- a/mm/2s2h/Rando/DrawFuncs.cpp +++ b/mm/2s2h/Rando/DrawFuncs.cpp @@ -5,6 +5,7 @@ extern "C" { #include #include "objects/gameplay_keep/gameplay_keep.h" +#include "objects/object_obj_tokeidai/object_obj_tokeidai.h" // Soul Effects #include "src/overlays/actors/ovl_Obj_Moon_Stone/z_obj_moon_stone.h" @@ -76,6 +77,11 @@ Gfx* EnKnight_BuildEmptyDL(GraphicsContext* gfxCtx); // Other Actor Includes /* Minifrog */ #include "objects/object_fr/object_fr.h" +/* Clock */ #include "overlays/actors/ovl_Obj_Tokeidai/z_obj_tokeidai.h" + +// Clock +void ObjTokeidai_RotateOnMinuteChange(ObjTokeidai* thisx, s32 playSfx); +void ObjTokeidai_RotateOnHourChange(ObjTokeidai* thisx, PlayState* play); // clang-format on } @@ -1214,4 +1220,80 @@ extern void DrawMinifrog(RandoItemId randoItemId, Actor* actor) { EnMinifrog_OverrideLimbDraw, EnMinifrogPostLimbDraw, actor); CLOSE_DISPS(gPlayState->state.gfxCtx); -} \ No newline at end of file +} + +extern void DrawClock(RandoItemId randoItemId, Actor* actor) { + OPEN_DISPS(gPlayState->state.gfxCtx); + + ObjTokeidai* clockActor = (ObjTokeidai*)actor; + static u32 lastUpdate = 0; + static f32 yTranslation = 0; + static f32 xRotation = 0; + static int16_t minuteRingOrExteriorGearRotation = 0; + static f32 clockFaceZTranslation = 0; + static int16_t clockFaceRotation = 0; + static int16_t sunMoonPanelRotation = 0; + + switch (randoItemId) { + case RI_TIME_DAY_1: + case RI_TIME_DAY_2: + case RI_TIME_DAY_3: + clockFaceRotation = 0xC000; + sunMoonPanelRotation = 0; + break; + case RI_TIME_NIGHT_1: + case RI_TIME_NIGHT_2: + case RI_TIME_NIGHT_3: + clockFaceRotation = 0; + sunMoonPanelRotation = 0x8000; + break; + case RI_TIME_PROGRESSIVE: + clockFaceRotation = gSaveContext.save.isNight ? 0 : 0xC000; + sunMoonPanelRotation = gSaveContext.save.isNight ? 0x8000 : 0; + break; + } + + if (clockActor != nullptr && clockActor->actor.id == ACTOR_OBJ_TOKEIDAI) { + clockActor->clockTime = gSaveContext.save.time; + + if (gPlayState != NULL && lastUpdate != gPlayState->state.frames) { + lastUpdate = gPlayState->state.frames; + ObjTokeidai_RotateOnMinuteChange(clockActor, true); + ObjTokeidai_RotateOnHourChange(clockActor, gPlayState); + yTranslation = clockActor->yTranslation; + xRotation = clockActor->xRotation; + minuteRingOrExteriorGearRotation = clockActor->minuteRingOrExteriorGearRotation; + clockFaceZTranslation = clockActor->clockFaceZTranslation; + clockFaceRotation = clockActor->clockFaceRotation; + sunMoonPanelRotation = clockActor->sunMoonPanelRotation; + } + } + + Gfx_SetupDL25_Opa(gPlayState->state.gfxCtx); + Matrix_Translate(0.0f, yTranslation, 0.0f, MTXMODE_APPLY); + Matrix_Scale(0.015f, 0.015f, 0.015f, MTXMODE_APPLY); + Matrix_Translate(0.0f, 0.0f, -1791.0f, MTXMODE_APPLY); + Matrix_RotateXS(-xRotation, MTXMODE_APPLY); + Matrix_Translate(0.0f, 0.0f, 1791.0f, MTXMODE_APPLY); + + Matrix_Push(); + Matrix_RotateZS(-minuteRingOrExteriorGearRotation, MTXMODE_APPLY); + MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, gPlayState->state.gfxCtx); + gSPDisplayList(POLY_OPA_DISP++, (Gfx*)gClockTowerMinuteRingDL); + Matrix_Pop(); + + Matrix_Translate(0.0f, 0.0f, clockFaceZTranslation, MTXMODE_APPLY); + MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, gPlayState->state.gfxCtx); + gSPDisplayList(POLY_OPA_DISP++, (Gfx*)gClockTowerClockCenterAndHandDL); + + Matrix_RotateZS(-clockFaceRotation * 2, MTXMODE_APPLY); + MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, gPlayState->state.gfxCtx); + gSPDisplayList(POLY_OPA_DISP++, (Gfx*)gClockTowerClockFaceDL); + + Matrix_Translate(0.0f, -1112.0f, -19.6f, MTXMODE_APPLY); + Matrix_RotateYS(sunMoonPanelRotation, MTXMODE_APPLY); + MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, gPlayState->state.gfxCtx); + gSPDisplayList(POLY_OPA_DISP++, (Gfx*)gClockTowerSunAndMoonPanelDL); + + CLOSE_DISPS(gPlayState->state.gfxCtx); +} diff --git a/mm/2s2h/Rando/DrawFuncs.h b/mm/2s2h/Rando/DrawFuncs.h index 2239189732..99623fdb9d 100644 --- a/mm/2s2h/Rando/DrawFuncs.h +++ b/mm/2s2h/Rando/DrawFuncs.h @@ -66,4 +66,7 @@ void DrawWolfos(); // Other Actor Functions void DrawMinifrog(RandoItemId randoItemId, Actor* actor); +// Clock Function +void DrawClock(RandoItemId randoItemId, Actor* actor); + #endif diff --git a/mm/2s2h/Rando/DrawItem.cpp b/mm/2s2h/Rando/DrawItem.cpp index 12eb2067ff..dd8eae0ace 100644 --- a/mm/2s2h/Rando/DrawItem.cpp +++ b/mm/2s2h/Rando/DrawItem.cpp @@ -556,6 +556,15 @@ void Rando::DrawItem(RandoItemId randoItemId, Actor* actor) { case RI_OWL_ZORA_CAPE: DrawOwlStatue(); break; + case RI_TIME_DAY_1: + case RI_TIME_NIGHT_1: + case RI_TIME_DAY_2: + case RI_TIME_NIGHT_2: + case RI_TIME_DAY_3: + case RI_TIME_NIGHT_3: + case RI_TIME_PROGRESSIVE: + DrawClock(randoItemId, actor); + break; case RI_PROGRESSIVE_LULLABY: case RI_PROGRESSIVE_MAGIC: case RI_PROGRESSIVE_BOW: @@ -661,6 +670,7 @@ void Rando::DrawItem(RandoItemId randoItemId, Actor* actor) { case RI_PROGRESSIVE_MAGIC: case RI_SINGLE_MAGIC: case RI_DOUBLE_MAGIC: + case RI_TIME_PROGRESSIVE: DrawSparkles(randoItemId, actor); break; default: diff --git a/mm/2s2h/Rando/GiveItem.cpp b/mm/2s2h/Rando/GiveItem.cpp index 729789cca5..8778fd5a00 100644 --- a/mm/2s2h/Rando/GiveItem.cpp +++ b/mm/2s2h/Rando/GiveItem.cpp @@ -1,6 +1,7 @@ #include "Rando/Rando.h" #include "Rando/ActorBehavior/Souls.h" #include "Rando/MiscBehavior/MiscBehavior.h" +#include "Rando/MiscBehavior/ClockShuffle.h" extern "C" { #include "variables.h" @@ -259,6 +260,26 @@ void Rando::GiveItem(RandoItemId randoItemId) { case RI_OWL_ZORA_CAPE: Sram_ActivateOwl(OWL_WARP_ZORA_CAPE); break; + case RI_TIME_DAY_1: + case RI_TIME_NIGHT_1: + case RI_TIME_DAY_2: + case RI_TIME_NIGHT_2: + case RI_TIME_DAY_3: + case RI_TIME_NIGHT_3: { + int index = Rando::ClockItems::GetHalfDayIndexFromClockItem(randoItemId); + if (index != Rando::ClockItems::INVALID) { + Flags_SetRandoInf(static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + index)); + } + break; + } + case RI_TIME_PROGRESSIVE: { + // Convert to actual half-day per mode + RandoItemId concrete = Rando::ConvertItem(RI_TIME_PROGRESSIVE); + if (concrete != RI_JUNK) { + Rando::GiveItem(concrete); + } + break; + } case RI_HEART_CONTAINER: case RI_HEART_PIECE: gSaveContext.healthAccumulator = gSaveContext.save.saveInfo.playerData.healthCapacity + 0x10; diff --git a/mm/2s2h/Rando/Logic/GeneratePools.cpp b/mm/2s2h/Rando/Logic/GeneratePools.cpp new file mode 100644 index 0000000000..99b1aac709 --- /dev/null +++ b/mm/2s2h/Rando/Logic/GeneratePools.cpp @@ -0,0 +1,270 @@ +#include "Logic.h" +#include "Rando/MiscBehavior/ClockShuffle.h" +#include +#include + +extern "C" { +#include "variables.h" +#include "ShipUtils.h" +} + +namespace Rando { + +namespace Logic { + +void GeneratePools(RandoSaveInfo& saveInfo, std::vector& checkPool, std::vector& itemPool) { + std::vector startingItems = convertStartingItemsToRandoItemId(saveInfo.randoStartingItems, ","); + + if (saveInfo.randoSaveOptions[RO_STARTING_MAPS_AND_COMPASSES]) { + std::vector MapsAndCompasses = { + RI_GREAT_BAY_COMPASS, RI_GREAT_BAY_MAP, RI_SNOWHEAD_COMPASS, RI_SNOWHEAD_MAP, + RI_STONE_TOWER_COMPASS, RI_STONE_TOWER_MAP, RI_TINGLE_MAP_CLOCK_TOWN, RI_TINGLE_MAP_GREAT_BAY, + RI_TINGLE_MAP_ROMANI_RANCH, RI_TINGLE_MAP_SNOWHEAD, RI_TINGLE_MAP_STONE_TOWER, RI_TINGLE_MAP_WOODFALL, + RI_WOODFALL_COMPASS, RI_WOODFALL_MAP, + }; + + for (RandoItemId itemId : MapsAndCompasses) { + startingItems.push_back(itemId); + } + } + + std::vector excludedChecks; + std::string excludedChecksList = CVarGetString("gRando.ExcludedChecks", ""); + std::string word; + std::istringstream stream(excludedChecksList); + while (std::getline(stream, word, ',')) { + excludedChecks.push_back((RandoCheckId)std::stoi(word)); + } + + // First loop through all regions and add checks/items to the pool + for (auto& [randoRegionId, randoRegion] : Rando::Logic::Regions) { + for (auto& [randoCheckId, _] : randoRegion.checks) { + auto& randoStaticCheck = Rando::StaticData::Checks[randoCheckId]; + + // Initialize the check with it's vanilla item + if (randoStaticCheck.randoCheckId != RC_UNKNOWN) { + saveInfo.randoSaveChecks[randoCheckId].randoItemId = randoStaticCheck.randoItemId; + } + + // Skip checks that are already in the pool + if (std::find(checkPool.begin(), checkPool.end(), randoCheckId) != checkPool.end()) { + continue; + } + + // TODO: We may never shuffle these 2 pots, leaving this decision for later + if (randoStaticCheck.sceneId == SCENE_LAST_BS) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_SKULL_TOKEN && + saveInfo.randoSaveOptions[RO_SHUFFLE_GOLD_SKULLTULAS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_OWL && + saveInfo.randoSaveOptions[RO_SHUFFLE_OWL_STATUES] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_POT && + saveInfo.randoSaveOptions[RO_SHUFFLE_POT_DROPS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_CRATE && + saveInfo.randoSaveOptions[RO_SHUFFLE_CRATE_DROPS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_BARREL && + saveInfo.randoSaveOptions[RO_SHUFFLE_BARREL_DROPS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_GRASS && + saveInfo.randoSaveOptions[RO_SHUFFLE_GRASS_DROPS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_FREESTANDING && + saveInfo.randoSaveOptions[RO_SHUFFLE_FREESTANDING_ITEMS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_SNOWBALL && + saveInfo.randoSaveOptions[RO_SHUFFLE_SNOWBALL_DROPS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_FROG && + saveInfo.randoSaveOptions[RO_SHUFFLE_FROGS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_REMAINS && + saveInfo.randoSaveOptions[RO_SHUFFLE_BOSS_REMAINS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_COW && + saveInfo.randoSaveOptions[RO_SHUFFLE_COWS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_ENEMY_DROP && + saveInfo.randoSaveOptions[RO_SHUFFLE_ENEMY_DROPS] == RO_GENERIC_NO) { + continue; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_TINGLE_SHOP && + saveInfo.randoSaveOptions[RO_SHUFFLE_TINGLE_SHOPS] == RO_GENERIC_NO) { + continue; + } else { + int price = Ship_Random(0, 200); + saveInfo.randoSaveChecks[randoCheckId].price = price; + } + + if (randoStaticCheck.randoCheckType == RCTYPE_SHOP) { + // We always want shuffle RC_CURIOSITY_SHOP_SPECIAL_ITEM & + // RC_BOMB_SHOP_ITEM_04_OR_CURIOSITY_SHOP_ITEM + if (saveInfo.randoSaveOptions[RO_SHUFFLE_SHOPS] == RO_GENERIC_NO && + randoCheckId != RC_CURIOSITY_SHOP_SPECIAL_ITEM && + randoCheckId != RC_BOMB_SHOP_ITEM_04_OR_CURIOSITY_SHOP_ITEM) { + continue; + } else { + // We may come up with a better solution for this in the future, but for now we choose a + // random price ahead of time, logic will account for whatever price we choose + int price = Ship_Random(0, 200); + saveInfo.randoSaveChecks[randoCheckId].price = price; + } + } + + // When a check is skipped, we still want to add it's vanilla item to the pool, but we don't add the check. + // Mark it as skipped and set it to junk. These leaves an inbalance in the pools that will get sorted + // automatically if there is enough space. + if (saveInfo.randoSaveOptions[RO_LOGIC] != RO_LOGIC_VANILLA) { + auto it = std::find(excludedChecks.begin(), excludedChecks.end(), randoCheckId); + if (it != excludedChecks.end()) { + itemPool.push_back(randoStaticCheck.randoItemId); + + saveInfo.randoSaveChecks[randoCheckId].shuffled = true; + saveInfo.randoSaveChecks[randoCheckId].randoItemId = RI_JUNK; + saveInfo.randoSaveChecks[randoCheckId].skipped = true; + continue; + } + } + + checkPool.emplace_back(randoCheckId); + itemPool.push_back(randoStaticCheck.randoItemId); + } + } + + // Add sword and shield to the pool because they don't have a vanilla location, if you are starting with + // them they will be removed from the pool in the next step + itemPool.push_back(RI_PROGRESSIVE_SWORD); + itemPool.push_back(RI_SHIELD_HERO); + + // Add other items that don't have a vanilla location like Sun's Song or Song of Double Time + + // Boss Souls + if (saveInfo.randoSaveOptions[RO_SHUFFLE_BOSS_SOULS] == RO_GENERIC_YES) { + for (int i = RI_SOUL_BOSS_GOHT; i <= RI_SOUL_BOSS_TWINMOLD; i++) { + if (i == RI_SOUL_BOSS_MAJORA && saveInfo.randoSaveOptions[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) { + continue; + } + itemPool.push_back((RandoItemId)i); + } + } + + // Enemy Souls + if (saveInfo.randoSaveOptions[RO_SHUFFLE_ENEMY_SOULS] == RO_GENERIC_YES) { + for (int i = RI_SOUL_ENEMY_ALIEN; i <= RI_SOUL_ENEMY_WOLFOS; i++) { + itemPool.push_back((RandoItemId)i); + } + } + + // Initialize shuffle time settings and item pool + ClockShuffle::InitializeFileClocks(itemPool); + + // Abilities + if (saveInfo.randoSaveOptions[RO_SHUFFLE_SWIM] == RO_GENERIC_YES) { + itemPool.push_back(RI_ABILITY_SWIM); + } + + // Shuffle Triforce Pieces into the Pool + if (saveInfo.randoSaveOptions[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) { + int piecesToShuffle = saveInfo.randoSaveOptions[RO_TRIFORCE_PIECES_MAX]; + while (piecesToShuffle) { + itemPool.push_back(RI_TRIFORCE_PIECE); + piecesToShuffle--; + } + } + + // Remove starting items from the pool (but only one per entry in startingItems) + for (RandoItemId startingItem : startingItems) { + auto it = std::find(itemPool.begin(), itemPool.end(), startingItem); + if (it != itemPool.end()) { + itemPool.erase(it); + } + } + + // Plentiful + if (saveInfo.randoSaveOptions[RO_PLENTIFUL_ITEMS] == RO_GENERIC_YES) { + int replaceableItems = 0; + std::vector plentifulItems; + std::vector potentialPlentifulItems; + for (size_t i = 0; i < itemPool.size(); i++) { + // The user can specify exactly how many pieces they want to shuffle, so skip those + if (itemPool[i] == RI_TRIFORCE_PIECE) { + continue; + } + + switch (Rando::StaticData::Items[itemPool[i]].randoItemType) { + case RITYPE_BOSS_KEY: + case RITYPE_SMALL_KEY: + case RITYPE_MASK: + case RITYPE_MAJOR: + plentifulItems.push_back(itemPool[i]); + break; + case RITYPE_LESSER: + case RITYPE_SKULLTULA_TOKEN: + case RITYPE_STRAY_FAIRY: + if (Ship_Random(0, 2) == 1) { + potentialPlentifulItems.push_back(itemPool[i]); + } + break; + case RITYPE_HEALTH: + case RITYPE_JUNK: + default: + replaceableItems++; + break; + } + } + + if (replaceableItems > plentifulItems.size()) { + for (RandoItemId plentifulItem : plentifulItems) { + itemPool.push_back(plentifulItem); + } + } + + // Only add potentialPlentifulItems if we think we have enough room (this might not be perfect) + if ((replaceableItems - plentifulItems.size() - 10) > potentialPlentifulItems.size()) { + for (RandoItemId plentifulItem : potentialPlentifulItems) { + itemPool.push_back(plentifulItem); + } + } + } + + // Traps + if (saveInfo.randoSaveOptions[RO_SHUFFLE_TRAPS] == RO_GENERIC_YES) { + int trapsToShuffle = saveInfo.randoSaveOptions[RO_TRAP_AMOUNT]; + while (trapsToShuffle) { + itemPool.push_back(RI_TRAP); + trapsToShuffle--; + } + } +} + +} // namespace Logic + +} // namespace Rando diff --git a/mm/2s2h/Rando/Logic/GlitchlessLogic.cpp b/mm/2s2h/Rando/Logic/GlitchlessLogic.cpp index 940fe3ad6a..f92c6528cd 100644 --- a/mm/2s2h/Rando/Logic/GlitchlessLogic.cpp +++ b/mm/2s2h/Rando/Logic/GlitchlessLogic.cpp @@ -25,6 +25,9 @@ void ApplyGlitchlessLogicToSaveContext(std::vector& checkPool, std std::map checksInLogic; std::set>*> eventsInLogic; + // Initialize time states using shared function + std::unordered_map regionTimeStates = InitializeRegionTimeStates(RR_MAX); + RandoCheckId checkWithJunk = RC_UNKNOWN; std::set nonJunkItemsThatWeHaveTried; std::vector checksWithJunk; @@ -68,7 +71,7 @@ void ApplyGlitchlessLogicToSaveContext(std::vector& checkPool, std // Crawl through all reachable regions and add any new reachable regions auto prevRegionsInLogicSize = regionsInLogic.size(); for (RandoRegionId regionId : regionsInLogic) { - FindReachableRegions(regionId, regionsInLogic); + FindReachableRegions(regionId, regionsInLogic, regionTimeStates); } if (regionsInLogic.size() != prevRegionsInLogicSize) { regionsInLogicChanged = true; @@ -77,18 +80,31 @@ void ApplyGlitchlessLogicToSaveContext(std::vector& checkPool, std for (RandoRegionId regionId : regionsInLogic) { auto& randoRegion = Regions[regionId]; + // Set current region time for check evaluation + SetCurrentRegionTime(regionTimeStates, regionId); + // Apply any new events for (auto& randoEvent : randoRegion.events) { - if (!eventsInLogic.contains(&randoEvent) && randoEvent.second()) { - RANDO_EVENTS[randoEvent.first]++; - eventsInLogic.insert(&randoEvent); - eventsInLogicChanged = true; + // When Clock Shuffle is active, always check events (don't skip based on eventsInLogic) + bool skipEventCheck = !SettingClocks() && eventsInLogic.contains(&randoEvent); + + if (!skipEventCheck && randoEvent.second()) { + // Only increment if not already triggered + if (!eventsInLogic.contains(&randoEvent)) { + RANDO_EVENTS[randoEvent.first]++; + eventsInLogic.insert(&randoEvent); + eventsInLogicChanged = true; + } } } // Apply any new checks for (auto& [randoCheckId, checkLogic] : randoRegion.checks) { if (checksInLogic.find(randoCheckId) == checksInLogic.end() && checkLogic.first()) { + // VALIDATION: Verify check is reachable with owned time + TimeLogic::ValidateRegionTimeOwnership(regionId, randoCheckId, + regionTimeStates[regionId].timeSlices, "Glitchless"); + auto it = std::find(checkPool.begin(), checkPool.end(), randoCheckId); bool isShuffled = it != checkPool.end(); checksInLogic.insert({ randoCheckId, isShuffled }); @@ -98,17 +114,7 @@ void ApplyGlitchlessLogicToSaveContext(std::vector& checkPool, std RandoItemId randoItemId; - if (RANDO_SAVE_CHECKS[randoCheckId].skipped) { - uint32_t index = 0; - for (auto& item : itemPool) { - if (Rando::StaticData::Items[item].randoItemType == RITYPE_JUNK) { - randoItemId = item; - itemPool.erase(itemPool.begin() + index); - break; - } - index++; - } - } else if (isShuffled) { + if (isShuffled) { randoItemId = itemPool.back(); itemPool.pop_back(); @@ -126,6 +132,28 @@ void ApplyGlitchlessLogicToSaveContext(std::vector& checkPool, std RANDO_SAVE_CHECKS[randoCheckId].randoItemId = randoItemId; RANDO_SAVE_CHECKS[randoCheckId].shuffled = isShuffled; GiveItem(ConvertItem(randoItemId)); + + // Update time states for all regions when time items are obtained + if (randoItemId >= RI_TIME_DAY_1 && randoItemId <= RI_TIME_PROGRESSIVE) { + uint64_t newTimeSlices = TimeLogic::GetOwnedTimeSlices(); + // Update RR_MAX time state first - this is the source for new region discoveries + if (regionTimeStates.find(RR_MAX) != regionTimeStates.end()) { + regionTimeStates[RR_MAX].timeSlices = newTimeSlices; + } + // Update existing region time states to reflect new owned time + for (auto& [regionId, timeState] : regionTimeStates) { + timeState.timeSlices = newTimeSlices; + // Expand time forward based on region's stay restrictions + if (timeState.canStayOverTime) { + timeState.timeSlices = TimeLogic::ExpandTimeForward(newTimeSlices, Regions[regionId]); + } + } + // Trigger region re-exploration to discover new regions accessible with expanded time + // Also trigger event re-evaluation since time-gated events may now be accessible + regionsInLogicChanged = true; + eventsInLogicChanged = true; + } + checksInLogicChanged = true; } } diff --git a/mm/2s2h/Rando/Logic/Logic.cpp b/mm/2s2h/Rando/Logic/Logic.cpp index b763975571..b72dde87a1 100644 --- a/mm/2s2h/Rando/Logic/Logic.cpp +++ b/mm/2s2h/Rando/Logic/Logic.cpp @@ -9,6 +9,9 @@ namespace Logic { std::map Regions = {}; +// Thread-local storage for current region time during check evaluation +thread_local uint64_t gCurrentRegionTime = 0; + RandoRegionId GetRegionIdFromEntrance(s32 entrance) { static std::map entranceToRegionId; if (entranceToRegionId.empty()) { @@ -32,23 +35,102 @@ RandoRegionId GetRegionIdFromEntrance(s32 entrance) { return RR_MAX; } -void FindReachableRegions(RandoRegionId currentRegion, std::set& reachableRegions) { - auto& randoRegion = Rando::Logic::Regions[currentRegion]; +// Helper: Convert runtime game time to TimeSlice enum for dynamic time checking +TimeSlice TimeSliceFromGameTime(s32 day, u16 time) { + // Handle edge cases: day 0 or invalid inputs + if (day < 1 || day > 3) { + return TIME_DAY1_AM_06_00; // Default fallback + } + + // Convert to time slice based on day/time ranges + // This is approximate - exact mapping would need game time constants + bool isNight = (time >= GAME_TIME_NIGHT_START || time < GAME_TIME_DAY_START); + int halfDayOffset = (day - 1) * 2 + (isNight ? 1 : 0); + + // Map to approximate time slice within the half-day + if (halfDayOffset >= 6) + return TIME_NIGHT3_AM_05_00; + + const auto& range = HALF_DAY_TIME_RANGES[halfDayOffset]; + return static_cast(range.startSlice); +} + +// Helper: Returns the initial time state for logic solving (start at Day 1, 6:00 AM) +RegionTimeState InitialTimeState() { + return { .timeSlices = (TIME_BIT_ONE << TIME_DAY1_AM_06_00), .canStayOverTime = false }; +} + +// Shared initialization function for region time states +std::unordered_map InitializeRegionTimeStates(RandoRegionId startRegion) { + std::unordered_map states; + + // Start with appropriate time based on Clock Shuffle + if (SettingClocks()) { + // Clock Shuffle: start with owned time slices only + states[startRegion] = { .timeSlices = TimeLogic::GetOwnedTimeSlices(), .canStayOverTime = false }; + } else { + // No Clock Shuffle: start at Day 1 6am + states[startRegion] = InitialTimeState(); + } + + return states; +} - for (auto& [connectedRegionId, condition] : randoRegion.connections) { - // Check if the region is accessible and hasn’t been visited yet +// Helper to ensure region time state exists +void EnsureRegionTimeState(std::unordered_map& regionTimeStates, + RandoRegionId regionId) { + if (regionTimeStates.find(regionId) == regionTimeStates.end()) { + auto& region = Regions[regionId]; + regionTimeStates[regionId] = { .timeSlices = TimeLogic::GetOwnedTimeSlices(), + .canStayOverTime = region.canStayOverTime }; + } +} + +// Time expansion during region traversal with stay restrictions +// Time expansion semantics: if canStayOverTime, sequentially test each future time slice +// Stop permanently if any timeStayRestrictions check fails +void FindReachableRegions(RandoRegionId currentRegion, std::set& reachableRegions, + std::unordered_map& regionTimeStates) { + // Ensure current region has time state + EnsureRegionTimeState(regionTimeStates, currentRegion); + + auto& sourceRegion = Regions[currentRegion]; + auto& sourceTimeState = regionTimeStates[currentRegion]; + + // Expand time if player can wait in this region + uint64_t currentTime = sourceTimeState.timeSlices; + if (sourceTimeState.canStayOverTime) { + currentTime = TimeLogic::ExpandTimeForward(currentTime, sourceRegion); + sourceTimeState.timeSlices = currentTime; + } + + // Set global time for check evaluation + gCurrentRegionTime = currentTime; + + // Explore connections + for (auto& [connectedRegionId, condition] : sourceRegion.connections) { if (reachableRegions.count(connectedRegionId) == 0 && condition.first()) { - reachableRegions.insert(connectedRegionId); // Mark region as visited - FindReachableRegions(connectedRegionId, reachableRegions); // Recursively visit neighbors + reachableRegions.insert(connectedRegionId); + + auto& targetRegion = Regions[connectedRegionId]; + regionTimeStates[connectedRegionId] = { .timeSlices = currentTime, + .canStayOverTime = targetRegion.canStayOverTime }; + + FindReachableRegions(connectedRegionId, reachableRegions, regionTimeStates); } } - for (auto& [exitId, regionExit] : randoRegion.exits) { + // Explore exits + for (auto& [exitId, regionExit] : sourceRegion.exits) { RandoRegionId connectedRegionId = GetRegionIdFromEntrance(exitId); - // Check if the region is accessible and hasn’t been visited yet if (reachableRegions.count(connectedRegionId) == 0 && regionExit.condition()) { - reachableRegions.insert(connectedRegionId); // Mark region as visited - FindReachableRegions(connectedRegionId, reachableRegions); // Recursively visit neighbors + reachableRegions.insert(connectedRegionId); + + auto& targetRegion = Regions[connectedRegionId]; + regionTimeStates[connectedRegionId] = { .timeSlices = currentTime, + .canStayOverTime = targetRegion.canStayOverTime }; + + FindReachableRegions(connectedRegionId, reachableRegions, regionTimeStates); } } } diff --git a/mm/2s2h/Rando/Logic/Logic.h b/mm/2s2h/Rando/Logic/Logic.h index f5939b88ab..f22e7bdd6a 100644 --- a/mm/2s2h/Rando/Logic/Logic.h +++ b/mm/2s2h/Rando/Logic/Logic.h @@ -18,8 +18,114 @@ namespace Rando { namespace Logic { -void FindReachableRegions(RandoRegionId currentRegion, std::set& reachableRegions); +// Time slice enum - 45 granular time points throughout MM's 3-day cycle +enum TimeSlice { + TIME_DAY1_AM_06_00 = 0, + TIME_DAY1_AM_07_00, + TIME_DAY1_AM_08_00, + TIME_DAY1_AM_10_00, + TIME_DAY1_PM_01_45, + TIME_DAY1_PM_03_00, + TIME_DAY1_PM_04_00, + TIME_NIGHT1_PM_06_00, + TIME_NIGHT1_PM_08_00, + TIME_NIGHT1_PM_09_00, + TIME_NIGHT1_PM_10_00, + TIME_NIGHT1_PM_11_00, + TIME_NIGHT1_AM_12_00, + TIME_NIGHT1_AM_02_30, + TIME_NIGHT1_AM_04_00, + TIME_NIGHT1_AM_05_00, + TIME_DAY2_AM_06_00, + TIME_DAY2_AM_07_00, + TIME_DAY2_AM_08_00, + TIME_DAY2_AM_10_00, + TIME_DAY2_AM_11_30, + TIME_DAY2_PM_02_00, + TIME_DAY2_PM_04_00, + TIME_NIGHT2_PM_06_00, + TIME_NIGHT2_PM_08_00, + TIME_NIGHT2_PM_09_00, + TIME_NIGHT2_PM_10_00, + TIME_NIGHT2_PM_11_00, + TIME_NIGHT2_AM_12_00, + TIME_NIGHT2_AM_04_00, + TIME_NIGHT2_AM_05_00, + TIME_DAY3_AM_06_00, + TIME_DAY3_AM_07_00, + TIME_DAY3_AM_08_00, + TIME_DAY3_AM_10_00, + TIME_DAY3_AM_11_30, + TIME_DAY3_PM_01_00, + TIME_NIGHT3_PM_06_00, + TIME_NIGHT3_PM_08_00, + TIME_NIGHT3_PM_09_00, + TIME_NIGHT3_PM_10_00, + TIME_NIGHT3_PM_11_00, + TIME_NIGHT3_AM_12_00, + TIME_NIGHT3_AM_04_00, + TIME_NIGHT3_AM_05_00 // = 44 +}; + +// Time slice count and bitmask constants +// Derived from enum - update if last enum value changes +constexpr int TIME_SLICE_COUNT = TIME_NIGHT3_AM_05_00 + 1; +constexpr uint64_t TIME_BIT_ONE = 1ULL; // Base value for bit shifting +constexpr uint64_t TIME_ALL_SLICES = 0x1FFFFFFFFFFF; // All 45 time bits set + +// Half-day period definitions for Clock Shuffle +struct HalfDayRange { + int startSlice; + int endSlice; +}; + +// Map half-day indices to their time slice ranges +// Index 0-5 correspond to HALF_DAY1_DAY through HALF_DAY3_NIGHT +constexpr HalfDayRange HALF_DAY_TIME_RANGES[6] = { + { 0, 6 }, // HALF_DAY1_DAY (TIME_DAY1_AM_06_00 to TIME_DAY1_PM_04_00) + { 7, 15 }, // HALF_DAY1_NIGHT (TIME_NIGHT1_PM_06_00 to TIME_NIGHT1_AM_05_00) + { 16, 22 }, // HALF_DAY2_DAY (TIME_DAY2_AM_06_00 to TIME_DAY2_PM_04_00) + { 23, 30 }, // HALF_DAY2_NIGHT (TIME_NIGHT2_PM_06_00 to TIME_NIGHT2_AM_05_00) + { 31, 36 }, // HALF_DAY3_DAY (TIME_DAY3_AM_06_00 to TIME_DAY3_PM_01_00) + { 37, 44 }, // HALF_DAY3_NIGHT (TIME_NIGHT3_PM_06_00 to TIME_NIGHT3_AM_05_00) +}; + +// Game time constants for day/night transitions +constexpr u16 GAME_TIME_DAY_START = 0x4000; // 6:00 AM +constexpr u16 GAME_TIME_NIGHT_START = 0xc000; // 6:00 PM + +// Time state for tracking time accessibility during logic solving +struct RegionTimeState { + uint64_t timeSlices; + bool canStayOverTime; +}; + +// Thread-local current region time for check evaluation +extern thread_local uint64_t gCurrentRegionTime; + +// Helper: Convert runtime game time to TimeSlice enum +TimeSlice TimeSliceFromGameTime(s32 day, u16 time); + +// Helper: Returns the initial time state for logic solving +RegionTimeState InitialTimeState(); + +// Shared initialization function for region time states +std::unordered_map InitializeRegionTimeStates(RandoRegionId startRegion); + +// Helper to ensure region time state exists +void EnsureRegionTimeState(std::unordered_map& regionTimeStates, + RandoRegionId regionId); + +// Helper to set current region time +inline void SetCurrentRegionTime(const std::unordered_map& regionTimeStates, + RandoRegionId regionId) { + gCurrentRegionTime = regionTimeStates.at(regionId).timeSlices; +} + +void FindReachableRegions(RandoRegionId currentRegion, std::set& reachableRegions, + std::unordered_map& regionTimeStates); RandoRegionId GetRegionIdFromEntrance(s32 entrance); +void GeneratePools(RandoSaveInfo& saveInfo, std::vector& checkPool, std::vector& itemPool); void ApplyGlitchlessLogicToSaveContext(std::vector& checkPool, std::vector& itemPool); void ApplyNearlyNoLogicToSaveContext(std::vector& checkPool, std::vector& itemPool); void ApplyNoLogicToSaveContext(std::vector& checkPool, std::vector& itemPool); @@ -38,10 +144,31 @@ struct RandoRegion { std::map, std::string>> connections; std::vector>> events; std::set oneWayEntrances; + + // Time logic fields for Clock Shuffle and time-based region access + uint64_t timeSlices = 0; // Bitfield: accessible time slices (bits 0-44) - unused in current implementation + bool canStayOverTime = true; // Can player wait for time to pass? Set false for dungeons, shops with closing hours + std::unordered_map> + timeStayRestrictions; // Time slices where staying is restricted - use STAY() macro in region definitions }; extern std::map Regions; +// ============================================================================ +// TIME LOGIC NAMESPACE +// ============================================================================ +namespace TimeLogic { +// Core expansion function - sequential time expansion with stay restrictions +uint64_t ExpandTimeForward(uint64_t timeSlices, const RandoRegion& region); + +// Owned time calculation - aggregates all owned half-day time slices +uint64_t GetOwnedTimeSlices(); + +// Validation helper for clock ownership during logic generation +void ValidateRegionTimeOwnership(RandoRegionId regionId, RandoCheckId checkId, uint64_t regionTime, + const char* context); +} // namespace TimeLogic + // TODO: This may not stay here #define IS_DEKU (GET_PLAYER_FORM == PLAYER_FORM_DEKU) #define IS_ZORA (GET_PLAYER_FORM == PLAYER_FORM_ZORA) @@ -59,7 +186,9 @@ extern std::map Regions; #define CHECK_MAX_HP(TARGET_HP) ((TARGET_HP * 16) <= gSaveContext.save.saveInfo.playerData.healthCapacity) #define HAS_MAGIC (gSaveContext.save.saveInfo.playerData.isMagicAcquired) #define CAN_HOOK_SCARECROW (HAS_ITEM(ITEM_OCARINA_OF_TIME) && HAS_ITEM(ITEM_HOOKSHOT)) -#define CAN_USE_EXPLOSIVE ((HAS_ITEM(ITEM_BOMB) || HAS_ITEM(ITEM_BOMBCHU) || HAS_ITEM(ITEM_MASK_BLAST))) +#define CAN_USE_EXPLOSIVE \ + ((HAS_ITEM(ITEM_BOMB) || HAS_ITEM(ITEM_BOMBCHU) || HAS_ITEM(ITEM_MASK_BLAST) || \ + (HAS_ITEM(ITEM_POWDER_KEG) && CAN_BE_GORON))) #define CAN_USE_HUMAN_SWORD (GET_CUR_EQUIP_VALUE(EQUIP_TYPE_SWORD) >= EQUIP_VALUE_SWORD_KOKIRI) #define CAN_USE_SWORD (CAN_USE_HUMAN_SWORD || HAS_ITEM(ITEM_SWORD_GREAT_FAIRY) || CAN_BE_DEITY) // Be careful here, as some checks require you to play the song as a specific form @@ -83,6 +212,10 @@ extern std::map Regions; #define CAN_GROW_BEAN_PLANT \ (HAS_ITEM(ITEM_MAGIC_BEANS) && \ (CAN_PLAY_SONG(STORMS) || (HAS_BOTTLE && (CAN_ACCESS(SPRING_WATER) || CAN_ACCESS(HOT_SPRING_WATER))))) +// Bean patches that auto-water on Day 2 (Doggy Racetrack, Deku Palace Upper) +// Rain only occurs Day 2 from 7:00 AM to 5:30 PM (not Night 2) +// Requires magic beans and (Day 2 OR manual watering) +#define CAN_USE_DAY2_RAIN_BEAN (CAN_GROW_BEAN_PLANT || (HAS_ITEM(ITEM_MAGIC_BEANS) && CLOCK_DAY2())) #define CAN_USE_MAGIC_ARROW(arrowType) (HAS_ITEM(ITEM_BOW) && HAS_ITEM(ITEM_ARROW_##arrowType) && HAS_MAGIC) #define CAN_LIGHT_TORCH_NEAR_ANOTHER (HAS_ITEM(ITEM_DEKU_STICK) || CAN_USE_MAGIC_ARROW(FIRE)) #define KEY_COUNT(dungeon) (gSaveContext.save.shipSaveInfo.rando.foundDungeonKeys[DUNGEON_SCENE_INDEX_##dungeon]) @@ -120,6 +253,16 @@ extern std::map Regions; [] { return condition; }, LogicString(#condition) \ } \ } +// STAY macro for region time restrictions - defines when player can stay in a region over time +// Usage in region definitions: .timeStayRestrictions = { STAY(TIME_NIGHT1_PM_08_00, !HAS_ROOM_KEY) } +// If condition is false at the specified time, player is kicked out (expansion stops permanently) +// Examples: +// STAY(TIME_NIGHT1_PM_08_00, !Flags_GetRandoInf(RANDO_INF_OBTAINED_ROOM_KEY)) // Kicked out without room key +// STAY(TIME_NIGHT3_PM_10_00, false) // Always kicked out at this time (shop closes) +#define STAY(timeSlice, condition) \ + { \ + timeSlice, [] { return condition; } \ + } inline std::string LogicString(std::string condition) { if (condition == "true") @@ -187,6 +330,185 @@ inline bool MeetsMoonRequirements() { MoonMaskCount() >= RANDO_SAVE_OPTIONS[RO_ACCESS_MOON_MASKS_COUNT]; } +// ============================================================================ +// CLOCK OWNERSHIP HELPERS +// ============================================================================ + +inline uint32_t ClockCount() { + uint32_t count = 0; + for (int i = 0; i < 6; ++i) { + if (Flags_GetRandoInf(static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + i))) { + count++; + } + } + return count; +} + +inline bool SettingClocks() { + return RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE] != 0; +} + +// Centralized clock ownership check +inline bool OwnsClockHalfDay(int halfDayIndex) { + if (halfDayIndex < 0 || halfDayIndex >= 6) + return false; + RandoInf clockFlag = static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + halfDayIndex); + return Flags_GetRandoInf(clockFlag); +} + +// New consolidated helper that encapsulates ascending/descending/random logic +inline bool OwnsHalfDayForMode(int halfDayIndex) { + if (!SettingClocks() || halfDayIndex < 0 || halfDayIndex >= 6) { + return !SettingClocks(); // If not shuffling clocks, all time is available + } + + int clockMode = RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE_PROGRESSIVE]; + uint32_t totalClocks = ClockCount(); + + switch (clockMode) { + case RO_CLOCK_SHUFFLE_RANDOM: + // Random mode: check if this specific half-day is owned + return OwnsClockHalfDay(halfDayIndex); + + case RO_CLOCK_SHUFFLE_ASCENDING: + // Ascending: own first N half-days in sequence (0,1,2,3,4,5) + return totalClocks > halfDayIndex; + + case RO_CLOCK_SHUFFLE_DESCENDING: + // Descending: own last N half-days in reverse sequence (5,4,3,2,1,0) + return totalClocks > (5 - halfDayIndex); + + default: + return false; + } +} + +// ============================================================================ +// TIME OPERATOR FUNCTIONS +// ============================================================================ + +inline bool RawAt(TimeSlice slice) { + return (gCurrentRegionTime & (TIME_BIT_ONE << slice)) != 0; +} + +inline bool RawBefore(TimeSlice slice) { + if (slice == 0) + return false; + uint64_t mask = (TIME_BIT_ONE << slice) - 1; + return (gCurrentRegionTime & mask) != 0; +} + +inline bool RawAfter(TimeSlice slice) { + uint64_t mask = ~((TIME_BIT_ONE << slice) - 1) & TIME_ALL_SLICES; + return (gCurrentRegionTime & mask) != 0; +} + +inline bool RawBetween(TimeSlice start, TimeSlice end) { + uint64_t mask = ((TIME_BIT_ONE << end) - 1) & ~((TIME_BIT_ONE << start) - 1); + return (gCurrentRegionTime & mask) != 0; +} + +// Generate bitmask for a half-day period's time slices +inline constexpr uint64_t GetHalfDayTimeMask(int halfDayIndex) { + if (halfDayIndex < 0 || halfDayIndex >= 6) + return 0; + + uint64_t mask = 0; + const auto& range = HALF_DAY_TIME_RANGES[halfDayIndex]; + for (int slice = range.startSlice; slice <= range.endSlice; ++slice) { + mask |= (1ULL << slice); + } + return mask; +} + +// ============================================================================ +// CLOCK ITEM MACROS +// ============================================================================ + +// Simplified clock macros using consolidated helper +#define CLOCK_DAY1() OwnsHalfDayForMode(0) +#define CLOCK_NIGHT1() OwnsHalfDayForMode(1) +#define CLOCK_DAY2() OwnsHalfDayForMode(2) +#define CLOCK_NIGHT2() OwnsHalfDayForMode(3) +#define CLOCK_DAY3() OwnsHalfDayForMode(4) +#define CLOCK_NIGHT3() OwnsHalfDayForMode(5) + +// ============================================================================ +// CLOCK SHUFFLE VALIDATION FUNCTIONS +// ============================================================================ + +// Validation: Check if a time slice is in an owned half-day period +inline bool IsTimeSliceOwned(TimeSlice slice) { + if (!SettingClocks()) + return true; + + for (int i = 0; i < 6; ++i) { + const auto& range = HALF_DAY_TIME_RANGES[i]; + if (slice >= range.startSlice && slice <= range.endSlice) { + return OwnsClockHalfDay(i); + } + } + return false; +} + +// Validation: Check if any time in the timeslice mask is owned +inline bool HasAnyOwnedTime(uint64_t timeSlices) { + if (!SettingClocks()) + return true; + + for (int i = 0; i < 6; ++i) { + if (OwnsClockHalfDay(i)) { + uint64_t halfDayMask = GetHalfDayTimeMask(i); + if (timeSlices & halfDayMask) { + return true; + } + } + } + return false; +} + +// ============================================================================ +// COMPOSITE TIME CHECKS +// ============================================================================ + +#define IS_DAY1() (RawBefore(TIME_NIGHT1_PM_06_00) && CLOCK_DAY1()) +#define IS_NIGHT1() (RawBetween(TIME_NIGHT1_PM_06_00, TIME_DAY2_AM_06_00) && CLOCK_NIGHT1()) +#define IS_DAY2() (RawBetween(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_06_00) && CLOCK_DAY2()) +#define IS_NIGHT2() (RawBetween(TIME_NIGHT2_PM_06_00, TIME_DAY3_AM_06_00) && CLOCK_NIGHT2()) +#define IS_DAY3() (RawBetween(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_06_00) && CLOCK_DAY3()) +#define IS_NIGHT3() (RawAfter(TIME_NIGHT3_PM_06_00) && CLOCK_NIGHT3()) + +// Global clock filter for owned time periods +// Returns true if Clock Shuffle is disabled OR if player has access to any time period +inline bool ClockFilter() { + if (!SettingClocks()) + return true; + + return IS_DAY1() || IS_NIGHT1() || IS_DAY2() || IS_NIGHT2() || IS_DAY3() || IS_NIGHT3(); +} + +#define IS_DAY() (IS_DAY1() || IS_DAY2() || IS_DAY3()) +#define IS_NIGHT() (IS_NIGHT1() || IS_NIGHT2() || IS_NIGHT3()) +#define FIRST_DAY() (IS_DAY1() || IS_NIGHT1()) +#define SECOND_DAY() (IS_DAY2() || IS_NIGHT2()) +#define FINAL_DAY() (IS_DAY3() || IS_NIGHT3()) + +// ============================================================================ +// PUBLIC TIME API +// ============================================================================ + +#define AT(slice) (RawAt(slice) && ClockFilter()) + +#define BEFORE(slice) (RawBefore(slice) && ClockFilter()) + +#define AFTER(slice) (RawAfter(slice) && ClockFilter()) + +#define BETWEEN(s, e) (RawBetween(s, e) && ClockFilter()) + +#define MIDNIGHT() \ + (BETWEEN(TIME_NIGHT1_AM_12_00, TIME_DAY2_AM_06_00) || BETWEEN(TIME_NIGHT2_AM_12_00, TIME_DAY3_AM_06_00) || \ + AFTER(TIME_NIGHT3_AM_12_00)) + inline bool CanKillEnemy(ActorId EnemyId) { // If enemy souls are shuffled, and the relevant soul is not obtained, we cannot kill that enemy. if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_ENEMY_SOULS] && !HaveEnemySoul(EnemyId)) { diff --git a/mm/2s2h/Rando/Logic/NearlyNoLogic.cpp b/mm/2s2h/Rando/Logic/NearlyNoLogic.cpp index fd3b7df212..56c9e73969 100644 --- a/mm/2s2h/Rando/Logic/NearlyNoLogic.cpp +++ b/mm/2s2h/Rando/Logic/NearlyNoLogic.cpp @@ -47,21 +47,8 @@ void ApplyNearlyNoLogicToSaveContext(std::vector& checkPool, std:: continue; } - RandoItemId randoItemId = RI_NONE; - if (RANDO_SAVE_CHECKS[randoCheckId].skipped) { - uint32_t index = 0; - for (auto& item : itemPool) { - if (Rando::StaticData::Items[item].randoItemType == RITYPE_JUNK) { - randoItemId = item; - itemPool.erase(itemPool.begin() + index); - break; - } - index++; - } - } else { - randoItemId = itemPool.back(); - itemPool.pop_back(); - } + RandoItemId randoItemId = itemPool.back(); + itemPool.pop_back(); RANDO_SAVE_CHECKS[randoCheckId].shuffled = true; RANDO_SAVE_CHECKS[randoCheckId].randoItemId = randoItemId; diff --git a/mm/2s2h/Rando/Logic/NoLogic.cpp b/mm/2s2h/Rando/Logic/NoLogic.cpp index e4604fcff1..8dba3dddf2 100644 --- a/mm/2s2h/Rando/Logic/NoLogic.cpp +++ b/mm/2s2h/Rando/Logic/NoLogic.cpp @@ -10,21 +10,6 @@ namespace Rando { namespace Logic { void ApplyNoLogicToSaveContext(std::vector& checkPool, std::vector& itemPool) { - std::vector junkPool; - for (auto& randoCheckId : checkPool) { - if (RANDO_SAVE_CHECKS[randoCheckId].skipped) { - uint32_t index = 0; - for (auto& item : itemPool) { - if (Rando::StaticData::Items[item].randoItemType == RITYPE_JUNK) { - junkPool.push_back(item); - itemPool.erase(itemPool.begin() + index); - break; - } - index++; - } - } - } - for (size_t i = 0; i < itemPool.size(); i++) { std::swap(itemPool[i], itemPool[Ship_Random(0, itemPool.size() - 1)]); } @@ -35,11 +20,6 @@ void ApplyNoLogicToSaveContext(std::vector& checkPool, std::vector } RANDO_SAVE_CHECKS[randoCheckId].shuffled = true; - if (RANDO_SAVE_CHECKS[randoCheckId].skipped == true) { - RANDO_SAVE_CHECKS[randoCheckId].randoItemId = junkPool.back(); - junkPool.pop_back(); - continue; - } RANDO_SAVE_CHECKS[randoCheckId].randoItemId = itemPool.back(); itemPool.pop_back(); } diff --git a/mm/2s2h/Rando/Logic/Regions/Central.cpp b/mm/2s2h/Rando/Logic/Regions/Central.cpp index ac83416f94..07c031ab41 100644 --- a/mm/2s2h/Rando/Logic/Regions/Central.cpp +++ b/mm/2s2h/Rando/Logic/Regions/Central.cpp @@ -90,23 +90,29 @@ static RegisterShipInitFunc initFunc([]() { .checks = { CHECK(RC_CLOCK_TOWN_EAST_SMALL_CRATE_01, true), CHECK(RC_CLOCK_TOWN_EAST_SMALL_CRATE_02, true), - CHECK(RC_CLOCK_TOWN_EAST_POSTMAN_HAT, Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_MAMA)), - CHECK(RC_CLOCK_TOWN_STRAY_FAIRY, CAN_BE_DEKU), + CHECK(RC_CLOCK_TOWN_EAST_POSTMAN_HAT, RANDO_EVENTS[RE_POSTMAN_FREEDOM] && BETWEEN(TIME_NIGHT3_PM_06_00, TIME_NIGHT3_AM_05_00)), + CHECK(RC_CLOCK_TOWN_STRAY_FAIRY, CAN_BE_DEKU && IS_NIGHT()), CHECK(RC_CLOCK_TOWN_EAST_UPPER_CHEST, true), + CHECK(RC_CLOCK_TOWN_BOMBERS_NOTEBOOK, RANDO_EVENTS[RE_BOMBER_CODE]), }, .exits = { // TO FROM EXIT(ENTRANCE(TERMINA_FIELD, 7), ENTRANCE(EAST_CLOCK_TOWN, 0), true), EXIT(ENTRANCE(SOUTH_CLOCK_TOWN, 7), ENTRANCE(EAST_CLOCK_TOWN, 1), true), // To lower EXIT(ENTRANCE(ASTRAL_OBSERVATORY, 0), ENTRANCE(EAST_CLOCK_TOWN, 2), CAN_USE_PROJECTILE), EXIT(ENTRANCE(SOUTH_CLOCK_TOWN, 2), ENTRANCE(EAST_CLOCK_TOWN, 3), true), // To upper - EXIT(ENTRANCE(TREASURE_CHEST_SHOP, 0), ENTRANCE(EAST_CLOCK_TOWN, 4), true), + EXIT(ENTRANCE(TREASURE_CHEST_SHOP, 0), ENTRANCE(EAST_CLOCK_TOWN, 4), BEFORE(TIME_NIGHT1_PM_10_00) || BETWEEN(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_10_00) || BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_10_00)), EXIT(ENTRANCE(NORTH_CLOCK_TOWN, 1), ENTRANCE(EAST_CLOCK_TOWN, 5), true), - EXIT(ENTRANCE(HONEY_AND_DARLINGS_SHOP, 0), ENTRANCE(EAST_CLOCK_TOWN, 6), true), - EXIT(ENTRANCE(MAYORS_RESIDENCE, 0), ENTRANCE(EAST_CLOCK_TOWN, 7), true), - EXIT(ENTRANCE(TOWN_SHOOTING_GALLERY, 0), ENTRANCE(EAST_CLOCK_TOWN, 8), true), - EXIT(ENTRANCE(STOCK_POT_INN, 0), ENTRANCE(EAST_CLOCK_TOWN, 9), true), // To lobby + EXIT(ENTRANCE(HONEY_AND_DARLINGS_SHOP, 0), ENTRANCE(EAST_CLOCK_TOWN, 6), BEFORE(TIME_NIGHT1_PM_10_00) || BETWEEN(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_10_00) || BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_10_00)), + EXIT(ENTRANCE(MAYORS_RESIDENCE, 0), ENTRANCE(EAST_CLOCK_TOWN, 7), BETWEEN(TIME_DAY1_AM_10_00, TIME_NIGHT1_PM_08_00) || BETWEEN(TIME_DAY2_AM_10_00, TIME_NIGHT2_PM_08_00) || AFTER(TIME_DAY3_AM_10_00)), + EXIT(ENTRANCE(TOWN_SHOOTING_GALLERY, 0), ENTRANCE(EAST_CLOCK_TOWN, 8), BEFORE(TIME_NIGHT1_PM_10_00) || BETWEEN(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_10_00) || BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_10_00)), + EXIT(ENTRANCE(STOCK_POT_INN, 0), ENTRANCE(EAST_CLOCK_TOWN, 9), HAS_ITEM(ITEM_ROOM_KEY) || BETWEEN(TIME_DAY1_AM_08_00, TIME_NIGHT1_PM_08_00) || BETWEEN(TIME_DAY2_AM_08_00, TIME_NIGHT2_PM_08_00) || AFTER(TIME_DAY3_AM_08_00)), EXIT(ENTRANCE(STOCK_POT_INN, 1), ENTRANCE(EAST_CLOCK_TOWN, 10), CAN_BE_DEKU), // To upstairs - EXIT(ENTRANCE(MILK_BAR, 0), ENTRANCE(EAST_CLOCK_TOWN, 11), HAS_ITEM(ITEM_MASK_ROMANI)), + EXIT(ENTRANCE(MILK_BAR, 0), ENTRANCE(EAST_CLOCK_TOWN, 11), (BETWEEN(TIME_DAY1_AM_10_00, TIME_NIGHT1_PM_09_00) || + BETWEEN(TIME_DAY2_AM_10_00, TIME_NIGHT2_PM_09_00) || + BETWEEN(TIME_DAY3_AM_10_00, TIME_NIGHT3_PM_09_00)) || + (HAS_ITEM(ITEM_MASK_ROMANI) && (BETWEEN(TIME_NIGHT1_PM_10_00, TIME_NIGHT1_AM_05_00) || + BETWEEN(TIME_NIGHT2_PM_10_00, TIME_NIGHT2_AM_05_00) || + AFTER(TIME_NIGHT3_PM_10_00)))), }, }; Regions[RR_CLOCK_TOWN_GREAT_FAIRY_FOUNTAIN] = RandoRegion{ .name = "Clock Town", .sceneId = SCENE_YOUSEI_IZUMI, @@ -120,12 +126,12 @@ static RegisterShipInitFunc initFunc([]() { }; Regions[RR_CLOCK_TOWN_LAUNDRY] = RandoRegion{ .sceneId = SCENE_ALLEY, .checks = { - CHECK(RC_CLOCK_TOWN_STRAY_FAIRY, true), - CHECK(RC_CLOCK_TOWN_LAUNDRY_FREESTANDING_RUPEE_01, CAN_USE_ABILITY(SWIM) || CAN_BE_ZORA), - CHECK(RC_CLOCK_TOWN_LAUNDRY_FREESTANDING_RUPEE_02, CAN_USE_ABILITY(SWIM) || CAN_BE_ZORA), - CHECK(RC_CLOCK_TOWN_LAUNDRY_FREESTANDING_RUPEE_03, CAN_USE_ABILITY(SWIM) || CAN_BE_ZORA), + CHECK(RC_CLOCK_TOWN_STRAY_FAIRY, IS_DAY()), + CHECK(RC_CLOCK_TOWN_LAUNDRY_FREESTANDING_RUPEE_01, (CAN_USE_ABILITY(SWIM) || CAN_BE_ZORA) && IS_NIGHT2()), + CHECK(RC_CLOCK_TOWN_LAUNDRY_FREESTANDING_RUPEE_02, (CAN_USE_ABILITY(SWIM) || CAN_BE_ZORA) && IS_NIGHT2()), + CHECK(RC_CLOCK_TOWN_LAUNDRY_FREESTANDING_RUPEE_03, (CAN_USE_ABILITY(SWIM) || CAN_BE_ZORA) && IS_NIGHT2()), CHECK(RC_CLOCK_TOWN_LAUNDRY_FROG, HAS_ITEM(ITEM_MASK_DON_GERO)), - CHECK(RC_CLOCK_TOWN_LAUNDRY_GURU_GURU, true), + CHECK(RC_CLOCK_TOWN_LAUNDRY_GURU_GURU, IS_NIGHT1() || IS_NIGHT2()), CHECK(RC_CLOCK_TOWN_LAUNDRY_SMALL_CRATE, true), CHECK(RC_CLOCK_TOWN_LAUNDRY_POOL_GRASS_01, true), CHECK(RC_CLOCK_TOWN_LAUNDRY_POOL_GRASS_02, true), @@ -133,16 +139,19 @@ static RegisterShipInitFunc initFunc([]() { }, .exits = { // TO FROM EXIT(ENTRANCE(SOUTH_CLOCK_TOWN, 6), ENTRANCE(LAUNDRY_POOL, 0), true), - EXIT(ENTRANCE(CURIOSITY_SHOP, 1), ENTRANCE(LAUNDRY_POOL, 1), Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI)) + EXIT(ENTRANCE(CURIOSITY_SHOP, 1), ENTRANCE(LAUNDRY_POOL, 1), (Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI) && BETWEEN(TIME_DAY2_PM_02_00, TIME_NIGHT2_PM_10_00)) || (RANDO_EVENTS[RE_MEET_KAFEI] && BETWEEN(TIME_DAY3_PM_01_00, TIME_NIGHT3_PM_10_00))), + }, + .events = { + EVENT(RE_MEET_KAFEI, Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI) && BETWEEN(TIME_DAY2_PM_02_00, TIME_NIGHT2_PM_10_00)), }, }; Regions[RR_CLOCK_TOWN_NORTH] = RandoRegion{ .sceneId = SCENE_BACKTOWN, .checks = { - CHECK(RC_CLOCK_TOWN_NORTH_TINGLE_MAP_01, CAN_USE_PROJECTILE && CAN_AFFORD(RC_CLOCK_TOWN_NORTH_TINGLE_MAP_01)), - CHECK(RC_CLOCK_TOWN_NORTH_TINGLE_MAP_02, CAN_USE_PROJECTILE && CAN_AFFORD(RC_CLOCK_TOWN_NORTH_TINGLE_MAP_02)), + CHECK(RC_CLOCK_TOWN_NORTH_TINGLE_MAP_01, CAN_USE_PROJECTILE && CAN_AFFORD(RC_CLOCK_TOWN_NORTH_TINGLE_MAP_01) && IS_DAY()), + CHECK(RC_CLOCK_TOWN_NORTH_TINGLE_MAP_02, CAN_USE_PROJECTILE && CAN_AFFORD(RC_CLOCK_TOWN_NORTH_TINGLE_MAP_02) && IS_DAY()), CHECK(RC_CLOCK_TOWN_NORTH_TREE_PIECE_OF_HEART, true), - CHECK(RC_CLOCK_TOWN_NORTH_BOMB_LADY, CAN_USE_SWORD || CAN_BE_ZORA || CAN_BE_GORON), - CHECK(RC_CLOCK_TOWN_BOMBERS_NOTEBOOK, CAN_USE_PROJECTILE), // TODO: This will have to check for access with entrance rando + CHECK(RC_CLOCK_TOWN_NORTH_BOMB_LADY, RANDO_EVENTS[RE_SAVE_BOMB_SHOP_LADY]), + CHECK(RC_CLOCK_TOWN_BOMBERS_NOTEBOOK, RANDO_EVENTS[RE_BOMBER_CODE]), CHECK(RC_CLOCK_TOWN_POSTBOX, HAS_ITEM(ITEM_MASK_POSTMAN)), CHECK(RC_KEATON_QUIZ, HAS_ITEM(ITEM_MASK_KEATON)), }, @@ -154,11 +163,32 @@ static RegisterShipInitFunc initFunc([]() { EXIT(ENTRANCE(DEKU_SCRUB_PLAYGROUND, 0), ENTRANCE(NORTH_CLOCK_TOWN, 4), CAN_BE_DEKU), }, .events = { - EVENT(RE_ACCESS_PICTOGRAPH_TINGLE, HAS_ITEM(ITEM_PICTOGRAPH_BOX)), // Only in the day + EVENT(RE_ACCESS_PICTOGRAPH_TINGLE, HAS_ITEM(ITEM_PICTOGRAPH_BOX) && IS_DAY()), // Refer to z_en_suttari's damage table for more info. Damage effect 0xF stops him nonlethally, while 0xE kills. // FD sword beams can also kill him, but currently FD is not logically considered. - EVENT(RE_SAVE_BOMB_SHOP_LADY, CAN_USE_SWORD || CAN_BE_ZORA || CAN_BE_GORON), - EVENT(RE_KILL_SAKON, HAS_ITEM(ITEM_BOW) || HAS_ITEM(ITEM_HOOKSHOT) || CAN_BE_ZORA || CAN_USE_EXPLOSIVE), + EVENT(RE_SAVE_BOMB_SHOP_LADY, (CAN_USE_SWORD || CAN_BE_ZORA || CAN_BE_GORON) && AT(TIME_NIGHT1_AM_12_00)), + EVENT(RE_KILL_SAKON, (HAS_ITEM(ITEM_BOW) || HAS_ITEM(ITEM_HOOKSHOT) || CAN_BE_ZORA || CAN_USE_EXPLOSIVE) && AT(TIME_NIGHT1_AM_12_00)), + // Hide and seek events + EVENT(RE_HIDE_SEEK_DAY1, CAN_USE_PROJECTILE && FIRST_DAY()), + EVENT(RE_HIDE_SEEK_DAY2, CAN_USE_PROJECTILE && SECOND_DAY()), + EVENT(RE_HIDE_SEEK_DAY3, CAN_USE_PROJECTILE && FINAL_DAY()), + // North bomber events + EVENT(RE_BOMBERS_NORTH_DAY1, RANDO_EVENTS[RE_HIDE_SEEK_DAY1]), + EVENT(RE_BOMBERS_NORTH_DAY2, RANDO_EVENTS[RE_HIDE_SEEK_DAY2]), + EVENT(RE_BOMBERS_NORTH_DAY3, RANDO_EVENTS[RE_HIDE_SEEK_DAY3]), + // East bomber events + EVENT(RE_BOMBERS_EAST_DAY1, RANDO_EVENTS[RE_HIDE_SEEK_DAY1]), + EVENT(RE_BOMBERS_EAST_DAY2, RANDO_EVENTS[RE_HIDE_SEEK_DAY2]), + EVENT(RE_BOMBERS_EAST_DAY3, RANDO_EVENTS[RE_HIDE_SEEK_DAY3]), + // West bomber events + EVENT(RE_BOMBERS_WEST_DAY1, RANDO_EVENTS[RE_HIDE_SEEK_DAY1]), + EVENT(RE_BOMBERS_WEST_DAY2, RANDO_EVENTS[RE_HIDE_SEEK_DAY2]), + EVENT(RE_BOMBERS_WEST_DAY3, RANDO_EVENTS[RE_HIDE_SEEK_DAY3]), + // Bomber code event + EVENT(RE_BOMBER_CODE, + (RANDO_EVENTS[RE_BOMBERS_NORTH_DAY1] && RANDO_EVENTS[RE_BOMBERS_WEST_DAY1] && RANDO_EVENTS[RE_BOMBERS_EAST_DAY1]) || + (RANDO_EVENTS[RE_BOMBERS_NORTH_DAY2] && RANDO_EVENTS[RE_BOMBERS_WEST_DAY2] && RANDO_EVENTS[RE_BOMBERS_EAST_DAY2]) || + (RANDO_EVENTS[RE_BOMBERS_NORTH_DAY3] && RANDO_EVENTS[RE_BOMBERS_WEST_DAY3] && RANDO_EVENTS[RE_BOMBERS_EAST_DAY3])), }, }; Regions[RR_CLOCK_TOWN_SOUTH] = RandoRegion{ .sceneId = SCENE_CLOCKTOWER, @@ -166,7 +196,7 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_CLOCK_TOWN_POSTBOX, HAS_ITEM(ITEM_MASK_POSTMAN)), CHECK(RC_CLOCK_TOWN_SOUTH_PLATFORM_PIECE_OF_HEART, true), CHECK(RC_CLOCK_TOWN_SCRUB_DEED, Flags_GetRandoInf(RANDO_INF_OBTAINED_MOONS_TEAR)), - CHECK(RC_CLOCK_TOWN_SOUTH_CHEST_UPPER, (CAN_BE_DEKU && Flags_GetRandoInf(RANDO_INF_OBTAINED_MOONS_TEAR)) || HAS_ITEM(ITEM_HOOKSHOT)), + CHECK(RC_CLOCK_TOWN_SOUTH_CHEST_UPPER, ((CAN_BE_DEKU && Flags_GetRandoInf(RANDO_INF_OBTAINED_MOONS_TEAR)) || HAS_ITEM(ITEM_HOOKSHOT)) && FINAL_DAY()), CHECK(RC_CLOCK_TOWN_SOUTH_CHEST_LOWER, (CAN_BE_DEKU && Flags_GetRandoInf(RANDO_INF_OBTAINED_MOONS_TEAR)) || HAS_ITEM(ITEM_HOOKSHOT)), CHECK(RC_CLOCK_TOWN_SOUTH_OWL_STATUE, CAN_USE_SWORD), }, @@ -179,7 +209,7 @@ static RegisterShipInitFunc initFunc([]() { EXIT(ENTRANCE(WEST_CLOCK_TOWN, 1), ENTRANCE(SOUTH_CLOCK_TOWN, 5), true), // To lower EXIT(ENTRANCE(LAUNDRY_POOL, 0), ENTRANCE(SOUTH_CLOCK_TOWN, 6), true), EXIT(ENTRANCE(EAST_CLOCK_TOWN, 1), ENTRANCE(SOUTH_CLOCK_TOWN, 7), true), // To lower - EXIT(ENTRANCE(CLOCK_TOWER_ROOFTOP, 0), ONE_WAY_EXIT, true), + EXIT(ENTRANCE(CLOCK_TOWER_ROOFTOP, 0), ONE_WAY_EXIT, AFTER(TIME_NIGHT3_AM_12_00)), // Clock Tower Platform accessible only after midnight Night 3 }, .connections = { CONNECTION(RR_MAX, true), @@ -194,61 +224,78 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_CLOCK_TOWN_WEST_BANK_ADULTS_WALLET, true), CHECK(RC_CLOCK_TOWN_WEST_BANK_PIECE_OF_HEART, CUR_UPG_VALUE(UPG_WALLET) >= 1), CHECK(RC_CLOCK_TOWN_WEST_BANK_INTEREST, CUR_UPG_VALUE(UPG_WALLET) >= 1), - CHECK(RC_CLOCK_TOWN_WEST_SISTERS_PIECE_OF_HEART, HAS_ITEM(ITEM_MASK_KAMARO)), + CHECK(RC_CLOCK_TOWN_WEST_SISTERS_PIECE_OF_HEART, HAS_ITEM(ITEM_MASK_KAMARO) && (IS_NIGHT1() || IS_NIGHT2())), }, .exits = { // TO FROM EXIT(ENTRANCE(TERMINA_FIELD, 0), ENTRANCE(WEST_CLOCK_TOWN, 0), true), EXIT(ENTRANCE(SOUTH_CLOCK_TOWN, 5), ENTRANCE(WEST_CLOCK_TOWN, 1), true), // To lower EXIT(ENTRANCE(SOUTH_CLOCK_TOWN, 3), ENTRANCE(WEST_CLOCK_TOWN, 2), true), // To upper - EXIT(ENTRANCE(SWORDMANS_SCHOOL, 0), ENTRANCE(WEST_CLOCK_TOWN, 3), true), - EXIT(ENTRANCE(CURIOSITY_SHOP, 0), ENTRANCE(WEST_CLOCK_TOWN, 4), true), + EXIT(ENTRANCE(SWORDMANS_SCHOOL, 0), ENTRANCE(WEST_CLOCK_TOWN, 3), FIRST_DAY() || SECOND_DAY() || BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_11_00) || AFTER(TIME_NIGHT3_AM_12_00)), + EXIT(ENTRANCE(CURIOSITY_SHOP, 0), ENTRANCE(WEST_CLOCK_TOWN, 4), BETWEEN(TIME_NIGHT1_PM_10_00, TIME_DAY2_AM_06_00) || BETWEEN(TIME_NIGHT2_PM_10_00, TIME_DAY3_AM_06_00) || AFTER(TIME_NIGHT3_PM_10_00)), EXIT(ENTRANCE(TRADING_POST, 0), ENTRANCE(WEST_CLOCK_TOWN, 5), true), EXIT(ENTRANCE(BOMB_SHOP, 0), ENTRANCE(WEST_CLOCK_TOWN, 6), true), - EXIT(ENTRANCE(POST_OFFICE, 0), ENTRANCE(WEST_CLOCK_TOWN, 7), true), - EXIT(ENTRANCE(LOTTERY_SHOP, 0), ENTRANCE(WEST_CLOCK_TOWN, 8), true), + EXIT(ENTRANCE(POST_OFFICE, 0), ENTRANCE(WEST_CLOCK_TOWN, 7), BETWEEN(TIME_DAY1_PM_03_00, TIME_NIGHT1_AM_12_00) || (Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI) && BETWEEN(TIME_NIGHT2_PM_06_00, TIME_NIGHT2_AM_12_00)) || IS_NIGHT3()), + EXIT(ENTRANCE(LOTTERY_SHOP, 0), ENTRANCE(WEST_CLOCK_TOWN, 8), IS_DAY() || (BEFORE(TIME_NIGHT1_PM_11_00) || BETWEEN(TIME_NIGHT2_PM_06_00, TIME_NIGHT2_PM_11_00) || BETWEEN(TIME_NIGHT3_PM_06_00, TIME_NIGHT3_PM_11_00))), }, }; Regions[RR_CURIOSITY_SHOP_BACK] = RandoRegion{ .name = "Back", .sceneId = SCENE_AYASHIISHOP, .checks = { - CHECK(RC_KAFEIS_HIDEOUT_KEATON_MASK, true), - CHECK(RC_KAFEIS_HIDEOUT_LETTER_TO_MAMA, true), - CHECK(RC_KAFEIS_HIDEOUT_PENDANT_OF_MEMORIES, Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI)), + CHECK(RC_KAFEIS_HIDEOUT_KEATON_MASK, BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_10_00)), + CHECK(RC_KAFEIS_HIDEOUT_LETTER_TO_MAMA, BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_10_00)), + CHECK(RC_KAFEIS_HIDEOUT_PENDANT_OF_MEMORIES, Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI) && BETWEEN(TIME_DAY2_PM_02_00, TIME_NIGHT2_PM_10_00)), }, .exits = { // TO FROM EXIT(ENTRANCE(LAUNDRY_POOL, 1), ENTRANCE(CURIOSITY_SHOP, 1), true) }, + .timeStayRestrictions = { + STAY(TIME_NIGHT2_PM_10_00, false), + STAY(TIME_NIGHT3_PM_10_00, false), + STAY(TIME_DAY2_AM_06_00, false), // Kick at start of day + STAY(TIME_DAY3_AM_06_00, false), + }, }; Regions[RR_CURIOSITY_SHOP_FRONT] = RandoRegion{ .name = "Front", .sceneId = SCENE_AYASHIISHOP, .checks = { - CHECK(RC_BOMB_SHOP_ITEM_04_OR_CURIOSITY_SHOP_ITEM, CAN_AFFORD(RC_BOMB_SHOP_ITEM_04_OR_CURIOSITY_SHOP_ITEM)), - CHECK(RC_CURIOSITY_SHOP_SPECIAL_ITEM, CAN_AFFORD(RC_CURIOSITY_SHOP_SPECIAL_ITEM) && (RANDO_EVENTS[RE_SAVE_BOMB_SHOP_LADY] || RANDO_EVENTS[RE_KILL_SAKON])), + CHECK(RC_BOMB_SHOP_ITEM_04_OR_CURIOSITY_SHOP_ITEM, CAN_AFFORD(RC_BOMB_SHOP_ITEM_04_OR_CURIOSITY_SHOP_ITEM) && IS_NIGHT3()), + CHECK(RC_CURIOSITY_SHOP_SPECIAL_ITEM, CAN_AFFORD(RC_CURIOSITY_SHOP_SPECIAL_ITEM) && (RANDO_EVENTS[RE_SAVE_BOMB_SHOP_LADY] || RANDO_EVENTS[RE_KILL_SAKON]) && IS_NIGHT3()), }, .exits = { // TO FROM - EXIT(ENTRANCE(WEST_CLOCK_TOWN, 4), ENTRANCE(CURIOSITY_SHOP, 0), true) + EXIT(ENTRANCE(WEST_CLOCK_TOWN, 4), ENTRANCE(CURIOSITY_SHOP, 0), true), }, }; Regions[RR_HONEY_AND_DARLING] = RandoRegion{ .sceneId = SCENE_BOWLING, .checks = { - CHECK(RC_CLOCK_TOWN_EAST_HONEY_DARLING_ALL_DAYS, HAS_ITEM(ITEM_BOW) && HAS_ITEM(ITEM_BOMBCHU) && HAS_ITEM(ITEM_BOMB)), - CHECK(RC_CLOCK_TOWN_EAST_HONEY_DARLING_ANY_DAY, HAS_ITEM(ITEM_BOW) || HAS_ITEM(ITEM_BOMBCHU) || HAS_ITEM(ITEM_BOMB)), + CHECK(RC_CLOCK_TOWN_EAST_HONEY_DARLING_ALL_DAYS, RANDO_EVENTS[RE_HONEY_DARLING_REWARD_DAY1] && RANDO_EVENTS[RE_HONEY_DARLING_REWARD_DAY2] && RANDO_EVENTS[RE_HONEY_DARLING_REWARD_DAY3]), + CHECK(RC_CLOCK_TOWN_EAST_HONEY_DARLING_ANY_DAY, (RANDO_EVENTS[RE_HONEY_DARLING_REWARD_DAY1] || RANDO_EVENTS[RE_HONEY_DARLING_REWARD_DAY2] || RANDO_EVENTS[RE_HONEY_DARLING_REWARD_DAY3])), }, .exits = { // TO FROM EXIT(ENTRANCE(EAST_CLOCK_TOWN, 6), ENTRANCE(HONEY_AND_DARLINGS_SHOP, 0), true), }, + .events = { + EVENT(RE_HONEY_DARLING_REWARD_DAY1, (HAS_ITEM(ITEM_BOMB) || HAS_ITEM(ITEM_BOMBCHU)) && BEFORE(TIME_NIGHT1_PM_10_00)), + EVENT(RE_HONEY_DARLING_REWARD_DAY2, HAS_ITEM(ITEM_BOMB) && BETWEEN(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_10_00)), + EVENT(RE_HONEY_DARLING_REWARD_DAY3, HAS_ITEM(ITEM_BOW) && IS_DAY3()), + }, }; Regions[RR_INN] = RandoRegion{ .sceneId = SCENE_YADOYA, .checks = { - CHECK(RC_STOCK_POT_INN_COUPLES_MASK, HAS_ITEM(ITEM_MASK_KAFEIS_MASK) && Flags_GetRandoInf(RANDO_INF_OBTAINED_PENDANT_OF_MEMORIES) && RANDO_EVENTS[RE_RETRIEVE_SUN_MASK]), - CHECK(RC_STOCK_POT_INN_GRANDMA_LONG_STORY, HAS_ITEM(ITEM_MASK_ALL_NIGHT)), - CHECK(RC_STOCK_POT_INN_GRANDMA_SHORT_STORY, HAS_ITEM(ITEM_MASK_ALL_NIGHT)), - CHECK(RC_STOCK_POT_INN_GUEST_ROOM_CHEST,Flags_GetRandoInf(RANDO_INF_OBTAINED_ROOM_KEY)), - CHECK(RC_STOCK_POT_INN_LETTER_TO_KAFEI, HAS_ITEM(ITEM_MASK_KAFEIS_MASK)), - CHECK(RC_STOCK_POT_INN_ROOM_KEY, true), - CHECK(RC_STOCK_POT_INN_STAFF_ROOM_CHEST, true), + CHECK(RC_STOCK_POT_INN_COUPLES_MASK, HAS_ITEM(ITEM_MASK_KAFEIS_MASK) && Flags_GetRandoInf(RANDO_INF_OBTAINED_PENDANT_OF_MEMORIES) && RANDO_EVENTS[RE_RETRIEVE_SUN_MASK] && AFTER(TIME_NIGHT3_AM_04_00)), + CHECK(RC_STOCK_POT_INN_GRANDMA_LONG_STORY, HAS_ITEM(ITEM_MASK_ALL_NIGHT) && + ((BEFORE(TIME_DAY1_PM_04_00) && CLOCK_NIGHT1()) || BETWEEN(TIME_DAY2_AM_06_00, TIME_DAY2_PM_04_00) || + (IS_DAY1() && (CLOCK_NIGHT1() || CLOCK_DAY2() || CLOCK_NIGHT2() || CLOCK_DAY3() || CLOCK_NIGHT3())) || + (IS_DAY2() && (CLOCK_NIGHT2() || CLOCK_DAY3() || CLOCK_NIGHT3())))), + CHECK(RC_STOCK_POT_INN_GRANDMA_SHORT_STORY, HAS_ITEM(ITEM_MASK_ALL_NIGHT) && + ((IS_DAY1() && (CLOCK_DAY2() || CLOCK_NIGHT2() || CLOCK_DAY3() || CLOCK_NIGHT3())) || + (IS_DAY2() && (CLOCK_DAY3() || CLOCK_NIGHT3())))), + CHECK(RC_STOCK_POT_INN_GUEST_ROOM_CHEST, Flags_GetRandoInf(RANDO_INF_OBTAINED_ROOM_KEY)), + CHECK(RC_STOCK_POT_INN_LETTER_TO_KAFEI, HAS_ITEM(ITEM_MASK_KAFEIS_MASK) && RANDO_EVENTS[RE_ANJU_MIDNIGHT_MEETING]), + CHECK(RC_STOCK_POT_INN_ROOM_KEY, BETWEEN(TIME_DAY1_PM_01_45, TIME_DAY1_PM_04_00)), + CHECK(RC_STOCK_POT_INN_STAFF_ROOM_CHEST, IS_NIGHT3()), CHECK(RC_STOCK_POT_INN_TOILET_HAND, - Flags_GetRandoInf(RANDO_INF_OBTAINED_DEED_LAND) || Flags_GetRandoInf(RANDO_INF_OBTAINED_DEED_SWAMP) || + (Flags_GetRandoInf(RANDO_INF_OBTAINED_DEED_LAND) || Flags_GetRandoInf(RANDO_INF_OBTAINED_DEED_SWAMP) || Flags_GetRandoInf(RANDO_INF_OBTAINED_DEED_MOUNTAIN) || Flags_GetRandoInf(RANDO_INF_OBTAINED_DEED_OCEAN) || - Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_MAMA) || Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI) + Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_MAMA) || Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI)) && + MIDNIGHT() ), }, .exits = { // TO FROM @@ -258,11 +305,23 @@ static RegisterShipInitFunc initFunc([]() { .events = { EVENT(RE_ACCESS_FISH, true), EVENT(RE_ACCESS_BUGS, true), + EVENT(RE_SETUP_MEET_ANJU, HAS_ITEM(ITEM_MASK_KAFEIS_MASK) && BETWEEN(TIME_DAY1_PM_01_45, TIME_NIGHT1_PM_09_00)), + EVENT(RE_ANJU_MIDNIGHT_MEETING, RANDO_EVENTS[RE_SETUP_MEET_ANJU] && BETWEEN(TIME_NIGHT1_AM_12_00, TIME_DAY2_AM_06_00) && (Flags_GetRandoInf(RANDO_INF_OBTAINED_ROOM_KEY) || CAN_BE_DEKU)), + EVENT(RE_DELIVER_PENDANT, Flags_GetRandoInf(RANDO_INF_OBTAINED_PENDANT_OF_MEMORIES) && (BETWEEN(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_09_00) || BETWEEN(TIME_DAY3_AM_06_00, TIME_DAY3_AM_11_30))), + }, + .timeStayRestrictions = { + STAY(TIME_NIGHT1_PM_08_00, Flags_GetRandoInf(RANDO_INF_OBTAINED_ROOM_KEY)), + STAY(TIME_DAY2_AM_06_00, Flags_GetRandoInf(RANDO_INF_OBTAINED_ROOM_KEY)), + STAY(TIME_NIGHT2_PM_08_00, Flags_GetRandoInf(RANDO_INF_OBTAINED_ROOM_KEY)), + STAY(TIME_DAY3_AM_06_00, Flags_GetRandoInf(RANDO_INF_OBTAINED_ROOM_KEY)), }, }; Regions[RR_LOTTERY_SHOP] = RandoRegion{ .sceneId = SCENE_TAKARAKUJI, .checks = { - CHECK(RC_CLOCK_TOWN_WEST_LOTTERY, true), + CHECK(RC_CLOCK_TOWN_WEST_LOTTERY, + (IS_DAY1() && CLOCK_NIGHT1()) || + (IS_DAY2() && CLOCK_NIGHT2()) || + (IS_DAY3() && CLOCK_NIGHT3())), }, .exits = { // TO FROM EXIT(ENTRANCE(WEST_CLOCK_TOWN, 8), ENTRANCE(LOTTERY_SHOP, 0), true), @@ -270,8 +329,8 @@ static RegisterShipInitFunc initFunc([]() { }; Regions[RR_MAYOR_RESIDENCE] = RandoRegion{ .sceneId = SCENE_SONCHONOIE, .checks = { - CHECK(RC_MAYORS_OFFICE_PIECE_OF_HEART, HAS_ITEM(ITEM_MASK_COUPLE)), - CHECK(RC_MAYORS_OFFICE_KAFEIS_MASK, true) + CHECK(RC_MAYORS_OFFICE_PIECE_OF_HEART, HAS_ITEM(ITEM_MASK_COUPLE) && (BETWEEN(TIME_DAY1_AM_10_00, TIME_NIGHT1_PM_08_00) || BETWEEN(TIME_DAY2_AM_10_00, TIME_NIGHT2_PM_08_00) || BETWEEN(TIME_DAY3_AM_10_00, TIME_NIGHT3_PM_06_00))), + CHECK(RC_MAYORS_OFFICE_KAFEIS_MASK, BETWEEN(TIME_DAY1_AM_10_00, TIME_NIGHT1_PM_08_00) || BETWEEN(TIME_DAY2_AM_10_00, TIME_NIGHT2_PM_08_00)) }, .exits = { // TO FROM EXIT(ENTRANCE(EAST_CLOCK_TOWN, 7), ENTRANCE(MAYORS_RESIDENCE, 0), true), @@ -279,63 +338,82 @@ static RegisterShipInitFunc initFunc([]() { }; Regions[RR_MILK_BAR] = RandoRegion{ .sceneId = SCENE_MILK_BAR, .checks = { - CHECK(RC_MILK_BAR_CIRCUS_LEADER_MASK, CAN_BE_DEKU && CAN_BE_GORON && CAN_BE_ZORA && HAS_ITEM(ITEM_OCARINA_OF_TIME)), - CHECK(RC_MILK_BAR_MADAME_AROMA, HAS_ITEM(ITEM_MASK_KAFEIS_MASK) && Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_MAMA)), - CHECK(RC_MILK_BAR_PURCHASE_CHATEAU, CAN_AFFORD(RC_MILK_BAR_PURCHASE_CHATEAU) && HAS_ITEM(ITEM_MASK_ROMANI)), - CHECK(RC_MILK_BAR_PURCHASE_MILK, CAN_AFFORD(RC_MILK_BAR_PURCHASE_MILK) && HAS_ITEM(ITEM_MASK_ROMANI)), + CHECK(RC_MILK_BAR_CIRCUS_LEADER_MASK, CAN_BE_DEKU && CAN_BE_GORON && CAN_BE_ZORA && HAS_ITEM(ITEM_OCARINA_OF_TIME) && (BETWEEN(TIME_NIGHT1_PM_10_00, TIME_NIGHT1_AM_05_00) || BETWEEN(TIME_NIGHT2_PM_10_00, TIME_NIGHT2_AM_05_00))), + CHECK(RC_MILK_BAR_MADAME_AROMA, HAS_ITEM(ITEM_MASK_KAFEIS_MASK) && Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_MAMA) && (BETWEEN(TIME_NIGHT3_PM_06_00, TIME_NIGHT3_PM_09_00) || AFTER(TIME_NIGHT3_PM_10_00))), + CHECK(RC_MILK_BAR_PURCHASE_CHATEAU, CAN_AFFORD(RC_MILK_BAR_PURCHASE_CHATEAU) && HAS_ITEM(ITEM_MASK_ROMANI) && (BETWEEN(TIME_NIGHT1_PM_10_00, TIME_DAY2_AM_06_00) || BETWEEN(TIME_NIGHT2_PM_10_00, TIME_DAY3_AM_06_00) || BETWEEN(TIME_NIGHT3_PM_06_00, TIME_NIGHT3_PM_09_00) || AFTER(TIME_NIGHT3_PM_10_00))), + CHECK(RC_MILK_BAR_PURCHASE_MILK, CAN_AFFORD(RC_MILK_BAR_PURCHASE_MILK) && HAS_ITEM(ITEM_MASK_ROMANI) && (BETWEEN(TIME_NIGHT1_PM_10_00, TIME_DAY2_AM_06_00) || BETWEEN(TIME_NIGHT2_PM_10_00, TIME_DAY3_AM_06_00) || BETWEEN(TIME_NIGHT3_PM_06_00, TIME_NIGHT3_PM_09_00) || AFTER(TIME_NIGHT3_PM_10_00))), }, .exits = { // TO FROM EXIT(ENTRANCE(EAST_CLOCK_TOWN, 11), ENTRANCE(MILK_BAR, 0), true), }, + .timeStayRestrictions = { + STAY(TIME_NIGHT1_PM_10_00, false), + STAY(TIME_NIGHT1_AM_05_00, false), + STAY(TIME_NIGHT2_PM_10_00, false), + STAY(TIME_NIGHT2_AM_05_00, false), + STAY(TIME_NIGHT3_PM_10_00, false), + STAY(TIME_NIGHT3_AM_05_00, false), + }, }; Regions[RR_POST_OFFICE] = RandoRegion{ .sceneId = SCENE_POSTHOUSE, .checks = { // TODO: Trick for doing without the Bunny Hood - CHECK(RC_CLOCK_TOWN_WEST_POSTMAN_MINIGAME, HAS_ITEM(ITEM_MASK_BUNNY)), + CHECK(RC_CLOCK_TOWN_WEST_POSTMAN_MINIGAME, HAS_ITEM(ITEM_MASK_BUNNY) && (BETWEEN(TIME_DAY1_PM_03_00, TIME_NIGHT1_AM_12_00) || (Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI) && BETWEEN(TIME_NIGHT2_PM_06_00, TIME_NIGHT2_AM_12_00)))), }, .exits = { // TO FROM EXIT(ENTRANCE(WEST_CLOCK_TOWN, 7), ENTRANCE(POST_OFFICE, 0), true), }, + .events = { + EVENT(RE_POSTMAN_FREEDOM, Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_MAMA) && IS_NIGHT3()), + }, }; Regions[RR_SWORDSMAN_SCHOOL] = RandoRegion{ .sceneId = SCENE_DOUJOU, .checks = { - CHECK(RC_SWORDSMAN_SCHOOL_PIECE_OF_HEART, CAN_USE_HUMAN_SWORD), - CHECK(RC_SWORDSMAN_SCHOOL_POT_01, CAN_USE_HUMAN_SWORD), - CHECK(RC_SWORDSMAN_SCHOOL_POT_02, CAN_USE_HUMAN_SWORD), - CHECK(RC_SWORDSMAN_SCHOOL_POT_03, CAN_USE_HUMAN_SWORD), - CHECK(RC_SWORDSMAN_SCHOOL_POT_04, CAN_USE_HUMAN_SWORD), - CHECK(RC_SWORDSMAN_SCHOOL_POT_05, CAN_USE_HUMAN_SWORD), + CHECK(RC_SWORDSMAN_SCHOOL_PIECE_OF_HEART, CAN_USE_HUMAN_SWORD && BEFORE(TIME_NIGHT3_PM_11_00)), + CHECK(RC_SWORDSMAN_SCHOOL_POT_01, CAN_USE_HUMAN_SWORD && AFTER(TIME_NIGHT3_AM_12_00)), + CHECK(RC_SWORDSMAN_SCHOOL_POT_02, CAN_USE_HUMAN_SWORD && AFTER(TIME_NIGHT3_AM_12_00)), + CHECK(RC_SWORDSMAN_SCHOOL_POT_03, CAN_USE_HUMAN_SWORD && AFTER(TIME_NIGHT3_AM_12_00)), + CHECK(RC_SWORDSMAN_SCHOOL_POT_04, CAN_USE_HUMAN_SWORD && AFTER(TIME_NIGHT3_AM_12_00)), + CHECK(RC_SWORDSMAN_SCHOOL_POT_05, CAN_USE_HUMAN_SWORD && AFTER(TIME_NIGHT3_AM_12_00)), }, .exits = { // TO FROM EXIT(ENTRANCE(WEST_CLOCK_TOWN, 3), ENTRANCE(SWORDMANS_SCHOOL, 0), true), }, + .timeStayRestrictions = { + STAY(TIME_NIGHT3_PM_11_00, false), + }, }; Regions[RR_TOWN_DEKU_PLAYGROUND] = RandoRegion{ .sceneId = SCENE_DEKUTES, .checks = { - CHECK(RC_DEKU_PLAYGROUND_ALL_DAYS, CAN_BE_DEKU), + CHECK(RC_DEKU_PLAYGROUND_ALL_DAYS, CAN_BE_DEKU && RANDO_EVENTS[RE_DEKU_PLAYGROUND_1] && RANDO_EVENTS[RE_DEKU_PLAYGROUND_2] && RANDO_EVENTS[RE_DEKU_PLAYGROUND_3]), CHECK(RC_DEKU_PLAYGROUND_ANY_DAY, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_01, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_02, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_03, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_04, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_05, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_06, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_01, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_02, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_03, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_04, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_05, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_06, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_01, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_02, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_03, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_04, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_05, CAN_BE_DEKU), - CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_06, CAN_BE_DEKU), + CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_01, RANDO_EVENTS[RE_DEKU_PLAYGROUND_1]), + CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_02, RANDO_EVENTS[RE_DEKU_PLAYGROUND_1]), + CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_03, RANDO_EVENTS[RE_DEKU_PLAYGROUND_1]), + CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_04, RANDO_EVENTS[RE_DEKU_PLAYGROUND_1]), + CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_05, RANDO_EVENTS[RE_DEKU_PLAYGROUND_1]), + CHECK(RC_DEKU_PLAYGROUND_DAY_1_RUPEE_06, RANDO_EVENTS[RE_DEKU_PLAYGROUND_1]), + CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_01, RANDO_EVENTS[RE_DEKU_PLAYGROUND_2]), + CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_02, RANDO_EVENTS[RE_DEKU_PLAYGROUND_2]), + CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_03, RANDO_EVENTS[RE_DEKU_PLAYGROUND_2]), + CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_04, RANDO_EVENTS[RE_DEKU_PLAYGROUND_2]), + CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_05, RANDO_EVENTS[RE_DEKU_PLAYGROUND_2]), + CHECK(RC_DEKU_PLAYGROUND_DAY_2_RUPEE_06, RANDO_EVENTS[RE_DEKU_PLAYGROUND_2]), + CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_01, RANDO_EVENTS[RE_DEKU_PLAYGROUND_3]), + CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_02, RANDO_EVENTS[RE_DEKU_PLAYGROUND_3]), + CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_03, RANDO_EVENTS[RE_DEKU_PLAYGROUND_3]), + CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_04, RANDO_EVENTS[RE_DEKU_PLAYGROUND_3]), + CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_05, RANDO_EVENTS[RE_DEKU_PLAYGROUND_3]), + CHECK(RC_DEKU_PLAYGROUND_DAY_3_RUPEE_06, RANDO_EVENTS[RE_DEKU_PLAYGROUND_3]), }, .exits = { // TO FROM EXIT(ENTRANCE(NORTH_CLOCK_TOWN, 4), ENTRANCE(DEKU_SCRUB_PLAYGROUND, 0), true), }, + .events = { + EVENT(RE_DEKU_PLAYGROUND_1, CAN_BE_DEKU && FIRST_DAY()), + EVENT(RE_DEKU_PLAYGROUND_2, CAN_BE_DEKU && SECOND_DAY()), + EVENT(RE_DEKU_PLAYGROUND_3, CAN_BE_DEKU && FINAL_DAY()), + }, }; Regions[RR_TOWN_SHOOTING_GALLERY] = RandoRegion{ .sceneId = SCENE_SYATEKI_MIZU, .checks = { @@ -345,6 +423,11 @@ static RegisterShipInitFunc initFunc([]() { .exits = { // TO FROM EXIT(ENTRANCE(EAST_CLOCK_TOWN, 8), ENTRANCE(TOWN_SHOOTING_GALLERY, 0), true), }, + .timeStayRestrictions = { + STAY(TIME_NIGHT1_PM_10_00, false), + STAY(TIME_NIGHT2_PM_10_00, false), + STAY(TIME_NIGHT3_PM_10_00, false), + }, }; Regions[RR_TRADING_POST] = RandoRegion{ .sceneId = SCENE_8ITEMSHOP, .checks = { @@ -362,6 +445,14 @@ static RegisterShipInitFunc initFunc([]() { EXIT(ENTRANCE(WEST_CLOCK_TOWN, 5), ENTRANCE(TRADING_POST, 0), true), }, + .timeStayRestrictions = { + // Logic break at night + STAY(TIME_NIGHT1_PM_09_00, false), + STAY(TIME_NIGHT1_PM_10_00, false), + STAY(TIME_NIGHT2_PM_09_00, false), + STAY(TIME_NIGHT2_PM_10_00, false), + STAY(TIME_NIGHT3_PM_09_00, false), + }, }; Regions[RR_TREASURE_SHOP] = RandoRegion{ .sceneId = SCENE_TAKARAYA, .checks = { diff --git a/mm/2s2h/Rando/Logic/Regions/East.cpp b/mm/2s2h/Rando/Logic/Regions/East.cpp index 095ac26cb6..514ae967c5 100644 --- a/mm/2s2h/Rando/Logic/Regions/East.cpp +++ b/mm/2s2h/Rando/Logic/Regions/East.cpp @@ -153,7 +153,7 @@ static RegisterShipInitFunc initFunc([]() { Must NOT have helped the Bomb Shop old lady on this cycle(Kafei does not show up if you do, as Sakon never visits the shop to be followed.) Must have delivered the Letter to Kafei and met Kafei.(Sakon just does not show up otherwise, as odd as that may sound.) */ - EXIT(ENTRANCE(SAKONS_HIDEOUT, 0), ENTRANCE(IKANA_CANYON, 6), Flags_GetRandoInf(RANDO_INF_OBTAINED_LETTER_TO_KAFEI)), + EXIT(ENTRANCE(SAKONS_HIDEOUT, 0), ENTRANCE(IKANA_CANYON, 6), RANDO_EVENTS[RE_MEET_KAFEI] && AT(TIME_NIGHT3_PM_06_00)), EXIT(ENTRANCE(SECRET_SHRINE, 0), ENTRANCE(IKANA_CANYON, 12), CAN_USE_ABILITY(SWIM)), EXIT(ENTRANCE(SOUTHERN_SWAMP_POISONED, 9), ONE_WAY_EXIT, CAN_USE_ABILITY(SWIM)), }, @@ -171,7 +171,7 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_IKANA_CANYON_OWL_STATUE, CAN_USE_SWORD), CHECK(RC_IKANA_CANYON_TINGLE_MAP_01, CAN_USE_PROJECTILE && CAN_AFFORD(RC_IKANA_CANYON_TINGLE_MAP_01)), CHECK(RC_IKANA_CANYON_TINGLE_MAP_02, CAN_USE_PROJECTILE && CAN_AFFORD(RC_IKANA_CANYON_TINGLE_MAP_02)), - CHECK(RC_ENEMY_DROP_GUAY, CanKillEnemy(ACTOR_EN_CROW)), // Day only + CHECK(RC_ENEMY_DROP_GUAY, CanKillEnemy(ACTOR_EN_CROW) && IS_DAY()), // Day only }, .exits = { // TO FROM EXIT(ENTRANCE(GHOST_HUT, 0), ENTRANCE(IKANA_CANYON, 1), true), @@ -228,14 +228,14 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_IKANA_GRAVEYARD_GRASS_07, true), CHECK(RC_IKANA_GRAVEYARD_GRASS_08, true), CHECK(RC_IKANA_GRAVEYARD_GRASS_09, true), - CHECK(RC_ENEMY_DROP_STALCHILD, CanKillEnemy(ACTOR_EN_SKB)), // Night only - CHECK(RC_ENEMY_DROP_BAD_BAT, CanKillEnemy(ACTOR_EN_BAT)), // Day only + CHECK(RC_ENEMY_DROP_STALCHILD, CanKillEnemy(ACTOR_EN_SKB) && IS_NIGHT()), // Night only + CHECK(RC_ENEMY_DROP_BAD_BAT, CanKillEnemy(ACTOR_EN_BAT) && IS_DAY()), // Day only }, .exits = { // TO FROM EXIT(ENTRANCE(ROAD_TO_IKANA, 2), ENTRANCE(IKANA_GRAVEYARD, 0), true), - EXIT(ENTRANCE(DAMPES_HOUSE, 0), ONE_WAY_EXIT, HAS_ITEM(ITEM_MASK_CAPTAIN)), // Day 3 hole - EXIT(ENTRANCE(BENEATH_THE_GRAVERYARD, 0), ENTRANCE(IKANA_GRAVEYARD, 2), HAS_ITEM(ITEM_MASK_CAPTAIN)), // Day 2 hole - EXIT(ENTRANCE(BENEATH_THE_GRAVERYARD, 1), ENTRANCE(IKANA_GRAVEYARD, 3), HAS_ITEM(ITEM_MASK_CAPTAIN)), // Day 1 hole + EXIT(ENTRANCE(DAMPES_HOUSE, 0), ONE_WAY_EXIT, HAS_ITEM(ITEM_MASK_CAPTAIN) && IS_NIGHT3()), // Day 3 hole + EXIT(ENTRANCE(BENEATH_THE_GRAVERYARD, 0), ENTRANCE(IKANA_GRAVEYARD, 2), HAS_ITEM(ITEM_MASK_CAPTAIN) && IS_NIGHT2()), // Day 2 hole + EXIT(ENTRANCE(BENEATH_THE_GRAVERYARD, 1), ENTRANCE(IKANA_GRAVEYARD, 3), HAS_ITEM(ITEM_MASK_CAPTAIN) && IS_NIGHT1()), // Day 1 hole }, .connections = { CONNECTION(RR_IKANA_GRAVEYARD_UPPER, CAN_PLAY_SONG(SONATA)), @@ -249,7 +249,7 @@ static RegisterShipInitFunc initFunc([]() { .checks = { CHECK(RC_IKANA_GRAVEYARD_CAPTAIN_MASK, CanKillEnemy(ACTOR_EN_SKB) && CanKillEnemy(ACTOR_EN_BSB)), CHECK(RC_ENEMY_DROP_STALCHILD, CanKillEnemy(ACTOR_EN_SKB)), - CHECK(RC_ENEMY_DROP_BAD_BAT, CanKillEnemy(ACTOR_EN_BAT)), // Day only + CHECK(RC_ENEMY_DROP_BAD_BAT, CanKillEnemy(ACTOR_EN_BAT) && IS_DAY()), // Day only CHECK(RC_ENEMY_DROP_CAPTAIN_KEETA, CanKillEnemy(ACTOR_EN_SKB) && CanKillEnemy(ACTOR_EN_BSB)), }, .connections = { @@ -274,7 +274,7 @@ static RegisterShipInitFunc initFunc([]() { }; Regions[RR_ROAD_TO_IKANA_ABOVE_LEDGE] = RandoRegion{ .name = "Above Ledge", .sceneId = SCENE_IKANAMAE, .checks = { - CHECK(RC_ENEMY_DROP_NEJIRON, CanKillEnemy(ACTOR_EN_BAGUO)), // Day only + CHECK(RC_ENEMY_DROP_NEJIRON, CanKillEnemy(ACTOR_EN_BAGUO) && IS_DAY()), // Day only }, .exits = { // TO FROM EXIT(ENTRANCE(IKANA_CANYON, 0), ENTRANCE(ROAD_TO_IKANA, 1), true), @@ -287,8 +287,8 @@ static RegisterShipInitFunc initFunc([]() { .checks = { CHECK(RC_ROAD_TO_IKANA_POT, CAN_HOOK_SCARECROW), CHECK(RC_ROAD_TO_IKANA_STONE_MASK, HAS_ITEM(ITEM_LENS_OF_TRUTH) && HAS_MAGIC && HAS_BOTTLE && (CAN_ACCESS(RED_POTION_REFILL) || CAN_ACCESS(BLUE_POTION_REFILL))), - CHECK(RC_ENEMY_DROP_BLUE_BUBBLE, CanKillEnemy(ACTOR_EN_BB)), // Night only - CHECK(RC_ENEMY_DROP_REAL_BOMBCHU, CanKillEnemy(ACTOR_EN_RAT)), // Day only + CHECK(RC_ENEMY_DROP_BLUE_BUBBLE, CanKillEnemy(ACTOR_EN_BB) && IS_NIGHT()), // Night only + CHECK(RC_ENEMY_DROP_REAL_BOMBCHU, CanKillEnemy(ACTOR_EN_RAT) && IS_DAY()), // Day only }, .exits = { // TO FROM EXIT(ENTRANCE(IKANA_GRAVEYARD, 0), ENTRANCE(ROAD_TO_IKANA, 2), true) @@ -301,8 +301,8 @@ static RegisterShipInitFunc initFunc([]() { Regions[RR_ROAD_TO_IKANA_FIELD_SIDE] = RandoRegion{ .name = "Field Side", .sceneId = SCENE_IKANAMAE, .checks = { CHECK(RC_ROAD_TO_IKANA_CHEST, HAS_ITEM(ITEM_HOOKSHOT)), - CHECK(RC_ENEMY_DROP_BLUE_BUBBLE, CanKillEnemy(ACTOR_EN_BB)), // Night only - CHECK(RC_ENEMY_DROP_REAL_BOMBCHU, CanKillEnemy(ACTOR_EN_RAT)), // Day only + CHECK(RC_ENEMY_DROP_BLUE_BUBBLE, CanKillEnemy(ACTOR_EN_BB) && IS_NIGHT()), // Night only + CHECK(RC_ENEMY_DROP_REAL_BOMBCHU, CanKillEnemy(ACTOR_EN_RAT) && IS_DAY()), // Day only }, .exits = { // TO FROM EXIT(ENTRANCE(TERMINA_FIELD, 4), ENTRANCE(ROAD_TO_IKANA, 0), true), diff --git a/mm/2s2h/Rando/Logic/Regions/MilkRoad.cpp b/mm/2s2h/Rando/Logic/Regions/MilkRoad.cpp index 8f0de44c5e..03b3dc0e1a 100644 --- a/mm/2s2h/Rando/Logic/Regions/MilkRoad.cpp +++ b/mm/2s2h/Rando/Logic/Regions/MilkRoad.cpp @@ -17,13 +17,17 @@ static RegisterShipInitFunc initFunc([]() { .exits = { // TO FROM EXIT(ENTRANCE(ROMANI_RANCH, 4), ENTRANCE(CUCCO_SHACK, 0), true), }, + .timeStayRestrictions = { + STAY(TIME_NIGHT1_PM_08_00, false), + STAY(TIME_NIGHT2_PM_08_00, false), + STAY(TIME_NIGHT3_PM_08_00, false), + }, }; Regions[RR_DOGGY_RACETRACK] = RandoRegion{ .sceneId = SCENE_F01_B, .checks = { // TODO: Trick: Jumpslash to clip through (similar to Clock Town Straw). // Zora can just climb up, adding it to logic for now but if someone wants to make it a trick later feel free. - // TODO: This soil patch can be watered with the Day 2 rain. Clock shuffle will need to require Day 2 or another water source. - CHECK(RC_DOGGY_RACETRACK_CHEST, HAS_ITEM(ITEM_HOOKSHOT) || HAS_ITEM(ITEM_MAGIC_BEANS) || CAN_BE_ZORA), + CHECK(RC_DOGGY_RACETRACK_CHEST, HAS_ITEM(ITEM_HOOKSHOT) || CAN_USE_DAY2_RAIN_BEAN || CAN_BE_ZORA), CHECK(RC_DOGGY_RACETRACK_PIECE_OF_HEART, HAS_ITEM(ITEM_MASK_TRUTH)), CHECK(RC_DOGGY_RACETRACK_POT_01, true), CHECK(RC_DOGGY_RACETRACK_POT_02, true), @@ -33,21 +37,26 @@ static RegisterShipInitFunc initFunc([]() { .exits = { // TO FROM EXIT(ENTRANCE(ROMANI_RANCH, 5), ENTRANCE(DOGGY_RACETRACK, 0), true), }, + .timeStayRestrictions = { + STAY(TIME_NIGHT1_PM_08_00, false), + STAY(TIME_NIGHT2_PM_08_00, false), + STAY(TIME_NIGHT3_PM_08_00, false), + }, }; - Regions[RR_GORMAN_TRACK] = RandoRegion{ .sceneId = SCENE_KOEPONARACE, + Regions[RR_GORMAN_TRACK_FRONT] = RandoRegion{ .sceneId = SCENE_KOEPONARACE, .checks = { - CHECK(RC_GORMAN_MILK_PURCHASE, CAN_AFFORD(RC_GORMAN_MILK_PURCHASE)), - CHECK(RC_GORMAN_TRACK_GARO_MASK, CAN_PLAY_SONG(EPONA)), + CHECK(RC_GORMAN_MILK_PURCHASE, CAN_AFFORD(RC_GORMAN_MILK_PURCHASE) && IS_DAY()), + CHECK(RC_GORMAN_TRACK_GARO_MASK, CAN_PLAY_SONG(EPONA) && IS_DAY()), }, .exits = { // TO FROM EXIT(ENTRANCE(MILK_ROAD, 3), ENTRANCE(GORMAN_TRACK, 0), true), }, .connections = { // TODO: Also apparently can be reached using a trick with Goron mask and Bombs. Add trick later here - CONNECTION(RR_GORMAN_TRACK_INNER, RANDO_EVENTS[RE_COWS_FROM_ALIENS]), + CONNECTION(RR_GORMAN_TRACK, RANDO_EVENTS[RE_COWS_FROM_ALIENS] && IS_NIGHT2()), }, }; - Regions[RR_GORMAN_TRACK_INNER] = RandoRegion{ .sceneId = SCENE_KOEPONARACE, + Regions[RR_GORMAN_TRACK] = RandoRegion{ .sceneId = SCENE_KOEPONARACE, .checks = { // The grass is technically reachable while racing on Epona, but successfully picking up the drops can be // dubious. We can make this a trick in the future. For now, gate the entire region behind saving the ranch @@ -78,11 +87,17 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_GORMAN_TRACK_GRASS_23, true), CHECK(RC_GORMAN_TRACK_GRASS_24, true), }, + .connections = { + CONNECTION(RR_GORMAN_TRACK_FRONT, CAN_PLAY_SONG(EPONA) || (RANDO_EVENTS[RE_COWS_FROM_ALIENS] && IS_NIGHT2())), + CONNECTION(RR_GORMAN_TRACK_BACK, CAN_PLAY_SONG(EPONA) || (RANDO_EVENTS[RE_COWS_FROM_ALIENS] && IS_NIGHT2())), + }, + }; + Regions[RR_GORMAN_TRACK_BACK] = RandoRegion{ .sceneId = SCENE_KOEPONARACE, .exits = { // TO FROM - EXIT(ENTRANCE(MILK_ROAD, 2), ENTRANCE(GORMAN_TRACK, 3), RANDO_EVENTS[RE_COWS_FROM_ALIENS]), + EXIT(ENTRANCE(MILK_ROAD, 2), ENTRANCE(GORMAN_TRACK, 3), true), }, .connections = { - CONNECTION(RR_GORMAN_TRACK, RANDO_EVENTS[RE_COWS_FROM_ALIENS]), + CONNECTION(RR_GORMAN_TRACK, RANDO_EVENTS[RE_COWS_FROM_ALIENS] && IS_NIGHT2()), }, }; Regions[RR_MILK_ROAD] = RandoRegion{ .sceneId = SCENE_ROMANYMAE, @@ -94,41 +109,63 @@ static RegisterShipInitFunc initFunc([]() { }, .exits = { // TO FROM EXIT(ENTRANCE(TERMINA_FIELD, 5), ENTRANCE(MILK_ROAD, 0), true), - EXIT(ENTRANCE(ROMANI_RANCH, 0), ENTRANCE(MILK_ROAD, 1), true), - EXIT(ENTRANCE(GORMAN_TRACK, 3), ENTRANCE(MILK_ROAD, 2), RANDO_EVENTS[RE_COWS_FROM_ALIENS]), + EXIT(ENTRANCE(ROMANI_RANCH, 0), ENTRANCE(MILK_ROAD, 1), AFTER(TIME_DAY3_AM_06_00) || RANDO_EVENTS[RE_DESTROY_MILK_ROAD_BOULDER]), EXIT(ENTRANCE(GORMAN_TRACK, 0), ENTRANCE(MILK_ROAD, 3), true), }, + .connections = { + // TODO: Trick to Goron bomb jump over the fence + CONNECTION(RR_MILK_ROAD_BEHIND_FENCE, (RANDO_EVENTS[RE_COWS_FROM_ALIENS] && IS_NIGHT2()) || FINAL_DAY()), + }, .events = { EVENT(RE_ACCESS_PICTOGRAPH_TINGLE, HAS_ITEM(ITEM_PICTOGRAPH_BOX)), + EVENT(RE_DESTROY_MILK_ROAD_BOULDER, CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG)), }, .oneWayEntrances = { ENTRANCE(MILK_ROAD, 4), // From Song of Soaring } }; + Regions[RR_MILK_ROAD_BEHIND_FENCE] = RandoRegion{ .sceneId = SCENE_ROMANYMAE, + .exits = { // TO FROM + EXIT(ENTRANCE(GORMAN_TRACK, 3), ENTRANCE(MILK_ROAD, 2), true), + }, + .connections = { + // TODO: Trick to Goron bomb jump over the fence + CONNECTION(RR_MILK_ROAD, (RANDO_EVENTS[RE_COWS_FROM_ALIENS] && IS_NIGHT2()) || FINAL_DAY()), + }, + }; Regions[RR_RANCH_BARN] = RandoRegion{ .sceneId = SCENE_OMOYA, .checks = { - CHECK(RC_ROMANI_RANCH_BARN_COW_LEFT, CAN_PLAY_SONG(EPONA) && CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG)), - CHECK(RC_ROMANI_RANCH_BARN_COW_MIDDLE, CAN_PLAY_SONG(EPONA) && CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG)), - CHECK(RC_ROMANI_RANCH_BARN_COW_RIGHT, CAN_PLAY_SONG(EPONA) && CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG)) + CHECK(RC_ROMANI_RANCH_BARN_COW_LEFT, CAN_PLAY_SONG(EPONA) && (BETWEEN(TIME_NIGHT1_PM_06_00, TIME_NIGHT1_AM_02_30) || RANDO_EVENTS[RE_COWS_FROM_ALIENS])), + CHECK(RC_ROMANI_RANCH_BARN_COW_MIDDLE, CAN_PLAY_SONG(EPONA) && (BETWEEN(TIME_NIGHT1_PM_06_00, TIME_NIGHT1_AM_02_30) || RANDO_EVENTS[RE_COWS_FROM_ALIENS])), + CHECK(RC_ROMANI_RANCH_BARN_COW_RIGHT, CAN_PLAY_SONG(EPONA) && (BETWEEN(TIME_NIGHT1_PM_06_00, TIME_NIGHT1_AM_02_30) || RANDO_EVENTS[RE_COWS_FROM_ALIENS])) }, .exits = { // TO FROM EXIT(ENTRANCE(ROMANI_RANCH, 2), ENTRANCE(RANCH_HOUSE, 0), true), }, + .timeStayRestrictions = { + STAY(TIME_NIGHT1_AM_02_30, false), + STAY(TIME_NIGHT3_PM_08_00, false), + }, }; Regions[RR_RANCH_HOUSE] = RandoRegion{ .sceneId = SCENE_OMOYA, .exits = { // TO FROM EXIT(ENTRANCE(ROMANI_RANCH, 3), ENTRANCE(RANCH_HOUSE, 1), true), }, + .timeStayRestrictions = { + STAY(TIME_NIGHT1_PM_08_00, false), + STAY(TIME_NIGHT2_PM_08_00, false), + STAY(TIME_NIGHT3_PM_08_00, false), + }, }; Regions[RR_ROMANI_RANCH] = RandoRegion{ .sceneId = SCENE_F01, .checks = { CHECK(RC_ROMANI_RANCH_ALIENS, CanKillEnemy(ACTOR_EN_INVADEPOH) && CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG)), - CHECK(RC_ROMANI_RANCH_EPONAS_SONG, CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG)), - CHECK(RC_ROMANI_RANCH_FIELD_COW_ENTRANCE, CAN_PLAY_SONG(EPONA) && CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG)), - CHECK(RC_ROMANI_RANCH_FIELD_COW_NEAR_HOUSE_BACK, CAN_PLAY_SONG(EPONA) && CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG)), - CHECK(RC_ROMANI_RANCH_FIELD_COW_NEAR_HOUSE_FRONT, CAN_PLAY_SONG(EPONA) && CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG)), + CHECK(RC_ROMANI_RANCH_EPONAS_SONG, BEFORE(TIME_NIGHT1_PM_06_00)), + CHECK(RC_ROMANI_RANCH_FIELD_COW_ENTRANCE, CAN_PLAY_SONG(EPONA) && (BETWEEN(TIME_NIGHT1_PM_06_00, TIME_NIGHT1_AM_02_30) || RANDO_EVENTS[RE_COWS_FROM_ALIENS])), + CHECK(RC_ROMANI_RANCH_FIELD_COW_NEAR_HOUSE_BACK, CAN_PLAY_SONG(EPONA) && (BETWEEN(TIME_NIGHT1_PM_06_00, TIME_NIGHT1_AM_02_30) || RANDO_EVENTS[RE_COWS_FROM_ALIENS])), + CHECK(RC_ROMANI_RANCH_FIELD_COW_NEAR_HOUSE_FRONT, CAN_PLAY_SONG(EPONA) && (BETWEEN(TIME_NIGHT1_PM_06_00, TIME_NIGHT1_AM_02_30) || RANDO_EVENTS[RE_COWS_FROM_ALIENS])), CHECK(RC_ROMANI_RANCH_FIELD_LARGE_CRATE, true), - CHECK(RC_CREMIA_ESCORT, HAS_ITEM(ITEM_BOW) && RANDO_EVENTS[RE_COWS_FROM_ALIENS]), + CHECK(RC_CREMIA_ESCORT, HAS_ITEM(ITEM_BOW) && RANDO_EVENTS[RE_COWS_FROM_ALIENS] && AT(TIME_NIGHT2_PM_06_00)), CHECK(RC_ROMANI_RANCH_GRASS_01, true), CHECK(RC_ROMANI_RANCH_GRASS_02, true), CHECK(RC_ROMANI_RANCH_GRASS_03, true), @@ -186,17 +223,17 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_ROMANI_RANCH_GRASS_55, true), CHECK(RC_ROMANI_RANCH_GRASS_56, true), CHECK(RC_ROMANI_RANCH_GRASS_57, true), - CHECK(RC_ENEMY_DROP_ALIEN, CanKillEnemy(ACTOR_EN_INVADEPOH)), // Night 1 only + CHECK(RC_ENEMY_DROP_ALIEN, CanKillEnemy(ACTOR_EN_INVADEPOH) && IS_NIGHT1()), // Night 1 only }, .exits = { // TO FROM EXIT(ENTRANCE(MILK_ROAD, 1), ENTRANCE(ROMANI_RANCH, 0), true), - EXIT(ENTRANCE(RANCH_HOUSE, 0), ENTRANCE(ROMANI_RANCH, 2), true), // Barn - EXIT(ENTRANCE(RANCH_HOUSE, 1), ENTRANCE(ROMANI_RANCH, 3), true), // House - EXIT(ENTRANCE(CUCCO_SHACK, 0), ENTRANCE(ROMANI_RANCH, 4), true), - EXIT(ENTRANCE(DOGGY_RACETRACK, 0), ENTRANCE(ROMANI_RANCH, 5), true), + EXIT(ENTRANCE(RANCH_HOUSE, 0), ENTRANCE(ROMANI_RANCH, 2), BETWEEN(TIME_DAY1_AM_06_00, TIME_NIGHT1_AM_02_30) || SECOND_DAY() || BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_08_00)), // Barn + EXIT(ENTRANCE(RANCH_HOUSE, 1), ENTRANCE(ROMANI_RANCH, 3), BETWEEN(TIME_DAY1_AM_06_00, TIME_NIGHT1_PM_08_00) || BETWEEN(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_08_00) || BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_08_00)), // House + EXIT(ENTRANCE(CUCCO_SHACK, 0), ENTRANCE(ROMANI_RANCH, 4), BETWEEN(TIME_DAY1_AM_06_00, TIME_NIGHT1_PM_08_00) || BETWEEN(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_08_00) || BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_08_00)), + EXIT(ENTRANCE(DOGGY_RACETRACK, 0), ENTRANCE(ROMANI_RANCH, 5), BETWEEN(TIME_DAY1_AM_06_00, TIME_NIGHT1_PM_08_00) || BETWEEN(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_08_00) || BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_08_00)), }, .events = { - EVENT(RE_COWS_FROM_ALIENS, CAN_BE_GORON && HAS_ITEM(ITEM_POWDER_KEG) && HAS_ITEM(ITEM_BOW)), + EVENT(RE_COWS_FROM_ALIENS, (HAS_ITEM(ITEM_POWDER_KEG) && CAN_BE_GORON) && HAS_ITEM(ITEM_BOW) && AT(TIME_NIGHT1_AM_02_30)), }, }; }, {}); diff --git a/mm/2s2h/Rando/Logic/Regions/North.cpp b/mm/2s2h/Rando/Logic/Regions/North.cpp index 00e7636849..b4d8eea8aa 100644 --- a/mm/2s2h/Rando/Logic/Regions/North.cpp +++ b/mm/2s2h/Rando/Logic/Regions/North.cpp @@ -128,7 +128,7 @@ static RegisterShipInitFunc initFunc([]() { .exits = { // TO FROM // During First Day a NPC Goron can open the door to the the Shrine EXIT(ENTRANCE(PATH_TO_GORON_VILLAGE_WINTER, 1), ENTRANCE(GORON_VILLAGE_WINTER, 0), true), - EXIT(ENTRANCE(GORON_SHRINE, 0), ENTRANCE(GORON_VILLAGE_WINTER, 2), true), + EXIT(ENTRANCE(GORON_SHRINE, 0), ENTRANCE(GORON_VILLAGE_WINTER, 2), FIRST_DAY() || CAN_BE_GORON), }, .connections = { CONNECTION(RR_LONE_PEAK_SHRINE_ENTRANCE, true) @@ -252,7 +252,7 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_MOUNTAIN_VILLAGE_SPRING_GRASS_30, RANDO_EVENTS[RE_CLEARED_SNOWHEAD_TEMPLE]), CHECK(RC_MOUNTAIN_VILLAGE_LARGE_SNOWBALL_01, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_MOUNTAIN_VILLAGE_LARGE_SNOWBALL_02, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), - CHECK(RC_MOUNTAIN_VILLAGE_LARGE_SNOWBALL_03, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), + CHECK(RC_MOUNTAIN_VILLAGE_LARGE_SNOWBALL_03, (FIRST_DAY() || SECOND_DAY()) && CanKillEnemy(ACTOR_OBJ_SNOWBALL)), // Goron Elder inside on Final Day CHECK(RC_MOUNTAIN_VILLAGE_LARGE_SNOWBALL_04, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_MOUNTAIN_VILLAGE_LARGE_SNOWBALL_05, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_MOUNTAIN_VILLAGE_SMALL_SNOWBALL_01, true), @@ -269,8 +269,8 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_ENEMY_DROP_GUAY, RANDO_EVENTS[RE_CLEARED_SNOWHEAD_TEMPLE] && CanKillEnemy(ACTOR_EN_CROW)), CHECK(RC_ENEMY_DROP_GIANT_BEE, CanKillEnemy(ACTOR_EN_BEE) && RANDO_EVENTS[RE_CLEARED_SNOWHEAD_TEMPLE]), CHECK(RC_ENEMY_DROP_BOE, CanKillEnemy(ACTOR_EN_MKK) && RANDO_EVENTS[RE_CLEARED_SNOWHEAD_TEMPLE]), - CHECK(RC_ENEMY_DROP_TEKTITE, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_TITE)), // Day 1 and 3 only - CHECK(RC_ENEMY_DROP_WOLFOS, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_WF)), // Day 2 only + CHECK(RC_ENEMY_DROP_TEKTITE, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_TITE) && (FIRST_DAY() || FINAL_DAY())), // Day 1 and 3 only + CHECK(RC_ENEMY_DROP_WOLFOS, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_WF) && SECOND_DAY()), // Day 2 only }, .exits = { // TO FROM EXIT(ENTRANCE(MOUNTAIN_SMITHY, 0), ENTRANCE(MOUNTAIN_VILLAGE_WINTER, 1), true), @@ -345,19 +345,19 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_TWIN_ISLANDS_SPRING_GRASS_10, RANDO_EVENTS[RE_CLEARED_SNOWHEAD_TEMPLE]), CHECK(RC_TWIN_ISLANDS_SPRING_GRASS_11, RANDO_EVENTS[RE_CLEARED_SNOWHEAD_TEMPLE]), CHECK(RC_TWIN_ISLANDS_SPRING_GRASS_12, RANDO_EVENTS[RE_CLEARED_SNOWHEAD_TEMPLE]), - CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_01, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), + CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_01, (FIRST_DAY() || FINAL_DAY()) && CanKillEnemy(ACTOR_OBJ_SNOWBALL)), // Goron inside on Second Day CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_02, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_03, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_04, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_05, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_06, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_07, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), - CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_08, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), + CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_08, (SECOND_DAY() || FINAL_DAY()) && CanKillEnemy(ACTOR_OBJ_SNOWBALL)), // Goron inside on First Day CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_09, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_10, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_11, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), - CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_12, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), - CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_13, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), + CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_12, (FIRST_DAY() || FINAL_DAY()) && CanKillEnemy(ACTOR_OBJ_SNOWBALL)), // Does not exist on Second Day + CHECK(RC_TWIN_ISLANDS_LARGE_SNOWBALL_13, (FIRST_DAY() || FINAL_DAY()) && CanKillEnemy(ACTOR_OBJ_SNOWBALL)), // Goron inside on Second Day CHECK(RC_TWIN_ISLANDS_SMALL_SNOWBALL_01, true), CHECK(RC_TWIN_ISLANDS_SMALL_SNOWBALL_02, true), CHECK(RC_TWIN_ISLANDS_SMALL_SNOWBALL_03, true), @@ -394,9 +394,9 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_PATH_TO_MOUNTAIN_VILLAGE_SMALL_SNOWBALL_02, true), CHECK(RC_PATH_TO_MOUNTAIN_VILLAGE_SMALL_SNOWBALL_03, true), CHECK(RC_ENEMY_DROP_TEKTITE, CanKillEnemy(ACTOR_EN_TITE)), - CHECK(RC_ENEMY_DROP_BOE, CanKillEnemy(ACTOR_EN_MKK)), // Night only - CHECK(RC_ENEMY_DROP_WOLFOS, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_WF)), // Day 2 only - CHECK(RC_ENEMY_DROP_SNAPPER, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_KAME)), // Day 3 only + CHECK(RC_ENEMY_DROP_BOE, CanKillEnemy(ACTOR_EN_MKK) && IS_NIGHT()), // Night only + CHECK(RC_ENEMY_DROP_WOLFOS, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_WF) && SECOND_DAY()), // Day 2 only + CHECK(RC_ENEMY_DROP_SNAPPER, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_KAME) && FINAL_DAY()), // Day 3 only }, .exits = { // TO FROM EXIT(ENTRANCE(TERMINA_FIELD, 3), ENTRANCE(PATH_TO_MOUNTAIN_VILLAGE, 0), true), @@ -415,9 +415,9 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_PATH_TO_MOUNTAIN_VILLAGE_LARGE_SNOWBALL_10, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_PATH_TO_MOUNTAIN_VILLAGE_LARGE_SNOWBALL_11, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_PATH_TO_MOUNTAIN_VILLAGE_SMALL_SNOWBALL_04, true), - CHECK(RC_ENEMY_DROP_BOE, CanKillEnemy(ACTOR_EN_MKK)), // Night only - CHECK(RC_ENEMY_DROP_WOLFOS, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_WF)), // Day 2 only - CHECK(RC_ENEMY_DROP_SNAPPER, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_KAME)), // Day 3 only + CHECK(RC_ENEMY_DROP_BOE, CanKillEnemy(ACTOR_EN_MKK) && IS_NIGHT()), // Night only + CHECK(RC_ENEMY_DROP_WOLFOS, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_WF) && SECOND_DAY()), // Day 2 only + CHECK(RC_ENEMY_DROP_SNAPPER, CanKillEnemy(ACTOR_OBJ_SNOWBALL) && CanKillEnemy(ACTOR_EN_KAME) && FINAL_DAY()), // Day 3 only }, .exits = { // TO FROM EXIT(ENTRANCE(MOUNTAIN_VILLAGE_WINTER, 6), ENTRANCE(PATH_TO_MOUNTAIN_VILLAGE, 1), true), @@ -524,7 +524,7 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_SNOWHEAD_LARGE_SNOWBALL_06, CanKillEnemy(ACTOR_OBJ_SNOWBALL)), CHECK(RC_ENEMY_DROP_KEESE, CanKillEnemy(ACTOR_EN_FIREFLY)), CHECK(RC_ENEMY_DROP_WOLFOS, CanKillEnemy(ACTOR_EN_WF)), - CHECK(RC_ENEMY_DROP_BOE, CanKillEnemy(ACTOR_EN_MKK)), // Night only + CHECK(RC_ENEMY_DROP_BOE, CanKillEnemy(ACTOR_EN_MKK) && IS_NIGHT()), // Night only }, .connections = { CONNECTION(RR_SNOWHEAD_NEAR_PATH, true), diff --git a/mm/2s2h/Rando/Logic/Regions/South.cpp b/mm/2s2h/Rando/Logic/Regions/South.cpp index 2b29d7aac3..37cd95453e 100644 --- a/mm/2s2h/Rando/Logic/Regions/South.cpp +++ b/mm/2s2h/Rando/Logic/Regions/South.cpp @@ -136,8 +136,7 @@ static RegisterShipInitFunc initFunc([]() { }, .connections = { CONNECTION(RR_DEKU_PALACE_INSIDE_LOWER, CAN_BE_DEKU), - // TODO: This soil patch can be watered with the Day 2 rain. Clock shuffle will need to require Day 2 or another water source. - CONNECTION(RR_DEKU_PALACE_INSIDE_UPPER, (CAN_BE_DEKU || (RANDO_EVENTS[RE_CLEARED_WOODFALL_TEMPLE] && CAN_TRAVERSE_WAIST_DEEP_WATER)) && HAS_ITEM(ITEM_MAGIC_BEANS)), + CONNECTION(RR_DEKU_PALACE_INSIDE_UPPER, (CAN_BE_DEKU || (RANDO_EVENTS[RE_CLEARED_WOODFALL_TEMPLE] && CAN_TRAVERSE_WAIST_DEEP_WATER)) && CAN_USE_DAY2_RAIN_BEAN), }, }; Regions[RR_DEKU_SHRINE_ENTRANCE] = RandoRegion{ .name = "Entrance", .sceneId = SCENE_DANPEI, @@ -192,9 +191,9 @@ static RegisterShipInitFunc initFunc([]() { .checks = { CHECK(RC_HAGS_POTION_SHOP_FREESTANDING_RUPEE, true), // TODO: Add CAN_ACCESS(MUSHROOM) once that is shuffled. - CHECK(RC_HAGS_POTION_SHOP_ITEM_01, CAN_AFFORD(RC_HAGS_POTION_SHOP_ITEM_01) && HAS_ITEM(ITEM_MASK_SCENTS) && HAS_BOTTLE), - CHECK(RC_HAGS_POTION_SHOP_ITEM_02, CAN_AFFORD(RC_HAGS_POTION_SHOP_ITEM_02)), - CHECK(RC_HAGS_POTION_SHOP_ITEM_03, CAN_AFFORD(RC_HAGS_POTION_SHOP_ITEM_03)), + CHECK(RC_HAGS_POTION_SHOP_ITEM_01, (FIRST_DAY() || RANDO_EVENTS[RE_SAVED_KOUME]) && CAN_AFFORD(RC_HAGS_POTION_SHOP_ITEM_01) && HAS_ITEM(ITEM_MASK_SCENTS) && HAS_BOTTLE), + CHECK(RC_HAGS_POTION_SHOP_ITEM_02, (FIRST_DAY() || RANDO_EVENTS[RE_SAVED_KOUME]) && CAN_AFFORD(RC_HAGS_POTION_SHOP_ITEM_02)), + CHECK(RC_HAGS_POTION_SHOP_ITEM_03, (FIRST_DAY() || RANDO_EVENTS[RE_SAVED_KOUME]) && CAN_AFFORD(RC_HAGS_POTION_SHOP_ITEM_03)), CHECK(RC_HAGS_POTION_SHOP_KOTAKE, true), }, .exits = { // TO FROM @@ -253,14 +252,14 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_ROAD_TO_SOUTHERN_SWAMP_GRASS_19, true), CHECK(RC_ROAD_TO_SOUTHERN_SWAMP_GRASS_20, true), CHECK(RC_ENEMY_DROP_DEKU_BABA, CanKillEnemy(ACTOR_EN_DEKUBABA)), - CHECK(RC_ENEMY_DROP_CHUCHU, CanKillEnemy(ACTOR_EN_SLIME)), // Day only - CHECK(RC_ENEMY_DROP_WOLFOS, CanKillEnemy(ACTOR_EN_WF)), // Night only + CHECK(RC_ENEMY_DROP_CHUCHU, CanKillEnemy(ACTOR_EN_SLIME) && IS_DAY()), // Day only + CHECK(RC_ENEMY_DROP_WOLFOS, CanKillEnemy(ACTOR_EN_WF) && IS_NIGHT()), // Night only CHECK(RC_ENEMY_DROP_BAD_BAT, CanKillEnemy(ACTOR_EN_BAT)), }, .exits = { // TO FROM EXIT(ENTRANCE(TERMINA_FIELD, 1), ENTRANCE(ROAD_TO_SOUTHERN_SWAMP, 0), true), EXIT(ENTRANCE(SOUTHERN_SWAMP_POISONED, 0), ENTRANCE(ROAD_TO_SOUTHERN_SWAMP, 1), true), - EXIT(ENTRANCE(SWAMP_SHOOTING_GALLERY, 0), ENTRANCE(ROAD_TO_SOUTHERN_SWAMP, 2), true), + EXIT(ENTRANCE(SWAMP_SHOOTING_GALLERY, 0), ENTRANCE(ROAD_TO_SOUTHERN_SWAMP, 2), BEFORE(TIME_NIGHT1_PM_10_00) || BETWEEN(TIME_DAY2_AM_06_00, TIME_NIGHT2_PM_10_00) || BETWEEN(TIME_DAY3_AM_06_00, TIME_NIGHT3_PM_10_00)), }, .connections = { CONNECTION(RR_ROAD_TO_SOUTHERN_SWAMP_GROTTO, true), // TODO: Grotto mapping @@ -466,6 +465,11 @@ static RegisterShipInitFunc initFunc([]() { .exits = { // TO FROM EXIT(ENTRANCE(ROAD_TO_SOUTHERN_SWAMP, 2), ENTRANCE(SWAMP_SHOOTING_GALLERY, 0), true), }, + .timeStayRestrictions = { + STAY(TIME_NIGHT1_PM_10_00, false), + STAY(TIME_NIGHT2_PM_10_00, false), + STAY(TIME_NIGHT3_PM_10_00, false), + }, }; Regions[RR_TOURIST_INFORMATION] = RandoRegion{ .sceneId = SCENE_MAP_SHOP, .checks = { @@ -548,7 +552,7 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_ENEMY_DROP_MINI_BABA, CanKillEnemy(ACTOR_EN_KAREBABA)), }, .connections = { - CONNECTION(RR_WOODS_OF_MYSTERY, true), // TODO: Grotto mapping + CONNECTION(RR_WOODS_OF_MYSTERY, SECOND_DAY()), // TODO: Grotto mapping }, }; Regions[RR_WOODS_OF_MYSTERY] = RandoRegion{ .sceneId = SCENE_26SARUNOMORI, @@ -556,8 +560,8 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_WOODS_OF_MYSTERY_GRASS_01, true), CHECK(RC_WOODS_OF_MYSTERY_GRASS_02, true), CHECK(RC_WOODS_OF_MYSTERY_GRASS_03, true), - CHECK(RC_WOODS_OF_MYSTERY_GRASS_04, true), - CHECK(RC_WOODS_OF_MYSTERY_GRASS_05, true), + CHECK(RC_WOODS_OF_MYSTERY_GRASS_04, FIRST_DAY()), + CHECK(RC_WOODS_OF_MYSTERY_GRASS_05, FIRST_DAY()), CHECK(RC_WOODS_OF_MYSTERY_GRASS_06, true), CHECK(RC_WOODS_OF_MYSTERY_GRASS_07, true), CHECK(RC_WOODS_OF_MYSTERY_GRASS_08, true), @@ -573,16 +577,16 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_WOODS_OF_MYSTERY_GRASS_18, true), CHECK(RC_WOODS_OF_MYSTERY_GRASS_19, true), CHECK(RC_WOODS_OF_MYSTERY_GRASS_20, true), - CHECK(RC_WOODS_OF_MYSTERY_GRASS_21, true), - CHECK(RC_WOODS_OF_MYSTERY_GRASS_22, true), - CHECK(RC_WOODS_OF_MYSTERY_GRASS_23, true), + CHECK(RC_WOODS_OF_MYSTERY_GRASS_21, SECOND_DAY()), + CHECK(RC_WOODS_OF_MYSTERY_GRASS_22, FINAL_DAY()), + CHECK(RC_WOODS_OF_MYSTERY_GRASS_23, FINAL_DAY()), CHECK(RC_ENEMY_DROP_SNAPPER, CAN_BE_DEKU || CanKillEnemy(ACTOR_EN_KAME)), }, .exits = { // TO FROM EXIT(ENTRANCE(SOUTHERN_SWAMP_POISONED, 7), ENTRANCE(WOODS_OF_MYSTERY, 0), true), }, .connections = { - CONNECTION(RR_WOODS_OF_MYSTERY_GROTTO, true), // TODO: Grotto mapping + CONNECTION(RR_WOODS_OF_MYSTERY_GROTTO, SECOND_DAY()), // TODO: Grotto mapping }, .events = { EVENT(RE_SAVED_KOUME, HAS_BOTTLE && (CAN_ACCESS(RED_POTION_REFILL) || CAN_ACCESS(BLUE_POTION_REFILL))), diff --git a/mm/2s2h/Rando/Logic/Regions/TerminaField.cpp b/mm/2s2h/Rando/Logic/Regions/TerminaField.cpp index 4af56e091e..3ff118ef56 100644 --- a/mm/2s2h/Rando/Logic/Regions/TerminaField.cpp +++ b/mm/2s2h/Rando/Logic/Regions/TerminaField.cpp @@ -179,7 +179,7 @@ static RegisterShipInitFunc initFunc([]() { }; Regions[RR_TERMINA_FIELD_PEAHAT_GROTTO] = RandoRegion{ .name = "Termina Field Peahat", .sceneId = SCENE_KAKUSIANA, .checks = { - CHECK(RC_TERMINA_FIELD_PEAHAT_GROTTO_CHEST, CAN_USE_SWORD || CAN_BE_ZORA || CAN_BE_GORON), + CHECK(RC_TERMINA_FIELD_PEAHAT_GROTTO_CHEST, (CAN_USE_SWORD || CAN_BE_ZORA || CAN_BE_GORON) && IS_DAY()), CHECK(RC_TERMINA_FIELD_PEAHAT_GROTTO_GRASS_01, true), CHECK(RC_TERMINA_FIELD_PEAHAT_GROTTO_GRASS_02, true), CHECK(RC_TERMINA_FIELD_PEAHAT_GROTTO_GRASS_03, true), @@ -256,7 +256,7 @@ static RegisterShipInitFunc initFunc([]() { }; Regions[RR_TERMINA_FIELD] = RandoRegion{ .sceneId = SCENE_00KEIKOKU, .checks = { - CHECK(RC_TERMINA_FIELD_KAMARO_MASK, CAN_PLAY_SONG(HEALING)), + CHECK(RC_TERMINA_FIELD_KAMARO_MASK, CAN_PLAY_SONG(HEALING) && MIDNIGHT()), CHECK(RC_TERMINA_FIELD_POT, CAN_GROW_BEAN_PLANT), CHECK(RC_TERMINA_FIELD_TALL_GRASS_CHEST, true), CHECK(RC_TERMINA_FIELD_TREE_STUMP_CHEST, CAN_GROW_BEAN_PLANT || HAS_ITEM(ITEM_HOOKSHOT)), @@ -498,13 +498,13 @@ static RegisterShipInitFunc initFunc([]() { CHECK(RC_TERMINA_FIELD_GRASS_214, true), CHECK(RC_TERMINA_FIELD_GRASS_215, true), CHECK(RC_TERMINA_FIELD_GRASS_216, true), - CHECK(RC_ENEMY_DROP_BLUE_BUBBLE, CanKillEnemy(ACTOR_EN_BB)), // Night only + CHECK(RC_ENEMY_DROP_BLUE_BUBBLE, CanKillEnemy(ACTOR_EN_BB) && IS_NIGHT()), // Night only CHECK(RC_ENEMY_DROP_DEKU_BABA, CanKillEnemy(ACTOR_EN_DEKUBABA)), - CHECK(RC_ENEMY_DROP_CHUCHU, CanKillEnemy(ACTOR_EN_SLIME)), // Day only - CHECK(RC_ENEMY_DROP_REAL_BOMBCHU, CanKillEnemy(ACTOR_EN_RAT)), // Day only + CHECK(RC_ENEMY_DROP_CHUCHU, CanKillEnemy(ACTOR_EN_SLIME) && IS_DAY()), // Day only + CHECK(RC_ENEMY_DROP_REAL_BOMBCHU, CanKillEnemy(ACTOR_EN_RAT) && IS_DAY()), // Day only CHECK(RC_ENEMY_DROP_LEEVER, CanKillEnemy(ACTOR_EN_NEO_REEBA)), - CHECK(RC_ENEMY_DROP_DODONGO, CanKillEnemy(ACTOR_EN_DODONGO)), // Day only - CHECK(RC_ENEMY_DROP_EENO, CanKillEnemy(ACTOR_EN_SNOWMAN)), // Night only + CHECK(RC_ENEMY_DROP_DODONGO, CanKillEnemy(ACTOR_EN_DODONGO) && IS_DAY()), // Day only + CHECK(RC_ENEMY_DROP_EENO, CanKillEnemy(ACTOR_EN_SNOWMAN) && IS_NIGHT()), // Night only CHECK(RC_ENEMY_DROP_BAD_BAT, CanKillEnemy(ACTOR_EN_BAT)), CHECK(RC_ENEMY_DROP_TAKKURI, CanKillEnemy(ACTOR_EN_THIEFBIRD)), }, diff --git a/mm/2s2h/Rando/Logic/Regions/West.cpp b/mm/2s2h/Rando/Logic/Regions/West.cpp index 1732996c28..97474397fd 100644 --- a/mm/2s2h/Rando/Logic/Regions/West.cpp +++ b/mm/2s2h/Rando/Logic/Regions/West.cpp @@ -123,7 +123,7 @@ static RegisterShipInitFunc initFunc([]() { }; Regions[RR_GREAT_BAY_COAST] = RandoRegion{ .sceneId = SCENE_30GYOSON, .checks = { - CHECK(RC_GREAT_BAY_COAST_FISHERMAN_MINIGAME, RANDO_EVENTS[RE_CLEARED_GREAT_BAY_TEMPLE] && (HAS_ITEM(ITEM_HOOKSHOT) || CAN_USE_MAGIC_ARROW(ICE))), + CHECK(RC_GREAT_BAY_COAST_FISHERMAN_MINIGAME, RANDO_EVENTS[RE_CLEARED_GREAT_BAY_TEMPLE] && (HAS_ITEM(ITEM_HOOKSHOT) || CAN_USE_MAGIC_ARROW(ICE)) && (BETWEEN(TIME_DAY1_AM_07_00, TIME_NIGHT1_AM_04_00) || BETWEEN(TIME_DAY2_AM_07_00, TIME_NIGHT2_AM_04_00) || BETWEEN(TIME_DAY3_AM_07_00, TIME_NIGHT3_AM_04_00))), CHECK(RC_GREAT_BAY_COAST_MIKAU, CAN_USE_ABILITY(SWIM) && CAN_PLAY_SONG(HEALING)), CHECK(RC_GREAT_BAY_COAST_POT_03, true), CHECK(RC_GREAT_BAY_COAST_POT_04, true), diff --git a/mm/2s2h/Rando/Logic/TimeLogic.cpp b/mm/2s2h/Rando/Logic/TimeLogic.cpp new file mode 100644 index 0000000000..5514f5895b --- /dev/null +++ b/mm/2s2h/Rando/Logic/TimeLogic.cpp @@ -0,0 +1,123 @@ +#include "Logic.h" +#include + +extern "C" { +#include "ShipUtils.h" +} + +namespace Rando { +namespace Logic { +namespace TimeLogic { + +// Core expansion function - expands accessible time forward with stay restrictions +// Implements sequential expansion: if a stay restriction fails, expansion stops permanently +// For unrestricted regions without Clock Shuffle: fast bitwise fill across all time slices (O(1)) +// For Clock Shuffle or restricted regions: sequential expansion respecting boundaries +uint64_t ExpandTimeForward(uint64_t timeSlices, const RandoRegion& region) { + // Fast path: unrestricted time expansion using bitwise fill (only when Clock Shuffle is off) + if (region.timeStayRestrictions.empty() && !SettingClocks()) { + uint64_t expanded = timeSlices; + + // Non-Clock Shuffle: expand across ALL time slices using bitwise fill + expanded |= (expanded << 1); + expanded |= (expanded << 2); + expanded |= (expanded << 4); + expanded |= (expanded << 8); + expanded |= (expanded << 16); + expanded |= (expanded << 32); + expanded &= TIME_ALL_SLICES; + + return expanded; + } + + // Slow path: restricted time expansion with sequential checking + // In Clock Shuffle, filter input to only owned time + uint64_t filteredTimeSlices = timeSlices; + if (SettingClocks()) { + filteredTimeSlices &= GetOwnedTimeSlices(); + } + + uint64_t expanded = filteredTimeSlices; + bool canWait = false; + + for (int i = 0; i < TIME_SLICE_COUNT; ++i) { + uint64_t mask = (TIME_BIT_ONE << i); + + if (filteredTimeSlices & mask) { + // We can be at this time + canWait = true; + expanded |= mask; + } else if (canWait) { + // During Clock Shuffle, check if this time slice is owned + if (SettingClocks() && !IsTimeSliceOwned(static_cast(i))) { + canWait = false; // Can't expand into unowned time period + continue; + } + + // Check if we can wait to this time + auto it = region.timeStayRestrictions.find(static_cast(i)); + if (it != region.timeStayRestrictions.end()) { + // CLOCK SHUFFLE: Ignore item-gated restrictions during logic generation + // Player will obtain items eventually, so treat as permissive + if (SettingClocks()) { + expanded |= mask; // Allow expansion - player will get items eventually + } else if (it->second()) { + expanded |= mask; // Condition passed, add time + } else { + canWait = false; // Kicked out, STOP expansion + } + } else { + // No restriction = default true, can stay + expanded |= mask; + } + } + } + + // VALIDATION: In Clock Shuffle, expanded time must not exceed owned time + if (SettingClocks()) { + uint64_t ownedTimeSlices = GetOwnedTimeSlices(); + bool expandedBeyondOwned = (expanded & ~ownedTimeSlices) != 0; + assert(!expandedBeyondOwned && "Time expansion exceeded owned half-day boundaries!"); + } + + return expanded; +} + +// Owned time calculation - aggregates all owned half-day time slices +uint64_t GetOwnedTimeSlices() { + if (!RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE]) { + return TIME_ALL_SLICES; + } + + uint64_t timeSlices = 0; + for (int halfDayIndex = 0; halfDayIndex < 6; ++halfDayIndex) { + if (OwnsClockHalfDay(halfDayIndex)) { + timeSlices |= GetHalfDayTimeMask(halfDayIndex); + } + } + + // If no clocks are owned, ensure we at least have access to the start of the game (Day 1 6 AM) + return timeSlices ? timeSlices : (TIME_BIT_ONE << TIME_DAY1_AM_06_00); +} + +// Validation helper for clock ownership during logic generation +void ValidateRegionTimeOwnership(RandoRegionId regionId, RandoCheckId checkId, uint64_t regionTime, + const char* context) { + if (!SettingClocks()) + return; + + if (!HasAnyOwnedTime(regionTime)) { + auto& region = Regions[regionId]; + SPDLOG_ERROR("CLOCK SHUFFLE VALIDATION FAILED ({})!", context); + SPDLOG_ERROR("Check: {}", Rando::StaticData::Checks[checkId].name); + SPDLOG_ERROR("Region: {} - {}", Ship_GetSceneName(region.sceneId), region.name); + SPDLOG_ERROR("Region time mask: 0x{:X}", regionTime); + SPDLOG_ERROR("Owned clocks: D1={} N1={} D2={} N2={} D3={} N3={}", OwnsClockHalfDay(0), OwnsClockHalfDay(1), + OwnsClockHalfDay(2), OwnsClockHalfDay(3), OwnsClockHalfDay(4), OwnsClockHalfDay(5)); + assert(false && "Check placed in unowned time period during Clock Shuffle!"); + } +} + +} // namespace TimeLogic +} // namespace Logic +} // namespace Rando diff --git a/mm/2s2h/Rando/Menu.cpp b/mm/2s2h/Rando/Menu.cpp index 7bf8f5beaa..7e14418a94 100644 --- a/mm/2s2h/Rando/Menu.cpp +++ b/mm/2s2h/Rando/Menu.cpp @@ -2,8 +2,12 @@ #include "Rando/Spoiler/Spoiler.h" #include "2s2h/BenGui/UIWidgets.hpp" #include "Rando/CheckTracker/CheckTracker.h" +#include "Rando/MiscBehavior/ClockShuffle.h" #include "build.h" #include "2s2h/BenGui/BenMenu.h" +#include "2s2h/BenGui/BenGui.hpp" +#include "2s2h/Rando/Logic/Logic.h" +#include "2s2h/ShipInit.hpp" extern "C" { #include "overlays/actors/ovl_En_Sth/z_en_sth.h" @@ -32,12 +36,15 @@ std::unordered_map accessTrialsOptions = { { RO_ACCESS_TRIALS_OPEN, "Open" }, }; +// clang-format off std::vector incompatibleWithVanilla = { RO_SHUFFLE_BOSS_SOULS, RO_SHUFFLE_SWIM, RO_SHUFFLE_ENEMY_SOULS, RO_PLENTIFUL_ITEMS, + RO_CLOCK_SHUFFLE, }; +// clang-format on std::vector checkExclusionList; bool isExcludedInitialized = false; @@ -55,6 +62,36 @@ extern "C" { #include "archives/icon_item_24_static/icon_item_24_static_yar.h" } +// Clock UI rendering constants +static const ImVec4 CLOCK_DAY_TINT = ImVec4(1.0f, 0.85f, 0.3f, 1.0f); +static const ImVec4 CLOCK_NIGHT_TINT = ImVec4(0.3f, 0.5f, 1.0f, 1.0f); +static const float DISABLED_ITEM_ALPHA = 0.3f; +static const char* CLOCK_PROGRESSIVE_TOOLTIP = + "\n\nTime items are not compatible with Progressive Time modes.\nSwitch to Random mode to use starting time."; + +// Apply clock-specific rendering (tint colors and tooltips) based on progressive mode +static void ApplyClockItemRendering(RandoItemId item, ImVec4& tintColor, std::string& tooltipText, + bool isProgressiveMode) { + using namespace Rando::ClockItems; + + if (!IsClockItem(item)) { + return; // Not a clock item, no special handling needed + } + + // Apply day/night color tint + if (IsDayClock(item)) { + tintColor = CLOCK_DAY_TINT; + } else { + tintColor = CLOCK_NIGHT_TINT; + } + + // Grey out and add tooltip if progressive mode is active + if (isProgressiveMode) { + tintColor.w *= DISABLED_ITEM_ALPHA; + tooltipText += CLOCK_PROGRESSIVE_TOOLTIP; + } +} + void ClearIncompatibleSetting() { int32_t currentLogicSetting = CVarGetInteger(Rando::StaticData::Options[RO_LOGIC].cvar, Rando::StaticData::Options[RO_LOGIC].defaultValue); @@ -64,6 +101,7 @@ void ClearIncompatibleSetting() { CVarClear(Rando::StaticData::Options[RO_PLENTIFUL_ITEMS].cvar); CVarClear(Rando::StaticData::Options[RO_SHUFFLE_BOSS_SOULS].cvar); CVarClear(Rando::StaticData::Options[RO_SHUFFLE_SWIM].cvar); + CVarClear(Rando::StaticData::Options[RO_CLOCK_SHUFFLE].cvar); break; default: break; @@ -114,6 +152,83 @@ void LoadExcludedChecks() { SortExcludedChecks(); } +static int checksInPool = 0; +static int itemsInPool = 0; +static int junkInPool = 0; +static bool ableToBalance = true; +void RefreshMetrics() { + RandoSaveInfo randoSaveInfo; + std::vector checkPool; + std::vector itemPool; + + // Load options into CVars + for (auto& [randoOptionId, randoStaticOption] : Rando::StaticData::Options) { + randoSaveInfo.randoSaveOptions[randoOptionId] = + (uint32_t)CVarGetInteger(randoStaticOption.cvar, randoStaticOption.defaultValue); + } + std::string startingItemsString = CVarGetString("gRando.StartingItems", RANDO_STARTING_ITEMS_DEFAULT); + strncpy(randoSaveInfo.randoStartingItems, startingItemsString.c_str(), startingItemsString.size() + 1); + + Rando::Logic::GeneratePools(randoSaveInfo, checkPool, itemPool); + + checksInPool = checkPool.size(); + itemsInPool = itemPool.size(); + junkInPool = 0; + for (auto& item : itemPool) { + if (Rando::StaticData::Items[item].randoItemType == RITYPE_JUNK) { + junkInPool++; + } + } + ableToBalance = checksInPool >= (itemsInPool - junkInPool); +} + +static RegisterShipInitFunc refreshMetricsInit(RefreshMetrics, { + // I Don't love this, but it works... + "gRando.Options.RO_ACCESS_DUNGEONS", + "gRando.Options.RO_ACCESS_MAJORA_MASKS_COUNT", + "gRando.Options.RO_ACCESS_MAJORA_REMAINS_COUNT", + "gRando.Options.RO_ACCESS_MOON_MASKS_COUNT", + "gRando.Options.RO_ACCESS_MOON_REMAINS_COUNT", + "gRando.Options.RO_ACCESS_TRIALS", + "gRando.Options.RO_CLOCK_SHUFFLE", + "gRando.Options.RO_HINTS_BOSS_REMAINS", + "gRando.Options.RO_HINTS_GOSSIP_STONES", + "gRando.Options.RO_HINTS_HOOKSHOT", + "gRando.Options.RO_HINTS_OATH_TO_ORDER", + "gRando.Options.RO_HINTS_PURCHASEABLE", + "gRando.Options.RO_HINTS_SPIDER_HOUSES", + "gRando.Options.RO_TRAP_AMOUNT", + "gRando.Options.RO_LOGIC", + "gRando.Options.RO_MINIMUM_SKULLTULA_TOKENS", + "gRando.Options.RO_MINIMUM_STRAY_FAIRIES", + "gRando.Options.RO_PLENTIFUL_ITEMS", + "gRando.Options.RO_SHUFFLE_BARREL_DROPS", + "gRando.Options.RO_SHUFFLE_BOSS_REMAINS", + "gRando.Options.RO_SHUFFLE_BOSS_SOULS", + "gRando.Options.RO_SHUFFLE_COWS", + "gRando.Options.RO_SHUFFLE_CRATE_DROPS", + "gRando.Options.RO_SHUFFLE_ENEMY_DROPS", + "gRando.Options.RO_SHUFFLE_ENEMY_SOULS", + "gRando.Options.RO_SHUFFLE_FREESTANDING_ITEMS", + "gRando.Options.RO_SHUFFLE_FROGS", + "gRando.Options.RO_SHUFFLE_GOLD_SKULLTULAS", + "gRando.Options.RO_SHUFFLE_GRASS_DROPS", + "gRando.Options.RO_SHUFFLE_TRAPS", + "gRando.Options.RO_SHUFFLE_OWL_STATUES", + "gRando.Options.RO_SHUFFLE_POT_DROPS", + "gRando.Options.RO_SHUFFLE_SHOPS", + "gRando.Options.RO_SHUFFLE_SNOWBALL_DROPS", + "gRando.Options.RO_SHUFFLE_SWIM", + "gRando.Options.RO_SHUFFLE_TINGLE_SHOPS", + "gRando.Options.RO_SHUFFLE_TRIFORCE_PIECES", + "gRando.Options.RO_STARTING_CONSUMABLES", + "gRando.Options.RO_STARTING_HEALTH", + "gRando.Options.RO_STARTING_MAPS_AND_COMPASSES", + "gRando.Options.RO_STARTING_RUPEES", + "gRando.Options.RO_TRIFORCE_PIECES_MAX", + "gRando.Options.RO_TRIFORCE_PIECES_REQUIRED", + }); + static void DrawGeneralTab() { ImGui::BeginChild("randoSettings"); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 0.5f)); @@ -149,8 +264,35 @@ static void DrawGeneralTab() { } UIWidgets::PopStyleSlider(); - UIWidgets::CVarCheckbox("Generate Spoiler File", "gRando.GenerateSpoiler"); + UIWidgets::CVarCheckbox("Generate Spoiler File", "gRando.GenerateSpoiler", + CheckboxOptions().DefaultValue(true)); + } + + float mainWidth = 300.0f; // Arbitrary width for progress bars + float itemProgress = mainWidth * (static_cast(itemsInPool) / static_cast(checksInPool)); + float junkProgress = static_cast(junkInPool) / static_cast(itemsInPool); + + ImGui::SeparatorText("Current Settings Metrics"); + ImGui::Text("Checks in pool: %d", checksInPool); + ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f); + ImGui::PushStyleColor(ImGuiCol_PlotHistogram, UIWidgets::ColorValues.at(THEME_COLOR)); + ImGui::PushStyleColor(ImGuiCol_FrameBg, UIWidgets::ColorValues.at(UIWidgets::Colors::DarkGray)); + ImGui::ProgressBar(1.0f, ImVec2(mainWidth, 0.0f), ""); + ImGui::Text("Items in Pool: %d", itemsInPool); + ImGui::SameLine(); + ImGui::TextColored(ImVec4(1.0f, 1.0f, 1.0f, 0.5f), "(%d Junk Items)", junkInPool); + + ImGui::ProgressBar(1.0f - junkProgress, ImVec2(itemProgress, 0.0f), ""); + ImGui::PopStyleColor(2); + ImGui::PopStyleVar(); + ImGui::Text("Able to Balance:"); + ImGui::SameLine(); + if (ableToBalance) { + ImGui::TextColored(ImVec4(0.0f, 1.0f, 0.0f, 1.0f), "Yes"); + } else { + ImGui::TextColored(ImVec4(1.0f, 0.0f, 0.0f, 1.0f), "No"); } + ImGui::SeparatorText("Enhancements"); UIWidgets::CVarCheckbox("Container Style Matches Contents", "gRando.CSMC"); UIWidgets::Tooltip("This will make the contents of a container match the container itself. This currently only " @@ -166,9 +308,8 @@ static void DrawGeneralTab() { } static void DrawLogicConditionsTab() { - f32 columnWidth = ImGui::GetContentRegionAvail().x / 3 - (ImGui::GetStyle().ItemSpacing.x * 2); - f32 halfHeight = ImGui::GetContentRegionAvail().y / 2 - (ImGui::GetStyle().ItemSpacing.y * 2); - ImGui::BeginChild("randoLogicColumn1", ImVec2(columnWidth, halfHeight)); + f32 columnWidth = ImGui::GetContentRegionAvail().x / 2 - (ImGui::GetStyle().ItemSpacing.x * 2); + ImGui::BeginChild("randoLogicColumn1", ImVec2(columnWidth, 0)); if (UIWidgets::CVarCombobox("Logic", Rando::StaticData::Options[RO_LOGIC].cvar, &logicOptions)) { ClearIncompatibleSetting(); } @@ -185,7 +326,7 @@ static void DrawLogicConditionsTab() { "Not compatible with settings that add items to the pool, like Boss Souls or Plentiful Items."); ImGui::EndChild(); ImGui::SameLine(); - ImGui::BeginChild("randoLogicColumn2", ImVec2(columnWidth, halfHeight)); + ImGui::BeginChild("randoLogicColumn2", ImVec2(columnWidth, 0)); UIWidgets::CVarCombobox("Dungeon Access", Rando::StaticData::Options[RO_ACCESS_DUNGEONS].cvar, &accessDungeonOptions); @@ -208,14 +349,11 @@ static void DrawLogicConditionsTab() { IntSliderOptions().Min(0).Max(20).DefaultValue(0)); UIWidgets::CVarCombobox("Trials Access", Rando::StaticData::Options[RO_ACCESS_TRIALS].cvar, &accessTrialsOptions); ImGui::EndChild(); - ImGui::BeginChild("randoLogicTricks", ImVec2(0, 0)); - ImGui::SeparatorText("Tricks & Glitches"); - ImGui::EndChild(); } static void DrawShufflesTab() { - f32 columnWidth = ImGui::GetContentRegionAvail().x / 3 - (ImGui::GetStyle().ItemSpacing.x * 2); - f32 halfHeight = ImGui::GetContentRegionAvail().y / 2 - (ImGui::GetStyle().ItemSpacing.y * 2); + f32 columnWidth = ImGui::GetContentRegionAvail().x / 2 - (ImGui::GetStyle().ItemSpacing.x * 2); + f32 halfHeight = 0; ImGui::SeparatorText("Shuffle Options"); ImGui::BeginChild("randoShufflesColumn1", ImVec2(columnWidth, halfHeight)); CVarCheckbox("Shuffle Songs", "gPlaceholderBool", @@ -259,31 +397,6 @@ static void DrawShufflesTab() { ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("randoLocationsColumn3", ImVec2(columnWidth, halfHeight)); - CVarCheckbox("Triforce Hunt", Rando::StaticData::Options[RO_SHUFFLE_TRIFORCE_PIECES].cvar); - ImGui::BeginDisabled(!CVarGetInteger(Rando::StaticData::Options[RO_SHUFFLE_TRIFORCE_PIECES].cvar, RO_GENERIC_OFF)); - CVarSliderInt( - "Required Triforce Pieces", Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar, - IntSliderOptions({}) - .Min(1) - .Max(CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, DEFAULT_TRIFORCE_PIECES_MAX)) - .DefaultValue(DEFAULT_TRIFORCE_PIECES_MAX)); - if (CVarSliderInt( - "Shuffled Triforce Pieces", Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, - IntSliderOptions({}) - .Min(1) - .Max(1000) - .DefaultValue(DEFAULT_TRIFORCE_PIECES_MAX) - .Tooltip("If the maximum amount of placeable pieces exceeds what will allow the seed to generate, the " - "amount will be adjusted automatically."))) { - if (CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar, DEFAULT_TRIFORCE_PIECES_MAX) > - CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, DEFAULT_TRIFORCE_PIECES_MAX)) { - CVarGetInteger( - Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar, - CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, DEFAULT_TRIFORCE_PIECES_MAX)); - } - } - - ImGui::EndDisabled(); ImGui::EndChild(); } @@ -337,6 +450,77 @@ static void DrawItemsTab() { "that must be found in order for their corresponding enemy to spawn.", .disabled = IncompatibleWithLogicSetting(RO_SHUFFLE_ENEMY_SOULS), .disabledTooltip = "Incompatible with current Logic Setting" } })); + CVarCheckbox("Shuffle Time", Rando::StaticData::Options[RO_CLOCK_SHUFFLE].cvar, + CheckboxOptions({ { .tooltip = "Breaks the 3-day cycle into 6 separate half-days (Day 1 Day/Night, " + "Day 2 Day/Night, Day 3 Day/Night) that must be unlocked as items. " + "Players can only access time periods they've obtained. Attempting to " + "access unowned time redirects to the next owned half-day.", + .disabled = IncompatibleWithLogicSetting(RO_CLOCK_SHUFFLE), + .disabledTooltip = "Incompatible with current Logic Setting" } })); + // Only show time progression options when shuffle time is enabled + if (CVarGetInteger(Rando::StaticData::Options[RO_CLOCK_SHUFFLE].cvar, 0)) { + static std::unordered_map clockModeOptions = { + { RO_CLOCK_SHUFFLE_RANDOM, "Random" }, + { RO_CLOCK_SHUFFLE_ASCENDING, "Progressive: Ascending" }, + { RO_CLOCK_SHUFFLE_DESCENDING, "Progressive: Descending" }, + }; + { + int32_t value = + CVarGetInteger(Rando::StaticData::Options[RO_CLOCK_SHUFFLE_PROGRESSIVE].cvar, RO_CLOCK_SHUFFLE_RANDOM); + if (UIWidgets::Combobox("Time Progression Mode", &value, &clockModeOptions)) { + CVarSetInteger(Rando::StaticData::Options[RO_CLOCK_SHUFFLE_PROGRESSIVE].cvar, value); + Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); + } + UIWidgets::Tooltip("Random: All 6 half-days shuffled randomly. Player starts with one random half-day.\n\n" + "Progressive Ascending: Unlocks half-days in order (D1, N1, D2, N2, D3, N3).\n\n" + "Progressive Descending: Unlocks half-days in reverse order (N3, D3, N2, D2, N1, D1)."); + } + // Terminal time slider (Final Hours start time) + { + int32_t terminalMinutes = CVarGetInteger(Rando::StaticData::Options[RO_CLOCK_TERMINAL_TIME].cvar, 0); + int hours = terminalMinutes / 60; + int minutes = terminalMinutes % 60; + + ImGui::Spacing(); + ImGui::Text("Final Hours Start Time: %02d:%02d", hours, minutes); + ImGui::Spacing(); + UIWidgets::CVarSliderInt("Final Hours Start Time", Rando::StaticData::Options[RO_CLOCK_TERMINAL_TIME].cvar, + UIWidgets::IntSliderOptions().Min(0).Max(359).DefaultValue(0).LabelPosition( + UIWidgets::LabelPosition::None)); + ImGui::Spacing(); + + UIWidgets::Tooltip("Controls when the final hours countdown begins (00:00 to 05:59). " + "When you run out of owned half-days, this allows the player control over how much " + "time is left before the moon crash.\n\n" + "This setting is baked into the seed and cannot be changed after generation."); + } + } + + CVarCheckbox("Triforce Hunt", Rando::StaticData::Options[RO_SHUFFLE_TRIFORCE_PIECES].cvar); + ImGui::BeginDisabled(!CVarGetInteger(Rando::StaticData::Options[RO_SHUFFLE_TRIFORCE_PIECES].cvar, RO_GENERIC_OFF)); + CVarSliderInt( + "Required Triforce Pieces", Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar, + IntSliderOptions({}) + .Min(1) + .Max(CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, DEFAULT_TRIFORCE_PIECES_MAX)) + .DefaultValue(DEFAULT_TRIFORCE_PIECES_MAX)); + if (CVarSliderInt( + "Shuffled Triforce Pieces", Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, + IntSliderOptions({}) + .Min(1) + .Max(1000) + .DefaultValue(DEFAULT_TRIFORCE_PIECES_MAX) + .Tooltip("If the maximum amount of placeable pieces exceeds what will allow the seed to generate, the " + "amount will be adjusted automatically."))) { + if (CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar, DEFAULT_TRIFORCE_PIECES_MAX) > + CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, DEFAULT_TRIFORCE_PIECES_MAX)) { + CVarSetInteger( + Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar, + CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, DEFAULT_TRIFORCE_PIECES_MAX)); + } + } + + ImGui::EndDisabled(); ImGui::EndChild(); ImGui::SameLine(); ImGui::BeginChild("randoItemsColumn3", ImVec2(columnWidth, ImGui::GetContentRegionAvail().y)); @@ -351,7 +535,7 @@ static void DrawItemsTab() { .Color(UIWidgets::Colors(CVarGetInteger("gSettings.Menu.Theme", 5))) .Format("Traps: %i") .Min(1) - .Max(10) + .Max(100) .DefaultValue(5)); ImGui::SeparatorText("Toggle Trap Types"); CVarCheckbox( @@ -453,10 +637,15 @@ static void DrawStartingItemsTab() { const char* texturePath = Rando::StaticData::GetIconTexturePath(startingItem); ImTextureID textureId = Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(texturePath); - if (ImGui::ImageButton( - std::to_string(listIndex).c_str(), textureId, imageSize, ImVec2(0, 0), ImVec2(1, 1), ImVec4(0, 0, 0, 0), - Ship_GetItemColorTint(startingItem == RI_PROGRESSIVE_LULLABY ? ITEM_SONG_LULLABY - : randoStaticItem.itemId))) { + ImVec4 tintColor = + Ship_GetItemColorTint(startingItem == RI_PROGRESSIVE_LULLABY ? ITEM_SONG_LULLABY : randoStaticItem.itemId); + std::string tooltipText = randoStaticItem.name; + bool isProgressiveMode = CVarGetInteger(Rando::StaticData::Options[RO_CLOCK_SHUFFLE_PROGRESSIVE].cvar, + RO_CLOCK_SHUFFLE_RANDOM) != RO_CLOCK_SHUFFLE_RANDOM; + ApplyClockItemRendering(startingItem, tintColor, tooltipText, isProgressiveMode); + + if (ImGui::ImageButton(std::to_string(listIndex).c_str(), textureId, imageSize, ImVec2(0, 0), ImVec2(1, 1), + ImVec4(0, 0, 0, 0), tintColor)) { for (size_t i = 0; i < setStartingItemsList.size(); i++) { if (setStartingItemsList[i] == startingItem) { setStartingItemsList.erase(setStartingItemsList.begin() + i); @@ -466,7 +655,7 @@ static void DrawStartingItemsTab() { CVarSetString("gRando.StartingItems", CreateStartingItemsToCvar(setStartingItemsList).c_str()); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); } - UIWidgets::Tooltip(randoStaticItem.name); + UIWidgets::Tooltip(tooltipText.c_str()); listIndex++; if ((listIndex + 1) % 15 != 0) { @@ -486,6 +675,8 @@ static void DrawStartingItemsTab() { tableColumns = 5; if (category.first == STARTING_ITEMS_MASK) { tableColumns++; + } else if (category.first == STARTING_ITEMS_MISC) { + tableColumns = 6; // Need 6 columns for the 6 time items on their own row } ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); if (ImGui::BeginChild(std::to_string(category.first).c_str(), ImVec2(0, 0), @@ -512,15 +703,22 @@ static void DrawStartingItemsTab() { ImTextureID textureId = Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(texturePath); - if (item == RI_SONG_TIME) { + // Force new row for Song of Time, first frog, and first time item + if (item == RI_SONG_TIME || item == RI_FROG_BLUE || item == RI_TIME_DAY_1) { ImGui::TableNextRow(); } ImGui::TableNextColumn(); + + ImVec4 tintColor = Ship_GetItemColorTint(item == RI_PROGRESSIVE_LULLABY ? ITEM_SONG_LULLABY + : randoStaticItem.itemId); + std::string tooltipText = randoStaticItem.name; + bool isProgressiveMode = + CVarGetInteger(Rando::StaticData::Options[RO_CLOCK_SHUFFLE_PROGRESSIVE].cvar, + RO_CLOCK_SHUFFLE_RANDOM) != RO_CLOCK_SHUFFLE_RANDOM; + ApplyClockItemRendering(item, tintColor, tooltipText, isProgressiveMode); + if (ImGui::ImageButton(std::to_string(item).c_str(), textureId, imageSize, ImVec2(0, 0), - ImVec2(1, 1), ImVec4(0, 0, 0, 0), - Ship_GetItemColorTint(item == RI_PROGRESSIVE_LULLABY - ? ITEM_SONG_LULLABY - : randoStaticItem.itemId))) { + ImVec2(1, 1), ImVec4(0, 0, 0, 0), tintColor)) { std::string currentStartingItems = CVarGetString("gRando.StartingItems", RANDO_STARTING_ITEMS_DEFAULT); if (currentStartingItems.length() != 0) { @@ -530,7 +728,7 @@ static void DrawStartingItemsTab() { CVarSetString("gRando.StartingItems", currentStartingItems.c_str()); Ship::Context::GetInstance()->GetWindow()->GetGui()->SaveConsoleVariablesNextFrame(); } - UIWidgets::Tooltip(randoStaticItem.name); + UIWidgets::Tooltip(tooltipText.c_str()); } ImGui::EndTable(); } diff --git a/mm/2s2h/Rando/MiscBehavior/ClockShuffle.cpp b/mm/2s2h/Rando/MiscBehavior/ClockShuffle.cpp new file mode 100644 index 0000000000..d5c8f82f57 --- /dev/null +++ b/mm/2s2h/Rando/MiscBehavior/ClockShuffle.cpp @@ -0,0 +1,762 @@ +#include "ClockShuffle.h" +#include "Rando/Rando.h" +#include "Rando/Logic/Logic.h" +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/CustomMessage/CustomMessage.h" +#include +#include "2s2h/ShipUtils.h" + +extern "C" { +#include "z64game.h" +#include "overlays/gamestates/ovl_daytelop/z_daytelop.h" +#include "overlays/actors/ovl_En_Test4/z_en_test4.h" +#include "functions.h" +} + +using namespace Rando::Logic; + +namespace Rando { + +// ============================================================================ +// CLOCK ITEM MANAGEMENT +// ============================================================================ + +namespace ClockItems { + +// Internal half-day indices + +// Convert a rando item ID to its corresponding half-day index +int GetHalfDayIndexFromClockItem(RandoItemId clockItemId) { + switch (clockItemId) { + case RI_TIME_DAY_1: + return HALF_DAY1_DAY; + case RI_TIME_NIGHT_1: + return HALF_DAY1_NIGHT; + case RI_TIME_DAY_2: + return HALF_DAY2_DAY; + case RI_TIME_NIGHT_2: + return HALF_DAY2_NIGHT; + case RI_TIME_DAY_3: + return HALF_DAY3_DAY; + case RI_TIME_NIGHT_3: + return HALF_DAY3_NIGHT; + default: + return INVALID; + } +} + +// Convert a half-day index back to its rando item ID +RandoItemId GetClockItemFromHalfDayIndex(int halfDayIndex) { + if (halfDayIndex < 0 || halfDayIndex >= HALF_COUNT) { + return RI_UNKNOWN; + } + + // Map each half-day index to its corresponding rando item + static const RandoItemId clockItemMap[] = { + RI_TIME_DAY_1, // HALF_DAY1_DAY (index 0) + RI_TIME_NIGHT_1, // HALF_DAY1_NIGHT (index 1) + RI_TIME_DAY_2, // HALF_DAY2_DAY (index 2) + RI_TIME_NIGHT_2, // HALF_DAY2_NIGHT (index 3) + RI_TIME_DAY_3, // HALF_DAY3_DAY (index 4) + RI_TIME_NIGHT_3, // HALF_DAY3_NIGHT (index 5) + }; + + return clockItemMap[halfDayIndex]; +} + +u8 GetAllOwnedHalfDaysMask() { + u8 ownedMask = 0; + for (int i = 0; i < HALF_COUNT; ++i) { + if (OwnsClockHalfDay(i)) { + ownedMask |= (1 << i); + } + } + return ownedMask; +} + +int FindEarliestOwnedHalfDay(bool searchFromEnd) { + if (searchFromEnd) { + for (int i = HALF_COUNT - 1; i >= 0; --i) { + if (OwnsClockHalfDay(i)) + return i; + } + } else { + for (int i = 0; i < HALF_COUNT; ++i) { + if (OwnsClockHalfDay(i)) + return i; + } + } + return INVALID; +} + +int FindNextOwnedHalfDayAfter(int startHalfDay, u8 ownedMask) { + if (startHalfDay < 0 || startHalfDay >= HALF_COUNT) { + return TERMINAL_STATE; // Invalid input, go to terminal state + } + + // Search for the next owned half-day after the start point + for (int halfDayIndex = startHalfDay + 1; halfDayIndex < HALF_COUNT; ++halfDayIndex) { + if (ownedMask & (1 << halfDayIndex)) { + return halfDayIndex; + } + } + + return TERMINAL_STATE; // No owned half-days found after start point +} + +// Check if a rando item is a clock item +bool IsClockItem(RandoItemId itemId) { + return (itemId >= RI_TIME_DAY_1 && itemId <= RI_TIME_NIGHT_3) || itemId == RI_TIME_PROGRESSIVE; +} + +// Check if a clock item is a day clock (vs night clock) +bool IsDayClock(RandoItemId itemId) { + return itemId == RI_TIME_DAY_1 || itemId == RI_TIME_DAY_2 || itemId == RI_TIME_DAY_3; +} + +} // namespace ClockItems + +namespace ClockShuffle { + +// ============================================================================ +// INTERNAL TYPES AND DATA +// ============================================================================ + +// Configuration for each half-day's timing +struct HalfDayTimeConfig { + u8 dayNumber; // Which day (1, 2, or 3) + u16 startTime; // When this half-day begins (6:00 AM or 6:00 PM) + u16 endTime; // When this half-day ends (5:59 AM or 5:59 PM) +}; + +// ============================================================================ +// TIME CONFIGURATION DATA +// ============================================================================ + +constexpr u16 DAWN_TIME = CLOCK_TIME(6, 0); // 6:00 AM - start of day +constexpr u16 DUSK_TIME = CLOCK_TIME(18, 0); // 6:00 PM - start of night +constexpr u16 DAWN_END_TIME = CLOCK_TIME(5, 59); // 5:59 AM - end of night +constexpr u16 DUSK_END_TIME = CLOCK_TIME(17, 59); // 5:59 PM - end of day +constexpr u16 DAY_0_0559_TIME = CLOCK_TIME(6, 0) - 1; // Day 0, 5:59 AM - Cycle Reset Time +// Vanilla uses CLOCK_TIME(6, 0) - 1 = 16383, NOT CLOCK_TIME(5, 59) = 16338 for cycle resets +// This 45-unit difference has to be accounted for. + +// ============================================================================ +// TERMINAL TIME HELPER FUNCTIONS +// ============================================================================ + +// Convert slider minutes (0-359) to CLOCK_TIME format +u16 MinutesToClockTime(int minutes) { + return CLOCK_TIME(0, minutes); // 00:00 + minutes +} + +// Get the configured terminal state time from the saved rando option +u16 GetConfiguredTerminalTime() { + int terminalMinutes = RANDO_SAVE_OPTIONS[RO_CLOCK_TERMINAL_TIME]; + return MinutesToClockTime(terminalMinutes); +} + +// Configuration for each half-day's timing and behavior +constexpr HalfDayTimeConfig HALF_DAY_CONFIGS[] = { + /* HALF_DAY1_DAY */ { 1, DAWN_TIME, DUSK_END_TIME }, // Day 1: 6:00 AM - 5:59 PM + /* HALF_DAY1_NIGHT */ { 1, DUSK_TIME, DAWN_END_TIME }, // Day 1: 6:00 PM - 5:59 AM + /* HALF_DAY2_DAY */ { 2, DAWN_TIME, DUSK_END_TIME }, // Day 2: 6:00 AM - 5:59 PM + /* HALF_DAY2_NIGHT */ { 2, DUSK_TIME, DAWN_END_TIME }, // Day 2: 6:00 PM - 5:59 AM + /* HALF_DAY3_DAY */ { 3, DAWN_TIME, DUSK_END_TIME }, // Day 3: 6:00 AM - 5:59 PM + /* HALF_DAY3_NIGHT */ { 3, DUSK_TIME, DAWN_END_TIME }, // Day 3: 6:00 PM - 5:59 AM +}; + +// ============================================================================ +// TIME DETECTION AND CONFIGURATION +// ============================================================================ + +const HalfDayTimeConfig* GetHalfDayTimeConfig(int halfDayIndex) { + if (halfDayIndex < 0 || halfDayIndex >= ClockItems::HALF_COUNT) { + return nullptr; + } + + return &HALF_DAY_CONFIGS[halfDayIndex]; +} + +bool IsCurrentlyNightTime(u16 gameTime) { + return (gameTime >= DUSK_TIME) || (gameTime < DAWN_TIME); +} + +// Check if time value represents night (inline for performance) +inline bool IsNightTime(u16 time) { + return (time < GAME_TIME_DAY_START) || (time >= GAME_TIME_NIGHT_START); +} + +// Check if a given day/time is in the configured terminal state range +bool IsInTerminalRange(s32 day, u16 time) { + if (day != 3) { + return false; + } + + u16 terminalTime = GetConfiguredTerminalTime(); + + // Terminal range spans from terminal time until 6:00 AM (may wrap around midnight) + if (terminalTime >= DUSK_TIME) { + // Terminal starts in evening (18:00-23:59), range wraps through midnight to dawn + return (time >= terminalTime) || (time < DAWN_TIME); + } else { + // Terminal starts after midnight (0:00-5:59), range goes until dawn + return (time >= terminalTime && time < DAWN_TIME); + } +} + +// Calculate half-day index from day/time (extracted for reuse) +int GetHalfDayIndexFromTime(s32 day, u16 time) { + if (day < 1) + return 0; + int halfDay = (day - 1) * 2; + if (IsNightTime(time)) + halfDay++; + return halfDay; +} + +int GetCurrentHalfDayIndex() { + const u16 currentTime = gSaveContext.save.time; + const s32 currentDay = gSaveContext.save.day; + + // Handle moon crash/game over sequence: Day 4 or higher means the moon has fallen and the game is ending. + if (currentDay >= 4) { + return ClockItems::TERMINAL_STATE; // Use TERMINAL_STATE as a sentinel for post-crash state. + } + + // The game uses Day 0 as a transition before placing the player at the correct half-day. + if (currentDay == 0) { + return ClockItems::TERMINAL_STATE; // Signal: handle Day 0 as a reset/redirect. + } + + // This is what ClockShuffle treats as a "buffer" period before the moon crash. + // The buffer length is now configurable via RO_CLOCK_TERMINAL_TIME. + if (IsInTerminalRange(currentDay, currentTime)) { + return ClockItems::TERMINAL_STATE; + } + + const bool isNight = IsCurrentlyNightTime(currentTime); + + // Figure out which half-day we're in: + // - Each day has two halves: day (even indices) and night (odd indices) + // - Day 1's day is index 0, night is 1; Day 2's day is 2, night is 3, etc. + // - So: (currentDay - 1) * 2 gives us the starting index for that day (0 for Day 1, 2 for Day 2, 4 for Day 3) + // - If it's night, add 1; if it's day, add 0. + // Example: Day 2 night → (2-1)*2 + 1 = 2 + 1 = 3 + return (currentDay - 1) * 2 + (isNight ? 1 : 0); +} + +// ============================================================================ +// TIME MANIPULATION FUNCTIONS +// ============================================================================ + +// Apply a new time to the game, updating all related state +void SetGameTime(u8 day, u16 time) { + gSaveContext.save.day = day; + gSaveContext.save.time = time; + gSaveContext.save.isNight = IsCurrentlyNightTime(time); + gSaveContext.save.eventDayCount = day; +} + +// Set time to half-day start with proper music handling +void SetTimeToHalfDayStart(int halfDayIndex) { + // Don't try to set time for terminal state + if (halfDayIndex == ClockItems::TERMINAL_STATE) { + return; + } + + // Get the configuration for this half-day + const HalfDayTimeConfig* config = GetHalfDayTimeConfig(halfDayIndex); + if (!config) { + return; + } + + // Set time to the start of this half-day + SetGameTime(config->dayNumber, config->startTime); + + // Reset gSceneSeqState to prevent morning sequence from playing for night halves + // This is needed because DayTelop or previous transitions may have set it to SCENESEQ_MORNING + if (IsCurrentlyNightTime(config->startTime)) { + gSceneSeqState = SCENESEQ_DEFAULT; + } +} + +// Check if a scene needs to be reloaded for time skips (whitelist approach) +bool SceneNeedsReloadForTimeSkip(s16 sceneId) { + // Use a whitelist approach - only reload scenes that actually need it + // These scenes are extremely time-sensitive and require reload + switch (sceneId) { + case SCENE_BOWLING: // Honey & Darling - minigame mode changes based on day/time + return true; + case SCENE_CLOCKTOWER: // South Clock Town - Clock Tower platform appears at midnight Night 3 + return true; + default: + return false; // Most scenes handle time changes without reload + } +} + +// Force a scene transition to reload the current area +void ForceSceneReload() { + Player* player = GET_PLAYER(gPlayState); + + // Set up the transition parameters + gPlayState->nextEntrance = gSaveContext.save.entrance; + gPlayState->transitionTrigger = TRANS_TRIGGER_START; + gPlayState->transitionType = TRANS_TYPE_FADE_BLACK_FAST; + + // Set up respawn data to return to the same location + Play_SetRespawnData(gPlayState, RESPAWN_MODE_RETURN, gSaveContext.save.entrance, gPlayState->roomCtx.curRoom.num, + PLAYER_PARAMS(0xFF, PLAYER_START_MODE_B), &player->actor.world.pos, player->actor.world.rot.y); + + // Configure the transition + gSaveContext.nextTransitionType = TRANS_TYPE_FADE_BLACK; + gSaveContext.respawnFlag = 2; +} + +// ============================================================================ +// PROACTIVE TIME CHECKING +// ============================================================================ +// This is separated into two functions for clarity: +// - ShouldSkipTime: Pure decision logic to determine if a skip is needed +// - CheckAndSkipUnownedTime: Application logic that performs the time skip +// +// Key implementation patterns: +// - Lookahead calculation (current time + 1 minute) +// - Inline night check (time < GAME_TIME_DAY_START || time >= GAME_TIME_NIGHT_START) +// - Day transition detection via prevTime +// - Direct time/day modification followed by day-- trick for dawn +// - prevTime = newTime - 1 minute +// +// Additional features: +// - Terminal state: If no owned clocks, jump to configured terminal time (not Day 4) +// - RandoInf flags for tracking owned half-days +// - GameInteractor hooks for seamless integration + +// Decision function: Determines if we should skip time and calculates target half-day +// Returns true if skip is needed, false otherwise +bool ShouldSkipTime(s32 day, u16 time, int* outNextHalfDay) { + // Early exits + if (gPlayState->envCtx.sceneTimeSpeed == 0 || Play_InCsMode(gPlayState) || day >= 4) { + return false; + } + + // Check terminal range + if (IsInTerminalRange(day, time)) { + return false; + } + + // Calculate and check current half-day ownership + int currentHalfDay = GetHalfDayIndexFromTime(day, time); + if (currentHalfDay >= 0 && currentHalfDay < ClockItems::HALF_COUNT && OwnsClockHalfDay(currentHalfDay)) { + return false; + } + + // Find next owned half-day + int nextHalfDay = ClockItems::TERMINAL_STATE; + for (int i = currentHalfDay + 1; i < ClockItems::HALF_COUNT; ++i) { + if (OwnsClockHalfDay(i)) { + nextHalfDay = i; + break; + } + } + + *outNextHalfDay = nextHalfDay; + return true; +} + +// Apply time skip to target half-day (extracted helper) +void ApplyTimeSkip(int nextHalfDay, EnTest4* enTest4) { + s32 day = (nextHalfDay / 2) + 1; + u16 time = (nextHalfDay & 1) ? GAME_TIME_NIGHT_START : GAME_TIME_DAY_START; + + // Terminal state override + if (nextHalfDay == ClockItems::TERMINAL_STATE) { + day = 3; + time = GetConfiguredTerminalTime(); + } + + // Apply time change + gSaveContext.save.day = day; + gSaveContext.save.time = time; + + // Update actor state + enTest4->daytimeIndex = IsCurrentlyNightTime(time) ? 1 : 0; + + // Handle scene reload for time-sensitive scenes + if (SceneNeedsReloadForTimeSkip(gPlayState->sceneId)) { + ForceSceneReload(); + enTest4->prevTime = time - CLOCK_TIME(0, 1); + return; + } + + // Terminal state handling + if (nextHalfDay == ClockItems::TERMINAL_STATE) { + enTest4->prevTime = time - CLOCK_TIME(0, 1); + return; + } + + // Day transition handling + if (time == GAME_TIME_DAY_START) { + gSaveContext.save.day--; + } else { + Interface_NewDay(gPlayState, gSaveContext.save.day); + Environment_NewDay(&gPlayState->envCtx); + } + + enTest4->prevTime = time - CLOCK_TIME(0, 1); +} + +// Application function: Applies the time skip +// Check if time is about to cross into an unowned half-day and skip forward if needed +void CheckAndSkipUnownedTime(Actor* timeActor) { + // Get EnTest4 actor for state access + EnTest4* enTest4 = (EnTest4*)timeActor; + + // Skip if eventInf bit 0x0f is set (dog race) + if (gSaveContext.eventInf[0] & 0x0F) { + return; + } + + // Calculate lookahead day/time + s32 day = gSaveContext.save.day; + u16 time = gSaveContext.save.time + CLOCK_TIME(0, 1); + + // Check for day transition using actor's prevTime + // Night check: time < GAME_TIME_DAY_START || time >= GAME_TIME_NIGHT_START + bool prevWasNight = IsNightTime(enTest4->prevTime); + bool nextIsNight = IsNightTime(time); + if (prevWasNight && !nextIsNight) { + day++; + } + + if (day >= 4) { + return; // Don't process moon crash + } + + // Check if we should skip time (decision logic) + int nextHalfDay; + if (!ShouldSkipTime(day, time, &nextHalfDay)) { + return; // No skip needed + } + + // Apply the time skip using extracted helper + ApplyTimeSkip(nextHalfDay, enTest4); +} + +// ============================================================================ +// PUBLIC API +// ============================================================================ + +// Check if a specific day/time is owned by the player in ClockShuffle mode +bool IsTimeOwnedForClockShuffle(s32 day, u16 time) { + if (!RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE]) { + return true; + } + + if (day < 1 || day > 3) + return true; + if (IsInTerminalRange(day, time)) + return true; + + int halfDayIndex = GetHalfDayIndexFromTime(day, time); + return OwnsClockHalfDay(halfDayIndex); +} + +// Get a formatted description of a time period for error messages +std::string GetTimeDescriptionForMessage(s32 day, u16 time) { + // Handle terminal state + if (IsInTerminalRange(day, time)) { + return "%rFinal Hours%w"; + } + + // Determine if this is night time + bool isNight = IsCurrentlyNightTime(time); + + std::string description; + if (isNight) { + description = "%rNight of the "; + } else { + description = "%rDawn of the "; + } + + // Add day ordinal + if (day == 1) { + description += "First"; + } else if (day == 2) { + description += "Second"; + } else if (day == 3) { + description += "Third"; + } else { + description += "Unknown"; + } + + description += " Day%w"; + return description; +} + +// Helper for initial file load time correction +void CorrectInitialTime() { + const int earliestOwnedHalfDay = ClockItems::FindEarliestOwnedHalfDay(false); + if (earliestOwnedHalfDay == ClockItems::INVALID) + return; + + bool isDayHalf = (earliestOwnedHalfDay % 2 == 0); + int targetDay = (earliestOwnedHalfDay / 2) + 1; + + if (isDayHalf) { + SetGameTime(targetDay - 1, CLOCK_TIME(6, 0) - 1); + } else { + SetTimeToHalfDayStart(earliestOwnedHalfDay); + } +} + +void ProcessClockShuffleMessage(u16* textId, bool* loadFromMessageTable, bool isSongOfTime) { + auto entry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + + // Determine target half-day based on which song is used + int targetHalfDay; + if (isSongOfTime) { + // Song of Time: go to earliest owned half-day + targetHalfDay = ClockItems::FindEarliestOwnedHalfDay(false); + } else { + // Song of Double Time: go to next owned half-day after current + int currentHalfDay = GetCurrentHalfDayIndex(); + u8 ownedHalfDaysMask = ClockItems::GetAllOwnedHalfDaysMask(); + targetHalfDay = ClockItems::FindNextOwnedHalfDayAfter(currentHalfDay, ownedHalfDaysMask); + } + + std::string destinationText; + if (targetHalfDay == ClockItems::TERMINAL_STATE) { + destinationText = "%rFinal Hours%w"; + } else { + // Convert half-day index to readable text + int targetDay = (targetHalfDay / 2) + 1; + bool isNight = (targetHalfDay % 2 == 1); + + if (isNight) { + destinationText = "%rNight of "; + if (targetDay == 1) + destinationText += "First"; + else if (targetDay == 2) + destinationText += "Second"; + else if (targetDay == 3) + destinationText += "Third"; + destinationText += " Day%w"; + } else { + destinationText = "%rDawn of the "; + if (targetDay == 1) + destinationText += "First"; + else if (targetDay == 2) + destinationText += "Second"; + else if (targetDay == 3) + destinationText += "Third"; + destinationText += " Day%w"; + } + } + + // Use different message format for each song + if (isSongOfTime) { + entry.msg = "Save and return to " + destinationText + "?\n%gYes\nNo\xC2"; + } else { // Song of Double Time + entry.msg = "Time moves strangely...\nProceed to " + destinationText + "?\n%gYes\nNo\xC2"; + } + + CustomMessage::LoadCustomMessageIntoFont(entry); + *loadFromMessageTable = false; +} + +void OnFileLoad() { + bool shouldRegister = IS_RANDO && RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE]; + + // Correct Day 0 time on file load BEFORE scene initialization + // OnSaveLoad fires before Play_Init, ensuring time is correct before Environment_PlaySceneSequence processes audio + // This prevents bird chirps from playing when correcting to night half-days on initial spawn + if (shouldRegister) { + // Check if this is initial spawn (day=0) and needs correction + if (!gSaveContext.save.isOwlSave && (gSaveContext.save.day == 0 && gSaveContext.save.time == DAY_0_0559_TIME)) { + CorrectInitialTime(); + } + } + + // Hook EnTest4 BEFORE vanilla update to proactively check for time skips + // This is critical: we must modify time BEFORE vanilla processes it! + COND_ID_HOOK(ShouldActorUpdate, ACTOR_EN_TEST4, shouldRegister, [](Actor* actor, bool* should) { + CheckAndSkipUnownedTime(actor); + *should = true; // Always let vanilla continue with our modified time + }); + + // Hook Song of Time and Song of Double Time message IDs + COND_ID_HOOK(OnOpenText, 0x1B8A, shouldRegister, [](u16* textId, bool* loadFromMessageTable) { + ProcessClockShuffleMessage(textId, loadFromMessageTable, true); + }); + + auto onDoubleTime = [](u16* textId, bool* loadFromMessageTable) { + ProcessClockShuffleMessage(textId, loadFromMessageTable, false); + }; + + COND_ID_HOOK(OnOpenText, 0x1B91, shouldRegister, onDoubleTime); + COND_ID_HOOK(OnOpenText, 0x1B90, shouldRegister, onDoubleTime); + COND_ID_HOOK(OnOpenText, 0x1B8F, shouldRegister, onDoubleTime); + COND_ID_HOOK(OnOpenText, 0x1B92, shouldRegister, onDoubleTime); + COND_ID_HOOK(OnOpenText, 0x1B8E, shouldRegister, onDoubleTime); + + COND_VB_SHOULD(VB_TIME_UNTIL_MOON_CRASH_CALCULATION, shouldRegister, { + *should = false; // Skip vanilla calculation + + // Get the time variable that was passed + u32* timeVar = va_arg(args, u32*); + + // Calculate owned time remaining + u8 ownedHalfDaysMask = ClockItems::GetAllOwnedHalfDaysMask(); + u32 totalHours = 0; + + for (int halfDayIndex = 0; halfDayIndex < ClockItems::HALF_COUNT; ++halfDayIndex) { + if (ownedHalfDaysMask & (1 << halfDayIndex)) { + totalHours += 12; // Each half-day is 12 hours + } + } + + // Add final hours based on configured terminal time if we're in terminal state + // OR if we don't own Night 3 (which means we'll end up in terminal state) + bool shouldIncludeTerminalHours = + (GetCurrentHalfDayIndex() == ClockItems::TERMINAL_STATE) || !OwnsClockHalfDay(ClockItems::HALF_DAY3_NIGHT); + + if (shouldIncludeTerminalHours) { + // Calculate remaining hours from configured terminal time to 6:00 AM + u16 terminalTime = GetConfiguredTerminalTime(); + + // Calculate time difference (terminal time to dawn) + u32 terminalZeroed = ZERO_DAY_START(terminalTime); + u32 timeDiff = (DAY_LENGTH - terminalZeroed) % DAY_LENGTH; + + // Convert to hours (round up to ensure we don't under-calculate) + u32 terminalHours = (timeDiff + CLOCK_TIME_HOUR - 1) / CLOCK_TIME_HOUR; + totalHours += terminalHours; + } + + u32 ownedTime = totalHours * CLOCK_TIME_HOUR; + + // Compensate for the -1 minute offset that vanilla uses + // Vanilla uses CLOCK_TIME(6, 0) - 1, Clock Shuffle uses CLOCK_TIME(6, 0) + // So we need to add 1 minute to match vanilla behavior + ownedTime += CLOCK_TIME_MINUTE; + + // Set the time variable to our calculated value + *timeVar = ownedTime; + }); + + // Hook scarecrow dance time skip to redirect to next owned half-day + COND_VB_SHOULD(VB_SCARECROW_DANCE_SET_TIME, shouldRegister, { + *should = false; // Skip vanilla behavior + + // Calculate next owned half-day after current + int currentHalfDay = GetCurrentHalfDayIndex(); + u8 ownedHalfDaysMask = ClockItems::GetAllOwnedHalfDaysMask(); + int nextHalfDay = ClockItems::FindNextOwnedHalfDayAfter(currentHalfDay, ownedHalfDaysMask); + + if (nextHalfDay == ClockItems::TERMINAL_STATE) { + // Jump to terminal time + gSaveContext.save.day = 3; + gSaveContext.save.time = GetConfiguredTerminalTime(); + gSaveContext.respawnFlag = -8; // No daytelop for terminal + } else { + // Get target half-day configuration + const HalfDayTimeConfig* config = GetHalfDayTimeConfig(nextHalfDay); + bool isNightHalf = (nextHalfDay % 2 == 1); + s32 targetDay = config->dayNumber; + u16 targetTime = config->startTime; + + if (isNightHalf) { + // Advancing to night - use respawnFlag -8 (no daytelop) + gSaveContext.save.day = targetDay; + gSaveContext.save.time = targetTime; + gSaveContext.respawnFlag = -8; + } else { + // Advancing to dawn - use respawnFlag -4 (triggers daytelop) + // CRITICAL: Subtract 1 from day because daytelop will increment it + gSaveContext.save.day = targetDay - 1; + gSaveContext.save.time = targetTime; + gSaveContext.respawnFlag = -4; + SET_EVENTINF(EVENTINF_TRIGGER_DAYTELOP); + } + } + }); +} + +// Initialize clock settings and item pool for file creation +void InitializeFileClocks(std::vector& itemPool) { + if (!RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE]) { + return; // Skip if clocks not enabled + } + + int clockMode = RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE_PROGRESSIVE]; + + // Check if player has selected any starting time items + std::vector startingItems = + convertStartingItemsToRandoItemId(CVarGetString("gRando.StartingItems", RANDO_STARTING_ITEMS_DEFAULT), ","); + std::vector startingClockHalves; + + for (RandoItemId item : startingItems) { + if (ClockItems::IsClockItem(item)) { + int halfDayIndex = ClockItems::GetHalfDayIndexFromClockItem(item); + if (halfDayIndex != ClockItems::INVALID) { + startingClockHalves.push_back(halfDayIndex); + } + } + } + + // If player selected starting clocks, use those instead of random/progressive logic + if (!startingClockHalves.empty()) { + // Grant all selected starting time + for (int halfDayIndex : startingClockHalves) { + Flags_SetRandoInf(static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + halfDayIndex)); + } + + // Add remaining (non-starting) time items to pool + // Progressive mode items are added in the else block of this conditional + if (clockMode == RO_CLOCK_SHUFFLE_RANDOM) { + for (int i = 0; i < 6; ++i) { + // Skip if this clock was a starting item + if (std::find(startingClockHalves.begin(), startingClockHalves.end(), i) != startingClockHalves.end()) { + continue; + } + + RandoItemId clockItem = ClockItems::GetClockItemFromHalfDayIndex(i); + if (clockItem != RI_UNKNOWN) + itemPool.push_back(clockItem); + } + } + } else { + // No starting time selected - use default logic + int initialClockHalf; + + if (clockMode == RO_CLOCK_SHUFFLE_RANDOM) { + // Grant one random half-day + initialClockHalf = Ship_Random(0, 6); // 0..5 map to D1..N3 + } else { + // Progressive modes: grant first half-day in sequence + initialClockHalf = (clockMode == RO_CLOCK_SHUFFLE_ASCENDING) ? 0 : 5; + } + + // Own the selected half + Flags_SetRandoInf(static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + initialClockHalf)); + + if (clockMode == RO_CLOCK_SHUFFLE_RANDOM) { + // Add remaining 5 individual time items to pool + for (int i = 0; i < 6; ++i) { + if (i == initialClockHalf) + continue; + RandoItemId clockItem = ClockItems::GetClockItemFromHalfDayIndex(i); + if (clockItem != RI_UNKNOWN) + itemPool.push_back(clockItem); + } + } else { + // Add 5 progressive time items to pool (6 total - 1 granted = 5 remaining) + for (int i = 0; i < 5; ++i) + itemPool.push_back(RI_TIME_PROGRESSIVE); + } + } +} + +} // namespace ClockShuffle +} // namespace Rando diff --git a/mm/2s2h/Rando/MiscBehavior/ClockShuffle.h b/mm/2s2h/Rando/MiscBehavior/ClockShuffle.h new file mode 100644 index 0000000000..ea363b4bdb --- /dev/null +++ b/mm/2s2h/Rando/MiscBehavior/ClockShuffle.h @@ -0,0 +1,51 @@ +#ifndef RANDO_CLOCK_SHUFFLE_H +#define RANDO_CLOCK_SHUFFLE_H + +#include "Rando/Types.h" +#include +#include + +extern "C" { +#include "variables.h" +#include "functions.h" +} + +namespace Rando { +namespace ClockItems { + +// Internal half-day indices +enum ClockHalfIndex : int { + INVALID = -1, // Invalid/not found/uninitialized half-day index + HALF_DAY1_DAY = 0, // Day 1, 6:00 AM - 5:59 PM + HALF_DAY1_NIGHT = 1, // Day 1, 6:00 PM - 5:59 AM + HALF_DAY2_DAY = 2, // Day 2, 6:00 AM - 5:59 PM + HALF_DAY2_NIGHT = 3, // Day 2, 6:00 PM - 5:59 AM + HALF_DAY3_DAY = 4, // Day 3, 6:00 AM - 5:59 PM + HALF_DAY3_NIGHT = 5, // Day 3, 6:00 PM - 5:59 AM + TERMINAL_STATE = 6, // Terminal state (fallback for invalid/end states) + HALF_COUNT = 6, // Total number of regular half-days (0-5) +}; + +RandoItemId GetClockItemFromHalfDayIndex(int halfDayIndex); +int GetHalfDayIndexFromClockItem(RandoItemId clockItemId); +int FindEarliestOwnedHalfDay(bool searchFromEnd = false); +u8 GetAllOwnedHalfDaysMask(); +bool IsClockItem(RandoItemId itemId); +bool IsDayClock(RandoItemId itemId); + +} // namespace ClockItems + +namespace ClockShuffle { + +void InitializeFileClocks(std::vector& itemPool); +void OnFileLoad(); +void SetTimeToHalfDayStart(int halfDayIndex); + +bool IsTimeOwnedForClockShuffle(s32 day, u16 time); +int GetHalfDayIndexFromTime(s32 day, u16 time); +std::string GetTimeDescriptionForMessage(s32 day, u16 time); + +} // namespace ClockShuffle +} // namespace Rando + +#endif // RANDO_CLOCK_SHUFFLE_H diff --git a/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp b/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp index ceee5441fb..18b9e7cdbd 100644 --- a/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp +++ b/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp @@ -3,6 +3,7 @@ #include "Rando/Logic/Logic.h" #include "2s2h/ShipUtils.h" #include +#include "ClockShuffle.h" #include extern "C" { @@ -12,6 +13,47 @@ extern "C" { #include "overlays/actors/ovl_En_Sth/z_en_sth.h" } +void GrantStarters() { + std::vector startingItems = convertStartingItemsToRandoItemId(RANDO_STARTING_ITEMS, ","); + + if (RANDO_SAVE_OPTIONS[RO_STARTING_MAPS_AND_COMPASSES]) { + std::vector MapsAndCompasses = { + RI_GREAT_BAY_COMPASS, RI_GREAT_BAY_MAP, RI_SNOWHEAD_COMPASS, RI_SNOWHEAD_MAP, + RI_STONE_TOWER_COMPASS, RI_STONE_TOWER_MAP, RI_TINGLE_MAP_CLOCK_TOWN, RI_TINGLE_MAP_GREAT_BAY, + RI_TINGLE_MAP_ROMANI_RANCH, RI_TINGLE_MAP_SNOWHEAD, RI_TINGLE_MAP_STONE_TOWER, RI_TINGLE_MAP_WOODFALL, + RI_WOODFALL_COMPASS, RI_WOODFALL_MAP, + }; + + for (RandoItemId itemId : MapsAndCompasses) { + startingItems.push_back(itemId); + } + } + + if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_SWIM] != RO_GENERIC_YES) { + startingItems.push_back(RI_ABILITY_SWIM); + } + + for (RandoItemId startingItem : startingItems) { + Rando::GiveItem(Rando::ConvertItem(startingItem)); + } + + if (RANDO_SAVE_OPTIONS[RO_STARTING_HEALTH] != 3) { + gSaveContext.save.saveInfo.playerData.healthCapacity = gSaveContext.save.saveInfo.playerData.health = + RANDO_SAVE_OPTIONS[RO_STARTING_HEALTH] * 0x10; + } + + if (RANDO_SAVE_OPTIONS[RO_STARTING_CONSUMABLES]) { + Rando::GiveItem(RI_DEKU_STICK); + Rando::GiveItem(RI_DEKU_NUT); + AMMO(ITEM_DEKU_STICK) = CUR_CAPACITY(UPG_DEKU_STICKS); + AMMO(ITEM_DEKU_NUT) = CUR_CAPACITY(UPG_DEKU_NUTS); + } + + if (RANDO_SAVE_OPTIONS[RO_STARTING_RUPEES]) { + gSaveContext.save.saveInfo.playerData.rupees = CUR_CAPACITY(UPG_WALLET); + } +} + // Very primitive randomizer implementation, when a save is created, if rando is enabled // we set the save type to rando and shuffle all checks and persist the results to the save void Rando::MiscBehavior::OnFileCreate(s16 fileNum) { @@ -60,297 +102,18 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) { (uint32_t)CVarGetInteger(randoStaticOption.cvar, randoStaticOption.defaultValue); } - std::vector startingItems = convertStartingItemsToRandoItemId( - CVarGetString("gRando.StartingItems", RANDO_STARTING_ITEMS_DEFAULT), ","); - - std::string startingItemSave = CreateStartingItemsToCvar(startingItems); - strncpy(RANDO_STARTING_ITEMS, startingItemSave.c_str(), startingItemSave.size() + 1); - - if (RANDO_SAVE_OPTIONS[RO_STARTING_HEALTH] != 3) { - gSaveContext.save.saveInfo.playerData.healthCapacity = - gSaveContext.save.saveInfo.playerData.health = RANDO_SAVE_OPTIONS[RO_STARTING_HEALTH] * 0x10; - } - - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] != RO_GENERIC_OFF) { - RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_REQUIRED] = CVarGetInteger( - Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar, DEFAULT_TRIFORCE_PIECES_MAX); - } - - if (RANDO_SAVE_OPTIONS[RO_STARTING_CONSUMABLES]) { - GiveItem(RI_DEKU_STICK); - GiveItem(RI_DEKU_NUT); - AMMO(ITEM_DEKU_STICK) = CUR_CAPACITY(UPG_DEKU_STICKS); - AMMO(ITEM_DEKU_NUT) = CUR_CAPACITY(UPG_DEKU_NUTS); - } - - if (RANDO_SAVE_OPTIONS[RO_STARTING_MAPS_AND_COMPASSES]) { - std::vector MapsAndCompasses = { - RI_GREAT_BAY_COMPASS, RI_GREAT_BAY_MAP, RI_SNOWHEAD_COMPASS, - RI_SNOWHEAD_MAP, RI_STONE_TOWER_COMPASS, RI_STONE_TOWER_MAP, - RI_TINGLE_MAP_CLOCK_TOWN, RI_TINGLE_MAP_GREAT_BAY, RI_TINGLE_MAP_ROMANI_RANCH, - RI_TINGLE_MAP_SNOWHEAD, RI_TINGLE_MAP_STONE_TOWER, RI_TINGLE_MAP_WOODFALL, - RI_WOODFALL_COMPASS, RI_WOODFALL_MAP, - }; - - for (RandoItemId itemId : MapsAndCompasses) { - startingItems.push_back(itemId); - } - } - // If Skulltula tokens are not shuffled, use the vanilla requirement if (!RANDO_SAVE_OPTIONS[RO_SHUFFLE_GOLD_SKULLTULAS]) { RANDO_SAVE_OPTIONS[RO_MINIMUM_SKULLTULA_TOKENS] = SPIDER_HOUSE_TOKENS_REQUIRED; } + // Persist StartingItems to the save + std::string startingItemsString = CVarGetString("gRando.StartingItems", RANDO_STARTING_ITEMS_DEFAULT); + strncpy(RANDO_STARTING_ITEMS, startingItemsString.c_str(), startingItemsString.size() + 1); + std::vector checkPool; std::vector itemPool; - - // Create Excluded Checks List to eliminate excluded checks from the pool - std::vector excludedChecks; - std::string excludedChecksList = CVarGetString("gRando.ExcludedChecks", ""); - std::string word; - std::istringstream stream(excludedChecksList); - while (std::getline(stream, word, ',')) { - excludedChecks.push_back((RandoCheckId)std::stoi(word)); - } - - // First loop through all regions and add checks/items to the pool - for (auto& [randoRegionId, randoRegion] : Rando::Logic::Regions) { - for (auto& [randoCheckId, _] : randoRegion.checks) { - auto& randoStaticCheck = Rando::StaticData::Checks[randoCheckId]; - - // Initialize the check with it's vanilla item - if (randoStaticCheck.randoCheckId != RC_UNKNOWN) { - RANDO_SAVE_CHECKS[randoCheckId].randoItemId = randoStaticCheck.randoItemId; - } - - // Skip checks that are already in the pool - if (std::find(checkPool.begin(), checkPool.end(), randoCheckId) != checkPool.end()) { - continue; - } - - // TODO: We may never shuffle these 2 pots, leaving this decision for later - if (randoStaticCheck.sceneId == SCENE_LAST_BS) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_SKULL_TOKEN && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_GOLD_SKULLTULAS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_OWL && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_OWL_STATUES] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_POT && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_POT_DROPS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_CRATE && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_CRATE_DROPS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_BARREL && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_BARREL_DROPS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_GRASS && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_GRASS_DROPS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_FREESTANDING && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_FREESTANDING_ITEMS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_SNOWBALL && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_SNOWBALL_DROPS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_FROG && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_FROGS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_REMAINS && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_BOSS_REMAINS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_COW && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_COWS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_ENEMY_DROP && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_ENEMY_DROPS] == RO_GENERIC_NO) { - continue; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_TINGLE_SHOP && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_TINGLE_SHOPS] == RO_GENERIC_NO) { - continue; - } else { - int price = Ship_Random(0, 200); - RANDO_SAVE_CHECKS[randoCheckId].price = price; - } - - if (randoStaticCheck.randoCheckType == RCTYPE_SHOP) { - // We always want shuffle RC_CURIOSITY_SHOP_SPECIAL_ITEM & - // RC_BOMB_SHOP_ITEM_04_OR_CURIOSITY_SHOP_ITEM - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_SHOPS] == RO_GENERIC_NO && - randoCheckId != RC_CURIOSITY_SHOP_SPECIAL_ITEM && - randoCheckId != RC_BOMB_SHOP_ITEM_04_OR_CURIOSITY_SHOP_ITEM) { - continue; - } else { - // We may come up with a better solution for this in the future, but for now we choose a - // random price ahead of time, logic will account for whatever price we choose - int price = Ship_Random(0, 200); - RANDO_SAVE_CHECKS[randoCheckId].price = price; - } - } - - // Skip checks that have been excluded in the Locations menu and add their vanilla item to the - // pool except if Logic is set to Vanilla. - if (RANDO_SAVE_OPTIONS[RO_LOGIC] <= RO_LOGIC_NEARLY_NO_LOGIC) { - auto it = std::find(excludedChecks.begin(), excludedChecks.end(), randoCheckId); - if (it != excludedChecks.end()) { - RandoItemId vanillaItem = Rando::StaticData::Checks[randoCheckId].randoItemId; - itemPool.push_back(vanillaItem); - - RANDO_SAVE_CHECKS[randoCheckId].randoItemId = RI_JUNK; - RANDO_SAVE_CHECKS[randoCheckId].skipped = true; - - checkPool.emplace_back(randoCheckId); - continue; - } - } - - checkPool.emplace_back(randoCheckId); - itemPool.push_back(randoStaticCheck.randoItemId); - } - } - - // Add sword and shield to the pool because they don't have a vanilla location, if you are starting with - // them they will be removed from the pool in the next step - itemPool.push_back(RI_PROGRESSIVE_SWORD); - itemPool.push_back(RI_SHIELD_HERO); - - // Add other items that don't have a vanilla location like Souls or Triforce Pieces - std::vector bossSouls = { RI_SOUL_BOSS_GOHT, RI_SOUL_BOSS_GYORG, RI_SOUL_BOSS_ODOLWA, - RI_SOUL_BOSS_TWINMOLD, RI_SOUL_BOSS_MAJORA }; - bool shuffleMajoraSoul = (RANDO_SAVE_OPTIONS[RO_SHUFFLE_BOSS_SOULS] == RO_GENERIC_YES && - RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_NO); - for (auto& boss : bossSouls) { - if (boss == RI_SOUL_BOSS_MAJORA) { - if (shuffleMajoraSoul) { - itemPool.push_back(boss); - } else { - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_NO) { - Flags_SetRandoInf(RANDO_INF_OBTAINED_SOUL_OF_BOSS_MAJORA); - } - } - continue; - } - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_BOSS_SOULS] == RO_GENERIC_YES) { - itemPool.push_back(boss); - } else { - Flags_SetRandoInf(SOUL_RI_TO_RANDO_INF(boss)); - } - } - - for (int i = RI_SOUL_ENEMY_ALIEN; i <= RI_SOUL_ENEMY_WOLFOS; i++) { - bool shouldSkipSoul = false; - for (auto& boss : bossSouls) { - if (boss == (RandoItemId)i) { - shouldSkipSoul = true; - break; - } - } - if (!shouldSkipSoul) { - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_ENEMY_SOULS] == RO_GENERIC_YES) { - itemPool.push_back((RandoItemId)i); - } else { - Flags_SetRandoInf(SOUL_RI_TO_RANDO_INF(i)); - } - } - } - - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_SWIM] == RO_GENERIC_YES) { - itemPool.push_back(RI_ABILITY_SWIM); - } else { - Flags_SetRandoInf(RANDO_INF_OBTAINED_SWIM); - } - - // Remove starting items from the pool (but only one per entry in startingItems) - for (RandoItemId startingItem : startingItems) { - auto it = std::find(itemPool.begin(), itemPool.end(), startingItem); - if (it != itemPool.end()) { - itemPool.erase(it); - } - } - - // Shuffle Triforce Pieces into the Pool - int piecesShuffled = 0; - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) { - int piecesToShuffle = RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_MAX]; - for (auto& item : itemPool) { - if (piecesToShuffle == 0) { - break; - } - itemPool.push_back(RI_TRIFORCE_PIECE); - piecesToShuffle--; - piecesShuffled++; - } - } - - if (RANDO_SAVE_OPTIONS[RO_PLENTIFUL_ITEMS] == RO_GENERIC_YES) { - int replaceableItems = 0; - std::vector plentifulItems; - std::vector potentialPlentifulItems; - for (size_t i = 0; i < itemPool.size(); i++) { - switch (Rando::StaticData::Items[itemPool[i]].randoItemType) { - case RITYPE_BOSS_KEY: - case RITYPE_SMALL_KEY: - case RITYPE_MASK: - case RITYPE_MAJOR: - plentifulItems.push_back(itemPool[i]); - break; - case RITYPE_LESSER: - case RITYPE_SKULLTULA_TOKEN: - case RITYPE_STRAY_FAIRY: - if (Ship_Random(0, 2) == 1) { - potentialPlentifulItems.push_back(itemPool[i]); - } - break; - case RITYPE_HEALTH: - case RITYPE_JUNK: - default: - replaceableItems++; - break; - } - } - - if (replaceableItems > plentifulItems.size()) { - for (RandoItemId plentifulItem : plentifulItems) { - itemPool.push_back(plentifulItem); - } - } - - // Only add potentialPlentifulItems if we think we have enough room (this might not be perfect) - if ((replaceableItems - plentifulItems.size() - 10) > potentialPlentifulItems.size()) { - for (RandoItemId plentifulItem : potentialPlentifulItems) { - itemPool.push_back(plentifulItem); - } - } - } + Rando::Logic::GeneratePools(gSaveContext.save.shipSaveInfo.rando, checkPool, itemPool); if (checkPool.empty()) { throw std::runtime_error("No checks in logic"); @@ -359,20 +122,8 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) { throw std::runtime_error("No items in logic"); } - // Handle Shuffling Traps - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRAPS] == RO_GENERIC_YES) { - for (int i = 0; i < RANDO_SAVE_OPTIONS[RO_TRAP_AMOUNT]; i++) { - for (int j = 0; j < itemPool.size(); j++) { - if (itemPool[j] == RI_JUNK) { - itemPool[j] = RI_TRAP; - break; - } - } - } - } - + // Balance pools int heartPiecesRemoved = 0; - // Add/Remove junk items to/from the pool to make the item pool size match the check pool size while (checkPool.size() != itemPool.size()) { if (checkPool.size() > itemPool.size()) { itemPool.push_back(RI_JUNK); @@ -413,56 +164,14 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) { continue; } - // If Triforce Hunt is enabled, removed pieces as a last resort - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) { - bool removedTriforcePiece = false; - for (int i = 0; i < itemPool.size(); i++) { - if (Rando::StaticData::Items[itemPool[i]].randoItemId == RI_TRIFORCE_PIECE) { - itemPool.erase(itemPool.begin() + i); - removedTriforcePiece = true; - piecesShuffled--; - break; - } - } - - if (removedTriforcePiece) { - continue; - } - } - SPDLOG_ERROR("Could not match item pool size to check pool size {}/{}", itemPool.size(), checkPool.size()); throw std::runtime_error("Could not match item pool size to check pool size"); } } - // Update Required Triforce Pieces if piecesShuffled falls below max shuffled - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) { - if (piecesShuffled != RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_MAX]) { - float currentRatio = ((float)RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_REQUIRED] / - (float)RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_MAX]); - - RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_MAX] = piecesShuffled; - RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_REQUIRED] = (piecesShuffled * currentRatio) + 1; - } - } - - // Grant the starting items - for (RandoItemId startingItem : startingItems) { - GiveItem(ConvertItem(startingItem)); - } - - // Give INF for Enemy Soul if the option is OFF - if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_ENEMY_SOULS] == RO_GENERIC_NO) { - for (int i = RANDO_INF_OBTAINED_SOUL_OF_ENEMY_ALIENS; i <= RANDO_INF_OBTAINED_SOUL_OF_ENEMY_WOLFOS; - i++) { - Flags_SetRandoInf(i); - } - } - - if (RANDO_SAVE_OPTIONS[RO_STARTING_RUPEES]) { - gSaveContext.save.saveInfo.playerData.rupees = CUR_CAPACITY(UPG_WALLET); - } + // Grant the starting stuff + GrantStarters(); if (RANDO_SAVE_OPTIONS[RO_LOGIC] == RO_LOGIC_VANILLA) { GiveItem(RI_SWORD_KOKIRI); @@ -485,7 +194,7 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) { std::to_string(RANDO_SAVE_OPTIONS[RO_LOGIC])); } - if (CVarGetInteger("gRando.GenerateSpoiler", 0)) { + if (CVarGetInteger("gRando.GenerateSpoiler", 1)) { nlohmann::json spoiler = Rando::Spoiler::GenerateFromSaveContext(); spoiler["inputSeed"] = inputSeed; @@ -504,6 +213,8 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) { nlohmann::json spoiler = Rando::Spoiler::LoadFromFile(fileName); Rando::Spoiler::ApplyToSaveContext(spoiler); + // Grant the starting stuff + GrantStarters(); Audio_PlaySfx(NA_SE_SY_ATTENTION_SOUND); } diff --git a/mm/2s2h/Rando/MiscBehavior/Traps.cpp b/mm/2s2h/Rando/MiscBehavior/Traps.cpp index 6a7528dc27..27ade031fb 100644 --- a/mm/2s2h/Rando/MiscBehavior/Traps.cpp +++ b/mm/2s2h/Rando/MiscBehavior/Traps.cpp @@ -3,6 +3,7 @@ #include "MiscBehavior.h" #include "Rando/ActorBehavior/ActorBehavior.h" #include "2s2h/DeveloperTools/SaveEditor.h" +#include "2s2h/ShipUtils.h" extern "C" { #include "variables.h" @@ -13,11 +14,6 @@ void func_80833B18(PlayState* play, Player* thisx, s32 arg2, f32 speed, f32 velo void EnTimeTag_KickOut_Transition(EnTimeTag* enTimeTag, PlayState* play); } -#define MORNING_TIME 0x4000 -#define DAY_LENGTH 0x10000 -// Adjust so that 6 A.M. is 0 and 5:59 A.M. is 0xFFFF -#define ZERO_DAY_START(time) (((u16)(time - MORNING_TIME) % DAY_LENGTH)) - extern void UpdateGameTime(u16 gameTime); int roll = TRAP_FREEZE; diff --git a/mm/2s2h/Rando/Rando.cpp b/mm/2s2h/Rando/Rando.cpp index 4140756e49..09cd90cbc8 100644 --- a/mm/2s2h/Rando/Rando.cpp +++ b/mm/2s2h/Rando/Rando.cpp @@ -2,6 +2,7 @@ #include "2s2h/GameInteractor/GameInteractor.h" #include "Rando/ActorBehavior/ActorBehavior.h" #include "Rando/MiscBehavior/MiscBehavior.h" +#include "Rando/MiscBehavior/ClockShuffle.h" #include "Rando/Spoiler/Spoiler.h" #include "Rando/CheckTracker/CheckTracker.h" #include "2s2h/ShipInit.hpp" @@ -13,6 +14,7 @@ void OnSaveLoadHandler(s16 fileNum) { Rando::MiscBehavior::OnFileLoad(); Rando::ActorBehavior::OnFileLoad(); Rando::CheckTracker::OnFileLoad(); + Rando::ClockShuffle::OnFileLoad(); // Re-initalizes enhancements that are effected by the save being rando or not ShipInit::Init("IS_RANDO"); diff --git a/mm/2s2h/Rando/RemoveItem.cpp b/mm/2s2h/Rando/RemoveItem.cpp index e2bcc28f71..f7bebe0a83 100644 --- a/mm/2s2h/Rando/RemoveItem.cpp +++ b/mm/2s2h/Rando/RemoveItem.cpp @@ -1,5 +1,6 @@ #include "Rando/Rando.h" #include "Rando/ActorBehavior/Souls.h" +#include "Rando/MiscBehavior/ClockShuffle.h" extern "C" { #include "variables.h" @@ -283,6 +284,29 @@ void Rando::RemoveItem(RandoItemId randoItemId) { case RI_TINGLE_MAP_STONE_TOWER: CLEAR_WEEKEVENTREG(WEEKEVENTREG_TINGLE_MAP_BOUGHT_STONE_TOWER); break; + case RI_TIME_DAY_1: + case RI_TIME_NIGHT_1: + case RI_TIME_DAY_2: + case RI_TIME_NIGHT_2: + case RI_TIME_DAY_3: + case RI_TIME_NIGHT_3: { + int index = Rando::ClockItems::GetHalfDayIndexFromClockItem(randoItemId); + if (index != Rando::ClockItems::INVALID) { + Flags_ClearRandoInf(static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + index)); + } + break; + } + case RI_TIME_PROGRESSIVE: { + // Remove most recently earned half-day per current mode + const bool descending = (RANDO_SAVE_OPTIONS[RO_CLOCK_SHUFFLE_PROGRESSIVE] == RO_CLOCK_SHUFFLE_DESCENDING); + // For ascending mode, remove the latest (search from end) + // For descending mode, remove the earliest (search from front) + int toRemove = Rando::ClockItems::FindEarliestOwnedHalfDay(!descending); + if (toRemove >= 0) { + Flags_ClearRandoInf(static_cast(RANDO_INF_OBTAINED_CLOCK_DAY_1 + toRemove)); + } + break; + } case RI_HEART_CONTAINER: gSaveContext.save.saveInfo.playerData.healthCapacity -= 0x10; gSaveContext.save.saveInfo.playerData.health = @@ -359,6 +383,53 @@ void Rando::RemoveItem(RandoItemId randoItemId) { case RI_SOUL_BOSS_MAJORA: case RI_SOUL_BOSS_ODOLWA: case RI_SOUL_BOSS_TWINMOLD: + case RI_SOUL_ENEMY_ALIEN: + case RI_SOUL_ENEMY_ARMOS: + case RI_SOUL_ENEMY_BAD_BAT: + case RI_SOUL_ENEMY_BEAMOS: + case RI_SOUL_ENEMY_BOE: + case RI_SOUL_ENEMY_BUBBLE: + case RI_SOUL_ENEMY_CAPTAIN_KEETA: + case RI_SOUL_ENEMY_CHUCHU: + case RI_SOUL_ENEMY_DEATH_ARMOS: + case RI_SOUL_ENEMY_DEEP_PYTHON: + case RI_SOUL_ENEMY_DEKU_BABA: + case RI_SOUL_ENEMY_DEXIHAND: + case RI_SOUL_ENEMY_DINOLFOS: + case RI_SOUL_ENEMY_DODONGO: + case RI_SOUL_ENEMY_DRAGONFLY: + case RI_SOUL_ENEMY_EENO: + case RI_SOUL_ENEMY_EYEGORE: + case RI_SOUL_ENEMY_FREEZARD: + case RI_SOUL_ENEMY_GARO: + case RI_SOUL_ENEMY_GEKKO: + case RI_SOUL_ENEMY_GIANT_BEE: + case RI_SOUL_ENEMY_GOMESS: + case RI_SOUL_ENEMY_GUAY: + case RI_SOUL_ENEMY_HIPLOOP: + case RI_SOUL_ENEMY_IGOS_DU_IKANA: + case RI_SOUL_ENEMY_IRON_KNUCKLE: + case RI_SOUL_ENEMY_KEESE: + case RI_SOUL_ENEMY_LEEVER: + case RI_SOUL_ENEMY_LIKE_LIKE: + case RI_SOUL_ENEMY_MAD_SCRUB: + case RI_SOUL_ENEMY_NEJIRON: + case RI_SOUL_ENEMY_OCTOROK: + case RI_SOUL_ENEMY_PEAHAT: + case RI_SOUL_ENEMY_PIRATE: + case RI_SOUL_ENEMY_POE: + case RI_SOUL_ENEMY_REDEAD: + case RI_SOUL_ENEMY_SHELLBLADE: + case RI_SOUL_ENEMY_SKULLFISH: + case RI_SOUL_ENEMY_SKULLTULA: + case RI_SOUL_ENEMY_SNAPPER: + case RI_SOUL_ENEMY_STALCHILD: + case RI_SOUL_ENEMY_TAKKURI: + case RI_SOUL_ENEMY_TEKTITE: + case RI_SOUL_ENEMY_WALLMASTER: + case RI_SOUL_ENEMY_WART: + case RI_SOUL_ENEMY_WIZROBE: + case RI_SOUL_ENEMY_WOLFOS: Flags_ClearRandoInf(SOUL_RI_TO_RANDO_INF(randoItemId)); break; case RI_FROG_BLUE: diff --git a/mm/2s2h/Rando/Spoiler/Apply.cpp b/mm/2s2h/Rando/Spoiler/Apply.cpp index 8e96c6862d..4f3d6baaed 100644 --- a/mm/2s2h/Rando/Spoiler/Apply.cpp +++ b/mm/2s2h/Rando/Spoiler/Apply.cpp @@ -3,6 +3,10 @@ #include #include "ShipUtils.h" +extern "C" { +#include "overlays/actors/ovl_En_Sth/z_en_sth.h" +} + namespace Rando { namespace Spoiler { @@ -14,42 +18,12 @@ void ApplyToSaveContext(nlohmann::json spoiler) { RANDO_SAVE_OPTIONS[randoOptionId] = spoiler["options"][randoStaticOption.name].get(); } - std::string startingItemsSave = spoiler["startingItems"].get(); - strncpy(RANDO_STARTING_ITEMS, startingItemsSave.c_str(), startingItemsSave.size() + 1); - - if (RANDO_SAVE_OPTIONS[RO_STARTING_HEALTH] != 3) { - gSaveContext.save.saveInfo.playerData.healthCapacity = gSaveContext.save.saveInfo.playerData.health = - RANDO_SAVE_OPTIONS[RO_STARTING_HEALTH] * 0x10; - } - - if (RANDO_SAVE_OPTIONS[RO_STARTING_CONSUMABLES]) { - GiveItem(RI_DEKU_STICK); - GiveItem(RI_DEKU_NUT); - AMMO(ITEM_DEKU_STICK) = CUR_CAPACITY(UPG_DEKU_STICKS); - AMMO(ITEM_DEKU_NUT) = CUR_CAPACITY(UPG_DEKU_NUTS); - } - - std::vector startingItems = convertStartingItemsToRandoItemId(RANDO_STARTING_ITEMS, ","); - - if (RANDO_SAVE_OPTIONS[RO_STARTING_MAPS_AND_COMPASSES]) { - std::vector MapsAndCompasses = { - RI_GREAT_BAY_COMPASS, RI_GREAT_BAY_MAP, RI_SNOWHEAD_COMPASS, RI_SNOWHEAD_MAP, - RI_STONE_TOWER_COMPASS, RI_STONE_TOWER_MAP, RI_TINGLE_MAP_CLOCK_TOWN, RI_TINGLE_MAP_GREAT_BAY, - RI_TINGLE_MAP_ROMANI_RANCH, RI_TINGLE_MAP_SNOWHEAD, RI_TINGLE_MAP_STONE_TOWER, RI_TINGLE_MAP_WOODFALL, - RI_WOODFALL_COMPASS, RI_WOODFALL_MAP, - }; - for (RandoItemId itemId : MapsAndCompasses) { - startingItems.push_back(itemId); - } - } - - for (RandoItemId startingItem : startingItems) { - GiveItem(ConvertItem(startingItem)); + if (!RANDO_SAVE_OPTIONS[RO_SHUFFLE_GOLD_SKULLTULAS]) { + RANDO_SAVE_OPTIONS[RO_MINIMUM_SKULLTULA_TOKENS] = SPIDER_HOUSE_TOKENS_REQUIRED; } - if (RANDO_SAVE_OPTIONS[RO_STARTING_RUPEES]) { - gSaveContext.save.saveInfo.playerData.rupees = CUR_CAPACITY(UPG_WALLET); - } + std::string startingItemsSave = spoiler["startingItems"].get(); + strncpy(RANDO_STARTING_ITEMS, startingItemsSave.c_str(), startingItemsSave.size() + 1); for (auto& [randoCheckId, randoStaticCheck] : Rando::StaticData::Checks) { if (randoStaticCheck.randoCheckId == RC_UNKNOWN) { diff --git a/mm/2s2h/Rando/StaticData/Items.cpp b/mm/2s2h/Rando/StaticData/Items.cpp index 4fbab35730..50b156fc92 100644 --- a/mm/2s2h/Rando/StaticData/Items.cpp +++ b/mm/2s2h/Rando/StaticData/Items.cpp @@ -228,6 +228,13 @@ std::map Items = { RI(RI_STONE_TOWER_MAP, "the", "Stone Tower Map", RITYPE_LESSER, ITEM_DUNGEON_MAP, GI_MAP, GID_DUNGEON_MAP), RI(RI_STONE_TOWER_SMALL_KEY, "a", "Stone Tower Small Key", RITYPE_SMALL_KEY, ITEM_KEY_SMALL, GI_KEY_SMALL, GID_KEY_SMALL), RI(RI_STONE_TOWER_STRAY_FAIRY, "a", "Stone Tower Stray Fairy", RITYPE_STRAY_FAIRY, ITEM_STRAY_FAIRIES, GI_STRAY_FAIRY, GID_NONE), + RI(RI_TIME_DAY_1, "", "Time (Day 1)", RITYPE_MAJOR, ITEM_NONE, GI_NONE, GID_NONE), + RI(RI_TIME_DAY_2, "", "Time (Day 2)", RITYPE_MAJOR, ITEM_NONE, GI_NONE, GID_NONE), + RI(RI_TIME_DAY_3, "", "Time (Day 3)", RITYPE_MAJOR, ITEM_NONE, GI_NONE, GID_NONE), + RI(RI_TIME_NIGHT_1, "", "Time (Night 1)", RITYPE_MAJOR, ITEM_NONE, GI_NONE, GID_NONE), + RI(RI_TIME_NIGHT_2, "", "Time (Night 2)", RITYPE_MAJOR, ITEM_NONE, GI_NONE, GID_NONE), + RI(RI_TIME_NIGHT_3, "", "Time (Night 3)", RITYPE_MAJOR, ITEM_NONE, GI_NONE, GID_NONE), + RI(RI_TIME_PROGRESSIVE, "", "Progressive Time", RITYPE_MAJOR, ITEM_NONE, GI_NONE, GID_NONE), RI(RI_SWORD_GILDED, "the", "Gilded Sword", RITYPE_LESSER, ITEM_SWORD_GILDED, GI_SWORD_GILDED, GID_SWORD_GILDED), RI(RI_SWORD_KOKIRI, "the", "Kokiri Sword", RITYPE_MAJOR, ITEM_SWORD_KOKIRI, GI_SWORD_KOKIRI, GID_SWORD_KOKIRI), RI(RI_SWORD_RAZOR, "the", "Razor Sword", RITYPE_LESSER, ITEM_SWORD_RAZOR, GI_SWORD_RAZOR, GID_SWORD_RAZOR), @@ -275,7 +282,8 @@ std::unordered_map> StartingItems } }, { STARTING_ITEMS_MISC, { RI_SOUL_BOSS_GOHT, RI_SOUL_BOSS_GYORG, RI_SOUL_BOSS_MAJORA, RI_SOUL_BOSS_ODOLWA, RI_SOUL_BOSS_TWINMOLD, - RI_FROG_BLUE, RI_FROG_CYAN, RI_FROG_PINK, RI_FROG_WHITE + RI_FROG_BLUE, RI_FROG_CYAN, RI_FROG_PINK, RI_FROG_WHITE, + RI_TIME_DAY_1, RI_TIME_DAY_2, RI_TIME_DAY_3, RI_TIME_NIGHT_1, RI_TIME_NIGHT_2, RI_TIME_NIGHT_3 } }, }; // clang-format on @@ -450,6 +458,16 @@ const char* GetIconTexturePath(RandoItemId randoItemId) { return (const char*)gItemIconTingleMapTex; case RI_TRIFORCE_PIECE: return (const char*)gTriforcePieceTex; + case RI_TIME_DAY_1: + case RI_TIME_DAY_2: + case RI_TIME_DAY_3: + return (const char*)gThreeDayClockSunHourTex; + case RI_TIME_NIGHT_1: + case RI_TIME_NIGHT_2: + case RI_TIME_NIGHT_3: + return (const char*)gThreeDayClockMoonHourTex; + case RI_TIME_PROGRESSIVE: + return (const char*)gThreeDayClockSunHourTex; default: break; } diff --git a/mm/2s2h/Rando/StaticData/Options.cpp b/mm/2s2h/Rando/StaticData/Options.cpp index c839775002..1af0b8d508 100644 --- a/mm/2s2h/Rando/StaticData/Options.cpp +++ b/mm/2s2h/Rando/StaticData/Options.cpp @@ -59,6 +59,9 @@ std::map Options = { RO(RO_STARTING_RUPEES, RO_GENERIC_OFF), RO(RO_TRIFORCE_PIECES_MAX, DEFAULT_TRIFORCE_PIECES_MAX), RO(RO_TRIFORCE_PIECES_REQUIRED, DEFAULT_TRIFORCE_PIECES_MAX), + RO(RO_CLOCK_SHUFFLE, RO_GENERIC_OFF), + RO(RO_CLOCK_SHUFFLE_PROGRESSIVE, RO_CLOCK_SHUFFLE_RANDOM), + RO(RO_CLOCK_TERMINAL_TIME, 0), // Default: 00:00 (midnight) }; // clang-format on diff --git a/mm/2s2h/Rando/Types.h b/mm/2s2h/Rando/Types.h index 122afeb9eb..922dc68b27 100644 --- a/mm/2s2h/Rando/Types.h +++ b/mm/2s2h/Rando/Types.h @@ -2517,6 +2517,13 @@ typedef enum { RI_SWORD_GILDED, RI_SWORD_KOKIRI, RI_SWORD_RAZOR, + RI_TIME_DAY_1, + RI_TIME_DAY_2, + RI_TIME_DAY_3, + RI_TIME_NIGHT_1, + RI_TIME_NIGHT_2, + RI_TIME_NIGHT_3, + RI_TIME_PROGRESSIVE, RI_TINGLE_MAP_CLOCK_TOWN, RI_TINGLE_MAP_GREAT_BAY, RI_TINGLE_MAP_ROMANI_RANCH, @@ -2586,7 +2593,8 @@ typedef enum { RR_FISHERMANS_HUT, RR_GHOST_HUT, RR_GORMAN_TRACK, - RR_GORMAN_TRACK_INNER, + RR_GORMAN_TRACK_BACK, + RR_GORMAN_TRACK_FRONT, RR_GORON_GRAVEYARD, RR_GORON_RACETRACK, RR_GORON_SHOP, @@ -2650,6 +2658,7 @@ typedef enum { RR_MISCELLANEOUS, RR_MILK_BAR, RR_MILK_ROAD, + RR_MILK_ROAD_BEHIND_FENCE, RR_MOON_DEKU_TRIAL, RR_MOON_GORON_TRIAL, RR_MOON_LINK_TRIAL, @@ -2893,6 +2902,9 @@ typedef enum { RO_STARTING_RUPEES, RO_TRIFORCE_PIECES_MAX, RO_TRIFORCE_PIECES_REQUIRED, + RO_CLOCK_SHUFFLE, + RO_CLOCK_SHUFFLE_PROGRESSIVE, + RO_CLOCK_TERMINAL_TIME, RO_MAX, } RandoOptionId; @@ -2928,6 +2940,12 @@ typedef enum { RO_ACCESS_TRIALS_OPEN, } RandoOptionAccessTrials; +typedef enum { + RO_CLOCK_SHUFFLE_RANDOM, + RO_CLOCK_SHUFFLE_ASCENDING, + RO_CLOCK_SHUFFLE_DESCENDING, +} RandoClockShuffleOptions; + typedef enum { RANDO_INF_PURCHASED_BEANS_FROM_SOUTHERN_SWAMP_SCRUB, RANDO_INF_PURCHASED_BOMB_BAG_FROM_GORON_VILLAGE_SCRUB, @@ -2996,6 +3014,12 @@ typedef enum { RANDO_INF_OBTAINED_SOUL_OF_ENEMY_WIZROBES, RANDO_INF_OBTAINED_SOUL_OF_ENEMY_WOLFOS, RANDO_INF_OBTAINED_SWIM, + RANDO_INF_OBTAINED_CLOCK_DAY_1, + RANDO_INF_OBTAINED_CLOCK_NIGHT_1, + RANDO_INF_OBTAINED_CLOCK_DAY_2, + RANDO_INF_OBTAINED_CLOCK_NIGHT_2, + RANDO_INF_OBTAINED_CLOCK_DAY_3, + RANDO_INF_OBTAINED_CLOCK_NIGHT_3, RANDO_INF_MAX, } RandoInf; @@ -3055,6 +3079,32 @@ typedef enum { RE_ACCESS_PICTOGRAPH_TINGLE, RE_ACCESS_PICTOGRAPH_DEKU_KING, RE_ACCESS_PICTOGRAPH_SWAMP_GENERIC, + RE_MEET_KAFEI, + RE_SETUP_MEET_ANJU, + RE_ANJU_MIDNIGHT_MEETING, + RE_DELIVER_PENDANT, + RE_POSTMAN_FREEDOM, + RE_HONEY_DARLING_REWARD_DAY1, + RE_HONEY_DARLING_REWARD_DAY2, + RE_HONEY_DARLING_REWARD_DAY3, + RE_DEKU_PLAYGROUND_1, + RE_DEKU_PLAYGROUND_2, + RE_DEKU_PLAYGROUND_3, + RE_HIDE_SEEK_DAY1, + RE_HIDE_SEEK_DAY2, + RE_HIDE_SEEK_DAY3, + RE_BOMBERS_NORTH_DAY1, + RE_BOMBERS_NORTH_DAY2, + RE_BOMBERS_NORTH_DAY3, + RE_BOMBERS_WEST_DAY1, + RE_BOMBERS_WEST_DAY2, + RE_BOMBERS_WEST_DAY3, + RE_BOMBERS_EAST_DAY1, + RE_BOMBERS_EAST_DAY2, + RE_BOMBERS_EAST_DAY3, + RE_BOMBER_CODE, + RE_DESTROY_MILK_ROAD_BOULDER, + RE_MAX, } RandoEvent; diff --git a/mm/2s2h/ShipUtils.cpp b/mm/2s2h/ShipUtils.cpp index bb0b7a66dc..fb1c4ea39c 100644 --- a/mm/2s2h/ShipUtils.cpp +++ b/mm/2s2h/ShipUtils.cpp @@ -61,7 +61,7 @@ extern u16 sOwlWarpEntrancesForMods[OWL_WARP_MAX - 1] = { }; // These textures are not in existing lists that we iterate over. -std::array miscellaneousTextures = { +std::array miscellaneousTextures = { gArcheryScoreIconTex, gBarrelTrackerIcon, gChestTrackerIcon, @@ -86,6 +86,8 @@ std::array miscellaneousTextures = { gPauseUnusedCursorTex, gWorldMapOwlFaceTex, gItemIconTingleMapTex, + gThreeDayClockSunHourTex, + gThreeDayClockMoonHourTex, }; std::array digitList = { gCounterDigit0Tex, gCounterDigit1Tex, gCounterDigit2Tex, gCounterDigit3Tex, diff --git a/mm/2s2h/ShipUtils.h b/mm/2s2h/ShipUtils.h index 4845d15fc5..eaa66841af 100644 --- a/mm/2s2h/ShipUtils.h +++ b/mm/2s2h/ShipUtils.h @@ -3,6 +3,13 @@ #include "PR/ultratypes.h" +#include "macros.h" // For CLOCK_TIME and DAY_LENGTH + +// Time utilities for 2s2h enhancements +#define MORNING_TIME 0x4000 // 6:00 AM - start of day +// Normalize time so that 6:00 AM is 0 and 5:59 AM is 0xFFFF (wraps around) +#define ZERO_DAY_START(time) (((u16)((time)-MORNING_TIME) % DAY_LENGTH)) + #include "gbi.h" #ifdef __cplusplus diff --git a/mm/src/code/z_message.c b/mm/src/code/z_message.c index 0be1e96e8c..af383621c4 100644 --- a/mm/src/code/z_message.c +++ b/mm/src/code/z_message.c @@ -2203,7 +2203,9 @@ void Message_LoadTime(PlayState* play, u16 curChar, s32* offset, f32* arg3, s16* f32 timeInMinutes; if (curChar == 0x20F) { - dayTime = TIME_UNTIL_MOON_CRASH; + if (GameInteractor_Should(VB_TIME_UNTIL_MOON_CRASH_CALCULATION, true, &dayTime)) { + dayTime = TIME_UNTIL_MOON_CRASH; + } } else { dayTime = TIME_UNTIL_NEW_DAY; } @@ -2952,7 +2954,9 @@ void Message_Decode(PlayState* play) { decodedBufPos++; msgCtx->decodedBuffer.wchar[decodedBufPos] = 0x2000; } else if (curChar == 0x237) { - timeToMoonCrash = TIME_UNTIL_MOON_CRASH; + if (GameInteractor_Should(VB_TIME_UNTIL_MOON_CRASH_CALCULATION, true, &timeToMoonCrash)) { + timeToMoonCrash = TIME_UNTIL_MOON_CRASH; + } digits[0] = 0; digits[1] = TIME_TO_HOURS_F_ALT(timeToMoonCrash); diff --git a/mm/src/code/z_message_nes.c b/mm/src/code/z_message_nes.c index 79f6e74772..ec8b8d0b74 100644 --- a/mm/src/code/z_message_nes.c +++ b/mm/src/code/z_message_nes.c @@ -204,7 +204,9 @@ void Message_LoadTimeNES(PlayState* play, u8 curChar, s32* offset, f32* arg3, s1 s16 i; if (curChar == MESSAGE_TIME_UNTIL_MOON_CRASH) { - timeLeft = TIME_UNTIL_MOON_CRASH; + if (GameInteractor_Should(VB_TIME_UNTIL_MOON_CRASH_CALCULATION, true, &timeLeft)) { + timeLeft = TIME_UNTIL_MOON_CRASH; + } } else { timeLeft = TIME_UNTIL_NEW_DAY; } @@ -1714,7 +1716,9 @@ void Message_DecodeNES(PlayState* play) { msgCtx->decodedBuffer.schar[decodedBufPos] = 0; } else if (curChar == MESSAGE_HOURS_UNTIL_MOON_CRASH) { - timeToMoonCrash = TIME_UNTIL_MOON_CRASH; + if (GameInteractor_Should(VB_TIME_UNTIL_MOON_CRASH_CALCULATION, true, &timeToMoonCrash)) { + timeToMoonCrash = TIME_UNTIL_MOON_CRASH; + } digits[0] = 0; digits[1] = TIME_TO_HOURS_F_ALT(timeToMoonCrash); diff --git a/mm/src/code/z_parameter.c b/mm/src/code/z_parameter.c index 9cea7ffcec..7950c516de 100644 --- a/mm/src/code/z_parameter.c +++ b/mm/src/code/z_parameter.c @@ -9663,8 +9663,10 @@ void Interface_Update(PlayState* play) { Audio_PlaySfx(NA_SE_SY_RUPY_COUNT); } else { // Max rupees - gSaveContext.save.saveInfo.playerData.rupees = CUR_CAPACITY(UPG_WALLET); - gSaveContext.rupeeAccumulator = 0; + if (!GameInteractor_Should(VB_DISCARD_EXCESS_RUPEES, false)) { + gSaveContext.save.saveInfo.playerData.rupees = CUR_CAPACITY(UPG_WALLET); + gSaveContext.rupeeAccumulator = 0; + } } } else if (gSaveContext.save.saveInfo.playerData.rupees != 0) { if (gSaveContext.rupeeAccumulator <= -50) { diff --git a/mm/src/code/z_player_lib.c b/mm/src/code/z_player_lib.c index f51d17cfd4..11efb78e78 100644 --- a/mm/src/code/z_player_lib.c +++ b/mm/src/code/z_player_lib.c @@ -4,6 +4,7 @@ */ #include "global.h" +#include "2s2h/GameInteractor/GameInteractor.h" #include "objects/gameplay_keep/gameplay_keep.h" @@ -790,6 +791,8 @@ ItemId Player_GetItemOnButton(PlayState* play, Player* player, EquipSlot slot) { return ITEM_F2; } + GameInteractor_Should(VB_GET_ITEM_ON_BUTTON, item, slot, &item); + return item; } diff --git a/mm/src/overlays/actors/ovl_En_Bji_01/z_en_bji_01.c b/mm/src/overlays/actors/ovl_En_Bji_01/z_en_bji_01.c index 0fa2ab27a3..b65a73da1f 100644 --- a/mm/src/overlays/actors/ovl_En_Bji_01/z_en_bji_01.c +++ b/mm/src/overlays/actors/ovl_En_Bji_01/z_en_bji_01.c @@ -5,6 +5,7 @@ */ #include "z_en_bji_01.h" +#include "2s2h/GameInteractor/GameInteractor.h" #define FLAGS (ACTOR_FLAG_ATTENTION_ENABLED | ACTOR_FLAG_FRIENDLY | ACTOR_FLAG_UPDATE_CULLING_DISABLED) @@ -189,7 +190,10 @@ void func_809CD028(EnBji01* this, PlayState* play) { break; case 3: - timeUntilMoonCrash = TIME_UNTIL_MOON_CRASH; + if (GameInteractor_Should(VB_TIME_UNTIL_MOON_CRASH_CALCULATION, true, + &timeUntilMoonCrash)) { + timeUntilMoonCrash = TIME_UNTIL_MOON_CRASH; + } if (timeUntilMoonCrash < CLOCK_TIME_F(1, 0)) { this->textId = 0x5E8; } else { diff --git a/mm/src/overlays/actors/ovl_En_Kakasi/z_en_kakasi.c b/mm/src/overlays/actors/ovl_En_Kakasi/z_en_kakasi.c index 5a8425a7ee..1927f87add 100644 --- a/mm/src/overlays/actors/ovl_En_Kakasi/z_en_kakasi.c +++ b/mm/src/overlays/actors/ovl_En_Kakasi/z_en_kakasi.c @@ -956,13 +956,15 @@ void EnKakasi_DancingNightAway(EnKakasi* this, PlayState* play) { PLAYER_PARAMS(0xFF, PLAYER_START_MODE_B), &player->unk_3C0, player->unk_3CC); func_80169EFC(play); - if ((CURRENT_TIME > CLOCK_TIME(18, 0)) || (CURRENT_TIME < CLOCK_TIME(6, 0))) { - gSaveContext.save.time = CLOCK_TIME(6, 0); - gSaveContext.respawnFlag = -4; - SET_EVENTINF(EVENTINF_TRIGGER_DAYTELOP); - } else { - gSaveContext.save.time = CLOCK_TIME(18, 0); - gSaveContext.respawnFlag = -8; + if (GameInteractor_Should(VB_SCARECROW_DANCE_SET_TIME, true)) { + if ((CURRENT_TIME > CLOCK_TIME(18, 0)) || (CURRENT_TIME < CLOCK_TIME(6, 0))) { + gSaveContext.save.time = CLOCK_TIME(6, 0); + gSaveContext.respawnFlag = -4; + SET_EVENTINF(EVENTINF_TRIGGER_DAYTELOP); + } else { + gSaveContext.save.time = CLOCK_TIME(18, 0); + gSaveContext.respawnFlag = -8; + } } SET_WEEKEVENTREG(WEEKEVENTREG_83_01); this->unk190 = 0;