diff --git a/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL
new file mode 100644
index 000000000..966bef206
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_tri_0 b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_tri_0
new file mode 100644
index 000000000..dea47708c
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_tri_0
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_tri_1 b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_tri_1
new file mode 100644
index 000000000..36be4333f
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_tri_1
@@ -0,0 +1,7 @@
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_vtx_0 b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_vtx_0
new file mode 100644
index 000000000..6ca96db30
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_vtx_0
@@ -0,0 +1,54 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_vtx_1 b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_vtx_1
new file mode 100644
index 000000000..3a653966d
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_completed/gTriforcePieceCompletedDL_vtx_1
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_completed/mat_gTriforcePieceCompletedDL_f3dlite_triforce_edges b/OTRExporter/assets/objects/object_triforce_completed/mat_gTriforcePieceCompletedDL_f3dlite_triforce_edges
new file mode 100644
index 000000000..52591dfc8
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_completed/mat_gTriforcePieceCompletedDL_f3dlite_triforce_edges
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_completed/mat_gTriforcePieceCompletedDL_f3dlite_triforce_surface b/OTRExporter/assets/objects/object_triforce_completed/mat_gTriforcePieceCompletedDL_f3dlite_triforce_surface
new file mode 100644
index 000000000..06193ae61
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_completed/mat_gTriforcePieceCompletedDL_f3dlite_triforce_surface
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_completed/noise_tex b/OTRExporter/assets/objects/object_triforce_completed/noise_tex
new file mode 100644
index 000000000..a6d6cf945
Binary files /dev/null and b/OTRExporter/assets/objects/object_triforce_completed/noise_tex differ
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL
new file mode 100644
index 000000000..70d08c31d
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_tri_0 b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_tri_0
new file mode 100644
index 000000000..09e44f1b7
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_tri_0
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_tri_1 b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_tri_1
new file mode 100644
index 000000000..48001e3c3
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_tri_1
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_tri_2 b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_tri_2
new file mode 100644
index 000000000..e35e34492
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_tri_2
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_vtx_0 b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_vtx_0
new file mode 100644
index 000000000..a86fa98bf
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_vtx_0
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_vtx_1 b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_vtx_1
new file mode 100644
index 000000000..230fbb7f8
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_vtx_1
@@ -0,0 +1,22 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_vtx_2 b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_vtx_2
new file mode 100644
index 000000000..86d123825
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/gTriforcePiece0DL_vtx_2
@@ -0,0 +1,49 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/mat_gTriforcePiece0DL_f3dlite_shard_edge b/OTRExporter/assets/objects/object_triforce_piece_0/mat_gTriforcePiece0DL_f3dlite_shard_edge
new file mode 100644
index 000000000..f62631793
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/mat_gTriforcePiece0DL_f3dlite_shard_edge
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/mat_gTriforcePiece0DL_f3dlite_triforce_edges b/OTRExporter/assets/objects/object_triforce_piece_0/mat_gTriforcePiece0DL_f3dlite_triforce_edges
new file mode 100644
index 000000000..9355e7094
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/mat_gTriforcePiece0DL_f3dlite_triforce_edges
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/mat_gTriforcePiece0DL_f3dlite_triforce_surface b/OTRExporter/assets/objects/object_triforce_piece_0/mat_gTriforcePiece0DL_f3dlite_triforce_surface
new file mode 100644
index 000000000..e863b31c5
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_0/mat_gTriforcePiece0DL_f3dlite_triforce_surface
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_0/noise_tex b/OTRExporter/assets/objects/object_triforce_piece_0/noise_tex
new file mode 100644
index 000000000..a6d6cf945
Binary files /dev/null and b/OTRExporter/assets/objects/object_triforce_piece_0/noise_tex differ
diff --git a/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL
new file mode 100644
index 000000000..50a9264c6
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL
@@ -0,0 +1,13 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_tri_0 b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_tri_0
new file mode 100644
index 000000000..5f33f7347
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_tri_0
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_tri_1 b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_tri_1
new file mode 100644
index 000000000..43df6492b
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_tri_1
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_vtx_0 b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_vtx_0
new file mode 100644
index 000000000..e078b8246
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_vtx_0
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_vtx_1 b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_vtx_1
new file mode 100644
index 000000000..e0460194d
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_1/gTriforcePiece1DL_vtx_1
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_1/mat_gTriforcePiece1DL_f3dlite_shard_edge b/OTRExporter/assets/objects/object_triforce_piece_1/mat_gTriforcePiece1DL_f3dlite_shard_edge
new file mode 100644
index 000000000..b9e61293d
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_1/mat_gTriforcePiece1DL_f3dlite_shard_edge
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_1/mat_gTriforcePiece1DL_f3dlite_triforce_surface b/OTRExporter/assets/objects/object_triforce_piece_1/mat_gTriforcePiece1DL_f3dlite_triforce_surface
new file mode 100644
index 000000000..5f8dc51f9
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_1/mat_gTriforcePiece1DL_f3dlite_triforce_surface
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_1/noise_tex b/OTRExporter/assets/objects/object_triforce_piece_1/noise_tex
new file mode 100644
index 000000000..a6d6cf945
Binary files /dev/null and b/OTRExporter/assets/objects/object_triforce_piece_1/noise_tex differ
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL
new file mode 100644
index 000000000..5213cd53c
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_tri_0 b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_tri_0
new file mode 100644
index 000000000..b54e182d5
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_tri_0
@@ -0,0 +1,29 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_tri_1 b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_tri_1
new file mode 100644
index 000000000..00a32bfd8
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_tri_1
@@ -0,0 +1,11 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_tri_2 b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_tri_2
new file mode 100644
index 000000000..0993c1c1e
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_tri_2
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_vtx_0 b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_vtx_0
new file mode 100644
index 000000000..bf7dfcac6
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_vtx_0
@@ -0,0 +1,36 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_vtx_1 b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_vtx_1
new file mode 100644
index 000000000..e3237ab21
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_vtx_1
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_vtx_2 b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_vtx_2
new file mode 100644
index 000000000..ec4e73700
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/gTriforcePiece2DL_vtx_2
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/mat_gTriforcePiece2DL_f3dlite_shard_edge b/OTRExporter/assets/objects/object_triforce_piece_2/mat_gTriforcePiece2DL_f3dlite_shard_edge
new file mode 100644
index 000000000..c222fe68d
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/mat_gTriforcePiece2DL_f3dlite_shard_edge
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/mat_gTriforcePiece2DL_f3dlite_triforce_edges b/OTRExporter/assets/objects/object_triforce_piece_2/mat_gTriforcePiece2DL_f3dlite_triforce_edges
new file mode 100644
index 000000000..5968068f5
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/mat_gTriforcePiece2DL_f3dlite_triforce_edges
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/mat_gTriforcePiece2DL_f3dlite_triforce_surface b/OTRExporter/assets/objects/object_triforce_piece_2/mat_gTriforcePiece2DL_f3dlite_triforce_surface
new file mode 100644
index 000000000..d903f00bb
--- /dev/null
+++ b/OTRExporter/assets/objects/object_triforce_piece_2/mat_gTriforcePiece2DL_f3dlite_triforce_surface
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/OTRExporter/assets/objects/object_triforce_piece_2/noise_tex b/OTRExporter/assets/objects/object_triforce_piece_2/noise_tex
new file mode 100644
index 000000000..a6d6cf945
Binary files /dev/null and b/OTRExporter/assets/objects/object_triforce_piece_2/noise_tex differ
diff --git a/OTRExporter/assets/textures/parameter_static/gTriforcePiece.rgba32.png b/OTRExporter/assets/textures/parameter_static/gTriforcePiece.rgba32.png
new file mode 100644
index 000000000..cc67b6a13
Binary files /dev/null and b/OTRExporter/assets/textures/parameter_static/gTriforcePiece.rgba32.png differ
diff --git a/soh/assets/soh_assets.h b/soh/assets/soh_assets.h
index b1feac469..2b193053c 100644
--- a/soh/assets/soh_assets.h
+++ b/soh/assets/soh_assets.h
@@ -44,6 +44,18 @@ static const ALIGN_ASSET(2) char gTitleRandomizerSubtitleTex[] = dgTitleRandomiz
#define dgTitleBossRushSubtitleTex "__OTR__objects/object_mag/gTitleBossRushSubtitleTex"
static const ALIGN_ASSET(2) char gTitleBossRushSubtitleTex[] = dgTitleBossRushSubtitleTex;
+#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;
+
// overlays
#define dgOptionsDividerChangeLangVtx "__OTR__overlays/ovl_file_choose/gOptionsDividerChangeLangVtx"
static const ALIGN_ASSET(2) char gOptionsDividerChangeLangVtx[] = dgOptionsDividerChangeLangVtx;
@@ -58,6 +70,9 @@ static const ALIGN_ASSET(2) char gArrowUpTex[] = dgArrowUp;
#define dgArrowDown "__OTR__textures/parameter_static/gArrowDown"
static const ALIGN_ASSET(2) char gArrowDownTex[] = dgArrowDown;
+#define dgTriforcePiece "__OTR__textures/parameter_static/gTriforcePiece"
+static const ALIGN_ASSET(2) char gTriforcePieceTex[] = dgTriforcePiece;
+
#define dgFileSelMQButtonTex "__OTR__textures/title_static/gFileSelMQButtonTex"
static const ALIGN_ASSET(2) char gFileSelMQButtonTex[] = dgFileSelMQButtonTex;
diff --git a/soh/include/variables.h b/soh/include/variables.h
index 8fd457d18..ee73d003b 100644
--- a/soh/include/variables.h
+++ b/soh/include/variables.h
@@ -171,6 +171,7 @@ extern "C"
extern u8 gWalkSpeedToggle1;
extern u8 gWalkSpeedToggle2;
extern f32 iceTrapScale;
+ extern f32 triforcePieceScale;
extern const s16 D_8014A6C0[];
#define gTatumsPerBeat (D_8014A6C0[1])
diff --git a/soh/include/z64item.h b/soh/include/z64item.h
index 65f6e93d9..2f0038872 100644
--- a/soh/include/z64item.h
+++ b/soh/include/z64item.h
@@ -511,6 +511,7 @@ typedef enum {
/* 0x79 */ GID_SONG_SUN,
/* 0x7A */ GID_SONG_TIME,
/* 0x7B */ GID_SONG_STORM,
+ /* 0x7C */ GID_TRIFORCE_PIECE,
/* 0x7C */ GID_MAXIMUM
} GetItemDrawID;
diff --git a/soh/include/z64player.h b/soh/include/z64player.h
index 99445f1d7..094e28eb2 100644
--- a/soh/include/z64player.h
+++ b/soh/include/z64player.h
@@ -620,7 +620,7 @@ typedef struct Player {
/* 0x0858 */ f32 unk_858;
/* 0x085C */ f32 unk_85C; // stick length among other things
/* 0x0860 */ s16 unk_860; // stick flame timer among other things
- /* 0x0862 */ s8 unk_862; // get item draw ID + 1
+ /* 0x0862 */ s16 unk_862; // get item draw ID + 1
/* 0x0864 */ f32 unk_864;
/* 0x0868 */ f32 unk_868;
/* 0x086C */ f32 unk_86C;
diff --git a/soh/include/z64save.h b/soh/include/z64save.h
index e4aeb4e31..50f9ba44b 100644
--- a/soh/include/z64save.h
+++ b/soh/include/z64save.h
@@ -324,6 +324,7 @@ typedef struct {
/* */ u8 seedIcons[5];
/* */ u16 randomizerInf[10];
/* */ u16 adultTradeItems;
+ /* */ u8 triforcePiecesCollected;
// #endregion
} SaveContext; // size = 0x1428
diff --git a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp
index 0f9c56669..cf60e61da 100644
--- a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp
+++ b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp
@@ -10,6 +10,7 @@
#include
#include
#include
+#include "soh_assets.h"
extern "C" {
#include
@@ -144,6 +145,10 @@ std::map gregMapping = {
{ITEM_RUPEE_GREEN, {ITEM_RUPEE_GREEN, "ITEM_RUPEE_GREEN", "ITEM_RUPEE_GREEN_Faded", gRupeeCounterIconTex}}
};
+std::map triforcePieceMapping = {
+ {RG_TRIFORCE_PIECE, {RG_TRIFORCE_PIECE, "RG_TRIFORCE_PIECE", "RG_TRIFORCE_PIECE_Faded", gTriforcePieceTex}}
+};
+
// Maps entries in the GS flag array to the area name it represents
std::vector gsMapping = {
"Deku Tree",
@@ -509,6 +514,10 @@ void DrawInfoTab() {
}
UIWidgets::InsertHelpHoverText("Z-Targeting behavior");
+ if (gSaveContext.n64ddFlag && OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT)) {
+ ImGui::InputScalar("Triforce Pieces", ImGuiDataType_U16, &gSaveContext.triforcePiecesCollected);
+ UIWidgets::InsertHelpHoverText("Currently obtained Triforce Pieces. For Triforce Hunt.");
+ }
ImGui::PushItemWidth(ImGui::GetFontSize() * 10);
static std::array minigameHS = { "Horseback Archery",
@@ -1790,6 +1799,10 @@ void SaveEditorWindow::InitElement() {
LUS::Context::GetInstance()->GetWindow()->GetGui()->LoadGuiTexture(entry.second.name, entry.second.texturePath, gregGreen);
LUS::Context::GetInstance()->GetWindow()->GetGui()->LoadGuiTexture(entry.second.nameFaded, entry.second.texturePath, gregFadedGreen);
}
+ for (const auto& entry : triforcePieceMapping) {
+ LUS::Context::GetInstance()->GetWindow()->GetGui()->LoadGuiTexture(entry.second.name, entry.second.texturePath, ImVec4(1, 1, 1, 1));
+ LUS::Context::GetInstance()->GetWindow()->GetGui()->LoadGuiTexture(entry.second.nameFaded, entry.second.texturePath, ImVec4(1, 1, 1, 0.3f));
+ }
for (const auto& entry : questMapping) {
LUS::Context::GetInstance()->GetWindow()->GetGui()->LoadGuiTexture(entry.second.name, entry.second.texturePath, ImVec4(1, 1, 1, 1));
LUS::Context::GetInstance()->GetWindow()->GetGui()->LoadGuiTexture(entry.second.nameFaded, entry.second.texturePath, ImVec4(1, 1, 1, 0.3f));
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h
index 036483f2e..008cfb53c 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor.h
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h
@@ -83,6 +83,8 @@ uint8_t GameInteractor_GetRandomWindActive();
uint8_t GameInteractor_GetRandomBonksActive();
uint8_t GameInteractor_GetSlipperyFloorActive();
uint8_t GameInteractor_SecondCollisionUpdate();
+void GameInteractor_SetTriforceHuntPieceGiven(uint8_t state);
+void GameInteractor_SetTriforceHuntCreditsWarpActive(uint8_t state);
#ifdef __cplusplus
}
#endif
@@ -123,6 +125,8 @@ public:
static uint8_t RandomBonksActive;
static uint8_t SlipperyFloorActive;
static uint8_t SecondCollisionUpdate;
+ static uint8_t TriforceHuntPieceGiven;
+ static uint8_t TriforceHuntCreditsWarpActive;
static void SetPacifistMode(bool active);
};
diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_State.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_State.cpp
index 881fb7ff5..21642dded 100644
--- a/soh/soh/Enhancements/game-interactor/GameInteractor_State.cpp
+++ b/soh/soh/Enhancements/game-interactor/GameInteractor_State.cpp
@@ -20,6 +20,8 @@ uint8_t GameInteractor::State::RandomWindSecondsSinceLastDirectionChange = 0;
uint8_t GameInteractor::State::RandomBonksActive = 0;
uint8_t GameInteractor::State::SlipperyFloorActive = 0;
uint8_t GameInteractor::State::SecondCollisionUpdate = 0;
+uint8_t GameInteractor::State::TriforceHuntPieceGiven = 0;
+uint8_t GameInteractor::State::TriforceHuntCreditsWarpActive = 0;
void GameInteractor::State::SetPacifistMode(bool active) {
PacifistModeActive = active;
@@ -127,3 +129,13 @@ uint8_t GameInteractor_GetSlipperyFloorActive() {
uint8_t GameInteractor_SecondCollisionUpdate() {
return GameInteractor::State::SecondCollisionUpdate;
}
+
+// MARK: - GameInteractor::State::TriforceHuntPieceGiven
+void GameInteractor_SetTriforceHuntPieceGiven(uint8_t state) {
+ GameInteractor::State::TriforceHuntPieceGiven = state;
+}
+
+// MARK: - GameInteractor::State::TriforceHuntCreditsWarpActive
+void GameInteractor_SetTriforceHuntCreditsWarpActive(uint8_t state) {
+ GameInteractor::State::TriforceHuntCreditsWarpActive = state;
+}
diff --git a/soh/soh/Enhancements/gameplaystats.cpp b/soh/soh/Enhancements/gameplaystats.cpp
index 6276da8b2..ea95afe8e 100644
--- a/soh/soh/Enhancements/gameplaystats.cpp
+++ b/soh/soh/Enhancements/gameplaystats.cpp
@@ -793,6 +793,7 @@ void SetupDisplayNames() {
strcpy(itemTimestampDisplayName[TIMESTAMP_DEFEAT_GANON], "Ganon Defeated: ");
strcpy(itemTimestampDisplayName[TIMESTAMP_BOSSRUSH_FINISH], "Boss Rush Finished: ");
strcpy(itemTimestampDisplayName[TIMESTAMP_FOUND_GREG], "Greg Found: ");
+ strcpy(itemTimestampDisplayName[TIMESTAMP_TRIFORCE_COMPLETED], "Triforce Completed: ");
}
void SetupDisplayColors() {
@@ -839,6 +840,7 @@ void SetupDisplayColors() {
case ITEM_ARROW_LIGHT:
case TIMESTAMP_DEFEAT_GANONDORF:
case TIMESTAMP_DEFEAT_GANON:
+ case TIMESTAMP_TRIFORCE_COMPLETED:
itemTimestampDisplayColor[i] = COLOR_YELLOW;
break;
case ITEM_SONG_STORMS:
diff --git a/soh/soh/Enhancements/gameplaystats.h b/soh/soh/Enhancements/gameplaystats.h
index 8bcf524d8..8772dd37f 100644
--- a/soh/soh/Enhancements/gameplaystats.h
+++ b/soh/soh/Enhancements/gameplaystats.h
@@ -35,6 +35,7 @@ typedef enum {
/* 0xA9 */ TIMESTAMP_DEFEAT_GANON, // z_boss_ganon2.c
/* 0xA9 */ TIMESTAMP_BOSSRUSH_FINISH, // z_boss_ganon2.c
/* 0xAA */ TIMESTAMP_FOUND_GREG, // z_parameter.c
+ /* 0xAA */ TIMESTAMP_TRIFORCE_COMPLETED, // z_parameter.c
/* 0xAB */ TIMESTAMP_MAX
}GameplayStatTimestamp;
diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp
index 6fc3b35b2..927ae1870 100644
--- a/soh/soh/Enhancements/mods.cpp
+++ b/soh/soh/Enhancements/mods.cpp
@@ -2,10 +2,12 @@
#include
#include "game-interactor/GameInteractor.h"
#include "tts/tts.h"
+#include "soh/OTRGlobals.h"
#include "soh/Enhancements/boss-rush/BossRushTypes.h"
#include "soh/Enhancements/enhancementTypes.h"
#include "soh/Enhancements/randomizer/3drando/random.hpp"
#include "soh/Enhancements/cosmetics/authenticGfxPatches.h"
+#include
#include "soh/Enhancements/nametag.h"
#include "src/overlays/actors/ovl_En_Bb/z_en_bb.h"
@@ -613,6 +615,45 @@ void RegisterMirrorModeHandler() {
});
}
+f32 triforcePieceScale;
+
+void RegisterTriforceHunt() {
+ GameInteractor::Instance->RegisterGameHook([]() {
+ if (!GameInteractor::IsGameplayPaused() &&
+ OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT)) {
+
+ // Warp to credits
+ if (GameInteractor::State::TriforceHuntCreditsWarpActive) {
+ gPlayState->nextEntranceIndex = 0x6B;
+ gSaveContext.nextCutsceneIndex = 0xFFF2;
+ gPlayState->sceneLoadFlag = 0x14;
+ gPlayState->fadeTransition = 3;
+ GameInteractor::State::TriforceHuntCreditsWarpActive = 0;
+ }
+
+ // Reset Triforce Piece scale for GI animation. Triforce Hunt allows for multiple triforce models,
+ // and cycles through them based on the amount of triforce pieces collected. It takes a little while
+ // for the count to increase during the GI animation, so the model is entirely hidden until that piece
+ // has been added. That scale has to be reset after the textbox is closed, and this is the best way
+ // to ensure it's done at that point in time specifically.
+ if (GameInteractor::State::TriforceHuntPieceGiven) {
+ triforcePieceScale = 0.0f;
+ GameInteractor::State::TriforceHuntPieceGiven = 0;
+ }
+
+ uint8_t currentPieces = gSaveContext.triforcePiecesCollected;
+ uint8_t requiredPieces = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED);
+
+ // Give Boss Key when player loads back into the savefile.
+ if (currentPieces >= requiredPieces && gPlayState->sceneLoadFlag != 0x14 &&
+ (1 << 0 & gSaveContext.inventory.dungeonItems[SCENE_GANONS_TOWER]) == 0) {
+ GetItemEntry getItemEntry = ItemTableManager::Instance->RetrieveItemEntry(MOD_RANDOMIZER, RG_GANONS_CASTLE_BOSS_KEY);
+ GiveItemEntryWithoutActor(gPlayState, getItemEntry);
+ }
+ }
+ });
+}
+
//this map is used for enemies that can be uniquely identified by their id
//and that are always counted
//enemies that can't be uniquely identified by their id
@@ -996,6 +1037,7 @@ void InitMods() {
RegisterBonkDamage();
RegisterMenuPathFix();
RegisterMirrorModeHandler();
+ RegisterTriforceHunt();
RegisterEnemyDefeatCounts();
RegisterAltTrapTypes();
RegisterRandomizerSheikSpawn();
diff --git a/soh/soh/Enhancements/randomizer/3drando/hints.cpp b/soh/soh/Enhancements/randomizer/3drando/hints.cpp
index 0bae15676..c1610ce07 100644
--- a/soh/soh/Enhancements/randomizer/3drando/hints.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/hints.cpp
@@ -503,7 +503,9 @@ static std::vector CalculateBarrenRegions() {
if (Location(loc)->GetPlacedItem().IsMajorItem() || ElementInContainer(loc, wothLocations)) {
AddElementsToPool(potentiallyUsefulLocations, std::vector{loc});
} else {
- if (loc != LINKS_POCKET) { //Nobody cares to know if Link's Pocket is barren
+ // Link's Pocket & Triforce Hunt "reward" shouldn't be considered for barren areas because it's clear what
+ // they have to a player.
+ if (loc != LINKS_POCKET && loc != TRIFORCE_COMPLETED) {
AddElementsToPool(barrenLocations, std::vector{loc});
}
}
@@ -723,6 +725,9 @@ static Text BuildGanonBossKeyText() {
} else if (GanonsBossKey.Is(GANONSBOSSKEY_LACS_TOKENS)) {
ganonBossKeyText = BuildCountReq(LACS_TOKENS_HINT, LACSTokenCount);
+
+ } else if (GanonsBossKey.Is(GANONSBOSSKEY_TRIFORCE_HUNT)) {
+ ganonBossKeyText = Hint(GANON_BK_TRIFORCE_HINT).GetText();
}
return Text()+"$b"+ganonBossKeyText+"^";
diff --git a/soh/soh/Enhancements/randomizer/3drando/item_list.cpp b/soh/soh/Enhancements/randomizer/3drando/item_list.cpp
index dd7533b98..417203a5b 100644
--- a/soh/soh/Enhancements/randomizer/3drando/item_list.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/item_list.cpp
@@ -242,6 +242,7 @@ void ItemTable_Init() { // RandomizerGet
itemTable[BUY_RED_POTION_40] = Item(RG_BUY_RED_POTION_40, Text{"Buy Red Potion [40]", "Acheter: Potion Rouge [40]", "Comprar poción roja [40]"}, ITEMTYPE_SHOP, 0x30, false, &noVariable, BOTTLE_WITH_RED_POTION, 40);
itemTable[BUY_RED_POTION_50] = Item(RG_BUY_RED_POTION_50, Text{"Buy Red Potion [50]", "Acheter: Potion Rouge [50]", "Comprar poción roja [50]"}, ITEMTYPE_SHOP, 0x31, false, &noVariable, BOTTLE_WITH_RED_POTION, 50);
+ itemTable[TRIFORCE_PIECE] = Item(RG_TRIFORCE_PIECE, Text{"Triforce Piece", "Triforce Piece", "Triforce Piece"}, ITEMTYPE_ITEM, 0xDF, true, &TriforcePieces, TRIFORCE_PIECE);
itemTable[TRIFORCE] = Item(RG_TRIFORCE, Text{"Triforce", "Triforce", "Trifuerza"}, ITEMTYPE_EVENT, GI_RUPEE_RED_LOSE, false, &noVariable, NONE);
itemTable[HINT] = Item(RG_HINT, Text{"Hint", "Indice", "Pista"}, ITEMTYPE_EVENT, GI_RUPEE_BLUE_LOSE, false, &noVariable, NONE);
diff --git a/soh/soh/Enhancements/randomizer/3drando/item_location.cpp b/soh/soh/Enhancements/randomizer/3drando/item_location.cpp
index 2327a4a4d..1c085021c 100644
--- a/soh/soh/Enhancements/randomizer/3drando/item_location.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/item_location.cpp
@@ -918,6 +918,7 @@ void LocationTable_Init() {
locationTable[DMC_UPPER_GROTTO_GOSSIP_STONE] = ItemLocation::HintStone(RC_DMC_UPPER_GROTTO_GOSSIP_STONE, "DMC Upper Grotto Gossip Stone");
locationTable[GANONDORF_HINT] = ItemLocation::OtherHint(RC_GANONDORF_HINT, "Ganondorf Hint");
+ locationTable[TRIFORCE_COMPLETED] = ItemLocation::Reward (RC_TRIFORCE_COMPLETED, 0xFF, "Completed Triforce", NONE, TRIFORCE_COMPLETED, {}, SpoilerCollectionCheck::None(), SpoilerCollectionCheckGroup::GROUP_NO_GROUP);
for (int i = NONE; i != KEY_ENUM_MAX; i++)
locationLookupTable.insert(std::make_pair(locationTable[i].GetRandomizerCheck(), static_cast(i)));
@@ -1523,6 +1524,9 @@ void GenerateLocationPool() {
allLocations.clear();
AddLocation(LINKS_POCKET);
+ if (Settings::TriforceHunt.Is(TRIFORCE_HUNT_ON)) {
+ AddLocation(TRIFORCE_COMPLETED);
+ }
AddLocations(overworldLocations);
for (auto dungeon : Dungeon::dungeonList) {
diff --git a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp
index 9b55eb0b4..485fc4154 100644
--- a/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/item_pool.cpp
@@ -662,9 +662,17 @@ void GenerateItemPool() {
IceTrapModels.push_back(0xD3);
}
+ if (TriforceHunt.Is(TRIFORCE_HUNT_ON)) {
+ IceTrapModels.push_back(0xDF);
+ AddItemToMainPool(TRIFORCE_PIECE, Settings::TriforceHuntTotal.Value());
+ PlaceItemInLocation(TRIFORCE_COMPLETED, TRIFORCE); // Win condition
+ PlaceItemInLocation(GANON, GetJunkItem(), false, true);
+ } else {
+ PlaceItemInLocation(GANON, TRIFORCE); // Win condition
+ }
+
//Fixed item locations
PlaceItemInLocation(HC_ZELDAS_LETTER, ZELDAS_LETTER);
- PlaceItemInLocation(GANON, TRIFORCE); //The Triforce is only used to make sure Ganon is accessible
PlaceItemInLocation(MARKET_BOMBCHU_BOWLING_BOMBCHUS, BOMBCHU_DROP);
if (ShuffleKokiriSword) {
@@ -1135,7 +1143,7 @@ void GenerateItemPool() {
if (GanonsBossKey.Is(GANONSBOSSKEY_FINAL_GS_REWARD)) {
PlaceItemInLocation(KAK_100_GOLD_SKULLTULA_REWARD, GANONS_CASTLE_BOSS_KEY);
- } else if (GanonsBossKey.Value() >= GANONSBOSSKEY_LACS_VANILLA) {
+ } else if (GanonsBossKey.Value() >= GANONSBOSSKEY_LACS_VANILLA && GanonsBossKey.IsNot(GANONSBOSSKEY_TRIFORCE_HUNT)) {
PlaceItemInLocation(TOT_LIGHT_ARROWS_CUTSCENE, GANONS_CASTLE_BOSS_KEY);
} else if (GanonsBossKey.Is(GANONSBOSSKEY_VANILLA)) {
PlaceItemInLocation(GANONS_TOWER_BOSS_KEY_CHEST, GANONS_CASTLE_BOSS_KEY);
diff --git a/soh/soh/Enhancements/randomizer/3drando/keys.hpp b/soh/soh/Enhancements/randomizer/3drando/keys.hpp
index 38011b8b8..6e9e87f4e 100644
--- a/soh/soh/Enhancements/randomizer/3drando/keys.hpp
+++ b/soh/soh/Enhancements/randomizer/3drando/keys.hpp
@@ -202,6 +202,7 @@ typedef enum {
TRIFORCE,
TRIFORCE_PIECE,
+ TRIFORCE_COMPLETED,
EPONA,
HINT,
diff --git a/soh/soh/Enhancements/randomizer/3drando/location_access.cpp b/soh/soh/Enhancements/randomizer/3drando/location_access.cpp
index a78d00a30..6be67ea84 100644
--- a/soh/soh/Enhancements/randomizer/3drando/location_access.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/location_access.cpp
@@ -256,7 +256,8 @@ void AreaTable_Init() {
//name, scene, hint text, events, locations, exits
areaTable[ROOT] = Area("Root", "", LINKS_POCKET, NO_DAY_NIGHT_CYCLE, {}, {
//Locations
- LocationAccess(LINKS_POCKET, {[]{return true;}})
+ LocationAccess(LINKS_POCKET, {[]{return true;}}),
+ LocationAccess(TRIFORCE_COMPLETED, { [] { return CanCompleteTriforce;}}),
}, {
//Exits
Entrance(ROOT_EXITS, {[]{return true;}})
diff --git a/soh/soh/Enhancements/randomizer/3drando/logic.cpp b/soh/soh/Enhancements/randomizer/3drando/logic.cpp
index 6fe26a417..ddf53824e 100644
--- a/soh/soh/Enhancements/randomizer/3drando/logic.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/logic.cpp
@@ -150,6 +150,9 @@ namespace Logic {
uint8_t BottomOfTheWellKeys = 0;
uint8_t TreasureGameKeys = 0;
+ //Triforce Pieces
+ uint8_t TriforcePieces = 0;
+
//Boss Keys
bool BossKeyForestTemple = false;
bool BossKeyFireTemple = false;
@@ -309,6 +312,7 @@ namespace Logic {
bool AtDay = false;
bool AtNight = false;
uint8_t Age = 0;
+ bool CanCompleteTriforce = false;
//Events
bool ShowedMidoSwordAndShield = false;
@@ -620,7 +624,7 @@ namespace Logic {
(LACSCondition == LACSCONDITION_REWARDS && StoneCount + MedallionCount + (Greg && GregInLogic ? 1 : 0) >= LACSRewardCount.Value()) ||
(LACSCondition == LACSCONDITION_DUNGEONS && DungeonCount + (Greg && GregInLogic ? 1 : 0) >= LACSDungeonCount.Value()) ||
(LACSCondition == LACSCONDITION_TOKENS && GoldSkulltulaTokens >= LACSTokenCount.Value());
-
+ CanCompleteTriforce = TriforcePieces >= TriforceHuntRequired.Value();
}
bool SmallKeys(Key dungeon, uint8_t requiredAmount) {
@@ -873,7 +877,8 @@ namespace Logic {
NumBottles = 0;
NoBottles = false;
-
+ //Triforce Pieces
+ TriforcePieces = 0;
//Drops and Bottle Contents Access
DekuNutDrop = false;
diff --git a/soh/soh/Enhancements/randomizer/3drando/logic.hpp b/soh/soh/Enhancements/randomizer/3drando/logic.hpp
index 77f942168..4d24fd3ff 100644
--- a/soh/soh/Enhancements/randomizer/3drando/logic.hpp
+++ b/soh/soh/Enhancements/randomizer/3drando/logic.hpp
@@ -143,6 +143,9 @@ extern uint8_t GerudoFortressKeys;
extern uint8_t GanonsCastleKeys;
extern uint8_t TreasureGameKeys;
+// Triforce Pieces
+extern uint8_t TriforcePieces;
+
// Boss Keys
extern bool BossKeyForestTemple;
extern bool BossKeyFireTemple;
@@ -298,6 +301,7 @@ extern bool AtDay;
extern bool AtNight;
extern bool LinksCow;
extern uint8_t Age;
+extern bool CanCompleteTriforce;
// Events
extern bool ShowedMidoSwordAndShield;
diff --git a/soh/soh/Enhancements/randomizer/3drando/settings.cpp b/soh/soh/Enhancements/randomizer/3drando/settings.cpp
index 286f249fc..42fdae5c1 100644
--- a/soh/soh/Enhancements/randomizer/3drando/settings.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/settings.cpp
@@ -101,6 +101,9 @@ namespace Settings {
Option BombchusInLogic = Option::Bool("Bombchus in Logic", {"Off", "On"});
Option AmmoDrops = Option::U8 ("Ammo Drops", {"On", "On + Bombchu", "Off"}, OptionCategory::Setting, AMMODROPS_BOMBCHU);
Option HeartDropRefill = Option::U8 ("Heart Drops and Refills",{"On", "No Drop", "No Refill", "Off"}, OptionCategory::Setting, HEARTDROPREFILL_VANILLA);
+ Option TriforceHunt = Option::U8 ("Triforce Hunt", {"Off", "On"});
+ Option TriforceHuntTotal = Option::U8 ("Triforce Hunt Total Pieces", {NumOpts(0, 100)});
+ Option TriforceHuntRequired = Option::U8 ("Triforce Hunt Required Pieces", {NumOpts(0, 100)});
Option MQDungeonCount = Option::U8(
"MQ Dungeon Count", { MultiVecOpts({ NumOpts(0, 12), { "Random" }, { "Selection" } }) });
uint8_t MQSet;
@@ -139,6 +142,9 @@ namespace Settings {
&BombchusInLogic,
&AmmoDrops,
&HeartDropRefill,
+ &TriforceHunt,
+ &TriforceHuntTotal,
+ &TriforceHuntRequired,
&MQDungeonCount,
&SetDungeonTypes,
&MQDeku,
@@ -219,7 +225,7 @@ namespace Settings {
Option Keysanity = Option::U8 ("Small Keys", {"Start With", "Vanilla", "Own Dungeon", "Any Dungeon", "Overworld", "Anywhere"}, OptionCategory::Setting, KEYSANITY_OWN_DUNGEON);
Option GerudoKeys = Option::U8 ("Gerudo Fortress Keys", {"Vanilla", "Any Dungeon", "Overworld", "Anywhere"});
Option BossKeysanity = Option::U8 ("Boss Keys", {"Start With", "Vanilla", "Own Dungeon", "Any Dungeon", "Overworld", "Anywhere"}, OptionCategory::Setting, BOSSKEYSANITY_OWN_DUNGEON);
- Option GanonsBossKey = Option::U8 ("Ganon's Boss Key", {"Vanilla", "Own dungeon", "Start with", "Any Dungeon", "Overworld", "Anywhere", "LACS-Vanilla", "LACS-Stones", "LACS-Medallions", "LACS-Rewards", "LACS-Dungeons", "LACS-Tokens", "100 GS Reward"}, OptionCategory::Setting, GANONSBOSSKEY_VANILLA);
+ Option GanonsBossKey = Option::U8 ("Ganon's Boss Key", {"Vanilla", "Own dungeon", "Start with", "Any Dungeon", "Overworld", "Anywhere", "LACS-Vanilla", "LACS-Stones", "LACS-Medallions", "LACS-Rewards", "LACS-Dungeons", "LACS-Tokens", "100 GS Reward", "Triforce Hunt"}, OptionCategory::Setting, GANONSBOSSKEY_VANILLA);
uint8_t LACSCondition = 0;
Option LACSStoneCount = Option::U8 ("Stone Count", {NumOpts(0, 4)}, OptionCategory::Setting, 1, true);
Option LACSMedallionCount = Option::U8 ("Medallion Count", {NumOpts(0, 7)}, OptionCategory::Setting, 1, true);
@@ -1685,6 +1691,13 @@ namespace Settings {
IncludeAndHide({KAK_100_GOLD_SKULLTULA_REWARD});
}
+ //Force include Triforce Hunt if it's off
+ if (TriforceHunt) {
+ Unhide({ TRIFORCE_COMPLETED });
+ } else {
+ IncludeAndHide({ TRIFORCE_COMPLETED });
+ }
+
//Force include Map and Compass Chests when Vanilla
std::vector mapChests = GetLocations(everyPossibleLocation, Category::cVanillaMap);
std::vector compassChests = GetLocations(everyPossibleLocation, Category::cVanillaCompass);
@@ -2365,7 +2378,11 @@ namespace Settings {
Keysanity.SetSelectedIndex(cvarSettings[RSK_KEYSANITY]);
GerudoKeys.SetSelectedIndex(cvarSettings[RSK_GERUDO_KEYS]);
BossKeysanity.SetSelectedIndex(cvarSettings[RSK_BOSS_KEYSANITY]);
- GanonsBossKey.SetSelectedIndex(cvarSettings[RSK_GANONS_BOSS_KEY]);
+ if (cvarSettings[RSK_TRIFORCE_HUNT]) {
+ GanonsBossKey.SetSelectedIndex(RO_GANON_BOSS_KEY_TRIFORCE_HUNT);
+ } else {
+ GanonsBossKey.SetSelectedIndex(cvarSettings[RSK_GANONS_BOSS_KEY]);
+ }
LACSStoneCount.SetSelectedIndex(cvarSettings[RSK_LACS_STONE_COUNT]);
LACSMedallionCount.SetSelectedIndex(cvarSettings[RSK_LACS_MEDALLION_COUNT]);
LACSRewardCount.SetSelectedIndex(cvarSettings[RSK_LACS_REWARD_COUNT]);
@@ -2470,6 +2487,10 @@ namespace Settings {
}
}
+ TriforceHunt.SetSelectedIndex(cvarSettings[RSK_TRIFORCE_HUNT]);
+ TriforceHuntTotal.SetSelectedIndex(cvarSettings[RSK_TRIFORCE_HUNT_PIECES_TOTAL]);
+ TriforceHuntRequired.SetSelectedIndex(cvarSettings[RSK_TRIFORCE_HUNT_PIECES_REQUIRED]);
+
//Set key ring for each dungeon
for (size_t i = 0; i < dungeons.size(); i++) {
dungeons[i]->ClearKeyRing();
diff --git a/soh/soh/Enhancements/randomizer/3drando/settings.hpp b/soh/soh/Enhancements/randomizer/3drando/settings.hpp
index dcb9f4e2c..0d3d3c898 100644
--- a/soh/soh/Enhancements/randomizer/3drando/settings.hpp
+++ b/soh/soh/Enhancements/randomizer/3drando/settings.hpp
@@ -91,6 +91,11 @@ typedef enum {
LACS_OPTION_WILDCARD,
} LACSRewardOptionsSetting;
+typedef enum {
+ TRIFORCE_HUNT_OFF,
+ TRIFORCE_HUNT_ON,
+} TriforceHuntSetting;
+
typedef enum {
AGE_CHILD,
AGE_ADULT,
@@ -249,6 +254,7 @@ typedef enum {
GANONSBOSSKEY_LACS_DUNGEONS,
GANONSBOSSKEY_LACS_TOKENS,
GANONSBOSSKEY_FINAL_GS_REWARD,
+ GANONSBOSSKEY_TRIFORCE_HUNT,
} GanonsBossKeySetting;
typedef enum {
@@ -780,6 +786,9 @@ void UpdateSettings(std::unordered_map cvarSettin
extern Option BombchusInLogic;
extern Option AmmoDrops;
extern Option HeartDropRefill;
+ extern Option TriforceHunt;
+ extern Option TriforceHuntTotal;
+ extern Option TriforceHuntRequired;
extern Option MQDungeonCount;
extern Option SetDungeonTypes;
diff --git a/soh/soh/Enhancements/randomizer/3drando/shops.cpp b/soh/soh/Enhancements/randomizer/3drando/shops.cpp
index ac6755da3..8c47ac976 100644
--- a/soh/soh/Enhancements/randomizer/3drando/shops.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/shops.cpp
@@ -13,7 +13,7 @@ using namespace Settings;
std::vector NonShopItems = {};
-static std::array, 0xD5> trickNameTable; //Table of trick names for ice traps
+static std::array, 0xE0> trickNameTable; // Table of trick names for ice traps
bool initTrickNames = false; //Indicates if trick ice trap names have been initialized yet
//Set vanilla shop item locations before potentially shuffling
@@ -698,6 +698,10 @@ void InitTrickNames() {
Text{"Crystal Heart", "Cœur de cristal", "Corazón de cristal"},
Text{"Life Heart", "Cœur de vie", "Vida Corazón"},
Text{"Lots of Love", "Beaucoup d'amour", "Mucho amor"}};
+ trickNameTable[0xDF] = {
+ Text{"Piece of Cheese", "Morceau de Fromage", "Piece of Cheese"},
+ Text{"Triforce Shard", "Éclat de Triforce", "Triforce Shard"},
+ Text{"Shiny Rock", "Caiiloux Brillant", "Shiny Rock"}};
/*
//Names for individual upgrades, in case progressive names are replaced
diff --git a/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp b/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp
index 26ea673bc..77b11203e 100644
--- a/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp
+++ b/soh/soh/Enhancements/randomizer/3drando/starting_inventory.cpp
@@ -54,7 +54,9 @@ void GenerateStartingInventory() {
AddItemToInventory(SHADOW_TEMPLE_BOSS_KEY);
}
- if (GanonsBossKey.Is(GANONSBOSSKEY_START_WITH)) {
+ // Add Ganon's Boss key with Triforce Hunt so the game thinks it's obtainable from the start.
+ // During save init, the boss key isn't actually given and it's instead given when completing the triforce.
+ if (GanonsBossKey.Is(GANONSBOSSKEY_START_WITH) || GanonsBossKey.Is(GANONSBOSSKEY_TRIFORCE_HUNT)) {
AddItemToInventory(GANONS_CASTLE_BOSS_KEY);
}
diff --git a/soh/soh/Enhancements/randomizer/draw.cpp b/soh/soh/Enhancements/randomizer/draw.cpp
index 34c17356c..c2895dd62 100644
--- a/soh/soh/Enhancements/randomizer/draw.cpp
+++ b/soh/soh/Enhancements/randomizer/draw.cpp
@@ -3,17 +3,23 @@
#include "z64.h"
#include "macros.h"
#include "functions.h"
+#include "variables.h"
+#include "soh/OTRGlobals.h"
#include "randomizerTypes.h"
#include
#include "objects/object_gi_key/object_gi_key.h"
#include "objects/object_gi_bosskey/object_gi_bosskey.h"
#include "objects/object_gi_hearts/object_gi_hearts.h"
#include "objects/gameplay_field_keep/gameplay_field_keep.h"
+#include "soh_assets.h"
+
+extern "C" {
+extern SaveContext gSaveContext;
+}
extern "C" u8 Randomizer_GetSettingValue(RandomizerSettingKey randoSettingKey);
extern "C" void Randomizer_DrawSmallKey(PlayState* play, GetItemEntry* getItemEntry) {
- s32 pad;
s8 keysCanBeOutsideDungeon = getItemEntry->getItemId == RG_GERUDO_FORTRESS_SMALL_KEY ?
Randomizer_GetSettingValue(RSK_GERUDO_KEYS) != RO_GERUDO_KEYS_VANILLA :
DUNGEON_ITEMS_CAN_BE_OUTSIDE_DUNGEON(RSK_KEYSANITY);
@@ -53,7 +59,6 @@ extern "C" void Randomizer_DrawSmallKey(PlayState* play, GetItemEntry* getItemEn
}
extern "C" void Randomizer_DrawBossKey(PlayState* play, GetItemEntry* getItemEntry) {
- s32 pad;
s8 keysCanBeOutsideDungeon = getItemEntry->getItemId == RG_GANONS_CASTLE_BOSS_KEY ?
DUNGEON_ITEMS_CAN_BE_OUTSIDE_DUNGEON(RSK_GANONS_BOSS_KEY) :
DUNGEON_ITEMS_CAN_BE_OUTSIDE_DUNGEON(RSK_BOSS_KEYSANITY);
@@ -107,8 +112,6 @@ extern "C" void Randomizer_DrawBossKey(PlayState* play, GetItemEntry* getItemEnt
}
extern "C" void Randomizer_DrawKeyRing(PlayState* play, GetItemEntry* getItemEntry) {
- s32 pad;
-
s16 color_slot = getItemEntry->getItemId - RG_FOREST_TEMPLE_KEY_RING;
s16 colors[9][3] = {
{ 4, 195, 46 }, // Forest Temple
@@ -155,7 +158,6 @@ extern "C" void Randomizer_DrawKeyRing(PlayState* play, GetItemEntry* getItemEnt
}
extern "C" void Randomizer_DrawDoubleDefense(PlayState* play, GetItemEntry getItemEntry) {
- s32 pad;
OPEN_DISPS(play->state.gfxCtx);
Gfx_SetupDL_25Xlu(play->state.gfxCtx);
@@ -173,3 +175,84 @@ extern "C" void Randomizer_DrawDoubleDefense(PlayState* play, GetItemEntry getIt
CLOSE_DISPS(play->state.gfxCtx);
}
+
+Gfx* Randomizer_GetTriforcePieceDL(uint8_t index) {
+ switch (index) {
+ case 1:
+ return (Gfx*)gTriforcePiece1DL;
+ case 2:
+ return (Gfx*)gTriforcePiece2DL;
+ default:
+ return (Gfx*)gTriforcePiece0DL;
+ }
+}
+
+extern "C" void Randomizer_DrawTriforcePiece(PlayState* play, GetItemEntry getItemEntry) {
+ OPEN_DISPS(play->state.gfxCtx);
+
+ Gfx_SetupDL_25Xlu(play->state.gfxCtx);
+
+ uint16_t current = gSaveContext.triforcePiecesCollected;
+
+ Matrix_Scale(0.035f, 0.035f, 0.035f, MTXMODE_APPLY);
+
+ gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx, (char*)__FILE__, __LINE__),
+ G_MTX_MODELVIEW | G_MTX_LOAD);
+
+ Gfx* triforcePieceDL = Randomizer_GetTriforcePieceDL(current % 3);
+
+ gSPDisplayList(POLY_XLU_DISP++, triforcePieceDL);
+
+ CLOSE_DISPS(play->state.gfxCtx);
+}
+
+// Seperate draw function for drawing the Triforce piece when in the GI state.
+// Needed for delaying showing the triforce piece slightly so the triforce shard doesn't
+// suddenly snap to the new piece model or completed triforce because the piece is
+// given mid textbox. Also makes it so the overworld models don't turn into the completed
+// model when the player has exactly the required amount of pieces.
+extern "C" void Randomizer_DrawTriforcePieceGI(PlayState* play, GetItemEntry getItemEntry) {
+ OPEN_DISPS(play->state.gfxCtx);
+
+ Gfx_SetupDL_25Xlu(play->state.gfxCtx);
+
+ uint16_t current = gSaveContext.triforcePiecesCollected;
+ uint16_t required = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED);
+
+ Matrix_Scale(triforcePieceScale, triforcePieceScale, triforcePieceScale, MTXMODE_APPLY);
+
+ // For creating a delay before showing the model so the model doesn't swap visually when the triforce piece
+ // is given when the textbox just appears.
+ if (triforcePieceScale < 0.0001f) {
+ triforcePieceScale += 0.00003f;
+ }
+
+ // Animation. When not the completed triforce, create delay before showing the piece to bypass interpolation.
+ // If the completed triforce, make it grow slowly.
+ if (current != required) {
+ if (triforcePieceScale > 0.00008f && triforcePieceScale < 0.034f) {
+ triforcePieceScale = 0.034f;
+ } else if (triforcePieceScale < 0.035f) {
+ triforcePieceScale += 0.0005f;
+ }
+ } else if (triforcePieceScale > 0.00008f && triforcePieceScale < 0.035f) {
+ triforcePieceScale += 0.0005f;
+ }
+
+ gSPMatrix(POLY_XLU_DISP++, Matrix_NewMtx(play->state.gfxCtx, (char*)__FILE__, __LINE__),
+ G_MTX_MODELVIEW | G_MTX_LOAD);
+
+ // Show piece when not currently completing the triforce. Use the scale to create a delay so interpolation doesn't
+ // make the triforce twitch when the size is set to a higher value.
+ if (current != required && triforcePieceScale > 0.035f) {
+ // Get shard DL. Remove one before division to account for triforce piece given in the textbox
+ // to match up the shard from the overworld model.
+ Gfx* triforcePieceDL = Randomizer_GetTriforcePieceDL((current - 1) % 3);
+
+ gSPDisplayList(POLY_XLU_DISP++, triforcePieceDL);
+ } else if (current == required && triforcePieceScale > 0.00008f) {
+ gSPDisplayList(POLY_XLU_DISP++, (Gfx*)gTriforcePieceCompletedDL);
+ }
+
+ CLOSE_DISPS(play->state.gfxCtx);
+}
diff --git a/soh/soh/Enhancements/randomizer/draw.h b/soh/soh/Enhancements/randomizer/draw.h
index d9f0ed36b..19510c2f4 100644
--- a/soh/soh/Enhancements/randomizer/draw.h
+++ b/soh/soh/Enhancements/randomizer/draw.h
@@ -6,10 +6,18 @@
typedef struct PlayState PlayState;
-extern "C" void Randomizer_DrawSmallKey(PlayState* play, GetItemEntry* getItemEntry);
-extern "C" void Randomizer_DrawKeyRing(PlayState* play, GetItemEntry* getItemEntry);
-extern "C" void Randomizer_DrawBossKey(PlayState* play, GetItemEntry* getItemEntry);
-extern "C" void Randomizer_DrawDoubleDefense(PlayState* play, GetItemEntry getItemEntry);
-extern "C" void Randomizer_DrawIceTrap(PlayState* play, GetItemEntry getItemEntry);
+#ifdef __cplusplus
+extern "C" {
+#endif
+void Randomizer_DrawSmallKey(PlayState* play, GetItemEntry* getItemEntry);
+void Randomizer_DrawKeyRing(PlayState* play, GetItemEntry* getItemEntry);
+void Randomizer_DrawBossKey(PlayState* play, GetItemEntry* getItemEntry);
+void Randomizer_DrawDoubleDefense(PlayState* play, GetItemEntry getItemEntry);
+void Randomizer_DrawTriforcePiece(PlayState* play, GetItemEntry getItemEntry);
+void Randomizer_DrawTriforcePieceGI(PlayState* play, GetItemEntry getItemEntry);
+#ifdef __cplusplus
+};
+#endif
+
#endif
diff --git a/soh/soh/Enhancements/randomizer/randomizer.cpp b/soh/soh/Enhancements/randomizer/randomizer.cpp
index d50c23335..78a9d0525 100644
--- a/soh/soh/Enhancements/randomizer/randomizer.cpp
+++ b/soh/soh/Enhancements/randomizer/randomizer.cpp
@@ -55,6 +55,7 @@ const std::string Randomizer::getItemMessageTableID = "Randomizer";
const std::string Randomizer::hintMessageTableID = "RandomizerHints";
const std::string Randomizer::merchantMessageTableID = "RandomizerMerchants";
const std::string Randomizer::rupeeMessageTableID = "RandomizerRupees";
+const std::string Randomizer::triforcePieceMessageTableID = "RandomizerTriforcePiece";
const std::string Randomizer::NaviRandoMessageTableID = "RandomizerNavi";
const std::string Randomizer::IceTrapRandoMessageTableID = "RandomizerIceTrap";
const std::string Randomizer::randoMiscHintsTableID = "RandomizerMiscHints";
@@ -320,6 +321,9 @@ std::unordered_map SpoilerfileSettingNameToEn
{ "World Settings:Mix Interiors", RSK_MIX_INTERIOR_ENTRANCES },
{ "World Settings:Mix Grottos", RSK_MIX_GROTTO_ENTRANCES },
{ "World Settings:Decouple Entrances", RSK_DECOUPLED_ENTRANCES },
+ { "World Settings:Triforce Hunt", RSK_TRIFORCE_HUNT },
+ { "World Settings:Triforce Hunt Total Pieces", RSK_TRIFORCE_HUNT_PIECES_TOTAL },
+ { "World Settings:Triforce Hunt Required Pieces", RSK_TRIFORCE_HUNT_PIECES_REQUIRED },
{ "Misc Settings:Gossip Stone Hints", RSK_GOSSIP_STONE_HINTS },
{ "Misc Settings:Hint Clarity", RSK_HINT_CLARITY },
{ "Misc Settings:ToT Altar Hint", RSK_TOT_ALTAR_HINT },
@@ -799,6 +803,8 @@ void Randomizer::ParseRandomizerSettingsFile(const char* spoilerFileName) {
case RSK_BIG_POE_COUNT:
case RSK_CUCCO_COUNT:
case RSK_STARTING_SKULLTULA_TOKEN:
+ case RSK_TRIFORCE_HUNT_PIECES_TOTAL:
+ case RSK_TRIFORCE_HUNT_PIECES_REQUIRED:
numericValueString = it.value();
gSaveContext.randoSettings[index].value = std::stoi(numericValueString);
break;
@@ -908,6 +914,7 @@ void Randomizer::ParseRandomizerSettingsFile(const char* spoilerFileName) {
case RSK_DECOUPLED_ENTRANCES:
case RSK_SHOPSANITY_PRICES_AFFORDABLE:
case RSK_ALL_LOCATIONS_REACHABLE:
+ case RSK_TRIFORCE_HUNT:
if(it.value() == "Off") {
gSaveContext.randoSettings[index].value = RO_GENERIC_OFF;
} else if(it.value() == "On") {
@@ -1061,6 +1068,8 @@ void Randomizer::ParseRandomizerSettingsFile(const char* spoilerFileName) {
gSaveContext.randoSettings[index].value = RO_GANON_BOSS_KEY_LACS_TOKENS;
} else if(it.value() == "100 GS Reward") {
gSaveContext.randoSettings[index].value = RO_GANON_BOSS_KEY_KAK_TOKENS;
+ } else if(it.value() == "Triforce Hunt") {
+ gSaveContext.randoSettings[index].value = RO_GANON_BOSS_KEY_TRIFORCE_HUNT;
}
break;
case RSK_RANDOM_MQ_DUNGEONS:
@@ -1980,6 +1989,7 @@ ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerGet(RandomizerGe
case RG_BUY_DEKU_NUT_10:
case RG_BUY_DEKU_STICK_1:
case RG_BUY_HEART:
+ case RG_TRIFORCE_PIECE:
default:
return CAN_OBTAIN;
}
@@ -2990,6 +3000,10 @@ void GenerateRandomizerImgui(std::string seed = "") {
cvarSettings[RSK_MQ_DUNGEON_COUNT] = 0;
}
+ cvarSettings[RSK_TRIFORCE_HUNT] = CVarGetInteger("gRandomizeTriforceHunt", 0);
+ cvarSettings[RSK_TRIFORCE_HUNT_PIECES_TOTAL] = CVarGetInteger("gRandomizeTriforceHuntTotalPieces", 30);
+ cvarSettings[RSK_TRIFORCE_HUNT_PIECES_REQUIRED] = CVarGetInteger("gRandomizeTriforceHuntRequiredPieces", 20);
+
cvarSettings[RSK_MQ_DEKU_TREE] = CVarGetInteger("gRandomizeMqDungeonsDekuTree", 0);
cvarSettings[RSK_MQ_DODONGOS_CAVERN] = CVarGetInteger("gRandomizeMqDungeonsDodongosCavern", 0);
cvarSettings[RSK_MQ_JABU_JABU] = CVarGetInteger("gRandomizeMqDungeonsJabuJabu", 0);
@@ -3502,6 +3516,7 @@ void RandomizerSettingsWindow::DrawElement() {
UIWidgets::PaddedSeparator();
+ // Master Quest Dungeons
if (OTRGlobals::Instance->HasMasterQuest() && OTRGlobals::Instance->HasOriginal()) {
ImGui::PushItemWidth(-FLT_MIN);
ImGui::Text("Master Quest Dungeons");
@@ -3549,8 +3564,43 @@ void RandomizerSettingsWindow::DrawElement() {
UIWidgets::EnhancementCheckbox("Ganon's Castle##RandomizeMqDungeons",
"gRandomizeMqDungeonsGanonsCastle");
}
+
+ UIWidgets::PaddedSeparator();
}
+ // Triforce Hunt
+ UIWidgets::EnhancementCheckbox("Triforce Hunt", "gRandomizeTriforceHunt");
+ UIWidgets::InsertHelpHoverText(
+ "Pieces of the Triforce of Courage have been scattered across the world. Find them all to finish the game!\n\n"
+ "When the required amount of pieces have been found, the game is saved and Ganon's Boss key is given "
+ "to you when you load back into the game if you desire to beat Ganon afterwards.\n\n"
+ "Keep in mind Ganon might not be logically beatable when \"All Locations Reachable\" is turned off."
+ );
+
+ if (CVarGetInteger("gRandomizeTriforceHunt", 0)) {
+ // Triforce Hunt (total pieces)
+ UIWidgets::Spacer(0);
+ int totalPieces = CVarGetInteger("gRandomizeTriforceHuntTotalPieces", 30);
+ ImGui::Text("Triforce Pieces in the world: %d", totalPieces);
+ UIWidgets::InsertHelpHoverText(
+ "The amount of Triforce pieces that will be placed in the world. "
+ "Keep in mind seed generation can fail if more pieces are placed than there are junk items in the item pool."
+ );
+ ImGui::SameLine();
+ UIWidgets::EnhancementSliderInt("", "##TriforceHuntTotalPieces", "gRandomizeTriforceHuntTotalPieces", 1, 100, "", 30);
+
+ // Triforce Hunt (required pieces)
+ int requiredPieces = CVarGetInteger("gRandomizeTriforceHuntRequiredPieces", 20);
+ ImGui::Text("Triforce Pieces to win: %d", requiredPieces);
+ UIWidgets::InsertHelpHoverText(
+ "The amount of Triforce pieces required to win the game."
+ );
+ ImGui::SameLine();
+ UIWidgets::EnhancementSliderInt("", "##TriforceHuntRequiredPieces", "gRandomizeTriforceHuntRequiredPieces", 1, totalPieces, "", 20);
+ }
+
+ UIWidgets::PaddedSeparator();
+
ImGui::EndChild();
// COLUMN 3 - Shuffle Entrances
@@ -4144,7 +4194,11 @@ void RandomizerSettingsWindow::DrawElement() {
"\n"
"100 GS Reward - Ganon's Boss Key will be awarded by the cursed rich man after you collect 100 Gold Skulltula Tokens."
);
- UIWidgets::EnhancementCombobox("gRandomizeShuffleGanonBossKey", randoShuffleGanonsBossKey, RO_GANON_BOSS_KEY_VANILLA);
+ bool disableGBK = CVarGetInteger("gRandomizeTriforceHunt", 0);
+ static const char* disableGBKText = "This option is disabled because Triforce Hunt is enabled. Ganon's Boss key\nwill instead be given to you after Triforce Hunt completion.";
+ UIWidgets::EnhancementCombobox("gRandomizeShuffleGanonBossKey", randoShuffleGanonsBossKey,
+ RO_GANON_BOSS_KEY_VANILLA, disableGBK, disableGBKText,
+ RO_GANON_BOSS_KEY_VANILLA);
ImGui::PopItemWidth();
switch (CVarGetInteger("gRandomizeShuffleGanonBossKey", RO_GANON_BOSS_KEY_VANILLA)) {
case RO_GANON_BOSS_KEY_LACS_STONES:
@@ -5461,6 +5515,70 @@ CustomMessage Randomizer::GetRupeeMessage(u16 rupeeTextId) {
return messageEntry;
}
+void CreateTriforcePieceMessages() {
+ CustomMessage TriforcePieceMessages[NUM_TRIFORCE_PIECE_MESSAGES] = {
+
+ { "You found a %yTriforce Piece%w!&%g{{current}}%w down, %c{{remaining}}%w to go. It's a start!",
+ "Ein %yTriforce-Splitter%w! Du hast&%g{{current}}%w von %c{{required}}%w gefunden. Es ist ein&Anfang!",
+ "Vous trouvez un %yFragment de la&Triforce%w! Vous en avez %g{{current}}%w, il en&reste %c{{remaining}}%w à trouver. C'est un début!" },
+
+ { "You found a %yTriforce Piece%w!&%g{{current}}%w down, %c{{remaining}}%w to go. Progress!",
+ "Ein %yTriforce-Splitter%w! Du hast&%g{{current}}%w von %c{{required}}%w gefunden. Es geht voran!",
+ "Vous trouvez un %yFragment de la&Triforce%w! Vous en avez %g{{current}}%w, il en&reste %c{{remaining}}%w à trouver. Ça avance!" },
+
+ { "You found a %yTriforce Piece%w!&%g{{current}}%w down, %c{{remaining}}%w to go. Over half-way&there!",
+ "Ein %yTriforce-Splitter%w! Du hast&schon %g{{current}}%w von %c{{required}}%w gefunden. Schon&über die Hälfte!",
+ "Vous trouvez un %yFragment de la&Triforce%w! Vous en avez %g{{current}}%w, il en&reste %c{{remaining}}%w à trouver. Il en reste un&peu moins que la moitié!" },
+
+ { "You found a %yTriforce Piece%w!&%g{{current}}%w down, %c{{remaining}}%w to go. Almost done!",
+ "Ein %yTriforce-Splitter%w! Du hast&schon %g{{current}}%w von %c{{required}}%w gefunden. Fast&geschafft!",
+ "Vous trouvez un %yFragment de la&Triforce%w! Vous en avez %g{{current}}%w, il en&reste %c{{remaining}}%w à trouver. C'est presque&terminé!" },
+
+ { "You completed the %yTriforce of&Courage%w! %gGG%w!",
+ "Das %yTriforce des Mutes%w! Du hast&alle Splitter gefunden. %gGut gemacht%w!",
+ "Vous avez complété la %yTriforce&du Courage%w! %gFélicitations%w!" },
+
+ { "You found a spare %yTriforce Piece%w!&You only needed %c{{required}}%w, but you have %g{{current}}%w!",
+ "Ein übriger %yTriforce-Splitter%w! Du&hast nun %g{{current}}%w von %c{{required}}%w nötigen gefunden.",
+ "Vous avez trouvé un %yFragment de&Triforce%w en plus! Vous n'aviez besoin&que de %c{{required}}%w, mais vous en avez %g{{current}}%w en&tout!" },
+ };
+ CustomMessageManager* customMessageManager = CustomMessageManager::Instance;
+ customMessageManager->AddCustomMessageTable(Randomizer::triforcePieceMessageTableID);
+ for (unsigned int i = 0; i <= (NUM_TRIFORCE_PIECE_MESSAGES - 1); i++) {
+ customMessageManager->CreateMessage(Randomizer::triforcePieceMessageTableID, i, TriforcePieceMessages[i]);
+ }
+}
+
+CustomMessage Randomizer::GetTriforcePieceMessage() {
+ // Item is only given after the textbox, so reflect that inside the textbox.
+ uint16_t current = gSaveContext.triforcePiecesCollected + 1;
+ uint16_t required = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED);
+ uint16_t remaining = required - current;
+ float percentageCollected = (float)current / (float)required;
+ uint8_t messageIndex;
+
+ if (percentageCollected <= 0.25) {
+ messageIndex = TH_MESSAGE_START;
+ } else if (percentageCollected <= 0.5) {
+ messageIndex = TH_MESSAGE_PROGRESS;
+ } else if (percentageCollected <= 0.75) {
+ messageIndex = TH_MESSAGE_HALFWAY;
+ } else if (percentageCollected < 1) {
+ messageIndex = TH_MESSAGE_ALMOSTDONE;
+ } else if (current == required) {
+ messageIndex = TH_MESSAGE_FINISHED;
+ } else {
+ messageIndex = TH_MESSAGE_SURPLUS;
+ }
+
+ CustomMessage messageEntry =
+ CustomMessageManager::Instance->RetrieveMessage(Randomizer::triforcePieceMessageTableID, messageIndex);
+ messageEntry.Replace("{{current}}", std::to_string(current), std::to_string(current), std::to_string(current));
+ messageEntry.Replace("{{remaining}}", std::to_string(remaining), std::to_string(remaining), std::to_string(remaining));
+ messageEntry.Replace("{{required}}", std::to_string(required), std::to_string(required), std::to_string(required));
+ return messageEntry;
+}
+
void CreateNaviRandoMessages() {
CustomMessage NaviMessages[NUM_NAVI_MESSAGES] = {
@@ -5988,6 +6106,7 @@ void Randomizer::CreateCustomMessages() {
}};
CreateGetItemMessages(&getItemMessages);
CreateRupeeMessages();
+ CreateTriforcePieceMessages();
CreateNaviRandoMessages();
CreateIceTrapRandoMessages();
CreateFireTempleGoronMessages();
@@ -6098,6 +6217,7 @@ void InitRandoItemTable() {
GET_ITEM(RG_MAGIC_BEAN_PACK, OBJECT_GI_BEAN, GID_BEAN, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, RG_MAGIC_BEAN_PACK),
GET_ITEM(RG_TYCOON_WALLET, OBJECT_GI_PURSE, GID_WALLET_GIANT, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_LESSER, MOD_RANDOMIZER, RG_TYCOON_WALLET),
GET_ITEM(RG_PROGRESSIVE_BOMBCHUS, OBJECT_GI_BOMB_2, GID_BOMBCHU, 0x33, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, RG_PROGRESSIVE_BOMBCHUS),
+ GET_ITEM(RG_TRIFORCE_PIECE, OBJECT_GI_BOMB_2, GID_TRIFORCE_PIECE, TEXT_RANDOMIZER_CUSTOM_ITEM, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_MAJOR, MOD_RANDOMIZER, RG_TRIFORCE_PIECE),
};
ItemTableManager::Instance->AddItemTable(MOD_RANDOMIZER);
for (int i = 0; i < ARRAY_COUNT(extendedVanillaGetItemTable); i++) {
@@ -6113,6 +6233,8 @@ void InitRandoItemTable() {
randoGetItemTable[i].drawFunc = (CustomDrawFunc)Randomizer_DrawBossKey;
} else if (randoGetItemTable[i].itemId == RG_DOUBLE_DEFENSE) {
randoGetItemTable[i].drawFunc = (CustomDrawFunc)Randomizer_DrawDoubleDefense;
+ } else if (randoGetItemTable[i].itemId == RG_TRIFORCE_PIECE) {
+ randoGetItemTable[i].drawFunc = (CustomDrawFunc)Randomizer_DrawTriforcePiece;
}
ItemTableManager::Instance->AddItemEntry(MOD_RANDOMIZER, randoGetItemTable[i].itemId, randoGetItemTable[i]);
}
diff --git a/soh/soh/Enhancements/randomizer/randomizer.h b/soh/soh/Enhancements/randomizer/randomizer.h
index c8a333593..82347d994 100644
--- a/soh/soh/Enhancements/randomizer/randomizer.h
+++ b/soh/soh/Enhancements/randomizer/randomizer.h
@@ -14,6 +14,7 @@
#include "soh/Enhancements/item-tables/ItemTableTypes.h"
#define MAX_SEED_STRING_SIZE 1024
+#define NUM_TRIFORCE_PIECE_MESSAGES 6
#define NUM_NAVI_MESSAGES 19
#define NUM_ICE_TRAP_MESSAGES 23
#define NUM_GORON_MESSAGES 9
@@ -48,6 +49,7 @@ class Randomizer {
static const std::string hintMessageTableID;
static const std::string merchantMessageTableID;
static const std::string rupeeMessageTableID;
+ static const std::string triforcePieceMessageTableID;
static const std::string NaviRandoMessageTableID;
static const std::string IceTrapRandoMessageTableID;
static const std::string randoMiscHintsTableID;
@@ -102,6 +104,7 @@ class Randomizer {
CustomMessage GetMapGetItemMessageWithHint(GetItemEntry itemEntry);
static void CreateCustomMessages();
static CustomMessage GetRupeeMessage(u16 rupeeTextId);
+ static CustomMessage GetTriforcePieceMessage();
bool CheckContainsVanillaItem(RandomizerCheck randoCheck);
};
diff --git a/soh/soh/Enhancements/randomizer/randomizerTypes.h b/soh/soh/Enhancements/randomizer/randomizerTypes.h
index f80ef7595..9d77a9a29 100644
--- a/soh/soh/Enhancements/randomizer/randomizerTypes.h
+++ b/soh/soh/Enhancements/randomizer/randomizerTypes.h
@@ -847,6 +847,7 @@ typedef enum {
RC_ZR_NEAR_GROTTOS_GOSSIP_STONE,
RC_ZR_OPEN_GROTTO_GOSSIP_STONE,
RC_GANONDORF_HINT,
+ RC_TRIFORCE_COMPLETED,
RC_MAX
} RandomizerCheck;
@@ -1302,6 +1303,7 @@ typedef enum {
RG_BUY_RED_POTION_40,
RG_BUY_RED_POTION_50,
RG_TRIFORCE,
+ RG_TRIFORCE_PIECE,
RG_HINT,
RG_TYCOON_WALLET,
RG_MAX
@@ -1453,6 +1455,9 @@ typedef enum {
RSK_ALL_LOCATIONS_REACHABLE,
RSK_SHUFFLE_BOSS_ENTRANCES,
RSK_SHUFFLE_100_GS_REWARD,
+ RSK_TRIFORCE_HUNT,
+ RSK_TRIFORCE_HUNT_PIECES_TOTAL,
+ RSK_TRIFORCE_HUNT_PIECES_REQUIRED,
RSK_MAX
} RandomizerSettingKey;
@@ -1613,6 +1618,7 @@ typedef enum {
RO_GANON_BOSS_KEY_LACS_DUNGEONS,
RO_GANON_BOSS_KEY_LACS_TOKENS,
RO_GANON_BOSS_KEY_KAK_TOKENS,
+ RO_GANON_BOSS_KEY_TRIFORCE_HUNT,
} RandoOptionGanonsBossKey;
// LACS Reward Options settings (Standard rewards, Greg as reward, Greg as wildcard)
@@ -1806,3 +1812,12 @@ typedef enum {
TRACKER_COMBO_BUTTON_D_LEFT,
TRACKER_COMBO_BUTTON_D_RIGHT,
} TrackerComboButton;
+
+typedef enum {
+ TH_MESSAGE_START,
+ TH_MESSAGE_PROGRESS,
+ TH_MESSAGE_HALFWAY,
+ TH_MESSAGE_ALMOSTDONE,
+ TH_MESSAGE_FINISHED,
+ TH_MESSAGE_SURPLUS,
+} TriforceHuntMessages;
diff --git a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp
index 884f70071..bfb44c6c7 100644
--- a/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp
+++ b/soh/soh/Enhancements/randomizer/randomizer_item_tracker.cpp
@@ -80,6 +80,10 @@ std::vector gregItems = {
ITEM_TRACKER_ITEM(ITEM_RUPEE_GREEN, 0, DrawItem),
};
+std::vector triforcePieces = {
+ ITEM_TRACKER_ITEM(RG_TRIFORCE_PIECE, 0, DrawItem),
+};
+
std::vector itemTrackerDungeonsWithMapsHorizontal = {
{ SCENE_DEKU_TREE, { ITEM_DUNGEON_MAP, ITEM_COMPASS } },
{ SCENE_DODONGOS_CAVERN, { ITEM_DUNGEON_MAP, ITEM_COMPASS } },
@@ -243,6 +247,11 @@ typedef enum {
KEYS_CURRENT_MAX
} ItemTrackerKeysNumberOption;
+typedef enum {
+ TRIFORCE_PIECE_COLLECTED_REQUIRED,
+ TRIFORCE_PIECE_COLLECTED_REQUIRED_MAX
+} ItemTrackerTriforcePieceNumberOption;
+
typedef enum {
SECTION_DISPLAY_HIDDEN,
SECTION_DISPLAY_MAIN_WINDOW,
@@ -488,6 +497,37 @@ void DrawItemCount(ItemTrackerItem item) {
ImGui::PushStyleColor(ImGuiCol_Text, maxColor);
ImGui::Text("%s", maxString.c_str());
ImGui::PopStyleColor();
+ } else if (item.id == RG_TRIFORCE_PIECE && gSaveContext.n64ddFlag &&
+ OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT) && IsValidSaveFile()) {
+ std::string currentString = "";
+ std::string requiredString = "";
+ std::string maxString = "";
+ uint8_t piecesRequired = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED);
+ uint8_t piecesTotal = OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT_PIECES_TOTAL);
+ ImU32 currentColor = gSaveContext.triforcePiecesCollected >= piecesRequired ? IM_COL_GREEN : IM_COL_WHITE;
+ ImU32 maxColor = IM_COL_GREEN;
+ int32_t trackerTriforcePieceNumberDisplayMode = CVarGetInteger("gItemTrackerTriforcePieceTrack", TRIFORCE_PIECE_COLLECTED_REQUIRED_MAX);
+
+ currentString += std::to_string(gSaveContext.triforcePiecesCollected);
+ currentString += "/";
+ // gItemTrackerTriforcePieceTrack
+ if (trackerTriforcePieceNumberDisplayMode == TRIFORCE_PIECE_COLLECTED_REQUIRED_MAX) {
+ currentString += std::to_string(piecesRequired);
+ currentString += "/";
+ maxString += std::to_string(piecesTotal);
+ } else if (trackerTriforcePieceNumberDisplayMode == TRIFORCE_PIECE_COLLECTED_REQUIRED) {
+ maxString += std::to_string(piecesRequired);
+ }
+
+ ImGui::SetCursorScreenPos(
+ ImVec2(p.x + (iconSize / 2) - (ImGui::CalcTextSize((currentString + maxString).c_str()).x / 2), p.y - 14));
+ ImGui::PushStyleColor(ImGuiCol_Text, currentColor);
+ ImGui::Text(currentString.c_str());
+ ImGui::PopStyleColor();
+ ImGui::SameLine(0, 0.0f);
+ ImGui::PushStyleColor(ImGuiCol_Text, maxColor);
+ ImGui::Text(maxString.c_str());
+ ImGui::PopStyleColor();
} else {
ImGui::SetCursorScreenPos(ImVec2(p.x, p.y - 14));
ImGui::Text("");
@@ -520,9 +560,11 @@ void DrawQuest(ItemTrackerItem item) {
};
void DrawItem(ItemTrackerItem item) {
+
uint32_t actualItemId = INV_CONTENT(item.id);
int iconSize = CVarGetInteger("gItemTrackerIconSize", 36);
bool hasItem = actualItemId != ITEM_NONE;
+ std::string itemName = "";
if (item.id == ITEM_NONE) {
return;
@@ -562,6 +604,11 @@ void DrawItem(ItemTrackerItem item) {
actualItemId = item.id;
hasItem = Flags_GetRandomizerInf(RAND_INF_GREG_FOUND);
break;
+ case RG_TRIFORCE_PIECE:
+ actualItemId = item.id;
+ hasItem = gSaveContext.n64ddFlag && OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_TRIFORCE_HUNT);
+ itemName = "Triforce Piece";
+ break;
}
if (hasItem && item.id != actualItemId && actualItemTrackerItemMap.find(actualItemId) != actualItemTrackerItemMap.end()) {
@@ -569,13 +616,18 @@ void DrawItem(ItemTrackerItem item) {
}
ImGui::BeginGroup();
+
ImGui::Image(LUS::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(hasItem && IsValidSaveFile() ? item.name : item.nameFaded),
ImVec2(iconSize, iconSize), ImVec2(0, 0), ImVec2(1, 1));
-
+
DrawItemCount(item);
ImGui::EndGroup();
- UIWidgets::SetLastItemHoverText(SohUtils::GetItemName(item.id));
+ if (itemName == "") {
+ itemName = SohUtils::GetItemName(item.id);
+ }
+
+ UIWidgets::SetLastItemHoverText(itemName);
}
void DrawBottle(ItemTrackerItem item) {
@@ -891,6 +943,19 @@ void UpdateVectors() {
mainWindowItems.insert(mainWindowItems.end(), gregItems.begin(), gregItems.end());
}
+ // If we're adding triforce pieces to the main window
+ if (CVarGetInteger("gItemTrackerTriforcePiecesDisplayType", SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_MAIN_WINDOW) {
+ // If Greg isn't on the main window, add empty items to place the triforce pieces on a new row.
+ if (CVarGetInteger("gItemTrackerGregDisplayType", SECTION_DISPLAY_EXTENDED_HIDDEN) != SECTION_DISPLAY_EXTENDED_MAIN_WINDOW) {
+ while (mainWindowItems.size() % 6) {
+ mainWindowItems.push_back(ITEM_TRACKER_ITEM(ITEM_NONE, 0, DrawItem));
+ }
+ }
+
+ // Add triforce pieces
+ mainWindowItems.insert(mainWindowItems.end(), triforcePieces.begin(), triforcePieces.end());
+ }
+
shouldUpdateVectors = false;
}
@@ -914,6 +979,7 @@ void ItemTrackerWindow::DrawElement() {
(CVarGetInteger("gItemTrackerSongsDisplayType", SECTION_DISPLAY_MAIN_WINDOW) == SECTION_DISPLAY_MAIN_WINDOW) ||
(CVarGetInteger("gItemTrackerDungeonItemsDisplayType", SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_MAIN_WINDOW) ||
(CVarGetInteger("gItemTrackerGregDisplayType", SECTION_DISPLAY_EXTENDED_HIDDEN) == SECTION_DISPLAY_EXTENDED_MAIN_WINDOW) ||
+ (CVarGetInteger("gItemTrackerTriforcePiecesDisplayType", SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_MAIN_WINDOW) ||
(CVarGetInteger("gItemTrackerNotesDisplayType", SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_MAIN_WINDOW)
) {
BeginFloatingWindows("Item Tracker##main window");
@@ -984,6 +1050,12 @@ void ItemTrackerWindow::DrawElement() {
EndFloatingWindows();
}
+ if (CVarGetInteger("gItemTrackerTriforcePiecesDisplayType", SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_SEPARATE) {
+ BeginFloatingWindows("Triforce Piece Tracker");
+ DrawItemsInRows(triforcePieces);
+ EndFloatingWindows();
+ }
+
if (CVarGetInteger("gItemTrackerNotesDisplayType", SECTION_DISPLAY_HIDDEN) == SECTION_DISPLAY_SEPARATE && CVarGetInteger("gItemTrackerDisplayType", TRACKER_DISPLAY_ALWAYS) == TRACKER_DISPLAY_ALWAYS) {
ImGui::SetNextWindowSize(ImVec2(400,300), ImGuiCond_FirstUseEver);
BeginFloatingWindows("Personal Notes", ImGuiWindowFlags_NoFocusOnAppearing);
@@ -995,6 +1067,7 @@ void ItemTrackerWindow::DrawElement() {
static const char* itemTrackerCapacityTrackOptions[5] = { "No Numbers", "Current Capacity", "Current Ammo", "Current Capacity / Max Capacity", "Current Ammo / Current Capacity" };
static const char* itemTrackerKeyTrackOptions[3] = { "Collected / Max", "Current / Collected / Max", "Current / Max" };
+static const char* itemTrackerTriforcePieceTrackOptions[2] = { "Collected / Required", "Collected / Required / Max" };
static const char* windowTypes[2] = { "Floating", "Window" };
static const char* displayModes[2] = { "Always", "Combo Button Hold" };
static const char* buttons[14] = { "A", "B", "C-Up", "C-Down", "C-Left", "C-Right", "L", "Z", "R", "Start", "D-Up", "D-Down", "D-Left", "D-Right" };
@@ -1002,7 +1075,7 @@ static const char* displayTypes[3] = { "Hidden", "Main Window", "Separate" };
static const char* extendedDisplayTypes[4] = { "Hidden", "Main Window", "Misc Window", "Separate" };
void ItemTrackerSettingsWindow::DrawElement() {
- ImGui::SetNextWindowSize(ImVec2(600,375), ImGuiCond_FirstUseEver);
+ ImGui::SetNextWindowSize(ImVec2(733, 472), ImGuiCond_FirstUseEver);
if (!ImGui::Begin("Item Tracker Settings", &mIsVisible, ImGuiWindowFlags_NoFocusOnAppearing)) {
ImGui::End();
@@ -1055,6 +1128,8 @@ void ItemTrackerSettingsWindow::DrawElement() {
UIWidgets::EnhancementSliderInt("Icon size : %dpx", "##ITEMTRACKERICONSIZE", "gItemTrackerIconSize", 25, 128, "", 36);
UIWidgets::EnhancementSliderInt("Icon margins : %dpx", "##ITEMTRACKERSPACING", "gItemTrackerIconSpacing", -5, 50, "", 12);
+ UIWidgets::Spacer(0);
+
ImGui::Text("Ammo/Capacity Tracking");
UIWidgets::EnhancementCombobox("gItemTrackerCapacityTrack", itemTrackerCapacityTrackOptions, ITEM_TRACKER_NUMBER_CURRENT_CAPACITY_ONLY);
UIWidgets::InsertHelpHoverText("Customize what the numbers under each item are tracking."
@@ -1064,10 +1139,19 @@ void ItemTrackerSettingsWindow::DrawElement() {
shouldUpdateVectors = true;
}
}
+
+ UIWidgets::Spacer(0);
+
ImGui::Text("Key Count Tracking");
UIWidgets::EnhancementCombobox("gItemTrackerKeyTrack", itemTrackerKeyTrackOptions, KEYS_COLLECTED_MAX);
UIWidgets::InsertHelpHoverText("Customize what numbers are shown for key tracking.");
+ UIWidgets::Spacer(0);
+
+ ImGui::Text("Triforce Piece Count Tracking");
+ UIWidgets::EnhancementCombobox("gItemTrackerTriforcePieceTrack", itemTrackerTriforcePieceTrackOptions, TRIFORCE_PIECE_COLLECTED_REQUIRED_MAX);
+ UIWidgets::InsertHelpHoverText("Customize what numbers are shown for triforce piece tracking.");
+
ImGui::TableNextColumn();
if (UIWidgets::LabeledRightAlignedEnhancementCombobox("Inventory", "gItemTrackerInventoryItemsDisplayType", displayTypes, SECTION_DISPLAY_MAIN_WINDOW)) {
@@ -1107,6 +1191,10 @@ void ItemTrackerSettingsWindow::DrawElement() {
shouldUpdateVectors = true;
}
+ if (UIWidgets::LabeledRightAlignedEnhancementCombobox("Triforce Pieces", "gItemTrackerTriforcePiecesDisplayType", displayTypes, SECTION_DISPLAY_HIDDEN)) {
+ shouldUpdateVectors = true;
+ }
+
if (CVarGetInteger("gItemTrackerDisplayType", TRACKER_DISPLAY_ALWAYS) == TRACKER_DISPLAY_ALWAYS) {
if (UIWidgets::LabeledRightAlignedEnhancementCombobox("Personal notes", "gItemTrackerNotesDisplayType", displayTypes, SECTION_DISPLAY_HIDDEN)) {
shouldUpdateVectors = true;
diff --git a/soh/soh/Enhancements/randomizer/savefile.cpp b/soh/soh/Enhancements/randomizer/savefile.cpp
index a6984fac0..ed7443910 100644
--- a/soh/soh/Enhancements/randomizer/savefile.cpp
+++ b/soh/soh/Enhancements/randomizer/savefile.cpp
@@ -436,5 +436,8 @@ extern "C" void Randomizer_InitSaveFile() {
gSaveContext.itemGetInf[3] |= 0x8000; // Obtained Mask of Truth
}
+ // Reset triforce pieces collected
+ gSaveContext.triforcePiecesCollected = 0;
+
SetStartingItems();
}
diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp
index 7e1845dd0..a9cf62515 100644
--- a/soh/soh/OTRGlobals.cpp
+++ b/soh/soh/OTRGlobals.cpp
@@ -1975,6 +1975,8 @@ extern "C" int CustomMessage_RetrieveIfExists(PlayState* play) {
if (CVarGetInteger("gLetItSnow", 0)) {
messageEntry = CustomMessageManager::Instance->RetrieveMessage(Randomizer::IceTrapRandoMessageTableID, NUM_ICE_TRAP_MESSAGES + 1);
}
+ } else if (player->getItemEntry.getItemId == RG_TRIFORCE_PIECE) {
+ messageEntry = Randomizer::GetTriforcePieceMessage();
} else {
messageEntry = Randomizer_GetCustomGetItemMessage(player);
}
diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp
index b17b2cbfe..9736a3e22 100644
--- a/soh/soh/SaveManager.cpp
+++ b/soh/soh/SaveManager.cpp
@@ -136,6 +136,8 @@ void SaveManager::LoadRandomizerVersion1() {
SaveManager::Instance->LoadData("adultTradeItems", gSaveContext.adultTradeItems);
+ SaveManager::Instance->LoadData("triforcePiecesCollected", gSaveContext.triforcePiecesCollected);
+
SaveManager::Instance->LoadData("pendingIceTrapCount", gSaveContext.pendingIceTrapCount);
std::shared_ptr randomizer = OTRGlobals::Instance->gRandomizer;
@@ -251,6 +253,8 @@ void SaveManager::LoadRandomizerVersion2() {
SaveManager::Instance->LoadData("adultTradeItems", gSaveContext.adultTradeItems);
+ SaveManager::Instance->LoadData("triforcePiecesCollected", gSaveContext.triforcePiecesCollected);
+
SaveManager::Instance->LoadData("pendingIceTrapCount", gSaveContext.pendingIceTrapCount);
std::shared_ptr randomizer = OTRGlobals::Instance->gRandomizer;
@@ -338,6 +342,8 @@ void SaveManager::SaveRandomizer(SaveContext* saveContext, int sectionID, bool f
SaveManager::Instance->SaveData("adultTradeItems", saveContext->adultTradeItems);
+ SaveManager::Instance->SaveData("triforcePiecesCollected", gSaveContext.triforcePiecesCollected);
+
SaveManager::Instance->SaveData("pendingIceTrapCount", saveContext->pendingIceTrapCount);
std::shared_ptr randomizer = OTRGlobals::Instance->gRandomizer;
diff --git a/soh/src/code/z_draw.c b/soh/src/code/z_draw.c
index 7c6b71c6c..cb28a498e 100644
--- a/soh/src/code/z_draw.c
+++ b/soh/src/code/z_draw.c
@@ -78,6 +78,8 @@
#include "objects/object_gi_sword_1/object_gi_sword_1.h"
#include "objects/object_st/object_st.h"
+#include "soh_assets.h"
+
// "Get Item" Model Draw Functions
void GetItem_DrawMaskOrBombchu(PlayState* play, s16 drawId);
void GetItem_DrawSoldOut(PlayState* play, s16 drawId);
@@ -110,6 +112,7 @@ void GetItem_DrawJewelKokiri(PlayState* play, s16 drawId);
void GetItem_DrawJewelGoron(PlayState* play, s16 drawId);
void GetItem_DrawJewelZora(PlayState* play, s16 drawId);
void GetItem_DrawGenericMusicNote(PlayState* play, s16 drawId);
+void GetItem_DrawTriforcePiece(PlayState* play, s16 drawId);
typedef struct {
/* 0x00 */ void (*drawFunc)(PlayState*, s16);
@@ -384,7 +387,8 @@ DrawItemTableEntry sDrawItemTable[] = {
{ GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, //Saria's song
{ GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, //Sun's song
{ GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, //Song of time
- { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } } //Song of storms
+ { GetItem_DrawGenericMusicNote, { gGiSongNoteDL } }, //Song of storms
+ { GetItem_DrawTriforcePiece, { gTriforcePiece0DL } } // Triforce Piece
};
/**
@@ -1031,3 +1035,33 @@ void GetItem_DrawWallet(PlayState* play, s16 drawId) {
CLOSE_DISPS(play->state.gfxCtx);
}
+
+void GetItem_DrawTriforcePiece(PlayState* play, s16 drawId) {
+ OPEN_DISPS(play->state.gfxCtx);
+
+ Gfx_SetupDL_25Opa(play->state.gfxCtx);
+
+ Matrix_Scale(0.035f, 0.035f, 0.035f, MTXMODE_APPLY);
+
+ uint16_t index = gSaveContext.triforcePiecesCollected % 3;
+ Gfx* triforcePieceDL;
+
+ switch (index) {
+ case 1:
+ triforcePieceDL = (Gfx*) gTriforcePiece1DL;
+ break;
+ case 2:
+ triforcePieceDL = (Gfx*) gTriforcePiece2DL;
+ break;
+ default:
+ triforcePieceDL = (Gfx*) gTriforcePiece0DL;
+ break;
+ }
+
+ gSPMatrix(POLY_OPA_DISP++, Matrix_NewMtx(play->state.gfxCtx, (char*)__FILE__, __LINE__),
+ G_MTX_MODELVIEW | G_MTX_LOAD);
+
+ gSPDisplayList(POLY_OPA_DISP++, triforcePieceDL);
+
+ CLOSE_DISPS(play->state.gfxCtx);
+}
diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c
index 82cb8a6b6..93bf8fbfd 100644
--- a/soh/src/code/z_parameter.c
+++ b/soh/src/code/z_parameter.c
@@ -2528,6 +2528,21 @@ u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry) {
return Return_Item_Entry(giEntry, RG_NONE);
}
+ if (item == RG_TRIFORCE_PIECE) {
+ gSaveContext.triforcePiecesCollected++;
+ GameInteractor_SetTriforceHuntPieceGiven(true);
+
+ // Teleport to credits when goal is reached.
+ if (gSaveContext.triforcePiecesCollected == Randomizer_GetSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED)) {
+ gSaveContext.sohStats.itemTimestamp[TIMESTAMP_TRIFORCE_COMPLETED] = GAMEPLAYSTAT_TOTAL_TIME;
+ gSaveContext.sohStats.gameComplete = 1;
+ Play_PerformSave(play);
+ GameInteractor_SetTriforceHuntCreditsWarpActive(true);
+ }
+
+ return Return_Item_Entry(giEntry, RG_NONE);
+ }
+
if (item == RG_PROGRESSIVE_BOMBCHUS) {
if (INV_CONTENT(ITEM_BOMBCHU) == ITEM_NONE) {
INV_CONTENT(ITEM_BOMBCHU) = ITEM_BOMBCHU;
diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c
index 4232bf5ea..7e480ad27 100644
--- a/soh/src/code/z_play.c
+++ b/soh/src/code/z_play.c
@@ -2318,7 +2318,10 @@ void Play_PerformSave(PlayState* play) {
} else {
Save_SaveFile();
}
- if (CVarGetInteger("gAutosave", AUTOSAVE_OFF) != AUTOSAVE_OFF) {
+ uint8_t triforceHuntCompleted =
+ gSaveContext.n64ddFlag && gSaveContext.triforcePiecesCollected == Randomizer_GetSettingValue(RSK_TRIFORCE_HUNT_PIECES_REQUIRED) &&
+ Randomizer_GetSettingValue(RSK_TRIFORCE_HUNT);
+ if (CVarGetInteger("gAutosave", AUTOSAVE_OFF) != AUTOSAVE_OFF || triforceHuntCompleted) {
Overlay_DisplayText(3.0f, "Game Saved");
}
}
diff --git a/soh/src/code/z_player_lib.c b/soh/src/code/z_player_lib.c
index 934df5f1b..98dacc982 100644
--- a/soh/src/code/z_player_lib.c
+++ b/soh/src/code/z_player_lib.c
@@ -7,6 +7,7 @@
#include "overlays/actors/ovl_Demo_Effect/z_demo_effect.h"
#include "soh/Enhancements/game-interactor/GameInteractor.h"
+#include "soh/Enhancements/randomizer/draw.h"
#include
@@ -1292,6 +1293,8 @@ void Player_DrawGetItemImpl(PlayState* play, Player* this, Vec3f* refPos, s32 dr
if (this->getItemEntry.modIndex == MOD_RANDOMIZER && this->getItemEntry.getItemId == RG_ICE_TRAP) {
Player_DrawGetItemIceTrap(play, this, refPos, drawIdPlusOne, height);
+ } else if (this->getItemEntry.modIndex == MOD_RANDOMIZER && this->getItemEntry.getItemId == RG_TRIFORCE_PIECE) {
+ Randomizer_DrawTriforcePieceGI(play, this->getItemEntry);
} else if (this->getItemEntry.drawFunc != NULL) {
this->getItemEntry.drawFunc(play, &this->getItemEntry);
} else {