summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorCaladius <Caladius@users.noreply.github.com>2025-10-15 12:48:06 -0400
committerGitHub <noreply@github.com>2025-10-15 12:48:06 -0400
commitc95a7739f92465d3fc2e1a4eca2912ec47c2ed3a (patch)
tree89193494f4c3b63a309b71fddfb7bede35c802f8
parentda767c966d1102fa2dfe8f78627400ab23f4634c (diff)
[Rando] Implement Triforce Hunt (#1071)
* Implement Triforce Hunt (needs Glitchless Seed Gen help) * Fix Credits Warping * Clean up logging * Fix Seed Gen * Remove test code. * Add Triforce Piece to item tracker * Allow for multiple models, pushing up because I dislike it and can't figure out a better way. * Majora Soul for Reward (needs work). Completed Triforce text * Clang * clean up comment * Remove SPDLOG * Updates based on Feedback * Cleanup * Cleanup again * Fix Ship Utils * Fix Tracker drawing * Feedback updates * clang because why not * Update to use ENTRANCE mapping. Thanks BalloonDude * Expand shuffle maximum and handle instances where a seed may not generate. * Undo menu text. * Fix Item Tracker... again and feedback changes. * Update define location to StaticData.h and update majora syntax * cleanup * Remove TriforceHunt.cpp again... * clean up * FOR REAL THIS TIME * Update ratio of required/max if the seed needs to adjust how many are in the pool due to generator issues. * clang
-rw-r--r--mm/2s2h/BenJsonConversions.hpp2
-rw-r--r--mm/2s2h/DeveloperTools/SaveEditor.cpp3
-rw-r--r--mm/2s2h/Enhancements/Trackers/ItemTracker.cpp25
-rw-r--r--mm/2s2h/Rando/ActorBehavior/Souls.cpp15
-rw-r--r--mm/2s2h/Rando/ConvertItem.cpp1
-rw-r--r--mm/2s2h/Rando/DrawItem.cpp33
-rw-r--r--mm/2s2h/Rando/GiveItem.cpp16
-rw-r--r--mm/2s2h/Rando/Menu.cpp27
-rw-r--r--mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp8
-rw-r--r--mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp50
-rw-r--r--mm/2s2h/Rando/StaticData/Items.cpp2
-rw-r--r--mm/2s2h/Rando/StaticData/Options.cpp3
-rw-r--r--mm/2s2h/Rando/StaticData/StaticData.h2
-rw-r--r--mm/2s2h/Rando/Types.h5
-rw-r--r--mm/2s2h/ShipUtils.cpp3
-rw-r--r--mm/assets/2s2h_assets.h12
-rw-r--r--mm/include/z64save.h1
17 files changed, 203 insertions, 5 deletions
diff --git a/mm/2s2h/BenJsonConversions.hpp b/mm/2s2h/BenJsonConversions.hpp
index 3bed702fa..8dc1fa7bf 100644
--- a/mm/2s2h/BenJsonConversions.hpp
+++ b/mm/2s2h/BenJsonConversions.hpp
@@ -56,6 +56,7 @@ void to_json(json& j, const RandoSaveInfo& rando) {
{ "finalSeed", rando.finalSeed },
{ "randoSaveOptions", rando.randoSaveOptions },
{ "foundDungeonKeys", rando.foundDungeonKeys },
+ { "foundTriforcePieces", rando.foundTriforcePieces },
};
}
@@ -66,6 +67,7 @@ void from_json(const json& j, RandoSaveInfo& rando) {
j.at("finalSeed").get_to(rando.finalSeed);
j.at("randoSaveOptions").get_to(rando.randoSaveOptions);
j.at("foundDungeonKeys").get_to(rando.foundDungeonKeys);
+ j.at("foundTriforcePieces").get_to(rando.foundTriforcePieces);
}
void to_json(json& j, const ShipSaveInfo& shipSaveInfo) {
diff --git a/mm/2s2h/DeveloperTools/SaveEditor.cpp b/mm/2s2h/DeveloperTools/SaveEditor.cpp
index 7d8a808b7..945c0af3d 100644
--- a/mm/2s2h/DeveloperTools/SaveEditor.cpp
+++ b/mm/2s2h/DeveloperTools/SaveEditor.cpp
@@ -902,6 +902,9 @@ void DrawItemsAndMasksTab() {
if (!riFilter.PassFilter(randoStaticItem.name)) {
continue;
}
+ if (randoItemId == RI_TRIFORCE_PIECE_PREVIOUS) {
+ continue;
+ }
std::string buttonLabel = "Give ";
buttonLabel += randoStaticItem.name;
diff --git a/mm/2s2h/Enhancements/Trackers/ItemTracker.cpp b/mm/2s2h/Enhancements/Trackers/ItemTracker.cpp
index 84970bc38..e64be32c8 100644
--- a/mm/2s2h/Enhancements/Trackers/ItemTracker.cpp
+++ b/mm/2s2h/Enhancements/Trackers/ItemTracker.cpp
@@ -3,6 +3,7 @@
#include "Context.h"
#include "config/Config.h"
#include <bit>
+#include "Rando/Rando.h"
extern "C" {
#include "z64save.h"
@@ -12,6 +13,7 @@ extern "C" {
#include "assets/interface/icon_item_dungeon_static/icon_item_dungeon_static.h"
#include "assets/interface/icon_item_field_static/icon_item_field_static.h"
#include "assets/interface/parameter_static/parameter_static.h"
+#include "2s2h_assets.h"
}
#define CFG_TRACKER_ITEM(var) ("ItemTracker." var)
@@ -29,6 +31,7 @@ typedef enum {
TRACKER_ITEM_KEY_SNOWHEAD,
TRACKER_ITEM_KEY_GREAT_BAY,
TRACKER_ITEM_KEY_STONE_TONER,
+ TRACKER_ITEM_TRIFORCE_PIECES,
} ItemTrackerItems;
using namespace Ship;
@@ -351,6 +354,7 @@ bool ItemTrackerWindow::HasItemCount(int itemId) {
case TRACKER_ITEM_KEY_SNOWHEAD:
case TRACKER_ITEM_KEY_GREAT_BAY:
case TRACKER_ITEM_KEY_STONE_TONER:
+ case TRACKER_ITEM_TRIFORCE_PIECES:
return true;
default:
return false;
@@ -416,6 +420,17 @@ ItemTrackerWindow::CountInfo ItemTrackerWindow::GetItemCountInfo(int itemId) {
.maxCap = sSmallKeyCounts[itemId - TRACKER_ITEM_KEY_WOODFALL + DUNGEON_SCENE_INDEX_WOODFALL_TEMPLE],
};
break;
+ case TRACKER_ITEM_TRIFORCE_PIECES: {
+ u32 triforcePieceCount = gSaveContext.save.shipSaveInfo.rando.foundTriforcePieces;
+ info = {
+ .cur = (uint16_t)triforcePieceCount,
+ .curCap =
+ (uint16_t)RANDO_SAVE_OPTIONS[Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].randoOptionId],
+ .maxCap =
+ (uint16_t)RANDO_SAVE_OPTIONS[Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].randoOptionId],
+ };
+ break;
+ }
default:
info = { 0 };
}
@@ -585,6 +600,14 @@ int ItemTrackerWindow::DrawMisc(int columns, int prevDrawnColumns) {
DrawOwlFace(gSaveContext.save.saveInfo.playerData.owlActivationFlags == 0);
DrawItemCount(TRACKER_ITEM_OWL_ACTIVATIONS, pos);
ImGui::EndGroup();
+ pos = ImVec2((0 * (mIconSize + mIconSpacing) + 8.0f),
+ ((prevDrawnColumns + 1) * (mIconSize + mIconSpacing)) + 8.0f + topPadding);
+ ImGui::SetCursorPos(pos);
+ ImGui::BeginGroup();
+ DrawItem((char*)gTriforcePieceTex,
+ RANDO_SAVE_OPTIONS[Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].randoOptionId] == 0, mIconSize);
+ DrawItemCount(TRACKER_ITEM_TRIFORCE_PIECES, pos);
+ ImGui::EndGroup();
// TODO: Heart counts once we have extra save stats
// pos = ImVec2((2 * (mIconSize + mIconSpacing) + 8.0f),
@@ -597,7 +620,7 @@ int ItemTrackerWindow::DrawMisc(int columns, int prevDrawnColumns) {
// ImGui::SetCursorPos(pos);
// DrawItem(const_cast<char*>(gQuestIconPieceOfHeartTex), false, mIconSize);
- return 1;
+ return 2;
}
static int RoundDown(int orig, int nearest) {
diff --git a/mm/2s2h/Rando/ActorBehavior/Souls.cpp b/mm/2s2h/Rando/ActorBehavior/Souls.cpp
index ff2604bd1..435a1cbe8 100644
--- a/mm/2s2h/Rando/ActorBehavior/Souls.cpp
+++ b/mm/2s2h/Rando/ActorBehavior/Souls.cpp
@@ -10,6 +10,17 @@ extern "C" {
void BossHakugin_DrawIce(BossHakugin*, PlayState*);
}
+bool shouldMajoraRegister() {
+ bool registerStatus = false;
+ if (IS_RANDO) {
+ if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_BOSS_SOULS] == RO_GENERIC_YES ||
+ RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) {
+ registerStatus = true;
+ }
+ }
+ return registerStatus;
+}
+
void ShouldActorUpdate(Actor* actor, bool* should, RandoInf randoInf) {
if (!Flags_GetRandoInf(randoInf)) {
*should = false;
@@ -44,7 +55,7 @@ void Rando::ActorBehavior::InitSoulsBehavior() {
COND_ID_HOOK(ShouldActorDraw, ACTOR_BOSS_03, shouldRegister,
[](Actor* actor, bool* should) { ShouldActorDraw(actor, should, RANDO_INF_OBTAINED_SOUL_OF_GYORG); });
- COND_ID_HOOK(ShouldActorDraw, ACTOR_BOSS_07, shouldRegister,
+ COND_ID_HOOK(ShouldActorDraw, ACTOR_BOSS_07, shouldMajoraRegister(),
[](Actor* actor, bool* should) { ShouldActorDraw(actor, should, RANDO_INF_OBTAINED_SOUL_OF_MAJORA); });
COND_ID_HOOK(ShouldActorDraw, ACTOR_BOSS_01, shouldRegister,
@@ -58,7 +69,7 @@ void Rando::ActorBehavior::InitSoulsBehavior() {
ShouldActorUpdate(actor, should, RANDO_INF_OBTAINED_SOUL_OF_GYORG);
});
- COND_ID_HOOK(ShouldActorUpdate, ACTOR_BOSS_07, shouldRegister, [](Actor* actor, bool* should) {
+ COND_ID_HOOK(ShouldActorUpdate, ACTOR_BOSS_07, shouldMajoraRegister(), [](Actor* actor, bool* should) {
ShouldActorUpdate(actor, should, RANDO_INF_OBTAINED_SOUL_OF_MAJORA);
});
diff --git a/mm/2s2h/Rando/ConvertItem.cpp b/mm/2s2h/Rando/ConvertItem.cpp
index f2d7142fb..26ba83a94 100644
--- a/mm/2s2h/Rando/ConvertItem.cpp
+++ b/mm/2s2h/Rando/ConvertItem.cpp
@@ -239,6 +239,7 @@ bool Rando::IsItemObtainable(RandoItemId randoItemId, RandoCheckId randoCheckId)
case RI_FROG_PINK:
case RI_FROG_WHITE:
case RI_ABILITY_SWIM:
+ case RI_TRIFORCE_PIECE:
if (hasObtainedCheck) {
return false;
}
diff --git a/mm/2s2h/Rando/DrawItem.cpp b/mm/2s2h/Rando/DrawItem.cpp
index 5fcfd13f5..54ccd568b 100644
--- a/mm/2s2h/Rando/DrawItem.cpp
+++ b/mm/2s2h/Rando/DrawItem.cpp
@@ -297,6 +297,35 @@ void DrawSkulltulaToken(RandoItemId randoItemId, Actor* actor) {
CLOSE_DISPS(gPlayState->state.gfxCtx);
}
+void DrawTriforcePiece(RandoItemId randoItemId) {
+ Gfx* triforcePieceModels[3] = {
+ (Gfx*)gTriforcePiece0DL,
+ (Gfx*)gTriforcePiece1DL,
+ (Gfx*)gTriforcePiece2DL,
+ };
+
+ u8 currentTriforcePieces = gSaveContext.save.shipSaveInfo.rando.foundTriforcePieces;
+
+ OPEN_DISPS(gPlayState->state.gfxCtx);
+
+ Gfx_SetupDL25_Xlu(gPlayState->state.gfxCtx);
+
+ Matrix_Scale(0.03f, 0.03f, 0.03f, MTXMODE_APPLY);
+
+ MATRIX_FINALIZE_AND_LOAD(POLY_XLU_DISP++, gPlayState->state.gfxCtx);
+ if (currentTriforcePieces >= RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_REQUIRED]) {
+ gSPDisplayList(POLY_XLU_DISP++, (Gfx*)gTriforcePieceCompletedDL);
+ } else {
+ if (randoItemId == RI_TRIFORCE_PIECE_PREVIOUS) {
+ gSPDisplayList(POLY_XLU_DISP++, (Gfx*)triforcePieceModels[(currentTriforcePieces - 1) % 3]);
+ } else {
+ gSPDisplayList(POLY_XLU_DISP++, (Gfx*)triforcePieceModels[currentTriforcePieces % 3]);
+ }
+ }
+
+ CLOSE_DISPS(gPlayState->state.gfxCtx);
+}
+
void DrawAbilityItem(RandoItemId randoItemId, Actor* actor) {
Gfx* abilityItemModel[1] = {
(Gfx*)gGiFlippersDL,
@@ -440,6 +469,10 @@ void Rando::DrawItem(RandoItemId randoItemId, Actor* actor) {
case RI_ABILITY_SWIM:
DrawAbilityItem(randoItemId, actor);
break;
+ case RI_TRIFORCE_PIECE_PREVIOUS:
+ case RI_TRIFORCE_PIECE:
+ DrawTriforcePiece(randoItemId);
+ break;
case RI_NONE:
case RI_UNKNOWN:
break;
diff --git a/mm/2s2h/Rando/GiveItem.cpp b/mm/2s2h/Rando/GiveItem.cpp
index 44863637d..28a9b1819 100644
--- a/mm/2s2h/Rando/GiveItem.cpp
+++ b/mm/2s2h/Rando/GiveItem.cpp
@@ -103,6 +103,22 @@ void Rando::GiveItem(RandoItemId randoItemId) {
gSaveContext.save.shipSaveInfo.rando.foundDungeonKeys[DUNGEON_SCENE_INDEX_STONE_TOWER_TEMPLE]++;
}
break;
+ case RI_TRIFORCE_PIECE:
+ case RI_TRIFORCE_PIECE_PREVIOUS:
+ gSaveContext.save.shipSaveInfo.rando.foundTriforcePieces++;
+ if (gSaveContext.save.shipSaveInfo.rando.foundTriforcePieces ==
+ RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_REQUIRED]) {
+ // Blocks the ability to beat the game through killing Majora until all Triforce Pieces are found.
+ if (!Flags_GetRandoInf(RANDO_INF_OBTAINED_SOUL_OF_MAJORA)) {
+ Rando::GiveItem(RI_SOUL_MAJORA);
+ }
+ GameInteractor::Instance->events.emplace_back(
+ GIEventTransition{ .entrance = ENTRANCE(TERMINA_FIELD, 0),
+ .cutsceneIndex = 0xFFF7,
+ .transitionTrigger = TRANS_TRIGGER_START,
+ .transitionType = TRANS_TYPE_FADE_BLACK });
+ }
+ break;
// Technically these should never be used, but leaving them here just in case
case RI_PROGRESSIVE_MAGIC:
case RI_PROGRESSIVE_BOW:
diff --git a/mm/2s2h/Rando/Menu.cpp b/mm/2s2h/Rando/Menu.cpp
index e7fa06f78..09c946c00 100644
--- a/mm/2s2h/Rando/Menu.cpp
+++ b/mm/2s2h/Rando/Menu.cpp
@@ -262,7 +262,32 @@ static void DrawShufflesTab() {
CheckboxOptions({ { .disabled = true, .disabledTooltip = "Coming Soon" } }));
ImGui::EndChild();
ImGui::SameLine();
- ImGui::BeginChild("randoShufflesColumn3", ImVec2(columnWidth, halfHeight));
+ ImGui::BeginChild("randoLocationsColumn3", ImVec2(columnWidth, halfHeight));
+ CVarCheckbox("Triforce Hunt", Rando::StaticData::Options[RO_SHUFFLE_TRIFORCE_PIECES].cvar);
+ ImGui::BeginDisabled(!CVarGetInteger(Rando::StaticData::Options[RO_SHUFFLE_TRIFORCE_PIECES].cvar, RO_GENERIC_OFF));
+ CVarSliderInt(
+ "Required Triforce Pieces", Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar,
+ IntSliderOptions({})
+ .Min(1)
+ .Max(CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, DEFAULT_TRIFORCE_PIECES_MAX))
+ .DefaultValue(DEFAULT_TRIFORCE_PIECES_MAX));
+ if (CVarSliderInt(
+ "Shuffled Triforce Pieces", Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar,
+ IntSliderOptions({})
+ .Min(1)
+ .Max(1000)
+ .DefaultValue(DEFAULT_TRIFORCE_PIECES_MAX)
+ .Tooltip("If the maximum amount of placeable pieces exceeds what will allow the seed to generate, the "
+ "amount will be adjusted automatically."))) {
+ if (CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar, DEFAULT_TRIFORCE_PIECES_MAX) >
+ CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, DEFAULT_TRIFORCE_PIECES_MAX)) {
+ CVarGetInteger(
+ Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar,
+ CVarGetInteger(Rando::StaticData::Options[RO_TRIFORCE_PIECES_MAX].cvar, DEFAULT_TRIFORCE_PIECES_MAX));
+ }
+ }
+
+ ImGui::EndDisabled();
ImGui::EndChild();
}
diff --git a/mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp b/mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp
index a9db0433f..bef7eb663 100644
--- a/mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp
+++ b/mm/2s2h/Rando/MiscBehavior/CheckQueue.cpp
@@ -49,6 +49,14 @@ void Rando::MiscBehavior::CheckQueue() {
if (randoItemId == RI_JUNK) {
randoItemId = Rando::CurrentJunkItem();
}
+ if (randoItemId == RI_TRIFORCE_PIECE) {
+ if (gSaveContext.save.shipSaveInfo.rando.foundTriforcePieces + 1 >=
+ RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_REQUIRED]) {
+ prefix = "You";
+ message = "completed the Triforce";
+ }
+ randoItemId = RI_TRIFORCE_PIECE_PREVIOUS;
+ }
CustomMessage::Entry entry = {
.textboxType = 2,
diff --git a/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp b/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp
index 93b9621bd..21dd965e6 100644
--- a/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp
+++ b/mm/2s2h/Rando/MiscBehavior/OnFileCreate.cpp
@@ -64,6 +64,11 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) {
gSaveContext.save.saveInfo.playerData.health = RANDO_SAVE_OPTIONS[RO_STARTING_HEALTH] * 0x10;
}
+ if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] != RO_GENERIC_OFF) {
+ RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_REQUIRED] = CVarGetInteger(
+ Rando::StaticData::Options[RO_TRIFORCE_PIECES_REQUIRED].cvar, DEFAULT_TRIFORCE_PIECES_MAX);
+ }
+
if (RANDO_SAVE_OPTIONS[RO_STARTING_CONSUMABLES]) {
GiveItem(RI_DEKU_STICK);
GiveItem(RI_DEKU_NUT);
@@ -241,6 +246,9 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) {
// Add other items that don't have a vanilla location like Sun's Song or Song of Double Time
if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_BOSS_SOULS] == RO_GENERIC_YES) {
for (int i = RI_SOUL_GOHT; i <= RI_SOUL_TWINMOLD; i++) {
+ if (i == RI_SOUL_MAJORA && RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) {
+ continue;
+ }
itemPool.push_back((RandoItemId)i);
}
}
@@ -257,6 +265,20 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) {
}
}
+ // Shuffle Triforce Pieces into the Pool
+ int piecesShuffled = 0;
+ if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) {
+ int piecesToShuffle = RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_MAX];
+ for (auto& item : itemPool) {
+ if (piecesToShuffle == 0) {
+ break;
+ }
+ itemPool.push_back(RI_TRIFORCE_PIECE);
+ piecesToShuffle--;
+ piecesShuffled++;
+ }
+ }
+
if (RANDO_SAVE_OPTIONS[RO_PLENTIFUL_ITEMS] == RO_GENERIC_YES) {
int replaceableItems = 0;
std::vector<RandoItemId> plentifulItems;
@@ -347,12 +369,40 @@ void Rando::MiscBehavior::OnFileCreate(s16 fileNum) {
continue;
}
+ // If Triforce Hunt is enabled, removed pieces as a last resort
+ if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) {
+ bool removedTriforcePiece = false;
+ for (int i = 0; i < itemPool.size(); i++) {
+ if (Rando::StaticData::Items[itemPool[i]].randoItemId == RI_TRIFORCE_PIECE) {
+ itemPool.erase(itemPool.begin() + i);
+ removedTriforcePiece = true;
+ piecesShuffled--;
+ break;
+ }
+ }
+
+ if (removedTriforcePiece) {
+ continue;
+ }
+ }
+
SPDLOG_ERROR("Could not match item pool size to check pool size {}/{}", itemPool.size(),
checkPool.size());
throw std::runtime_error("Could not match item pool size to check pool size");
}
}
+ // Update Required Triforce Pieces if piecesShuffled falls below max shuffled
+ if (RANDO_SAVE_OPTIONS[RO_SHUFFLE_TRIFORCE_PIECES] == RO_GENERIC_YES) {
+ if (piecesShuffled != RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_MAX]) {
+ float currentRatio = ((float)RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_REQUIRED] /
+ (float)RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_MAX]);
+
+ RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_MAX] = piecesShuffled;
+ RANDO_SAVE_OPTIONS[RO_TRIFORCE_PIECES_REQUIRED] = (piecesShuffled * currentRatio) + 1;
+ }
+ }
+
// Grant the starting items
for (RandoItemId startingItem : startingItems) {
GiveItem(ConvertItem(startingItem));
diff --git a/mm/2s2h/Rando/StaticData/Items.cpp b/mm/2s2h/Rando/StaticData/Items.cpp
index a2a127a97..b8a8064c0 100644
--- a/mm/2s2h/Rando/StaticData/Items.cpp
+++ b/mm/2s2h/Rando/StaticData/Items.cpp
@@ -186,6 +186,8 @@ std::map<RandoItemId, RandoStaticItem> Items = {
RI(RI_TINGLE_MAP_SNOWHEAD, "", "Tingle's Snowhead Map", RITYPE_LESSER, ITEM_TINGLE_MAP, GI_TINGLE_MAP_SNOWHEAD, GID_TINGLE_MAP),
RI(RI_TINGLE_MAP_STONE_TOWER, "", "Tingle's Stone Tower Map", RITYPE_LESSER, ITEM_TINGLE_MAP, GI_TINGLE_MAP_STONE_TOWER, GID_TINGLE_MAP),
RI(RI_TINGLE_MAP_WOODFALL, "", "Tingle's Woodfall Map", RITYPE_LESSER, ITEM_TINGLE_MAP, GI_TINGLE_MAP_WOODFALL, GID_TINGLE_MAP),
+ RI(RI_TRIFORCE_PIECE, "a", "Piece of the Triforce", RITYPE_MAJOR, ITEM_NONE, GI_NONE, GID_NONE),
+ RI(RI_TRIFORCE_PIECE_PREVIOUS, "a", "Piece of the Triforce", RITYPE_MAJOR, ITEM_NONE, GI_NONE, GID_NONE), // This only exists to aid in the drawing of unique models, it has no use outside of that.
RI(RI_WALLET_ADULT, "the", "Adult's Wallet", RITYPE_MAJOR, ITEM_WALLET_ADULT, GI_WALLET_ADULT, GID_WALLET_ADULT),
RI(RI_WALLET_GIANT, "the", "Giant's Wallet", RITYPE_LESSER, ITEM_WALLET_GIANT, GI_WALLET_GIANT, GID_WALLET_GIANT),
RI(RI_WOODFALL_BOSS_KEY, "the", "Woodfall Boss Key", RITYPE_BOSS_KEY, ITEM_KEY_BOSS, GI_KEY_BOSS, GID_KEY_BOSS),
diff --git a/mm/2s2h/Rando/StaticData/Options.cpp b/mm/2s2h/Rando/StaticData/Options.cpp
index 38a35e459..b8e695653 100644
--- a/mm/2s2h/Rando/StaticData/Options.cpp
+++ b/mm/2s2h/Rando/StaticData/Options.cpp
@@ -41,6 +41,7 @@ std::map<RandoOptionId, RandoStaticOption> Options = {
RO(RO_SHUFFLE_SNOWBALL_DROPS, RO_GENERIC_OFF),
RO(RO_SHUFFLE_SWIM, RO_GENERIC_OFF),
RO(RO_SHUFFLE_TINGLE_SHOPS, RO_GENERIC_OFF),
+ RO(RO_SHUFFLE_TRIFORCE_PIECES, RO_GENERIC_OFF),
RO(RO_STARTING_CONSUMABLES, RO_GENERIC_OFF),
RO(RO_STARTING_HEALTH, 3),
RO(RO_STARTING_ITEMS_1, 0),
@@ -48,6 +49,8 @@ std::map<RandoOptionId, RandoStaticOption> Options = {
RO(RO_STARTING_ITEMS_3, 2048),
RO(RO_STARTING_MAPS_AND_COMPASSES, RO_GENERIC_OFF),
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),
};
// clang-format on
diff --git a/mm/2s2h/Rando/StaticData/StaticData.h b/mm/2s2h/Rando/StaticData/StaticData.h
index 9a5a0d2c0..392651b9c 100644
--- a/mm/2s2h/Rando/StaticData/StaticData.h
+++ b/mm/2s2h/Rando/StaticData/StaticData.h
@@ -10,6 +10,8 @@ extern "C" {
#include "z64scene.h"
}
+#define DEFAULT_TRIFORCE_PIECES_MAX 15
+
namespace Rando {
namespace StaticData {
diff --git a/mm/2s2h/Rando/Types.h b/mm/2s2h/Rando/Types.h
index ef9725be7..8e1f9c064 100644
--- a/mm/2s2h/Rando/Types.h
+++ b/mm/2s2h/Rando/Types.h
@@ -2417,6 +2417,8 @@ typedef enum {
RI_TINGLE_MAP_SNOWHEAD,
RI_TINGLE_MAP_STONE_TOWER,
RI_TINGLE_MAP_WOODFALL,
+ RI_TRIFORCE_PIECE,
+ RI_TRIFORCE_PIECE_PREVIOUS,
RI_WALLET_ADULT,
RI_WALLET_GIANT,
RI_WOODFALL_BOSS_KEY,
@@ -2762,6 +2764,7 @@ typedef enum {
RO_SHUFFLE_SNOWBALL_DROPS,
RO_SHUFFLE_SWIM,
RO_SHUFFLE_TINGLE_SHOPS,
+ RO_SHUFFLE_TRIFORCE_PIECES,
RO_STARTING_CONSUMABLES,
RO_STARTING_HEALTH,
RO_STARTING_ITEMS_1,
@@ -2769,6 +2772,8 @@ typedef enum {
RO_STARTING_ITEMS_3,
RO_STARTING_MAPS_AND_COMPASSES,
RO_STARTING_RUPEES,
+ RO_TRIFORCE_PIECES_MAX,
+ RO_TRIFORCE_PIECES_REQUIRED,
RO_MAX,
} RandoOptionId;
diff --git a/mm/2s2h/ShipUtils.cpp b/mm/2s2h/ShipUtils.cpp
index 2edb19927..3d5193cb4 100644
--- a/mm/2s2h/ShipUtils.cpp
+++ b/mm/2s2h/ShipUtils.cpp
@@ -45,7 +45,7 @@ std::unordered_map<s16, const char*> sceneNames = {
#undef DEFINE_SCENE_UNSET
// These textures are not in existing lists that we iterate over.
-std::array<const char*, 21> miscellaneousTextures = {
+std::array<const char*, 22> miscellaneousTextures = {
gArcheryScoreIconTex,
gBarrelTrackerIcon,
gChestTrackerIcon,
@@ -63,6 +63,7 @@ std::array<const char*, 21> miscellaneousTextures = {
gStrayFairyStoneTowerIconTex,
gStrayFairyWoodfallIconTex,
gTimerClockIconTex,
+ gTriforcePieceTex,
gWorldMapOwlFaceTex,
gameplay_keep_Tex_053140,
gDungeonMapSkullTex,
diff --git a/mm/assets/2s2h_assets.h b/mm/assets/2s2h_assets.h
index 5581dad87..0ddb4497e 100644
--- a/mm/assets/2s2h_assets.h
+++ b/mm/assets/2s2h_assets.h
@@ -373,3 +373,15 @@ static const ALIGN_ASSET(2) char gBarrelTrackerIcon[] = dgBarrelTrackerIcon;
#define dgGiFlippersDL "__OTR__objects/object_ability_swim/gGiFlippersDL"
static const ALIGN_ASSET(2) char gGiFlippersDL[] = dgGiFlippersDL;
+
+#define dgTriforcePiece0DL "__OTR__objects/object_triforce_piece_0/gTriforcePiece0DL"
+static const ALIGN_ASSET(2) char gTriforcePiece0DL[] = dgTriforcePiece0DL;
+
+#define dgTriforcePiece1DL "__OTR__objects/object_triforce_piece_1/gTriforcePiece1DL"
+static const ALIGN_ASSET(2) char gTriforcePiece1DL[] = dgTriforcePiece1DL;
+
+#define dgTriforcePiece2DL "__OTR__objects/object_triforce_piece_2/gTriforcePiece2DL"
+static const ALIGN_ASSET(2) char gTriforcePiece2DL[] = dgTriforcePiece2DL;
+
+#define dgTriforcePieceCompletedDL "__OTR__objects/object_triforce_completed/gTriforcePieceCompletedDL"
+static const ALIGN_ASSET(2) char gTriforcePieceCompletedDL[] = dgTriforcePieceCompletedDL;
diff --git a/mm/include/z64save.h b/mm/include/z64save.h
index debcfe010..b2c14f752 100644
--- a/mm/include/z64save.h
+++ b/mm/include/z64save.h
@@ -386,6 +386,7 @@ typedef struct RandoSaveInfo {
u32 finalSeed;
u32 randoSaveOptions[RO_MAX]; // Type here may change in the future
s8 foundDungeonKeys[9]; // Tracks the number of dungeon keys found, opposed to the number of keys in the inventory
+ u8 foundTriforcePieces;
} RandoSaveInfo;
// These are values added by 2S2H that we need to be persisted to the save file