summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authormckinlee <mckinlee@ymail.com>2026-01-05 20:52:39 -0500
committerGitHub <noreply@github.com>2026-01-05 20:52:39 -0500
commit2b139c215f478ef5deca007dfa6f9b6052db3d1d (patch)
treefe84164b375f7bdb9ede52359efb1d1b3060100d
parent51209378530531e237893ef78cda16f5b6a4b186 (diff)
[Rando] Clock Shuffle + Logic (#1275)
* Add Clocks as Items feature * refactor * some clean up * forgot to clangify * use proper randoinf flags and some fixes * proper skipsotcutscene fix * Animated Clock Item (Thank you Caladius!) * Close attempt at animating the clock, not perfect but a good start. * Git gud Clock * fix clock item get crash Co-Authored-By: Caladius <Caladius@users.noreply.github.com> * some enhancements * logic refactor, minor changes, and new enhancements * huge rework * clang * fix a oops; any day, not current day * housekeeping and starting items menu support * splits up some milkroad regions and fixes a audio bug * i suck at merging * feedback update * scarecrow dance fix * add clocks to new item tracker * day 2 bean patches * add time logic to enemy drops and simplify day 2 bean macro * rebrand to "Shuffle Time" * more eblo feedback * some more feedback * rename feedback * pauseSaveEntrance fix * fix take 2 * Time Shuffle cleanup Fix pause save Time Shuffle bug Fix event queue daytelop bug Finish merge commit header include Clean up with shouldRegister Co-authored-by: mckinlee <mckinlee@ymail.com> --------- Co-authored-by: Caladius <Caladius@users.noreply.github.com> Co-authored-by: Eblo <7004497+Eblo@users.noreply.github.com>
-rw-r--r--mm/2s2h/DeveloperTools/SaveEditor.cpp67
-rw-r--r--mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp4
-rw-r--r--mm/2s2h/Enhancements/Songs/BetterSongOfDoubleTime.cpp18
-rw-r--r--mm/2s2h/Enhancements/Songs/SkipSoTCutscenes.cpp26
-rw-r--r--mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp17
-rw-r--r--mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp5
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor.cpp4
-rw-r--r--mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h16
-rw-r--r--mm/2s2h/Rando/CheckTracker/CheckTracker.cpp194
-rw-r--r--mm/2s2h/Rando/ConvertItem.cpp36
-rw-r--r--mm/2s2h/Rando/DrawFuncs.cpp84
-rw-r--r--mm/2s2h/Rando/DrawFuncs.h3
-rw-r--r--mm/2s2h/Rando/DrawItem.cpp10
-rw-r--r--mm/2s2h/Rando/GiveItem.cpp21
-rw-r--r--mm/2s2h/Rando/Logic/GeneratePools.cpp4
-rw-r--r--mm/2s2h/Rando/Logic/GlitchlessLogic.cpp48
-rw-r--r--mm/2s2h/Rando/Logic/Logic.cpp102
-rw-r--r--mm/2s2h/Rando/Logic/Logic.h325
-rw-r--r--mm/2s2h/Rando/Logic/Regions/Central.cpp247
-rw-r--r--mm/2s2h/Rando/Logic/Regions/East.cpp26
-rw-r--r--mm/2s2h/Rando/Logic/Regions/MilkRoad.cpp87
-rw-r--r--mm/2s2h/Rando/Logic/Regions/North.cpp30
-rw-r--r--mm/2s2h/Rando/Logic/Regions/South.cpp34
-rw-r--r--mm/2s2h/Rando/Logic/Regions/TerminaField.cpp14
-rw-r--r--mm/2s2h/Rando/Logic/Regions/West.cpp2
-rw-r--r--mm/2s2h/Rando/Logic/TimeLogic.cpp123
-rw-r--r--mm/2s2h/Rando/Menu.cpp115
-rw-r--r--mm/2s2h/Rando/MiscBehavior/ClockShuffle.cpp762
-rw-r--r--mm/2s2h/Rando/MiscBehavior/ClockShuffle.h51
-rw-r--r--mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp1
-rw-r--r--mm/2s2h/Rando/MiscBehavior/Traps.cpp6
-rw-r--r--mm/2s2h/Rando/Rando.cpp2
-rw-r--r--mm/2s2h/Rando/RemoveItem.cpp24
-rw-r--r--mm/2s2h/Rando/StaticData/Items.cpp20
-rw-r--r--mm/2s2h/Rando/StaticData/Options.cpp3
-rw-r--r--mm/2s2h/Rando/Types.h52
-rw-r--r--mm/2s2h/ShipUtils.cpp4
-rw-r--r--mm/2s2h/ShipUtils.h7
-rw-r--r--mm/src/code/z_message.c8
-rw-r--r--mm/src/code/z_message_nes.c8
-rw-r--r--mm/src/overlays/actors/ovl_En_Bji_01/z_en_bji_01.c6
-rw-r--r--mm/src/overlays/actors/ovl_En_Kakasi/z_en_kakasi.c16
42 files changed, 2269 insertions, 363 deletions
diff --git a/mm/2s2h/DeveloperTools/SaveEditor.cpp b/mm/2s2h/DeveloperTools/SaveEditor.cpp
index 8fbdcddca..a4a111b34 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<RandoInf>(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<RandoInf>(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/Saving/SavingEnhancements.cpp b/mm/2s2h/Enhancements/Saving/SavingEnhancements.cpp
index 01d9e07da..e725da230 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 6061fa6c3..612b7082d 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 bd5e283ed..d06287d94 100644
--- a/mm/2s2h/Enhancements/Songs/SkipSoTCutscenes.cpp
+++ b/mm/2s2h/Enhancements/Songs/SkipSoTCutscenes.cpp
@@ -1,6 +1,8 @@
#include <libultraship/bridge/consolevariablebridge.h>
#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/Trackers/ItemTracker/ItemTracker.cpp b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp
index e825bc62a..1aa6d2c5f 100644
--- a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp
+++ b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTracker.cpp
@@ -3,6 +3,7 @@
#include "2s2h/BenGui/UIWidgets.hpp"
#include "Rando/Rando.h"
+#include "Rando/MiscBehavior/ClockShuffle.h"
#include "2s2h/ShipUtils.h"
#include <spdlog/fmt/fmt.h>
@@ -108,6 +109,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<RandoInf>(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<RandoInf>(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 b20b57698..618744126 100644
--- a/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp
+++ b/mm/2s2h/Enhancements/Trackers/ItemTracker/ItemTrackerSettings.cpp
@@ -27,7 +27,7 @@ std::vector<std::string> listOrder = {
};
std::vector<std::string> randoListOrder = {
- "Frogs", "Boss Souls", "Owl Statues", "Tingle Maps", "Misc",
+ "Frogs", "Boss Souls", "Owl Statues", "Tingle Maps", "Time", "Misc",
};
std::map<std::string, std::tuple<int16_t, int16_t, int16_t>> defaultItemLists = {
@@ -46,6 +46,7 @@ std::map<std::string, std::tuple<int16_t, int16_t, int16_t>> randoItemLists = {
{ "Boss Souls", { RI_SOUL_GOHT, RI_SOUL_TWINMOLD, 5 } },
{ "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 } },
};
@@ -249,7 +250,7 @@ void DrawItemList(std::string listName, int columns) {
std::vector<int16_t> emptyList;
if (listName == "Frogs" || listName == "Boss Souls" || listName == "Owl Statues" ||
- listName == "Tingle Maps" || listName == "Misc") {
+ 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 7acfa594b..c50ee54e3 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<GIEventTransition>(&nextEvent)) {
diff --git a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h
index a4bbe1793..235a2ea84 100644
--- a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h
+++ b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h
@@ -1637,6 +1637,14 @@ typedef enum {
// #### `result`
// ```c
+ // true
+ // ```
+ // #### `args`
+ // - None
+ VB_SCARECROW_DANCE_SET_TIME,
+
+ // #### `result`
+ // ```c
// gSaveContext.save.saveInfo.inventory.items[ITEM_OCARINA_OF_TIME] == ITEM_NONE
// ```
// #### `args`
@@ -1952,6 +1960,14 @@ typedef enum {
// #### `result`
// ```c
+ // TIME_UNTIL_MOON_CRASH
+ // ```
+ // #### `args`
+ // - `*u32` (time variable)
+ VB_TIME_UNTIL_MOON_CRASH_CALCULATION,
+
+ // #### `result`
+ // ```c
// true
// ```
// #### `args`
diff --git a/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp b/mm/2s2h/Rando/CheckTracker/CheckTracker.cpp
index 2411b43d4..8a5c53a9f 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<RandoRegionId> 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<RandoRegionId> 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<std::pair<RandoCheckId, std::string>> availableChecks;
- std::vector<std::pair<std::string, std::string>> 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<RandoCheckId, bool> 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<RandoRegionId> 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<RandoRegionId, Rando::Logic::RegionTimeState> 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<RandoRegionId> 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<std::pair<RandoCheckId, std::string>> 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 26ba83a94..46bc5f214 100644
--- a/mm/2s2h/Rando/ConvertItem.cpp
+++ b/mm/2s2h/Rando/ConvertItem.cpp
@@ -1,4 +1,5 @@
#include "Rando/Rando.h"
+#include "Rando/MiscBehavior/ClockShuffle.h"
#include "2s2h/ShipUtils.h"
#include <cassert>
@@ -427,6 +428,16 @@ bool Rando::IsItemObtainable(RandoItemId randoItemId, RandoCheckId randoCheckId)
case RI_SOUL_ODOLWA:
case RI_SOUL_TWINMOLD:
return !Flags_GetRandoInf(RANDO_INF_OBTAINED_SOUL_OF_GOHT + (randoItemId - RI_SOUL_GOHT));
+ 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
@@ -471,6 +482,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<RandoInf>(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 5abf1c4bf..8eab3dbf2 100644
--- a/mm/2s2h/Rando/DrawFuncs.cpp
+++ b/mm/2s2h/Rando/DrawFuncs.cpp
@@ -5,6 +5,7 @@
extern "C" {
#include <functions.h>
#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"
@@ -22,6 +23,11 @@ s32 EnMinifrog_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec
// 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
}
@@ -261,4 +267,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 345e63e5c..818e73efb 100644
--- a/mm/2s2h/Rando/DrawFuncs.h
+++ b/mm/2s2h/Rando/DrawFuncs.h
@@ -13,4 +13,7 @@ void DrawTwinmold();
// 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 fa96c72db..29d9f8711 100644
--- a/mm/2s2h/Rando/DrawItem.cpp
+++ b/mm/2s2h/Rando/DrawItem.cpp
@@ -497,6 +497,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:
@@ -553,6 +562,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 5d404db23..6c6a23461 100644
--- a/mm/2s2h/Rando/GiveItem.cpp
+++ b/mm/2s2h/Rando/GiveItem.cpp
@@ -1,5 +1,6 @@
#include "Rando/Rando.h"
#include "Rando/MiscBehavior/MiscBehavior.h"
+#include "Rando/MiscBehavior/ClockShuffle.h"
extern "C" {
#include "variables.h"
@@ -258,6 +259,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<RandoInf>(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
index 0fd55094d..c3385ab33 100644
--- a/mm/2s2h/Rando/Logic/GeneratePools.cpp
+++ b/mm/2s2h/Rando/Logic/GeneratePools.cpp
@@ -1,4 +1,5 @@
#include "Logic.h"
+#include "Rando/MiscBehavior/ClockShuffle.h"
#include <libultraship/bridge/consolevariablebridge.h>
#include <sstream>
@@ -175,6 +176,9 @@ void GeneratePools(RandoSaveInfo& saveInfo, std::vector<RandoCheckId>& checkPool
}
}
+ // 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);
diff --git a/mm/2s2h/Rando/Logic/GlitchlessLogic.cpp b/mm/2s2h/Rando/Logic/GlitchlessLogic.cpp
index 1bd2fd745..f92c6528c 100644
--- a/mm/2s2h/Rando/Logic/GlitchlessLogic.cpp
+++ b/mm/2s2h/Rando/Logic/GlitchlessLogic.cpp
@@ -25,6 +25,9 @@ void ApplyGlitchlessLogicToSaveContext(std::vector<RandoCheckId>& checkPool, std
std::map<RandoCheckId, bool> checksInLogic;
std::set<std::pair<RandoEvent, std::function<bool()>>*> eventsInLogic;
+ // Initialize time states using shared function
+ std::unordered_map<RandoRegionId, RegionTimeState> regionTimeStates = InitializeRegionTimeStates(RR_MAX);
+
RandoCheckId checkWithJunk = RC_UNKNOWN;
std::set<RandoItemId> nonJunkItemsThatWeHaveTried;
std::vector<RandoCheckId> checksWithJunk;
@@ -68,7 +71,7 @@ void ApplyGlitchlessLogicToSaveContext(std::vector<RandoCheckId>& 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<RandoCheckId>& 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 });
@@ -116,6 +132,28 @@ void ApplyGlitchlessLogicToSaveContext(std::vector<RandoCheckId>& 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 b76397557..b72dde87a 100644
--- a/mm/2s2h/Rando/Logic/Logic.cpp
+++ b/mm/2s2h/Rando/Logic/Logic.cpp
@@ -9,6 +9,9 @@ namespace Logic {
std::map<RandoRegionId, RandoRegion> Regions = {};
+// Thread-local storage for current region time during check evaluation
+thread_local uint64_t gCurrentRegionTime = 0;
+
RandoRegionId GetRegionIdFromEntrance(s32 entrance) {
static std::map<s32, RandoRegionId> entranceToRegionId;
if (entranceToRegionId.empty()) {
@@ -32,23 +35,102 @@ RandoRegionId GetRegionIdFromEntrance(s32 entrance) {
return RR_MAX;
}
-void FindReachableRegions(RandoRegionId currentRegion, std::set<RandoRegionId>& 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<TimeSlice>(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<RandoRegionId, RegionTimeState> InitializeRegionTimeStates(RandoRegionId startRegion) {
+ std::unordered_map<RandoRegionId, RegionTimeState> 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<RandoRegionId, RegionTimeState>& 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<RandoRegionId>& reachableRegions,
+ std::unordered_map<RandoRegionId, RegionTimeState>& 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 ccee2a3a0..7654fc8c8 100644
--- a/mm/2s2h/Rando/Logic/Logic.h
+++ b/mm/2s2h/Rando/Logic/Logic.h
@@ -17,7 +17,112 @@ namespace Rando {
namespace Logic {
-void FindReachableRegions(RandoRegionId currentRegion, std::set<RandoRegionId>& 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<RandoRegionId, RegionTimeState> InitializeRegionTimeStates(RandoRegionId startRegion);
+
+// Helper to ensure region time state exists
+void EnsureRegionTimeState(std::unordered_map<RandoRegionId, RegionTimeState>& regionTimeStates,
+ RandoRegionId regionId);
+
+// Helper to set current region time
+inline void SetCurrentRegionTime(const std::unordered_map<RandoRegionId, RegionTimeState>& regionTimeStates,
+ RandoRegionId regionId) {
+ gCurrentRegionTime = regionTimeStates.at(regionId).timeSlices;
+}
+
+void FindReachableRegions(RandoRegionId currentRegion, std::set<RandoRegionId>& reachableRegions,
+ std::unordered_map<RandoRegionId, RegionTimeState>& regionTimeStates);
RandoRegionId GetRegionIdFromEntrance(s32 entrance);
void GeneratePools(RandoSaveInfo& saveInfo, std::vector<RandoCheckId>& checkPool, std::vector<RandoItemId>& itemPool);
void ApplyGlitchlessLogicToSaveContext(std::vector<RandoCheckId>& checkPool, std::vector<RandoItemId>& itemPool);
@@ -38,10 +143,31 @@ struct RandoRegion {
std::map<RandoRegionId, std::pair<std::function<bool()>, std::string>> connections;
std::vector<std::pair<RandoEvent, std::function<bool()>>> events;
std::set<s32> 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<TimeSlice, std::function<bool()>>
+ timeStayRestrictions; // Time slices where staying is restricted - use STAY() macro in region definitions
};
extern std::map<RandoRegionId, RandoRegion> 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 +185,9 @@ extern std::map<RandoRegionId, RandoRegion> 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 +211,10 @@ extern std::map<RandoRegionId, RandoRegion> 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 +252,16 @@ extern std::map<RandoRegionId, RandoRegion> 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 +329,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<RandoInf>(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<RandoInf>(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) {
switch (EnemyId) {
case ACTOR_BOSS_01: // Odolwa
diff --git a/mm/2s2h/Rando/Logic/Regions/Central.cpp b/mm/2s2h/Rando/Logic/Regions/Central.cpp
index ac83416f9..07c031ab4 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 e96dede92..7227f7e5a 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)),
},
@@ -170,7 +170,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),
@@ -227,14 +227,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)),
@@ -248,7 +248,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 = {
@@ -273,7 +273,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),
@@ -286,8 +286,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)
@@ -300,8 +300,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 8f0de44c5..03b3dc0e1 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 00e763684..b4d8eea8a 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 2b29d7aac..37cd95453 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 4af56e091..3ff118ef5 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 1732996c2..97474397f 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 000000000..5514f5895
--- /dev/null
+++ b/mm/2s2h/Rando/Logic/TimeLogic.cpp
@@ -0,0 +1,123 @@
+#include "Logic.h"
+#include <spdlog/spdlog.h>
+
+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<TimeSlice>(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<TimeSlice>(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 9bdbc7eb4..f141b1443 100644
--- a/mm/2s2h/Rando/Menu.cpp
+++ b/mm/2s2h/Rando/Menu.cpp
@@ -2,6 +2,7 @@
#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"
@@ -39,6 +40,7 @@ std::vector<int32_t> incompatibleWithVanilla = {
RO_SHUFFLE_BOSS_SOULS,
RO_SHUFFLE_SWIM,
RO_PLENTIFUL_ITEMS,
+ RO_CLOCK_SHUFFLE,
};
std::vector<RandoCheckId> checkExclusionList;
@@ -57,6 +59,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);
@@ -66,6 +98,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;
@@ -154,6 +187,7 @@ static RegisterShipInitFunc refreshMetricsInit(RefreshMetrics, {
"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",
@@ -408,6 +442,51 @@ static void DrawItemsTab() {
CheckboxOptions({ { .tooltip = "Shuffles the first drop from a non Boss Enemy." } }));
CVarCheckbox("Enemy Souls", "gPlaceholderBool",
CheckboxOptions({ { .disabled = true, .disabledTooltip = "Coming Soon" } }));
+ 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<int32_t, const char*> 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<int32_t>("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));
@@ -550,10 +629,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);
@@ -563,7 +647,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) {
@@ -583,6 +667,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),
@@ -609,15 +695,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) {
@@ -627,7 +720,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 000000000..d5c8f82f5
--- /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 <libultraship/bridge/consolevariablebridge.h>
+#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<RandoItemId>& 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<RandoItemId> startingItems =
+ convertStartingItemsToRandoItemId(CVarGetString("gRando.StartingItems", RANDO_STARTING_ITEMS_DEFAULT), ",");
+ std::vector<int> 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<RandoInf>(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<RandoInf>(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 000000000..ea363b4bd
--- /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 <string>
+#include <vector>
+
+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<RandoItemId>& 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 63ad54f95..18b9e7cdb 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 <libultraship/bridge/consolevariablebridge.h>
+#include "ClockShuffle.h"
#include <spdlog/spdlog.h>
extern "C" {
diff --git a/mm/2s2h/Rando/MiscBehavior/Traps.cpp b/mm/2s2h/Rando/MiscBehavior/Traps.cpp
index 6a7528dc2..27ade031f 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 4140756e4..09cd90cbc 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 6a3c79341..3ecfe3331 100644
--- a/mm/2s2h/Rando/RemoveItem.cpp
+++ b/mm/2s2h/Rando/RemoveItem.cpp
@@ -1,4 +1,5 @@
#include "Rando/Rando.h"
+#include "Rando/MiscBehavior/ClockShuffle.h"
extern "C" {
#include "variables.h"
@@ -282,6 +283,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<RandoInf>(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<RandoInf>(RANDO_INF_OBTAINED_CLOCK_DAY_1 + toRemove));
+ }
+ break;
+ }
case RI_HEART_CONTAINER:
gSaveContext.save.saveInfo.playerData.healthCapacity -= 0x10;
gSaveContext.save.saveInfo.playerData.health =
diff --git a/mm/2s2h/Rando/StaticData/Items.cpp b/mm/2s2h/Rando/StaticData/Items.cpp
index 8afe1c06d..4676d3435 100644
--- a/mm/2s2h/Rando/StaticData/Items.cpp
+++ b/mm/2s2h/Rando/StaticData/Items.cpp
@@ -181,6 +181,13 @@ std::map<RandoItemId, RandoStaticItem> 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),
@@ -228,7 +235,8 @@ std::unordered_map<StartingItemCategory, std::vector<RandoItemId>> StartingItems
} },
{ STARTING_ITEMS_MISC,
{ RI_SOUL_GOHT, RI_SOUL_GYORG, RI_SOUL_MAJORA, RI_SOUL_ODOLWA, RI_SOUL_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
@@ -356,6 +364,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 eb05930b0..ed1550166 100644
--- a/mm/2s2h/Rando/StaticData/Options.cpp
+++ b/mm/2s2h/Rando/StaticData/Options.cpp
@@ -58,6 +58,9 @@ std::map<RandoOptionId, RandoStaticOption> 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 6c640e59e..a6f6f4719 100644
--- a/mm/2s2h/Rando/Types.h
+++ b/mm/2s2h/Rando/Types.h
@@ -2470,6 +2470,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,
@@ -2539,7 +2546,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,
@@ -2603,6 +2611,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,
@@ -2845,6 +2854,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;
@@ -2881,6 +2893,12 @@ typedef enum {
} 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,
RANDO_INF_PURCHASED_POTION_FROM_IKANA_CANYON_SCRUB,
@@ -2900,6 +2918,12 @@ typedef enum {
RANDO_INF_OBTAINED_SOUL_OF_ODOLWA,
RANDO_INF_OBTAINED_SOUL_OF_TWINMOLD,
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;
@@ -2959,6 +2983,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 bb0b7a66d..fb1c4ea39 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<const char*, 24> miscellaneousTextures = {
+std::array<const char*, 26> miscellaneousTextures = {
gArcheryScoreIconTex,
gBarrelTrackerIcon,
gChestTrackerIcon,
@@ -86,6 +86,8 @@ std::array<const char*, 24> miscellaneousTextures = {
gPauseUnusedCursorTex,
gWorldMapOwlFaceTex,
gItemIconTingleMapTex,
+ gThreeDayClockSunHourTex,
+ gThreeDayClockMoonHourTex,
};
std::array<const char*, 11> digitList = { gCounterDigit0Tex, gCounterDigit1Tex, gCounterDigit2Tex, gCounterDigit3Tex,
diff --git a/mm/2s2h/ShipUtils.h b/mm/2s2h/ShipUtils.h
index 4845d15fc..eaa66841a 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 0be1e96e8..af383621c 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 79f6e7477..ec8b8d0b7 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/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 0fa2ab27a..b65a73da1 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 5a8425a7e..1927f87ad 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;