From 2957dc61c37bb5f764a44d87951b8001724d2df7 Mon Sep 17 00:00:00 2001 From: aMannus Date: Fri, 2 Jun 2023 03:40:10 +0200 Subject: [PATCH] [Feature] Boss Rush (#2923) * Ganon(dorf) cutscene skips * Remove leftover code * Load into chamber of sages * Fix loading into chamber without fast file select * Boss warps in chamber done * Change warps back to chamber * Initial proof of concept done * ganon(dorf) cutscene skips * Code cleanup & auto age equipment * Gameplay stats timer + tweaks * Scuffed timer * Better timer * remove arena props + fix arena exits * Fix blue warps * Attempt to fix build * Fix build again * And again.. * Try no. 9001 * Handle dying and saving * Child link face fire medallion * Fix build * Fix warps after reset/death * Disable doors and move player spawns in boss rooms * Fix boss rush logo rendering * Start of ingame options menu * File Select cleanup * Fix build * Render char text PoC * Move functions to be more generic * Fix build * Fix other builds * Initial text scaling/kerning * Special characters prep * All special characters work now * Attempt to fix build * Fix build question mark * Finish all kerning * Start of ingame options menu with vertical scrolling * Barebones functional options menu * More options menu progress * More visual elements for options menu * Options menu visual changes, implement all options, tons of cleanup * Cleanup and comments * Shorter enums * More options * Change default heart count * Finish French translations * Implement timer in cosmetics editor * Uncomment timer requirement * Variable name change * German translation & small UI tweaks * Animated up/down arrows in options UI * Better arrows in options UI * Cleaner timer + make it usable for general gameplay * More cleanup + ganon & ganondorf boss option * Implement never heal option * Slight up arrow in options UI tweak * Add BGS option * Reintroduce ganondorf cutscene skip * Change encoding to UTF on bossrush.cpp * Fix build hopefully * Fixed static variables leading to options not properly resetting * Fix BR completed timestamp * Change timer to render on top of everything * Offset final BR time by 0.1 second from boss timestamps * Add missing check for boss rush * Implement soh_assets.h * Revert merge mistake * Fix special characters with UTF-8 * Fix build * here's the fix you can merge from your phone * Fix quest select crash with oot.otr only * Use OoT's kerning * Fix HD textures on options menu * Fix special character kerning * "Heal every boss" fixes * Seperate headers + bunny hood option * Remove GetUnixTimestamp() externing * Clean up extern "C"'s * Address review comments * Fix build question mark * Remove accidental styling change --------- Co-authored-by: briaguya --- .../gTitleBossRushSubtitleTex.rgba32.png | Bin 0 -> 6291 bytes .../parameter_static/gArrowDown.ia16.png | Bin 0 -> 17227 bytes .../parameter_static/gArrowUp.ia16.png | Bin 0 -> 17217 bytes .../gFileSelBossRushSettingsENGTex.ia8.png | Bin 0 -> 4004 bytes .../gFileSelBossRushSettingsFRATex.ia8.png | Bin 0 -> 3942 bytes .../gFileSelBossRushSettingsGERTex.ia8.png | Bin 0 -> 3884 bytes soh/assets/soh_assets.h | 18 + soh/include/functions.h | 3 + soh/include/z64.h | 4 + soh/include/z64save.h | 4 + soh/soh/Enhancements/boss-rush/BossRush.cpp | 487 ++++++++++++++++++ soh/soh/Enhancements/boss-rush/BossRush.h | 20 + .../Enhancements/boss-rush/BossRushTypes.h | 91 ++++ .../CustomMessageInterfaceAddon.h | 9 + .../custom-message/CustomMessageManager.cpp | 21 + .../custom-message/CustomMessageManager.h | 2 +- .../Enhancements/debugger/debugSaveEditor.cpp | 2 +- soh/soh/Enhancements/gameplaystats.cpp | 12 + soh/soh/Enhancements/gameplaystats.h | 1 + soh/soh/Enhancements/mods.cpp | 8 +- soh/soh/OTRGlobals.h | 9 +- soh/soh/SaveManager.cpp | 8 +- soh/soh/z_play_otr.cpp | 4 + soh/src/code/z_kaleido_scope_call.c | 3 +- soh/src/code/z_kanfont.c | 4 + soh/src/code/z_message_PAL.c | 4 + soh/src/code/z_parameter.c | 88 +++- soh/src/code/z_play.c | 3 +- soh/src/code/z_sram.c | 4 + .../actors/ovl_Bg_Breakwall/z_bg_breakwall.c | 3 +- .../actors/ovl_Boss_Dodongo/z_boss_dodongo.c | 19 +- .../overlays/actors/ovl_Boss_Fd/z_boss_fd.c | 2 +- .../overlays/actors/ovl_Boss_Fd2/z_boss_fd2.c | 11 +- .../actors/ovl_Boss_Ganon/z_boss_ganon.c | 35 +- .../actors/ovl_Boss_Ganon2/z_boss_ganon2.c | 4 +- .../ovl_Boss_Ganondrof/z_boss_ganondrof.c | 18 +- .../actors/ovl_Boss_Goma/z_boss_goma.c | 17 +- .../overlays/actors/ovl_Boss_Mo/z_boss_mo.c | 17 +- .../overlays/actors/ovl_Boss_Sst/z_boss_sst.c | 14 +- .../overlays/actors/ovl_Boss_Tw/z_boss_tw.c | 17 +- .../overlays/actors/ovl_Boss_Va/z_boss_va.c | 18 +- .../overlays/actors/ovl_Demo_Sa/z_demo_sa.c | 19 +- .../actors/ovl_Door_Warp1/z_door_warp1.c | 39 +- soh/src/overlays/actors/ovl_En_Box/z_en_box.c | 5 + .../overlays/actors/ovl_En_Kusa/z_en_kusa.c | 5 + .../actors/ovl_Obj_Tsubo/z_obj_tsubo.c | 5 + .../actors/ovl_player_actor/z_player.c | 3 +- .../gamestates/ovl_file_choose/file_choose.h | 11 + .../ovl_file_choose/z_file_choose.c | 471 ++++++++++++----- .../ovl_kaleido_scope/z_kaleido_scope_PAL.c | 25 +- 50 files changed, 1351 insertions(+), 216 deletions(-) create mode 100644 OTRExporter/assets/objects/object_mag/gTitleBossRushSubtitleTex.rgba32.png create mode 100644 OTRExporter/assets/textures/parameter_static/gArrowDown.ia16.png create mode 100644 OTRExporter/assets/textures/parameter_static/gArrowUp.ia16.png create mode 100644 OTRExporter/assets/textures/title_static/gFileSelBossRushSettingsENGTex.ia8.png create mode 100644 OTRExporter/assets/textures/title_static/gFileSelBossRushSettingsFRATex.ia8.png create mode 100644 OTRExporter/assets/textures/title_static/gFileSelBossRushSettingsGERTex.ia8.png create mode 100644 soh/soh/Enhancements/boss-rush/BossRush.cpp create mode 100644 soh/soh/Enhancements/boss-rush/BossRush.h create mode 100644 soh/soh/Enhancements/boss-rush/BossRushTypes.h create mode 100644 soh/soh/Enhancements/custom-message/CustomMessageInterfaceAddon.h diff --git a/OTRExporter/assets/objects/object_mag/gTitleBossRushSubtitleTex.rgba32.png b/OTRExporter/assets/objects/object_mag/gTitleBossRushSubtitleTex.rgba32.png new file mode 100644 index 0000000000000000000000000000000000000000..624a29923c5f67bc47e8849e13357a86c4285073 GIT binary patch literal 6291 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 Pp$6A)D8G8crY(O1i2dBp literal 0 HcmV?d00001 diff --git a/OTRExporter/assets/textures/parameter_static/gArrowUp.ia16.png b/OTRExporter/assets/textures/parameter_static/gArrowUp.ia16.png new file mode 100644 index 0000000000000000000000000000000000000000..1ed3f1cdd9596322cc34c09509428082949304a8 GIT binary patch literal 17217 zcmeI4PmI)57{H4}ql+X4Vnkv>GlT?$Zu{D4XQs{$vOB;oGRwNGVV4A?e_nSRX4;|c z?#!-62#ASBFC_kn!FUj(2Sbb=OeCV4pk9mzVq(yEkwnFV9EcLb0bi%nH{ID;cnC^J z-Xyd0?d$ia@Atj;^=l7(acFSs(yon^ux4?HYG7W4~o%G6$ zQ!@=7mSF`tfsgD!N4x5T8@T?2yFf^Mc)`FZC#4dO-SK=Ym5!nPVDl70&_vqN(jS?w zLVg(f?xbhH%~NP9t2@o!4~AhYI|QzfdPhfrZ7tv@JhC7JShp823y(x*jWrdeI**{K zeIC>T*Bf!&av@r-Lvb$HzP`rtWY_Mp9n-D(>+*cNNo(P>yuqRtz=9~CwFpE`P)0yj z6=YS+trvhQ2ni>VW}A_g%NnG?Z?D5`0oieSAUVjl_3b{}XXnp&U{c}eOf z{FsJnxn4yJ3U);shkVr;&-1OpgeG2|YM)znJ#<>IAm#Z^!wIX#l2r#BKhPWl4ipP$ zpxd@h)lxvtl!3t+Izp(Y(}2?rN#-O8Xu2hunjqyG5KE<``8?M}*O&~q9daF2DAP5N z`_^fh0+0k?iJX-M84dtwa(Xr=ah4^^8BG_htP*7l1xh%Z7ifoPqXMdx6FT8sOcc!2 zv!ZUH5I|A@r|D^n({-qE8DOPFQxOy~jYD*BGcQ!X?W5H?omj!XjBP!tDXgJU?f&bz0c6X=cw)CmDjsf5Lp> z7_|xLTx)wpt{PekJMmpBsA(SDG>%pX$i(7TaTpoDAa zf($*sV0mtZ#k-gdTvn@A%eJ9~Ip(Jvvw3;LTO-bQ!pHwOS6`dD$t4F&OUOcr6BQX1 z4=F8kS%@~FY)V-}R5D6NPRCQcp(PhrV~|m0pn_~%jc?zQYwcKLLUYEUS>WTD#RGRz z=AdMnCaRE}EGiNQL0aQbxrCf$rZb`;T8as@cz*vXbM4nrnz?47zv=pNostq(9bAIq z6yBctVnWZpkqVPIOC!d_HfrjfGv}MGsbpPQw4LbAjQXOeADG*j#hvT?PATpve{#^(<<2qf<>d>lDkq*@ilb(!otDTAYUj?nK*`4{tP8)RCj^u+b$IK%SpB zmhjd%HX3%;<;h0poDl`|;X(MfX_`FTZaQ{t)8yfH)0X8^jvW-lX0*29gjIMq9Nntz zL{ETVL6K!i&hzcV?G{6J9hOInHoB+s!+QguG;P`sbTk}W$S`rBX&7yr#Iu@4Vxm#> zBnUl(;_*|cM9%QN$gLMzGrIM@u}2yNqaYC|E)q&uhvLF0NCb+Dgc8=FxG)M5f#M>e zgmow`jDkd3f<&OW zNGM?)iVLG45hyMaN?3>D!YD`tii?C2)}gpC3KD_hBB6wJC@zeGM4-4xC}ACn3!@+r zC@vC8Scl@mC`bf~i-Z!^p|~`p3(p@elPE{uXiptwjVVI7JKqaYC|E)q&uhvLF0 zNCb+Dgc8=FxG)M5f#M>egmow`jDkeCCa$j5dmW&IUgl6kZ*7=)J3WZr_`n+d!zG59 zS;H`gjxfyMSJCeUhM5!?=J$OJqrSl~54p#l{&EY_{dAzXX{7$c@nf@HXW09TUw?S; z$=M_IwMSF+%jd7~&u#tdqixD7Q|FGK39P;EO@8z8x$kygIlJe3SUA7o$nN#bZ$}q1 zmEv;1u+JY_aXxTYK5Ly=(tBx-{lXugt-8GKU4Hq1wygZ4a`v&=(-%jVZGY+1!*QT w_2JhJ{(QE7TLT=lu^+} zRIJxpY`33Y0%5s{&FwK7*V46J7!7L+Df?y=quP@!QDx&dAf_+YlQG$_?9q0ZHQcfs zb~n{P&+l9$cgPgGxhs8BQJrp*+KBLdNZ#1w-;oB4!TV1KzS@~AZkhIF6U!GhOCyEl z@~E#{5nr55=bYxo_e-63;@u0-6ctq2l=RN86y_f3Cww>>S$rMcruioq4ZjFY(?swu1=63JY`1yMNgK>U;etSKrM0`fI-Rk$}8rg|801 zCyG0I6CY&K^1i0!uXaQ4h*mh|HPMKYVnvu3{{0QPK8`s&t2%V6(`{<;$QVd_=wd%e znA!Br+7B4@`%~?T@CnNM2Xn)kOoZ|+Sr8-(x&+A#Z9hA|K^I4!MI08g5Nz}E8qQN# z9#0IfY{gWZAgI1i$6`C7jeQkMwaajfr!eFUNkuL+9l6?rsB&fXR=mDNxy5xzHQR=a z4e`r?n$QqB;z!i{P5Yn~{!nYmoR}CYq@KBX`kylMg33w}XrtG9@=amO7a^~=EFnip zdm=eCE~CZ8#l4v7HSN+z(={%}npv`@oSdAI*U#KOTZTM%@IX^BijE0)^V+qXo+Q2! z{Q@nmb7Aw^{`WpBmy1iTZ4&R((t2DfTD30d=qQg;wQk*-~D=k{00NV zm!qTC3wT=jxw*NRxH$3Y^AzexdMGI=X_0Y-<;Lr%hzS0B_j->5(#drk9SdcA{=Sgi zBq@ik{V7Y=;7yvEni7B3NrTi&5kkfJE`;p<`0-kQ|Doj4k~H0kYon7$n#LNwscs67C+!{_ z)V_Q7?nH}!9T5@H2Q>N)(vMKp^&FmdgN?0ZcGlY0-@n9Tc>tb{z1rH3O`_Y{+S+${ zd3z@(Cs!MwUaLbLhCbPs0>qHLUcm}gbUBn#$~gbNJGr=9ho!!|J`B}Gk54TgO8 z?p<$x|BW1{;r!JlY&tskYNj_$nrp7!6Cu=5H@_QRBL}g6|_`{PqeN7#SrzmNb?v zD_T#A2eJ{H0d!PUdMH#TU`lD#vo1As>&1x+a;RyubzOGxSywy~9nbM7e`rV#YS!>7 z&&=HXE^>EyP-^k-y5E*h1f+UgBY{&+Ra?8@%a^du&Zo;JCMLJE5$EEMya^aQK67fy zLW7r)n4IqUXgNw;N-8Na5sIM}ZkD&RyPF+!A|NU%s(OcyFA7kt%5COxvJfgJC50Jd zLU4W0B4)3nuip;BuT0bkf<*Uc$)$tX`W6-%mX@3VC5}-(0QVVk{>f-G!_5a~5fv4} zAYmGx#G}K>>4%E+`B+(5rTzB!A&-3iiYIfD@CyiVOG~Hb=E6?)*P}+90U9E~3hxOD z9?wN_b@%jqR!)5yoP%J_M#~*W{g?1WZj$k4{g; z{+U+-RT!^!pbtEq7?c9*5zx=C^gncs=R#3ctBQ#UGpNiz^AT5Q>hIT3R#EBe>rO(FuFVR* z@I?;gD1?((kS#$^cKU=ze*ew|7#85=eaf4J1pFO|6>cpAFzU6hcKThYt3^Ni_iuS) zqYO4pOl7!0+t|gWB$AG!_^V2Sj9X)(s-1&_I{NJV{EeKjC~-D9IeAH5o^rLr;2)ov zz=Ai_%=g5^^wA$OGD<^<$<@$sI6P>SxcTzpw3D`p?2NcDa5j`2hBPWM?CI&*IXpBr zGD-$=EN*Y)<>c9W~Bl6suRS4Ya;UmiMkH01Qvu9?6n1%(e1mY*dXgOs~ChOcr zTLVu?_z?)Jyu3Ur3X1nw`(Y4S{Kc3EkHt9!Nh9YO2WL)EQ8YEPP^J6alcj;oN!R-V z0?`z<+kCu~ ze&4D!1Bl3V1_q*QVGEK%rr$`?v7Nmi1%1!=N28;oui(_&ENfI|rmU$+tDvBuC5vzL z;<x(l@@;^jIB_nD!HrzL3-vpi>B%;wz^z;~X zb#-qrF^PH;Ojz02wzsy9P}6n8pvx6jRFGqahm%%T9E(dzpoL{+q&cmvS$XOy3`|TN z<*24q5gQ`;`v>q7iua^Gq9K4jwzjsZh~USxw6rRyu5zn3t!K}81Ox;CV0V8pYFkUD z_w@GK{s|-d!E=H8(F$*V1yJ zogP6j=wXp^1PnSx-;dDCVSlVk}PS$&_G9ZyiNJM0$S#wKEB1uSH zoz$56_;!6n=v;f$!s==dm>ORFM`bNl`4`_rtYc=TW^T?F8hSttn#P67@*+s_kIr{& zfQy}deX((IG+-A4gNV!i^TQc!YwO$Ea{D*Hs#M{X`1pPL9_JdkH3kw85CFLD-iq+u zfvv8t{vI9OB~%a2udnxQ&9w*K;o%{FWzMuj13f-IjucEM3W!suSea>SgB9iE$b)+_ zfhXVZ-;9L`06+q&qjMdye4kzsYjwE5uEq?&>A_X2{Ps<+Kr3?yz-9!8%K->xbuGeS zv6_a4h9}!yTwW_U?wZ=#dvLg(ot>SG|3RrysmW)npkrmAJR4}t&d#gMhaOJ_jY@TZ zQlpcS_B+|!RM8;ShA327B9Dp;%3eVswBNqEGDQ!9D9F!OLl0y=TJFD?oR}~$GRpt< z4K5)e(F2+(U~4jL1K(sUnO=sXF!}D7__^*!2v-g=i5H3XCX9Us8vb&sUbj7xHVO6; z-)IPw^EZf)5&cq8G2DJ2qX_76aZQb(N<6#nlP5npxO1%9f>>o=SHc8FEuPjn&b> z4HW`G)6A4H+-y0_@|XrKkks*K1f{c+Qz9&If5dp_;6SpsRx~78*y^uEwbL*!lvU2} z`*0-?+{NYP(E-TN@UUI=IBdMynTv}H_>02ALRIu{pnBYjk=>)C#x*rHfDdAelW$vM zUw4D{yuZIc@y#x_I>4;Uj-fVq)~v)MBb# z>FDWw%9fcpILa0mpD#-v1T~=1XbXS3t1mW0HHhin0K+CIsE!u5>%H-;lwBwLap3O) z?QHK&8PAm*C@~qohzKtFvw59sWPg`M^BMvQY?KQwM`34oSL|pE7_X0L^t@R%GZcug zBcE|VKr=9nnP-84fxz+b2@4AYXV=luafLNvG8!E{J@m=JMv0iifa746oR;r{o3Yix zv#YB`_4U%EBqS96O${7!)F0;2x20D|XGsoRr;_Bkw~`dc*k=W-rMASj1PQWAfG>9oQKwV3v)=6dXtU{wC2H-sz<;6e zH%+5xUJ+^Jp}T+SPH^&M!G`c^W$+_URtvH%bw R^pmb8q_UP$`4j7~{{a|frUw83 literal 0 HcmV?d00001 diff --git a/OTRExporter/assets/textures/title_static/gFileSelBossRushSettingsFRATex.ia8.png b/OTRExporter/assets/textures/title_static/gFileSelBossRushSettingsFRATex.ia8.png new file mode 100644 index 0000000000000000000000000000000000000000..8cbf6339cb3380fa37e0f7f6c2374b4bd232e7ad GIT binary patch literal 3942 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 literal 0 HcmV?d00001 diff --git a/OTRExporter/assets/textures/title_static/gFileSelBossRushSettingsGERTex.ia8.png b/OTRExporter/assets/textures/title_static/gFileSelBossRushSettingsGERTex.ia8.png new file mode 100644 index 0000000000000000000000000000000000000000..455390170f0ab2abc2bbb0937250548aa66ddb23 GIT binary patch literal 3884 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 D4SR)& literal 0 HcmV?d00001 diff --git a/soh/assets/soh_assets.h b/soh/assets/soh_assets.h index 416c20706..667a9ff1a 100644 --- a/soh/assets/soh_assets.h +++ b/soh/assets/soh_assets.h @@ -41,10 +41,19 @@ static const ALIGN_ASSET(2) char gSkullTreasureChestSideAndTopTex[] = dgSkullTre #define dgTitleRandomizerSubtitleTex "__OTR__objects/object_mag/gTitleRandomizerSubtitleTex" static const ALIGN_ASSET(2) char gTitleRandomizerSubtitleTex[] = dgTitleRandomizerSubtitleTex; +#define dgTitleBossRushSubtitleTex "__OTR__objects/object_mag/gTitleBossRushSubtitleTex" +static const ALIGN_ASSET(2) char gTitleBossRushSubtitleTex[] = dgTitleBossRushSubtitleTex; + // textures #define dgDPad "__OTR__textures/parameter_static/gDPad" static const ALIGN_ASSET(2) char gDPadTex[] = dgDPad; +#define dgArrowUp "__OTR__textures/parameter_static/gArrowUp" +static const ALIGN_ASSET(2) char gArrowUpTex[] = dgArrowUp; + +#define dgArrowDown "__OTR__textures/parameter_static/gArrowDown" +static const ALIGN_ASSET(2) char gArrowDownTex[] = dgArrowDown; + #define dgFileSelMQButtonTex "__OTR__textures/title_static/gFileSelMQButtonTex" static const ALIGN_ASSET(2) char gFileSelMQButtonTex[] = dgFileSelMQButtonTex; @@ -57,6 +66,15 @@ static const ALIGN_ASSET(2) char gFileSelPleaseChooseAQuestFRATex[] = dgFileSelP #define dgFileSelPleaseChooseAQuestGERTex "__OTR__textures/title_static/gFileSelPleaseChooseAQuestGERTex" static const ALIGN_ASSET(2) char gFileSelPleaseChooseAQuestGERTex[] = dgFileSelPleaseChooseAQuestGERTex; +#define dgFileSelBossRushSettingsENGTex "__OTR__textures/title_static/gFileSelBossRushSettingsENGTex" +static const ALIGN_ASSET(2) char gFileSelBossRushSettingsENGText[] = dgFileSelBossRushSettingsENGTex; + +#define dgFileSelBossRushSettingsFRATex "__OTR__textures/title_static/gFileSelBossRushSettingsFRATex" +static const ALIGN_ASSET(2) char gFileSelBossRushSettingsFRAText[] = dgFileSelBossRushSettingsFRATex; + +#define dgFileSelBossRushSettingsGERTex "__OTR__textures/title_static/gFileSelBossRushSettingsGERTex" +static const ALIGN_ASSET(2) char gFileSelBossRushSettingsGERText[] = dgFileSelBossRushSettingsGERTex; + #define dgFileSelRANDButtonTex "__OTR__textures/title_static/gFileSelRANDButtonTex" static const ALIGN_ASSET(2) char gFileSelRANDButtonTex[] = dgFileSelRANDButtonTex; diff --git a/soh/include/functions.h b/soh/include/functions.h index aae6a423f..3e0921afb 100644 --- a/soh/include/functions.h +++ b/soh/include/functions.h @@ -889,6 +889,7 @@ void KaleidoSetup_Init(PlayState* play); void KaleidoSetup_Destroy(PlayState* play); void func_8006EE50(Font* font, u16 arg1, u16 arg2); void Font_LoadChar(Font* font, u8 character, u16 codePointIndex); +void* Font_FetchCharTexture(u8 character); void Font_LoadMessageBoxIcon(Font* font, u16 icon); void Font_LoadOrderedFont(Font* font); s32 func_8006F0A0(s32 arg0); @@ -1054,6 +1055,7 @@ void func_800849EC(PlayState* play); void Interface_LoadItemIcon1(PlayState* play, u16 button); void Interface_LoadItemIcon2(PlayState* play, u16 button); void func_80084BF4(PlayState* play, u16 flag); +uint16_t Interface_DrawTextLine(GraphicsContext* gfx, char text[], int16_t x, int16_t y, uint16_t colorR, uint16_t colorG, uint16_t colorB, uint16_t colorA, float textScale, uint8_t textShadow); u8 Item_Give(PlayState* play, u8 item); u16 Randomizer_Item_Give(PlayState* play, GetItemEntry giEntry); u8 Item_CheckObtainability(u8 item); @@ -2402,6 +2404,7 @@ u8 Message_GetState(MessageContext* msgCtx); void Message_Draw(PlayState* play); void Message_Update(PlayState* play); void Message_SetTables(void); +f32 Message_GetCharacterWidth(unsigned char characterIndex); void GameOver_Init(PlayState* play); void GameOver_FadeInLights(PlayState* play); void GameOver_Update(PlayState* play); diff --git a/soh/include/z64.h b/soh/include/z64.h index 4d4021bf6..a04dda12b 100644 --- a/soh/include/z64.h +++ b/soh/include/z64.h @@ -1500,6 +1500,10 @@ typedef struct { f32 stickAnimTween; u8 arrowAnimState; u8 stickAnimState; + uint8_t bossRushIndex; + uint8_t bossRushOffset; + int16_t bossRushUIAlpha; + uint16_t bossRushArrowOffset; } FileChooseContext; // size = 0x1CAE0 typedef enum { diff --git a/soh/include/z64save.h b/soh/include/z64save.h index a6f1b96be..041bd7027 100644 --- a/soh/include/z64save.h +++ b/soh/include/z64save.h @@ -8,6 +8,7 @@ #include "soh/Enhancements/randomizer/randomizer_inf.h" #include "soh/Enhancements/gameplaystats.h" #include "soh/Enhancements/randomizer/randomizer_entrance.h" +#include "soh/Enhancements/boss-rush/BossRushTypes.h" typedef enum { /* 0x0 */ MAGIC_STATE_IDLE, // Regular gameplay @@ -280,6 +281,9 @@ typedef struct { // #region SOH [General] // Upstream TODO: Move these to their own struct or name to more obviously specific to SoH /* */ uint32_t isMasterQuest; + /* */ uint32_t isBossRush; + /* */ uint32_t isBossRushPaused; + /* */ uint8_t bossRushOptions[BOSSRUSH_OPTIONS_AMOUNT]; /* */ u8 mqDungeonCount; /* */ u8 pendingIceTrapCount; /* */ SohStats sohStats; diff --git a/soh/soh/Enhancements/boss-rush/BossRush.cpp b/soh/soh/Enhancements/boss-rush/BossRush.cpp new file mode 100644 index 000000000..4b613c861 --- /dev/null +++ b/soh/soh/Enhancements/boss-rush/BossRush.cpp @@ -0,0 +1,487 @@ +#include "BossRush.h" +#include "soh/OTRGlobals.h" +#include "functions.h" +#include "macros.h" +#include "variables.h" + +#include +#include +#include + +typedef struct BossRushSetting { + std::array name; + std::vector> choices; +} BossRushSetting; + +BossRushSetting BossRushOptions[BOSSRUSH_OPTIONS_AMOUNT] = { + { + { "BOSSES:", "BOSSE:", "BOSS:" }, + { + { "All", "Alle", "Tous" }, + { "Child", "Kind", "Enfant" }, + { "Adult", "Erwachsener", "Adulte" }, + { "Ganondorf & Ganon", "Ganondorf & Ganon", "Ganondorf & Ganon" } + } + }, + { + { "HEARTS:", "HERZEN:", "COEURS:" }, + { + { "10", "10", "10" }, + { "15", "15", "15" }, + { "20", "20", "20" }, + { "3", "3", "3" }, + { "5", "5", "5" }, + { "7", "7", "7" } + } + }, + { + { "AMMO:", "MUNITION:", "MUNITIONS:" }, + { + { "Limited", "Limitiert", "Limitées" }, + { "Full", "Voll", "Pleines" }, + { "Maxed", "Maximum", "Maximum" } + } + }, + { + { "HEAL:", "REGENERATION:", "SOIN:" }, + { + { "Before Ganondorf", "Vor Ganondorf", "Avant Ganondorf" }, + { "Every Boss", "Bei jedem Boss", "Tous les Boss" }, + { "Never", "Niemals", "Jamais" } + } + }, + { + { "HYPER BOSSES:", "HYPER-BOSSE:", "HYPER BOSS:" }, + { + { "No", "Nein", "Non" }, + { "Yes", "Ja", "Oui" } + } + }, + { + { "MAGIC:", "MAGIE:", "MAGIE:" }, + { + { "Single", "Einzel", "Simple" }, + { "Double", "Doppel", "Double" } + } + }, + { + { "BIG. SWORD:", "BIG.-SCHWERT:", "EPÉE DE BIG.:" }, + { + { "No", "Nein", "Non" }, + { "Yes", "Ja", "Oui" } + } + }, + { + { "BOTTLE:", "FLASCHEN:", "BOUTEILLE:" }, + { + { "No", "Nein", "Non" }, + { "Empty", "Leer", "Vide" }, + { "Fairy", "Fee", "Fée" }, + { "Red Potion", "Rotes Elixier", "Potion Rouge" }, + { "Green Potion", "Grünes Elixier", "Potion Verte" }, + { "Blue Potion", "Blaues Elixier", "Potion Bleue" } + } + }, + { + { "LONGSHOT:", "ENTERHAKEN:", "SUPER GRAPPIN:" }, + { + { "No", "Nein", "Non" }, + { "Yes", "Ja", "Oui" } + } + }, + { + { "HOVER BOOTS:", "GLEITSTIEFEL:", "BOTTES DES AIRS:" }, + { + { "No", "Nein", "Non" }, + { "Yes", "Ja", "Oui" } + } + }, + { + { "BUNNY HOOD:", "HASENOHREN:", "MASQUE DU LAPIN:" }, + { + { "No", "Nein", "Non" }, + { "Yes", "Ja", "Oui" } + } + }, + { + { "TIMER:", "TIMER:", "TIMER:" }, + { + { "Yes", "Ja", "Oui" }, + { "No", "Nein", "Non" } + } + } +}; + +const char* BossRush_GetSettingName(uint8_t optionIndex, uint8_t language) { + return BossRushOptions[optionIndex].name[language].c_str(); +} + +const char* BossRush_GetSettingChoiceName(uint8_t optionIndex, uint8_t choiceIndex, uint8_t language) { + return BossRushOptions[optionIndex].choices[choiceIndex][language].c_str(); +} + +uint8_t BossRush_GetSettingOptionsAmount(uint8_t optionIndex) { + return BossRushOptions[optionIndex].choices.size(); +} + +void BossRush_SpawnBlueWarps(PlayState* play) { + + // Spawn blue warps in Chamber of Sages based on what bosses have been defeated. + if (gSaveContext.linkAge == LINK_AGE_CHILD) { + // Forest Medallion (Gohma) + if (!Flags_GetRandomizerInf(RAND_INF_DUNGEONS_DONE_DEKU_TREE)) { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, -100, 6, -170, 0, 0, 0, -1, false); + } + // Fire Medallion (King Dodongo) + if (!Flags_GetRandomizerInf(RAND_INF_DUNGEONS_DONE_DODONGOS_CAVERN)) { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, 100, 6, -170, 0, 0, 0, -1, false); + } + // Water Medallion (Barinade) + if (!Flags_GetRandomizerInf(RAND_INF_DUNGEONS_DONE_JABU_JABUS_BELLY)) { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, 199, 6, 0, 0, 0, 0, -1, false); + } + } else { + // Light Medallion (Ganondorf) + if (CheckDungeonCount() == 8) { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, -199, 6, 0, 0, 0, 0, -1, false); + } + // Forest Medallion (Phantom Ganondorf) + if (!Flags_GetRandomizerInf(RAND_INF_DUNGEONS_DONE_FOREST_TEMPLE)) { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, -100, 6, -170, 0, 0, 0, -1, false); + } + // Fire Medallion (Volvagia) + if (!Flags_GetRandomizerInf(RAND_INF_DUNGEONS_DONE_FIRE_TEMPLE)) { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, 100, 6, -170, 0, 0, 0, -1, false); + } + // Water Medallion (Morpha) + if (!Flags_GetRandomizerInf(RAND_INF_DUNGEONS_DONE_WATER_TEMPLE)) { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, 199, 6, 0, 0, 0, 0, -1, false); + } + // Spirit Medallion (Twinrova) + if (!Flags_GetRandomizerInf(RAND_INF_DUNGEONS_DONE_SPIRIT_TEMPLE)) { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, 100, 6, 170, 0, 0, 0, -1, false); + } + // Shadow Medallion (Bongo Bongo) + if (!Flags_GetRandomizerInf(RAND_INF_DUNGEONS_DONE_SHADOW_TEMPLE)) { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, -100, 6, 170, 0, 0, 0, -1, false); + } + } +} + +void BossRush_HandleBlueWarp(PlayState* play, f32 warpPosX, f32 warpPosZ) { + + // If warping from Chamber of Sages, choose the correct boss room to teleport to. + if (play->sceneNum == SCENE_KENJYANOMA) { + // Gohma & Phantom Ganon + if (warpPosX == -100 && warpPosZ == -170) { + if (gSaveContext.linkAge == LINK_AGE_CHILD) { + play->nextEntranceIndex = 0x040F; + } else { + play->nextEntranceIndex = 0x000C; + } + // King Dodongo & Volvagia + } else if (warpPosX == 100 && warpPosZ == -170) { + if (gSaveContext.linkAge == LINK_AGE_CHILD) { + play->nextEntranceIndex = 0x040B; + } else { + play->nextEntranceIndex = 0x0305; + } + // Barinade & Morb + } else if (warpPosX == 199 && warpPosZ == 0) { + if (gSaveContext.linkAge == LINK_AGE_CHILD) { + play->nextEntranceIndex = 0x0301; + } else { + play->nextEntranceIndex = 0x0417; + } + // Twinrova + } else if (warpPosX == 100 && warpPosZ == 170) { + play->nextEntranceIndex = 0x05EC; + // Bongo Bongo + } else if (warpPosX == -100 && warpPosZ == 170) { + play->nextEntranceIndex = 0x0413; + // Ganondork + } else if (warpPosX == -199 && warpPosZ == 0) { + play->nextEntranceIndex = 0x041F; + } + // If coming from a boss room, teleport back to Chamber of Sages and set flag. + } else { + play->nextEntranceIndex = SCENE_HAIRAL_NIWA2; + + if (CheckDungeonCount() == 3) { + play->linkAgeOnLoad = LINK_AGE_ADULT; + gSaveContext.linkAge = LINK_AGE_ADULT; + + // Change to Adult Link. + if (gSaveContext.bossRushOptions[BR_OPTIONS_BOSSES] == BR_CHOICE_BOSSES_ALL) { + BossRush_SetEquipment(LINK_AGE_ADULT); + // Warp to credits. + } else if (gSaveContext.bossRushOptions[BR_OPTIONS_BOSSES] == BR_CHOICE_BOSSES_CHILD) { + play->nextEntranceIndex = 0x6B; + gSaveContext.nextCutsceneIndex = 0xFFF2; + play->sceneLoadFlag = 0x14; + play->fadeTransition = 3; + } + } + } +} + +void BossRush_HandleBlueWarpHeal(PlayState* play) { + + // This function gets called multiple times per blue warp, so only heal when player isn't at max HP. + if (gSaveContext.bossRushOptions[BR_OPTIONS_HEAL] == BR_CHOICE_HEAL_EVERYBOSS && + gSaveContext.health != gSaveContext.healthCapacity) { + Health_ChangeBy(play, 320); + } +} + +void BossRush_HandleCompleteBoss(PlayState* play) { + if (!gSaveContext.isBossRush) { + return; + } + + gSaveContext.isBossRushPaused = 1; + switch (play->sceneNum) { + case SCENE_YDAN_BOSS: + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_DEKU_TREE); + break; + case SCENE_DDAN_BOSS: + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_DODONGOS_CAVERN); + break; + case SCENE_BDAN_BOSS: + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_JABU_JABUS_BELLY); + break; + case SCENE_MORIBOSSROOM: + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_FOREST_TEMPLE); + break; + case SCENE_FIRE_BS: + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_FIRE_TEMPLE); + break; + case SCENE_MIZUSIN_BS: + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_WATER_TEMPLE); + break; + case SCENE_JYASINBOSS: + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_SPIRIT_TEMPLE); + break; + case SCENE_HAKADAN_BS: + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_SHADOW_TEMPLE); + break; + default: + break; + } + + // Fully heal the player after Ganondorf + if (gSaveContext.bossRushOptions[BR_OPTIONS_HEAL] == BR_CHOICE_HEAL_EVERYBOSS && + play->sceneNum == SCENE_GANON_BOSS) { + Health_ChangeBy(play, 320); + } + + if ((CheckDungeonCount() == 3 && gSaveContext.bossRushOptions[BR_OPTIONS_BOSSES] == BR_CHOICE_BOSSES_CHILD) || + play->sceneNum == SCENE_GANON_DEMO) { + gSaveContext.sohStats.playTimer += 2; + gSaveContext.sohStats.gameComplete = 1; + gSaveContext.sohStats.itemTimestamp[TIMESTAMP_BOSSRUSH_FINISH] = GAMEPLAYSTAT_TOTAL_TIME; + } +} + +void BossRush_InitSave() { + + // Set player name to Lonk for the few textboxes that show up during Boss Rush. Player can't input their own name. + std::array brPlayerName = { 21, 50, 49, 46, 62, 62, 62, 62 }; + for (int i = 0; i < ARRAY_COUNT(gSaveContext.playerName); i++) { + gSaveContext.playerName[i] = brPlayerName[i]; + } + + gSaveContext.isBossRushPaused = 1; + gSaveContext.entranceIndex = 107; + gSaveContext.cutsceneIndex = 0x8000; + gSaveContext.isMagicAcquired = 1; + + // Set magic + if (gSaveContext.bossRushOptions[BR_OPTIONS_MAGIC] == BR_CHOICE_MAGIC_SINGLE) { + gSaveContext.magicLevel = 1; + gSaveContext.magic = 48; + } else { + gSaveContext.isDoubleMagicAcquired = 1; + gSaveContext.magicLevel = 2; + gSaveContext.magic = 96; + } + + // Set health + uint16_t health = 16; + switch (gSaveContext.bossRushOptions[BR_OPTIONS_HEARTS]) { + case BR_CHOICE_HEARTS_7: + health *= 7; + break; + case BR_CHOICE_HEARTS_10: + health *= 10; + break; + case BR_CHOICE_HEARTS_15: + health *= 15; + break; + case BR_CHOICE_HEARTS_20: + health *= 20; + break; + case BR_CHOICE_HEARTS_3: + health *= 3; + break; + case BR_CHOICE_HEARTS_5: + health *= 5; + break; + default: + break; + } + + gSaveContext.healthCapacity = health; + gSaveContext.health = health; + + // Skip boss cutscenes + gSaveContext.eventChkInf[7] |= 1; // gohma + gSaveContext.eventChkInf[7] |= 2; // dodongo + gSaveContext.eventChkInf[7] |= 4; // phantom ganon + gSaveContext.eventChkInf[7] |= 8; // volvagia + gSaveContext.eventChkInf[7] |= 0x10; // morpha + gSaveContext.eventChkInf[7] |= 0x20; // twinrova + gSaveContext.eventChkInf[7] |= 0x40; // barinade + gSaveContext.eventChkInf[7] |= 0x80; // bongo bongo + + // Sets all rando flags to false + for (s32 i = 0; i < ARRAY_COUNT(gSaveContext.randomizerInf); i++) { + gSaveContext.randomizerInf[i] = 0; + } + + // Set items + std::array brItems = { + ITEM_STICK, ITEM_NUT, ITEM_BOMB, ITEM_BOW, ITEM_NONE, ITEM_NONE, + ITEM_SLINGSHOT, ITEM_NONE, ITEM_NONE, ITEM_HOOKSHOT, ITEM_NONE, ITEM_NONE, + ITEM_BOOMERANG, ITEM_LENS, ITEM_NONE, ITEM_HAMMER, ITEM_ARROW_LIGHT, ITEM_NONE, + ITEM_NONE, ITEM_NONE, ITEM_NONE, ITEM_NONE, ITEM_NONE, ITEM_NONE, + }; + + if (gSaveContext.bossRushOptions[BR_OPTIONS_LONGSHOT] == BR_CHOICE_LONGSHOT_YES) { + brItems[9] = ITEM_LONGSHOT; + } + + switch (gSaveContext.bossRushOptions[BR_OPTIONS_BOTTLE]) { + case BR_CHOICE_BOTTLE_EMPTY: + brItems[18] = ITEM_BOTTLE; + break; + case BR_CHOICE_BOTTLE_FAIRY: + brItems[18] = ITEM_FAIRY; + break; + case BR_CHOICE_BOTTLE_REDPOTION: + brItems[18] = ITEM_POTION_RED; + break; + case BR_CHOICE_BOTTLE_GREENPOTION: + brItems[18] = ITEM_POTION_GREEN; + break; + case BR_CHOICE_BOTTLE_BLUEPOTION: + brItems[18] = ITEM_POTION_BLUE; + break; + default: + break; + } + + if (gSaveContext.bossRushOptions[BR_OPTIONS_BUNNYHOOD] == BR_CHOICE_BUNNYHOOD_YES) { + brItems[23] = ITEM_MASK_BUNNY; + } + + for (int item = 0; item < ARRAY_COUNT(gSaveContext.inventory.items); item++) { + gSaveContext.inventory.items[item] = brItems[item]; + } + + // Set consumable counts + std::array brAmmo = { 5, 5, 10, 10, 0, 0, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + + if (gSaveContext.bossRushOptions[BR_OPTIONS_AMMO] == BR_CHOICE_AMMO_FULL) { + brAmmo = { 10, 20, 20, 30, 0, 0, 30, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + } else if (gSaveContext.bossRushOptions[BR_OPTIONS_AMMO] == BR_CHOICE_AMMO_MAXED) { + brAmmo = { 30, 40, 40, 50, 0, 0, 50, 0, 0, 0, 0, 0, 0, 0, 0, 0 }; + } + + for (int ammo = 0; ammo < ARRAY_COUNT(gSaveContext.inventory.ammo); ammo++) { + gSaveContext.inventory.ammo[ammo] = brAmmo[ammo]; + } + + // Equipment + gSaveContext.inventory.equipment |= 1 << 0; // Kokiri Sword + gSaveContext.inventory.equipment |= 1 << 1; // Master Sword + gSaveContext.inventory.equipment |= 1 << 4; // Deku Shield + gSaveContext.inventory.equipment |= 1 << 6; // Mirror Shield + gSaveContext.inventory.equipment |= 1 << 9; // Goron Tunic + if (gSaveContext.bossRushOptions[BR_OPTIONS_BGS] == BR_CHOICE_BGS_YES) { + gSaveContext.inventory.equipment |= 1 << 2; // Biggoron Sword + gSaveContext.bgsFlag = 1; + } + if (gSaveContext.bossRushOptions[BR_OPTIONS_HOVERBOOTS] == BR_CHOICE_HOVERBOOTS_YES) { + gSaveContext.inventory.equipment |= 1 << 14; // Hover Boots + } + + // Upgrades + uint8_t upgradeLevel = 1; + if (gSaveContext.bossRushOptions[BR_OPTIONS_AMMO] == BR_CHOICE_AMMO_MAXED) { + upgradeLevel = 3; + } + Inventory_ChangeUpgrade(UPG_QUIVER, upgradeLevel); + Inventory_ChangeUpgrade(UPG_BOMB_BAG, upgradeLevel); + Inventory_ChangeUpgrade(UPG_BULLET_BAG, upgradeLevel); + Inventory_ChangeUpgrade(UPG_STICKS, upgradeLevel); + Inventory_ChangeUpgrade(UPG_NUTS, upgradeLevel); + + // Set flags and Link's age based on chosen settings. + if (gSaveContext.bossRushOptions[BR_OPTIONS_BOSSES] == BR_CHOICE_BOSSES_ADULT || + gSaveContext.bossRushOptions[BR_OPTIONS_BOSSES] == BR_CHOICE_BOSSES_GANONDORF_GANON) { + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_DEKU_TREE); + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_DODONGOS_CAVERN); + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_JABU_JABUS_BELLY); + if (gSaveContext.bossRushOptions[BR_OPTIONS_BOSSES] == BR_CHOICE_BOSSES_GANONDORF_GANON) { + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_FOREST_TEMPLE); + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_FIRE_TEMPLE); + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_WATER_TEMPLE); + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_SPIRIT_TEMPLE); + Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_SHADOW_TEMPLE); + } + gSaveContext.linkAge = LINK_AGE_ADULT; + BossRush_SetEquipment(LINK_AGE_ADULT); + } else { + gSaveContext.linkAge = LINK_AGE_CHILD; + BossRush_SetEquipment(LINK_AGE_CHILD); + } +} + +void BossRush_SetEquipment(uint8_t linkAge) { + + std::array brButtonItems; + std::array brCButtonSlots; + + // Set Child Equipment. + if (linkAge == LINK_AGE_CHILD) { + brButtonItems = { + ITEM_SWORD_KOKIRI, ITEM_STICK, ITEM_NUT, ITEM_BOMB, ITEM_NONE, ITEM_NONE, ITEM_NONE, ITEM_NONE + }; + + brCButtonSlots = { SLOT_STICK, SLOT_NUT, SLOT_BOMB, SLOT_NONE, SLOT_NONE, SLOT_NONE, SLOT_NONE }; + + Inventory_ChangeEquipment(EQUIP_SWORD, PLAYER_SWORD_KOKIRI); + Inventory_ChangeEquipment(EQUIP_SHIELD, PLAYER_SHIELD_DEKU); + // Set Adult equipment. + } else { + brButtonItems = { ITEM_SWORD_MASTER, ITEM_BOW, ITEM_HAMMER, ITEM_BOMB, + ITEM_NONE, ITEM_NONE, ITEM_NONE, ITEM_NONE }; + + brCButtonSlots = { SLOT_BOW, SLOT_HAMMER, SLOT_BOMB, SLOT_NONE, SLOT_NONE, SLOT_NONE, SLOT_NONE }; + + Inventory_ChangeEquipment(EQUIP_SWORD, PLAYER_SWORD_MASTER); + Inventory_ChangeEquipment(EQUIP_SHIELD, PLAYER_SHIELD_MIRROR); + Inventory_ChangeEquipment(EQUIP_TUNIC, PLAYER_TUNIC_GORON + 1); // Game expects tunic + 1, don't ask me why. + } + + // Button Items + for (int button = 0; button < ARRAY_COUNT(gSaveContext.equips.buttonItems); button++) { + gSaveContext.equips.buttonItems[button] = brButtonItems[button]; + } + + // C buttons + for (int button = 0; button < ARRAY_COUNT(gSaveContext.equips.cButtonSlots); button++) { + gSaveContext.equips.cButtonSlots[button] = brCButtonSlots[button]; + } +} diff --git a/soh/soh/Enhancements/boss-rush/BossRush.h b/soh/soh/Enhancements/boss-rush/BossRush.h new file mode 100644 index 000000000..caa6ce86e --- /dev/null +++ b/soh/soh/Enhancements/boss-rush/BossRush.h @@ -0,0 +1,20 @@ +#pragma once + +#include "BossRushTypes.h" +#include "variables.h" + +#ifdef __cplusplus +extern "C" { +#endif +void BossRush_SpawnBlueWarps(PlayState* play); +void BossRush_HandleBlueWarp(PlayState* play, f32 warpPosX, f32 warpPosZ); +void BossRush_HandleBlueWarpHeal(PlayState* play); +void BossRush_InitSave(); +void BossRush_SetEquipment(uint8_t linkAge); +void BossRush_HandleCompleteBoss(PlayState* play); +const char* BossRush_GetSettingName(uint8_t optionIndex, uint8_t language); +const char* BossRush_GetSettingChoiceName(uint8_t optionIndex, uint8_t choiceIndex, uint8_t language); +uint8_t BossRush_GetSettingOptionsAmount(uint8_t optionIndex); +#ifdef __cplusplus +}; +#endif diff --git a/soh/soh/Enhancements/boss-rush/BossRushTypes.h b/soh/soh/Enhancements/boss-rush/BossRushTypes.h new file mode 100644 index 000000000..e39cba997 --- /dev/null +++ b/soh/soh/Enhancements/boss-rush/BossRushTypes.h @@ -0,0 +1,91 @@ +#pragma once + +#define BOSSRUSH_OPTIONS_AMOUNT 12 +#define BOSSRUSH_MAX_OPTIONS_ON_SCREEN 6 + +typedef enum { + BR_OPTIONS_BOSSES, + BR_OPTIONS_HEARTS, + BR_OPTIONS_AMMO, + BR_OPTIONS_HEAL, + BR_OPTIONS_HYPERBOSSES, + BR_OPTIONS_MAGIC, + BR_OPTIONS_BGS, + BR_OPTIONS_BOTTLE, + BR_OPTIONS_LONGSHOT, + BR_OPTIONS_HOVERBOOTS, + BR_OPTIONS_BUNNYHOOD, + BR_OPTIONS_TIMER +} BossRushOptionEnums; + +typedef enum { + BR_CHOICE_BOSSES_ALL, + BR_CHOICE_BOSSES_CHILD, + BR_CHOICE_BOSSES_ADULT, + BR_CHOICE_BOSSES_GANONDORF_GANON +} BossRushBossesChoices; + +typedef enum { + BR_CHOICE_HEARTS_10, + BR_CHOICE_HEARTS_15, + BR_CHOICE_HEARTS_20, + BR_CHOICE_HEARTS_3, + BR_CHOICE_HEARTS_5, + BR_CHOICE_HEARTS_7 +} BossRushHeartsChoices; + +typedef enum { + BR_CHOICE_AMMO_LIMITED, + BR_CHOICE_AMMO_FULL, + BR_CHOICE_AMMO_MAXED +} BossRushAmmoChoices; + +typedef enum { + BR_CHOICE_HEAL_GANONDORF, + BR_CHOICE_HEAL_EVERYBOSS, + BR_CHOICE_HEAL_NEVER +} BossRushHealChoices; + +typedef enum { + BR_CHOICE_HYPERBOSSES_NO, + BR_CHOICE_HYPERBOSSES_YES +} BossRushHyperBossesChoices; + +typedef enum { + BR_CHOICE_MAGIC_SINGLE, + BR_CHOICE_MAGIC_DOUBLE +} BossRushMagicChoices; + +typedef enum { + BR_CHOICE_BGS_NO, + BR_CHOICE_BGS_YES +} BossRushBgsChoices; + +typedef enum { + BR_CHOICE_BOTTLE_NO, + BR_CHOICE_BOTTLE_EMPTY, + BR_CHOICE_BOTTLE_FAIRY, + BR_CHOICE_BOTTLE_REDPOTION, + BR_CHOICE_BOTTLE_GREENPOTION, + BR_CHOICE_BOTTLE_BLUEPOTION +} BossRushBottleChoices; + +typedef enum { + BR_CHOICE_LONGSHOT_NO, + BR_CHOICE_LONGSHOT_YES +} BossRushLongshotChoices; + +typedef enum { + BR_CHOICE_HOVERBOOTS_NO, + BR_CHOICE_HOVERBOOTS_YES +} BossRushHoverBootsChoices; + +typedef enum { + BR_CHOICE_BUNNYHOOD_NO, + BR_CHOICE_BUNNYHOOD_YES +} BossRushBunnyHoodChoices; + +typedef enum { + BR_CHOICE_TIMER_YES, + BR_CHOICE_TIMER_NO +} BossRushTimerChoices; diff --git a/soh/soh/Enhancements/custom-message/CustomMessageInterfaceAddon.h b/soh/soh/Enhancements/custom-message/CustomMessageInterfaceAddon.h new file mode 100644 index 000000000..f981e5604 --- /dev/null +++ b/soh/soh/Enhancements/custom-message/CustomMessageInterfaceAddon.h @@ -0,0 +1,9 @@ +#pragma once + +#ifdef __cplusplus +extern "C" { +#endif +const char* Interface_ReplaceSpecialCharacters(char text[]); +#ifdef __cplusplus +}; +#endif diff --git a/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp b/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp index 4614ac731..e552a9672 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp +++ b/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp @@ -1,6 +1,9 @@ #include "CustomMessageManager.h" +#include "CustomMessageInterfaceAddon.h" #include #include +#include +#include using namespace std::literals::string_literals; @@ -148,6 +151,24 @@ void CustomMessage::ReplaceSpecialCharacters() { } } +const char* Interface_ReplaceSpecialCharacters(char text[]) { + std::string textString(text); + + for (auto specialCharacterPair : textBoxSpecialCharacters) { + size_t start_pos = 0; + std::string textBoxSpecialCharacterString = ""s; + textBoxSpecialCharacterString += specialCharacterPair.second; + while ((start_pos = textString.find(specialCharacterPair.first, start_pos)) != std::string::npos) { + textString.replace(start_pos, specialCharacterPair.first.length(), textBoxSpecialCharacterString); + start_pos += textBoxSpecialCharacterString.length(); + } + } + + char* textChar = new char[textString.length() + 1]; + strcpy(textChar, textString.c_str()); + return textChar; +} + void CustomMessage::ReplaceColors() { for (std::string* str : { &english, &french, &german }) { for (auto colorPair : colors) { diff --git a/soh/soh/Enhancements/custom-message/CustomMessageManager.h b/soh/soh/Enhancements/custom-message/CustomMessageManager.h index 03ffc5ddb..afb0f51ae 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageManager.h +++ b/soh/soh/Enhancements/custom-message/CustomMessageManager.h @@ -206,4 +206,4 @@ class MessageNotFoundException : public std::exception { sprintf(message, "Message from table %s with textId %u was not found", messageTableId.c_str(), textId); return message; } -}; \ No newline at end of file +}; diff --git a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp index ccfe09710..b48b1b3de 100644 --- a/soh/soh/Enhancements/debugger/debugSaveEditor.cpp +++ b/soh/soh/Enhancements/debugger/debugSaveEditor.cpp @@ -1087,7 +1087,7 @@ void DrawFlagsTab() { for (int i = 0; i < flagTables.size(); i++) { const FlagTable& flagTable = flagTables[i]; - if (flagTable.flagTableType == RANDOMIZER_INF && !gSaveContext.n64ddFlag) { + if (flagTable.flagTableType == RANDOMIZER_INF && !gSaveContext.n64ddFlag && !gSaveContext.isBossRush) { continue; } diff --git a/soh/soh/Enhancements/gameplaystats.cpp b/soh/soh/Enhancements/gameplaystats.cpp index 74f0a539b..e0f5ff7f6 100644 --- a/soh/soh/Enhancements/gameplaystats.cpp +++ b/soh/soh/Enhancements/gameplaystats.cpp @@ -798,6 +798,7 @@ void SetupDisplayNames() { strcpy(itemTimestampDisplayName[TIMESTAMP_DEFEAT_TWINROVA], "Twinrova Defeated: "); strcpy(itemTimestampDisplayName[TIMESTAMP_DEFEAT_GANONDORF], "Ganondorf Defeated: "); strcpy(itemTimestampDisplayName[TIMESTAMP_DEFEAT_GANON], "Ganon Defeated: "); + strcpy(itemTimestampDisplayName[TIMESTAMP_BOSSRUSH_FINISH], "Boss Rush Finished: "); strcpy(itemTimestampDisplayName[TIMESTAMP_FOUND_GREG], "Greg Found: "); } @@ -808,39 +809,50 @@ void SetupDisplayColors() { case ITEM_KOKIRI_EMERALD: case ITEM_SONG_SARIA: case ITEM_MEDALLION_FOREST: + case TIMESTAMP_DEFEAT_GOHMA: + case TIMESTAMP_DEFEAT_PHANTOM_GANON: case TIMESTAMP_FOUND_GREG: itemTimestampDisplayColor[i] = COLOR_GREEN; break; case ITEM_SONG_BOLERO: case ITEM_GORON_RUBY: case ITEM_MEDALLION_FIRE: + case TIMESTAMP_DEFEAT_KING_DODONGO: + case TIMESTAMP_DEFEAT_VOLVAGIA: itemTimestampDisplayColor[i] = COLOR_RED; break; case ITEM_SONG_SERENADE: case ITEM_ZORA_SAPPHIRE: case ITEM_MEDALLION_WATER: + case TIMESTAMP_DEFEAT_BARINADE: + case TIMESTAMP_DEFEAT_MORPHA: itemTimestampDisplayColor[i] = COLOR_BLUE; break; case ITEM_SONG_LULLABY: case ITEM_SONG_NOCTURNE: case ITEM_MEDALLION_SHADOW: + case TIMESTAMP_DEFEAT_BONGO_BONGO: itemTimestampDisplayColor[i] = COLOR_PURPLE; break; case ITEM_SONG_EPONA: case ITEM_SONG_REQUIEM: case ITEM_MEDALLION_SPIRIT: + case TIMESTAMP_DEFEAT_TWINROVA: itemTimestampDisplayColor[i] = COLOR_ORANGE; break; case ITEM_SONG_SUN: case ITEM_SONG_PRELUDE: case ITEM_MEDALLION_LIGHT: case ITEM_ARROW_LIGHT: + case TIMESTAMP_DEFEAT_GANONDORF: + case TIMESTAMP_DEFEAT_GANON: itemTimestampDisplayColor[i] = COLOR_YELLOW; break; case ITEM_SONG_STORMS: itemTimestampDisplayColor[i] = COLOR_GREY; break; case ITEM_SONG_TIME: + case TIMESTAMP_BOSSRUSH_FINISH: itemTimestampDisplayColor[i] = COLOR_LIGHT_BLUE; break; default: diff --git a/soh/soh/Enhancements/gameplaystats.h b/soh/soh/Enhancements/gameplaystats.h index 4cd28478b..a043efdae 100644 --- a/soh/soh/Enhancements/gameplaystats.h +++ b/soh/soh/Enhancements/gameplaystats.h @@ -33,6 +33,7 @@ typedef enum { /* 0xA7 */ TIMESTAMP_DEFEAT_TWINROVA, // z_boss_tw.c /* 0xA8 */ TIMESTAMP_DEFEAT_GANONDORF, // z_boss_ganon.c /* 0xA9 */ TIMESTAMP_DEFEAT_GANON, // z_boss_ganon2.c + /* 0xA9 */ TIMESTAMP_BOSSRUSH_FINISH, // z_boss_ganon2.c /* 0xAA */ TIMESTAMP_FOUND_GREG, // z_parameter.c /* 0xAB */ TIMESTAMP_MAX diff --git a/soh/soh/Enhancements/mods.cpp b/soh/soh/Enhancements/mods.cpp index c35326f6a..e9fa86982 100644 --- a/soh/soh/Enhancements/mods.cpp +++ b/soh/soh/Enhancements/mods.cpp @@ -2,6 +2,7 @@ #include #include "game-interactor/GameInteractor.h" #include "tts/tts.h" +#include "soh/Enhancements/boss-rush/BossRushTypes.h" extern "C" { #include @@ -440,8 +441,13 @@ void RegisterHyperBosses() { actor->id == ACTOR_BOSS_GANON || // Ganondorf actor->id == ACTOR_BOSS_GANON2; // Ganon + uint8_t hyperBossesActive = + CVarGetInteger("gHyperBosses", 0) || + (gSaveContext.isBossRush && + gSaveContext.bossRushOptions[BR_OPTIONS_HYPERBOSSES] == BR_CHOICE_HYPERBOSSES_YES); + // Don't apply during cutscenes because it causes weird behaviour and/or crashes on some bosses. - if (CVarGetInteger("gHyperBosses", 0) && isBossActor && !Player_InBlockingCsMode(gPlayState, player)) { + if (hyperBossesActive && isBossActor && !Player_InBlockingCsMode(gPlayState, player)) { // Barinade needs to be updated in sequence to avoid unintended behaviour. if (actor->id == ACTOR_BOSS_VA) { // params -1 is BOSSVA_BODY diff --git a/soh/soh/OTRGlobals.h b/soh/soh/OTRGlobals.h index 5c8604f03..013831f70 100644 --- a/soh/soh/OTRGlobals.h +++ b/soh/soh/OTRGlobals.h @@ -88,7 +88,6 @@ void Ctx_ReadSaveFile(uintptr_t addr, void* dramAddr, size_t size); void Ctx_WriteSaveFile(uintptr_t addr, void* dramAddr, size_t size); uint64_t GetPerfCounter(); -uint64_t GetUnixTimestamp(); struct SkeletonHeader* ResourceMgr_LoadSkeletonByName(const char* path, SkelAnime* skelAnime); void ResourceMgr_UnregisterSkeleton(SkelAnime* skelAnime); void ResourceMgr_ClearSkeletons(); @@ -149,4 +148,12 @@ void SaveManager_ThreadPoolWait(); int32_t GetGIID(uint32_t itemID); #endif +#ifdef __cplusplus +extern "C" { +#endif +uint64_t GetUnixTimestamp(); +#ifdef __cplusplus +}; +#endif + #endif diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index bbc884066..e7f4c852a 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -7,6 +7,7 @@ #include "macros.h" #include #include +#include "soh/Enhancements/boss-rush/BossRush.h" #include #define NOGDI // avoid various windows defines that conflict with things in z64.h @@ -583,6 +584,10 @@ void SaveManager::InitFileNormal() { gSaveContext.pendingSale = ITEM_NONE; gSaveContext.pendingSaleMod = MOD_NONE; + if (gSaveContext.isBossRush) { + BossRush_InitSave(); + } + //RANDOTODO (ADD ITEMLOCATIONS TO GSAVECONTEXT) } @@ -748,7 +753,8 @@ void SaveManager::SaveFileThreaded(int fileNum, SaveContext* saveContext, int se // SaveSection creates a copy of gSaveContext to prevent mid-save data modification, and passes its reference to SaveFileThreaded void SaveManager::SaveSection(int fileNum, int sectionID, bool threaded) { - if (fileNum == 0xFF) { + // Don't save in Boss rush. + if (fileNum == 0xFF || fileNum == 0xFE) { return; } // Don't save a nonexistent section diff --git a/soh/soh/z_play_otr.cpp b/soh/soh/z_play_otr.cpp index 1980e22f8..65c849bfa 100644 --- a/soh/soh/z_play_otr.cpp +++ b/soh/soh/z_play_otr.cpp @@ -76,6 +76,10 @@ void OTRPlay_InitScene(PlayState* play, s32 spawn) { gSaveContext.worldMapArea = 0; OTRScene_ExecuteCommands(play, (LUS::Scene*)play->sceneSegment); Play_InitEnvironment(play, play->skyboxId); + // Unpause the timer for Boss Rush when the scene loaded isn't the Chamber of Sages. + if (gSaveContext.isBossRush && play->sceneNum != SCENE_KENJYANOMA) { + gSaveContext.isBossRushPaused = 0; + } /* auto data = static_cast(LUS::Context::GetInstance() ->GetResourceManager() ->LoadResource("object_link_child\\object_link_childVtx_01FE08") diff --git a/soh/src/code/z_kaleido_scope_call.c b/soh/src/code/z_kaleido_scope_call.c index 5d1f6937d..6f0648e08 100644 --- a/soh/src/code/z_kaleido_scope_call.c +++ b/soh/src/code/z_kaleido_scope_call.c @@ -56,7 +56,8 @@ void KaleidoScopeCall_Update(PlayState* play) { KaleidoMgrOverlay* kaleidoScopeOvl = &gKaleidoMgrOverlayTable[KALEIDO_OVL_KALEIDO_SCOPE]; PauseContext* pauseCtx = &play->pauseCtx; - if (!gSaveContext.sohStats.gameComplete) { + if (!gSaveContext.sohStats.gameComplete && + (!gSaveContext.isBossRush || !gSaveContext.isBossRushPaused)) { gSaveContext.sohStats.pauseTimer++; } diff --git a/soh/src/code/z_kanfont.c b/soh/src/code/z_kanfont.c index 2dd9cda80..217ef3c0a 100644 --- a/soh/src/code/z_kanfont.c +++ b/soh/src/code/z_kanfont.c @@ -177,6 +177,10 @@ void Font_LoadChar(Font* font, u8 character, u16 codePointIndex) { memcpy(&font->charTexBuf[codePointIndex], fntTbl[character], strlen(fntTbl[character]) + 1); } +void* Font_FetchCharTexture(u8 character) { + return fntTbl[character]; +} + /** * Loads a message box icon from message_static, such as the ending triangle/square or choice arrow into the * icon buffer. diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index 762e06b55..881637d8d 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -709,6 +709,10 @@ f32 sFontWidths[144] = { 14.0f, // ? }; +f32 Message_GetCharacterWidth(unsigned char characterIndex) { + return sFontWidths[characterIndex] * (R_TEXT_CHAR_SCALE / 100.0f); +} + u16 Message_DrawItemIcon(PlayState* play, u16 itemId, Gfx** p, u16 i) { s32 pad; Gfx* gfx = *p; diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 83bc7fc29..83066aff4 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -8,6 +8,8 @@ #include "soh/Enhancements/randomizer/randomizer_entrance.h" #include "libultraship/bridge.h" #include "soh/Enhancements/gameplaystats.h" +#include "soh/Enhancements/boss-rush/BossRushTypes.h" +#include "soh/Enhancements/custom-message/CustomMessageInterfaceAddon.h" #ifdef _MSC_VER #include @@ -882,7 +884,8 @@ void func_80083108(PlayState* play) { Interface_ChangeAlpha(12); } } - } else if (play->sceneNum == SCENE_KENJYANOMA) { + // Don't hide the HUD in the Chamber of Sages when in Boss Rush. + } else if (play->sceneNum == SCENE_KENJYANOMA && !gSaveContext.isBossRush) { Interface_ChangeAlpha(1); } else if (play->sceneNum == SCENE_TURIBORI) { gSaveContext.unk_13E7 = 2; @@ -4942,7 +4945,10 @@ void Interface_Draw(PlayState* play) { PosX_RC = PosX_RC_ori; } gDPSetPrimColor(OVERLAY_DISP++, 0, 0, rColor.r, rColor.g, rColor.b, interfaceCtx->magicAlpha); - OVERLAY_DISP = Gfx_TextureIA8(OVERLAY_DISP, gRupeeCounterIconTex, 16, 16, PosX_RC, PosY_RC, 16, 16, 1 << 10, 1 << 10); + // Draw Rupee icon. Hide in Boss Rush. + if (!gSaveContext.isBossRush) { + OVERLAY_DISP = Gfx_TextureIA8(OVERLAY_DISP, gRupeeCounterIconTex, 16, 16, PosX_RC, PosY_RC, 16, 16, 1 << 10, 1 << 10); + } switch (play->sceneNum) { case SCENE_BMORI1: @@ -5057,10 +5063,12 @@ void Interface_Draw(PlayState* play) { svar2 = rupeeDigitsFirst[CUR_UPG_VALUE(UPG_WALLET)]; svar5 = rupeeDigitsCount[CUR_UPG_VALUE(UPG_WALLET)]; - for (svar1 = 0, svar3 = 16; svar1 < svar5; svar1++, svar2++, svar3 += 8) { - OVERLAY_DISP = - Gfx_TextureI8(OVERLAY_DISP, ((u8*)digitTextures[interfaceCtx->counterDigits[svar2]]), 8, 16, - PosX_RC+svar3, PosY_RC, 8, 16, 1 << 10, 1 << 10); + // Draw Rupee Counter. Hide in Boss Rush. + if (!gSaveContext.isBossRush) { + for (svar1 = 0, svar3 = 16; svar1 < svar5; svar1++, svar2++, svar3 += 8) { + OVERLAY_DISP = Gfx_TextureI8(OVERLAY_DISP, ((u8*)digitTextures[interfaceCtx->counterDigits[svar2]]), + 8, 16, PosX_RC + svar3, PosY_RC, 8, 16, 1 << 10, 1 << 10); + } } } else { @@ -5983,7 +5991,8 @@ void Interface_Draw(PlayState* play) { void Interface_DrawTotalGameplayTimer(PlayState* play) { // Draw timer based on the Gameplay Stats total time. - if (CVarGetInteger("gGameplayStats.ShowIngameTimer", 0) && gSaveContext.fileNum >= 0 && gSaveContext.fileNum <= 2) { + if ((gSaveContext.isBossRush && gSaveContext.bossRushOptions[BR_OPTIONS_TIMER] == BR_CHOICE_TIMER_YES) || + (CVarGetInteger("gGameplayStats.ShowIngameTimer", 0) && gSaveContext.fileNum >= 0 && gSaveContext.fileNum <= 2)) { s32 X_Margins_Timer = 0; if (CVarGetInteger("gIGTUseMargins", 0) != 0) { @@ -6065,6 +6074,8 @@ void Interface_DrawTotalGameplayTimer(PlayState* play) { // Draw regular text. Change color based on if the timer is paused, running or the game is completed. if (gSaveContext.sohStats.gameComplete) { gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 120, 255, 0, 255); + } else if (gSaveContext.isBossRushPaused) { + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 150, 150, 150, 255); } else { gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, 255); } @@ -6483,3 +6494,66 @@ void Interface_Update(PlayState* play) { } } } + +void Interface_DrawTextCharacter(GraphicsContext* gfx, int16_t x, int16_t y, void* texture, uint16_t colorR, + uint16_t colorG, uint16_t colorB, uint16_t colorA, float textScale, uint8_t textShadow) { + + int32_t scale = R_TEXT_CHAR_SCALE * textScale; + int32_t sCharTexSize = (scale / 100.0f) * 16.0f; + int32_t sCharTexScale = 1024.0f / (scale / 100.0f); + + OPEN_DISPS(gfx); + + gDPPipeSync(POLY_OPA_DISP++); + + gDPLoadTextureBlock_4b(POLY_OPA_DISP++, texture, G_IM_FMT_I, FONT_CHAR_TEX_WIDTH, FONT_CHAR_TEX_HEIGHT, 0, + G_TX_NOMIRROR | G_TX_CLAMP, G_TX_NOMIRROR | G_TX_CLAMP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, + G_TX_NOLOD); + + if (textShadow) { + // Draw drop shadow + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 0, 0, 0, colorA); + gSPTextureRectangle(POLY_OPA_DISP++, (x + R_TEXT_DROP_SHADOW_OFFSET) << 2, (y + R_TEXT_DROP_SHADOW_OFFSET) << 2, + (x + R_TEXT_DROP_SHADOW_OFFSET + sCharTexSize) << 2, + (y + R_TEXT_DROP_SHADOW_OFFSET + sCharTexSize) << 2, G_TX_RENDERTILE, 0, 0, sCharTexScale, + sCharTexScale); + } + + gDPPipeSync(POLY_OPA_DISP++); + + // Draw normal text + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, colorR, colorG, colorB, colorA); + gSPTextureRectangle(POLY_OPA_DISP++, x << 2, y << 2, (x + sCharTexSize) << 2, (y + sCharTexSize) << 2, + G_TX_RENDERTILE, 0, 0, sCharTexScale, sCharTexScale); + + CLOSE_DISPS(gfx); +} + +uint16_t Interface_DrawTextLine(GraphicsContext* gfx, char text[], int16_t x, int16_t y, uint16_t colorR, + uint16_t colorG, uint16_t colorB, uint16_t colorA, float textScale, uint8_t textShadow) { + + uint16_t textureIndex; + uint16_t kerningOffset = 0; + uint16_t lineOffset = 0; + void* texture; + const char* processedText = Interface_ReplaceSpecialCharacters(text); + uint8_t textLength = strlen(processedText); + + for (uint16_t i = 0; i < textLength; i++) { + if (processedText[i] == '\n') { + lineOffset += 15 * textScale; + kerningOffset = 0; + } else { + textureIndex = processedText[i] - 32; + + if (textureIndex != 0) { + texture = Font_FetchCharTexture(textureIndex); + Interface_DrawTextCharacter(gfx, x + kerningOffset, y + lineOffset, texture, colorR, colorG, colorB, + colorA, textScale, textShadow); + } + kerningOffset += (uint16_t)(Message_GetCharacterWidth(textureIndex) * textScale); + } + } + + return kerningOffset; +} diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index 44e5442db..f4d0cdbe6 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -1170,7 +1170,8 @@ void Play_Update(PlayState* play) { play->gameplayFrames++; // Gameplay stat tracking - if (!gSaveContext.sohStats.gameComplete) { + if (!gSaveContext.sohStats.gameComplete && + (!gSaveContext.isBossRush || (gSaveContext.isBossRush && !gSaveContext.isBossRushPaused))) { gSaveContext.sohStats.playTimer++; gSaveContext.sohStats.sceneTimer++; gSaveContext.sohStats.roomTimer++; diff --git a/soh/src/code/z_sram.c b/soh/src/code/z_sram.c index 1735a2eb9..ce5651539 100644 --- a/soh/src/code/z_sram.c +++ b/soh/src/code/z_sram.c @@ -29,6 +29,10 @@ void Sram_InitDebugSave(void) { Save_InitFile(true); } +void Sram_InitBossRushSave(void) { + Save_InitFile(false); +} + /** * Copy save currently on the buffer to Save Context and complete various tasks to open the save. * This includes: diff --git a/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c b/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c index ea0562857..4a0a97711 100644 --- a/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c +++ b/soh/src/overlays/actors/ovl_Bg_Breakwall/z_bg_breakwall.c @@ -274,7 +274,8 @@ void BgBreakwall_Wait(BgBreakwall* this, PlayState* play) { } } - if (this->collider.base.acFlags & 2 || blueFireArrowHit) { + // Break the floor immediately in Boss Rush so the player can jump in the hole immediately. + if (this->collider.base.acFlags & 2 || blueFireArrowHit || gSaveContext.isBossRush) { Vec3f effectPos; s32 wallType = ((this->dyna.actor.params >> 13) & 3) & 0xFF; diff --git a/soh/src/overlays/actors/ovl_Boss_Dodongo/z_boss_dodongo.c b/soh/src/overlays/actors/ovl_Boss_Dodongo/z_boss_dodongo.c index a7387efb5..c35c06149 100644 --- a/soh/src/overlays/actors/ovl_Boss_Dodongo/z_boss_dodongo.c +++ b/soh/src/overlays/actors/ovl_Boss_Dodongo/z_boss_dodongo.c @@ -4,6 +4,7 @@ #include "overlays/actors/ovl_Door_Warp1/z_door_warp1.h" #include "scenes/dungeons/ddan_boss/ddan_boss_room_1.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_HOSTILE | ACTOR_FLAG_UPDATE_WHILE_CULLED | ACTOR_FLAG_DRAW_WHILE_CULLED) @@ -1346,6 +1347,7 @@ void BossDodongo_DeathCutscene(BossDodongo* this, PlayState* play) { this->cameraAt.y = camera->at.y; this->cameraAt.z = camera->at.z; gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_KING_DODONGO] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); break; case 5: tempSin = Math_SinS(this->actor.shape.rot.y - 0x1388) * 150.0f; @@ -1630,10 +1632,12 @@ void BossDodongo_DeathCutscene(BossDodongo* this, PlayState* play) { if (this->unk_1DA == 820) { Audio_QueueSeqCmd(SEQ_PLAYER_BGM_MAIN << 24 | NA_BGM_BOSS_CLEAR); - Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, - Math_SinS(this->actor.shape.rot.y) * -50.0f + this->actor.world.pos.x, - this->actor.world.pos.y, - Math_CosS(this->actor.shape.rot.y) * -50.0f + this->actor.world.pos.z, 0, 0, 0, 0, true); + if (!gSaveContext.isBossRush) { + Actor_Spawn( + &play->actorCtx, play, ACTOR_ITEM_B_HEART, + Math_SinS(this->actor.shape.rot.y) * -50.0f + this->actor.world.pos.x, this->actor.world.pos.y, + Math_CosS(this->actor.shape.rot.y) * -50.0f + this->actor.world.pos.z, 0, 0, 0, 0, true); + } } if (this->unk_1DA == 600) { camera = Play_GetCamera(play, MAIN_CAM); @@ -1647,8 +1651,11 @@ void BossDodongo_DeathCutscene(BossDodongo* this, PlayState* play) { Play_ChangeCameraStatus(play, MAIN_CAM, CAM_STAT_ACTIVE); func_80064534(play, &play->csCtx); func_8002DF54(play, &this->actor, 7); - Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, -890.0f, -1523.76f, - -3304.0f, 0, 0, 0, WARP_DUNGEON_CHILD); + if (!gSaveContext.isBossRush) { + Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, -890.0f, -1523.76f, -3304.0f, 0, 0, 0, WARP_DUNGEON_CHILD); + } else { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, -890.0f, -1523.76f, -3304.0f, 0, 0, 0, WARP_DUNGEON_ADULT, false); + } this->skelAnime.playSpeed = 0.0f; Flags_SetClear(play, play->roomCtx.curRoom.num); } diff --git a/soh/src/overlays/actors/ovl_Boss_Fd/z_boss_fd.c b/soh/src/overlays/actors/ovl_Boss_Fd/z_boss_fd.c index 48a99fa9d..91193f866 100644 --- a/soh/src/overlays/actors/ovl_Boss_Fd/z_boss_fd.c +++ b/soh/src/overlays/actors/ovl_Boss_Fd/z_boss_fd.c @@ -913,7 +913,7 @@ void BossFd_Fly(BossFd* this, PlayState* play) { this->actionFunc = BossFd_Wait; this->actor.world.pos.y -= 1000.0f; } - if (this->timers[0] == 7) { + if (this->timers[0] == 7 && !gSaveContext.isBossRush) { Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, this->actor.world.pos.x, this->actor.world.pos.y, this->actor.world.pos.z, 0, 0, 0, 0, true); } diff --git a/soh/src/overlays/actors/ovl_Boss_Fd2/z_boss_fd2.c b/soh/src/overlays/actors/ovl_Boss_Fd2/z_boss_fd2.c index 1c5eed4ae..b001f09e8 100644 --- a/soh/src/overlays/actors/ovl_Boss_Fd2/z_boss_fd2.c +++ b/soh/src/overlays/actors/ovl_Boss_Fd2/z_boss_fd2.c @@ -10,6 +10,7 @@ #include "overlays/actors/ovl_Door_Warp1/z_door_warp1.h" #include "vt.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_HOSTILE | ACTOR_FLAG_UPDATE_WHILE_CULLED | ACTOR_FLAG_DRAW_WHILE_CULLED) @@ -788,8 +789,13 @@ void BossFd2_Death(BossFd2* this, PlayState* play) { this->deathCamera = 0; func_80064534(play, &play->csCtx); func_8002DF54(play, &this->actor, 7); - Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, 0.0f, 100.0f, 0.0f, - 0, 0, 0, WARP_DUNGEON_ADULT); + if (!gSaveContext.isBossRush) { + Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, 0.0f, 100.0f, 0.0f, 0, 0, + 0, WARP_DUNGEON_ADULT); + } else { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, 0.0f, 100.0f, 0.0f, 0, 0, 0, + WARP_DUNGEON_ADULT, true); + } Flags_SetClear(play, play->roomCtx.curRoom.num); } break; @@ -894,6 +900,7 @@ void BossFd2_CollisionCheck(BossFd2* this, PlayState* play) { Audio_PlayActorSound2(&this->actor, NA_SE_EN_VALVAISA_DEAD); Enemy_StartFinishingBlow(play, &this->actor); gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_VOLVAGIA] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); } else if (damage) { BossFd2_SetupDamaged(this, play); this->work[FD2_DAMAGE_FLASH_TIMER] = 10; diff --git a/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c b/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c index 48c9f19f9..411596d38 100644 --- a/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c +++ b/soh/src/overlays/actors/ovl_Boss_Ganon/z_boss_ganon.c @@ -11,6 +11,7 @@ #include "assets/scenes/dungeons/ganon_boss/ganon_boss_scene.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #include @@ -569,7 +570,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { Play_ChangeCameraStatus(play, this->csCamIndex, CAM_STAT_ACTIVE); this->csCamFov = 60.0f; - if (gSaveContext.eventChkInf[7] & 0x100 || gSaveContext.n64ddFlag) { + if (gSaveContext.eventChkInf[7] & 0x100 || gSaveContext.n64ddFlag || gSaveContext.isBossRush) { // watched cutscene already, skip most of it this->csState = 17; this->csTimer = 0; @@ -580,7 +581,9 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { BossGanon_SetIntroCsCamera(this, 11); this->unk_198 = 2; this->timers[2] = 110; - gSaveContext.healthAccumulator = 0x140; + if (!(gSaveContext.isBossRush && gSaveContext.bossRushOptions[BR_OPTIONS_HEAL] == BR_CHOICE_HEAL_NEVER)) { + gSaveContext.healthAccumulator = 0x140; + } Audio_QueueSeqCmd(NA_BGM_STOP); } else { this->useOpenHand = true; @@ -901,7 +904,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { this->csTimer = 0; this->csCamFov = 60.0f; BossGanon_SetIntroCsCamera(this, 12); - if (!gSaveContext.n64ddFlag) { + if (!gSaveContext.n64ddFlag && !gSaveContext.isBossRush) { Message_StartTextbox(play, 0x70CB, NULL); } } @@ -925,7 +928,9 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { this->csState = 19; this->csTimer = 0; - Message_StartTextbox(play, 0x70CC, NULL); + if (!gSaveContext.isBossRush) { + Message_StartTextbox(play, 0x70CC, NULL); + } Animation_MorphToPlayOnce(&this->skelAnime, &gGanondorfRaiseHandStartAnim, -5.0f); this->triforceType = GDF_TRIFORCE_DORF; this->fwork[GDF_TRIFORCE_SCALE] = 10.0f; @@ -967,7 +972,7 @@ void BossGanon_IntroCutscene(BossGanon* this, PlayState* play) { if ((this->csTimer > 80) && (Message_GetState(&play->msgCtx) == TEXT_STATE_NONE)) { // In rando, skip past dark waves section straight to title card phase of the cutscene. - if (gSaveContext.n64ddFlag) { + if (gSaveContext.n64ddFlag || gSaveContext.isBossRush) { this->timers[2] = 30; this->csCamAt.x = this->unk_1FC.x - 10.0f; this->csCamAt.y = this->unk_1FC.y + 30.0f; @@ -1274,19 +1279,16 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) { this->actor.shape.yOffset = -7000.0f; this->actor.shape.rot.y = 0; - // In rando, skip Ganondorf dying and go straight to next scene. - // Commented out for potential future use. + // Skip Ganondorf dying and go straight to next scene. // The cutscene skip met a mixed reaction, so until we figure out a better way of doing it, - // it will stay not-skipped. - /*if (!gSaveContext.n64ddFlag) { + // it will stay not-skipped outside of Boss Rush (originally implemented for randomizer). + if (!gSaveContext.isBossRush) { this->csState = 1; this->csTimer = 0; } else { this->csState = 9; this->csTimer = 170; - }*/ - this->csState = 1; - this->csTimer = 0; + } this->useOpenHand = true; // fallthrough case 1: @@ -1537,7 +1539,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) { if (this->csTimer == 180) { play->sceneLoadFlag = 0x14; - if (gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SKIP_TOWER_ESCAPE)) { + if ((gSaveContext.n64ddFlag && Randomizer_GetSettingValue(RSK_SKIP_TOWER_ESCAPE) || gSaveContext.isBossRush)) { Flags_SetEventChkInf(0xC7); play->nextEntranceIndex = 0x517; } @@ -1560,7 +1562,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) { sBossGanonZelda = (EnZl3*)Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_EN_ZL3, 0.0f, 6000.0f, 0.0f, 0, 0, 0, 0x2000); - if (!gSaveContext.n64ddFlag) { + if (!gSaveContext.n64ddFlag && !gSaveContext.isBossRush) { this->csState = 101; } else { this->skelAnime.playSpeed = 1.0f; @@ -1688,7 +1690,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) { // fallthrough case 104: // In rando, fade out the white here as the earlier part is skipped. - if (gSaveContext.n64ddFlag) { + if (gSaveContext.n64ddFlag || gSaveContext.isBossRush) { Math_ApproachZeroF(&this->whiteFillAlpha, 1.0f, 10.0f); } @@ -1710,7 +1712,7 @@ void BossGanon_DeathAndTowerCutscene(BossGanon* this, PlayState* play) { if (this->csTimer == 50) { // In rando, skip the rest of the cutscene after the crystal around Zelda dissapears. - if (!gSaveContext.n64ddFlag) { + if (!gSaveContext.n64ddFlag && !gSaveContext.isBossRush) { sBossGanonZelda->unk_3C8 = 4; } else { this->csState = 108; @@ -2811,6 +2813,7 @@ void BossGanon_UpdateDamage(BossGanon* this, PlayState* play) { Audio_QueueSeqCmd(0x100100FF); this->screenFlashTimer = 4; gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_GANONDORF] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); } else { Audio_PlayActorSound2(&this->actor, NA_SE_EN_GANON_DAMAGE2); Audio_PlayActorSound2(&this->actor, NA_SE_EN_GANON_CUTBODY); diff --git a/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c b/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c index 5e46b7746..6d5256b9f 100644 --- a/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c +++ b/soh/src/overlays/actors/ovl_Boss_Ganon2/z_boss_ganon2.c @@ -7,6 +7,7 @@ #include "objects/object_ganon_anime3/object_ganon_anime3.h" #include "objects/object_geff/object_geff.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #include @@ -233,7 +234,7 @@ void func_808FD5F4(BossGanon2* this, PlayState* play) { sBossGanon2Zelda->actor.shape.rot.y = -0x7000; // In rando, skip past the cutscene to the part where the player takes control again. - if (!gSaveContext.n64ddFlag) { + if (!gSaveContext.n64ddFlag && !gSaveContext.isBossRush) { this->csState = 1; this->csTimer = 0; } else { @@ -1681,6 +1682,7 @@ void func_8090120C(BossGanon2* this, PlayState* play) { (player->swordState != 0) && (player->heldItemAction == PLAYER_IA_SWORD_MASTER)) { func_80064520(play, &play->csCtx); gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_GANON] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); gSaveContext.sohStats.gameComplete = true; this->unk_39E = Play_CreateSubCamera(play); Play_ChangeCameraStatus(play, MAIN_CAM, CAM_STAT_WAIT); diff --git a/soh/src/overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.c b/soh/src/overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.c index 5514a3333..aac63ed8c 100644 --- a/soh/src/overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.c +++ b/soh/src/overlays/actors/ovl_Boss_Ganondrof/z_boss_ganondrof.c @@ -11,6 +11,7 @@ #include "overlays/effects/ovl_Effect_Ss_Fhg_Flash/z_eff_ss_fhg_flash.h" #include "overlays/effects/ovl_Effect_Ss_Hahen/z_eff_ss_hahen.h" #include "overlays/actors/ovl_Door_Warp1/z_door_warp1.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_HOSTILE | ACTOR_FLAG_UPDATE_WHILE_CULLED | ACTOR_FLAG_DRAW_WHILE_CULLED) @@ -958,12 +959,12 @@ void BossGanondrof_Death(BossGanondrof* this, PlayState* play) { case DEATH_THROES: switch (this->work[GND_ACTION_STATE]) { case DEATH_SPASM: - if (Animation_OnFrame(&this->skelAnime, this->fwork[GND_END_FRAME]) && !gSaveContext.n64ddFlag) { + if (Animation_OnFrame(&this->skelAnime, this->fwork[GND_END_FRAME]) && !gSaveContext.n64ddFlag && !gSaveContext.isBossRush) { this->fwork[GND_END_FRAME] = Animation_GetLastFrame(&gPhantomGanonAirDamageAnim); Animation_Change(&this->skelAnime, &gPhantomGanonAirDamageAnim, 0.5f, 0.0f, this->fwork[GND_END_FRAME], ANIMMODE_ONCE_INTERP, 0.0f); this->work[GND_ACTION_STATE] = DEATH_LIMP; - } else if (gSaveContext.n64ddFlag) { + } else if (gSaveContext.n64ddFlag || gSaveContext.isBossRush) { // Skip to death scream animation and move ganondrof to middle this->deathState = DEATH_SCREAM; this->timers[0] = 50; @@ -990,7 +991,7 @@ void BossGanondrof_Death(BossGanondrof* this, PlayState* play) { bodyDecayLevel = 1; break; } - if (gSaveContext.n64ddFlag) { + if (gSaveContext.n64ddFlag || gSaveContext.isBossRush) { break; } Math_ApproachS(&this->actor.shape.rot.y, this->work[GND_VARIANCE_TIMER] * -100, 5, 0xBB8); @@ -1087,8 +1088,8 @@ void BossGanondrof_Death(BossGanondrof* this, PlayState* play) { bodyDecayLevel = 10; if (this->timers[0] == 150) { Audio_QueueSeqCmd(SEQ_PLAYER_BGM_MAIN << 24 | NA_BGM_BOSS_CLEAR); - Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, GND_BOSSROOM_CENTER_X, - GND_BOSSROOM_CENTER_Y, GND_BOSSROOM_CENTER_Z, 0, 0, 0, WARP_DUNGEON_ADULT, true); + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, GND_BOSSROOM_CENTER_X, GND_BOSSROOM_CENTER_Y, + GND_BOSSROOM_CENTER_Z, 0, 0, 0, WARP_DUNGEON_ADULT, true); } Math_ApproachZeroF(&this->cameraEye.y, 0.05f, 1.0f); // GND_BOSSROOM_CENTER_Y + 33.0f @@ -1104,8 +1105,10 @@ void BossGanondrof_Death(BossGanondrof* this, PlayState* play) { this->deathCamera = 0; func_80064534(play, &play->csCtx); func_8002DF54(play, &this->actor, 7); - Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, GND_BOSSROOM_CENTER_X, - GND_BOSSROOM_CENTER_Y, GND_BOSSROOM_CENTER_Z + 200.0f, 0, 0, 0, 0, true); + if (!gSaveContext.isBossRush) { + Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, GND_BOSSROOM_CENTER_X, GND_BOSSROOM_CENTER_Y, + GND_BOSSROOM_CENTER_Z + 200.0f, 0, 0, 0, 0, true); + } this->actor.child = &horse->actor; this->killActor = true; horse->killActor = true; @@ -1246,6 +1249,7 @@ void BossGanondrof_CollisionCheck(BossGanondrof* this, PlayState* play) { BossGanondrof_SetupDeath(this, play); Enemy_StartFinishingBlow(play, &this->actor); gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_PHANTOM_GANON] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); return; } } diff --git a/soh/src/overlays/actors/ovl_Boss_Goma/z_boss_goma.c b/soh/src/overlays/actors/ovl_Boss_Goma/z_boss_goma.c index 9cbe7893a..d1166aade 100644 --- a/soh/src/overlays/actors/ovl_Boss_Goma/z_boss_goma.c +++ b/soh/src/overlays/actors/ovl_Boss_Goma/z_boss_goma.c @@ -4,6 +4,7 @@ #include "overlays/actors/ovl_En_Goma/z_en_goma.h" #include "overlays/actors/ovl_Door_Shutter/z_door_shutter.h" #include "overlays/actors/ovl_Door_Warp1/z_door_warp1.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_HOSTILE | ACTOR_FLAG_UPDATE_WHILE_CULLED | ACTOR_FLAG_DRAW_WHILE_CULLED) @@ -1117,8 +1118,10 @@ void BossGoma_Defeated(BossGoma* this, PlayState* play) { this->timer = 70; this->decayingProgress = 0; this->subCameraFollowSpeed = 0.0f; - Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, this->actor.world.pos.x, - this->actor.world.pos.y, this->actor.world.pos.z, 0, 0, 0, 0, true); + if (!gSaveContext.isBossRush) { + Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, this->actor.world.pos.x, + this->actor.world.pos.y, this->actor.world.pos.z, 0, 0, 0, 0, true); + } } break; @@ -1149,8 +1152,13 @@ void BossGoma_Defeated(BossGoma* this, PlayState* play) { } } - Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, childPos.x, - this->actor.world.pos.y, childPos.z, 0, 0, 0, WARP_DUNGEON_CHILD); + if (!gSaveContext.isBossRush) { + Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, childPos.x, + this->actor.world.pos.y, childPos.z, 0, 0, 0, WARP_DUNGEON_CHILD); + } else { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, childPos.x, this->actor.world.pos.y, + childPos.z, 0, 0, 0, WARP_DUNGEON_ADULT, false); + } Flags_SetClear(play, play->roomCtx.curRoom.num); } @@ -1834,6 +1842,7 @@ void BossGoma_UpdateHit(BossGoma* this, PlayState* play) { BossGoma_SetupDefeated(this, play); Enemy_StartFinishingBlow(play, &this->actor); gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_GOHMA] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); } this->invincibilityFrames = 10; diff --git a/soh/src/overlays/actors/ovl_Boss_Mo/z_boss_mo.c b/soh/src/overlays/actors/ovl_Boss_Mo/z_boss_mo.c index bc6537484..0c85e5f78 100644 --- a/soh/src/overlays/actors/ovl_Boss_Mo/z_boss_mo.c +++ b/soh/src/overlays/actors/ovl_Boss_Mo/z_boss_mo.c @@ -12,6 +12,7 @@ #include "vt.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #include @@ -1115,11 +1116,16 @@ void BossMo_Tentacle(BossMo* this, PlayState* play) { BossMo_SpawnDroplet(MO_FX_DROPLET, (BossMoEffect*)play->specialEffects, &spD4, &spE0, ((300 - indS1) * .0015f) + 0.13f); } - Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, - this->actor.world.pos.x, -280.0f, this->actor.world.pos.z, 0, 0, 0, - WARP_DUNGEON_ADULT); - Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, this->actor.world.pos.x + 200.0f, - -280.0f, this->actor.world.pos.z, 0, 0, 0, 0, true); + if (!gSaveContext.isBossRush) { + Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, + this->actor.world.pos.x, -280.0f, this->actor.world.pos.z, 0, 0, 0, + WARP_DUNGEON_ADULT); + Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, this->actor.world.pos.x + 200.0f, + -280.0f, this->actor.world.pos.z, 0, 0, 0, 0, true); + } else { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, this->actor.world.pos.x, -280.0f, + this->actor.world.pos.z, 0, 0, 0, WARP_DUNGEON_ADULT, true); + } Audio_QueueSeqCmd(SEQ_PLAYER_BGM_MAIN << 24 | NA_BGM_BOSS_CLEAR); Flags_SetClear(play, play->roomCtx.curRoom.num); } @@ -1788,6 +1794,7 @@ void BossMo_CoreCollisionCheck(BossMo* this, PlayState* play) { ((sMorphaTent1->csCamera == 0) && (sMorphaTent2 != NULL) && (sMorphaTent2->csCamera == 0))) { Enemy_StartFinishingBlow(play, &this->actor); gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_MORPHA] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); Audio_QueueSeqCmd(0x1 << 28 | SEQ_PLAYER_BGM_MAIN << 24 | 0x100FF); this->csState = MO_DEATH_START; sMorphaTent1->drawActor = false; diff --git a/soh/src/overlays/actors/ovl_Boss_Sst/z_boss_sst.c b/soh/src/overlays/actors/ovl_Boss_Sst/z_boss_sst.c index 52d2a9e60..2ea30cdee 100644 --- a/soh/src/overlays/actors/ovl_Boss_Sst/z_boss_sst.c +++ b/soh/src/overlays/actors/ovl_Boss_Sst/z_boss_sst.c @@ -11,6 +11,7 @@ #include "overlays/actors/ovl_Bg_Sst_Floor/z_bg_sst_floor.h" #include "overlays/actors/ovl_Door_Warp1/z_door_warp1.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_HOSTILE | ACTOR_FLAG_UPDATE_WHILE_CULLED | ACTOR_FLAG_DRAW_WHILE_CULLED | ACTOR_FLAG_DRAGGED_BY_HOOKSHOT) @@ -1201,11 +1202,13 @@ void BossSst_HeadFinish(BossSst* this, PlayState* play) { Flags_SetClear(play, play->roomCtx.curRoom.num); } } else if (this->effects[0].alpha == 0) { - Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, ROOM_CENTER_X, ROOM_CENTER_Y, ROOM_CENTER_Z, 0, - 0, 0, WARP_DUNGEON_ADULT, true); - Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, - (Math_SinS(this->actor.shape.rot.y) * 200.0f) + ROOM_CENTER_X, ROOM_CENTER_Y, - Math_CosS(this->actor.shape.rot.y) * 200.0f + ROOM_CENTER_Z, 0, 0, 0, 0, true); + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, ROOM_CENTER_X, ROOM_CENTER_Y, ROOM_CENTER_Z, 0, 0, 0, + WARP_DUNGEON_ADULT, true); + if (!gSaveContext.isBossRush) { + Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, + (Math_SinS(this->actor.shape.rot.y) * 200.0f) + ROOM_CENTER_X, ROOM_CENTER_Y, + Math_CosS(this->actor.shape.rot.y) * 200.0f + ROOM_CENTER_Z, 0, 0, 0, 0, true); + } BossSst_SetCameraTargets(1.0f, 7); this->effectMode = BONGO_NULL; } else if (this->timer == 0) { @@ -2561,6 +2564,7 @@ void BossSst_HeadCollisionCheck(BossSst* this, PlayState* play) { Enemy_StartFinishingBlow(play, &this->actor); BossSst_HeadSetupDeath(this, play); gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_BONGO_BONGO] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); } else { BossSst_HeadSetupDamage(this); } diff --git a/soh/src/overlays/actors/ovl_Boss_Tw/z_boss_tw.c b/soh/src/overlays/actors/ovl_Boss_Tw/z_boss_tw.c index 2de93e7bd..8765e4c51 100644 --- a/soh/src/overlays/actors/ovl_Boss_Tw/z_boss_tw.c +++ b/soh/src/overlays/actors/ovl_Boss_Tw/z_boss_tw.c @@ -4,6 +4,7 @@ #include "objects/object_tw/object_tw.h" #include "overlays/actors/ovl_Door_Warp1/z_door_warp1.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #include @@ -2364,7 +2365,7 @@ void BossTw_DeathCSMsgSfx(BossTw* this, PlayState* play) { sp35 = 0; // Skip ahead to last part of the cutscene in rando - if (this->work[CS_TIMER_2] == 10 && gSaveContext.n64ddFlag) { + if (this->work[CS_TIMER_2] == 10 && (gSaveContext.n64ddFlag || gSaveContext.isBossRush)) { this->work[CS_TIMER_2] = 860; } @@ -2549,7 +2550,7 @@ void BossTw_DeathCSMsgSfx(BossTw* this, PlayState* play) { // Add seperate timings for the "beam" that opens and closes around the sisters // Needed because we skip ahead in cutscene timer value so it never gets called otherwise - if (gSaveContext.n64ddFlag) { + if (gSaveContext.n64ddFlag || gSaveContext.isBossRush) { if (this->work[CS_TIMER_2] < 900) { Math_ApproachF(&this->workf[UNK_F18], 255.0f, 0.1f, 5.0f); } else if (this->work[CS_TIMER_2] > 910) { @@ -2794,9 +2795,14 @@ void BossTw_TwinrovaDeathCS(BossTw* this, PlayState* play) { func_80064534(play, &play->csCtx); func_8002DF54(play, &this->actor, 7); Audio_QueueSeqCmd(SEQ_PLAYER_BGM_MAIN << 24 | NA_BGM_BOSS_CLEAR); - Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, 600.0f, 230.0f, - 0.0f, 0, 0, 0, WARP_DUNGEON_ADULT); - Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, -600.0f, 230.f, 0.0f, 0, 0, 0, 0, true); + if (!gSaveContext.isBossRush) { + Actor_SpawnAsChild(&play->actorCtx, &this->actor, play, ACTOR_DOOR_WARP1, 600.0f, 230.0f, 0.0f, 0, + 0, 0, WARP_DUNGEON_ADULT); + Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, -600.0f, 230.f, 0.0f, 0, 0, 0, 0, true); + } else { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, 600.0f, 230.0f, 0.0f, 0, 0, 0, + WARP_DUNGEON_ADULT, true); + } this->actor.world.pos.y = -2000.0f; this->workf[UNK_F18] = 0.0f; sKoumePtr->visible = sKotakePtr->visible = false; @@ -5287,6 +5293,7 @@ void BossTw_TwinrovaDamage(BossTw* this, PlayState* play, u8 damage) { Enemy_StartFinishingBlow(play, &this->actor); Audio_PlayActorSound2(&this->actor, NA_SE_EN_TWINROBA_YOUNG_DEAD); gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_TWINROVA] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); return; } diff --git a/soh/src/overlays/actors/ovl_Boss_Va/z_boss_va.c b/soh/src/overlays/actors/ovl_Boss_Va/z_boss_va.c index 262b121fa..625a33eed 100644 --- a/soh/src/overlays/actors/ovl_Boss_Va/z_boss_va.c +++ b/soh/src/overlays/actors/ovl_Boss_Va/z_boss_va.c @@ -12,8 +12,10 @@ #include "objects/object_bv/object_bv.h" #include "overlays/actors/ovl_En_Boom/z_en_boom.h" #include "objects/gameplay_keep/gameplay_keep.h" +#include "overlays/actors/ovl_Door_Warp1/z_door_warp1.h" #include "soh/frame_interpolation.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #define FLAGS (ACTOR_FLAG_TARGETABLE | ACTOR_FLAG_HOSTILE | ACTOR_FLAG_UPDATE_WHILE_CULLED | ACTOR_FLAG_DRAW_WHILE_CULLED) @@ -1396,6 +1398,7 @@ void BossVa_BodyPhase4(BossVa* this, PlayState* play) { BossVa_SetupBodyDeath(this, play); Enemy_StartFinishingBlow(play, &this->actor); gSaveContext.sohStats.itemTimestamp[TIMESTAMP_DEFEAT_BARINADE] = GAMEPLAYSTAT_TOTAL_TIME; + BossRush_HandleCompleteBoss(play); return; } this->actor.speedXZ = -10.0f; @@ -1650,8 +1653,10 @@ void BossVa_BodyDeath(BossVa* this, PlayState* play) { func_8002DF54(play, &this->actor, 7); sCsState++; - Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, this->actor.world.pos.x, - this->actor.world.pos.y, this->actor.world.pos.z, 0, 0, 0, 0, true); + if (!gSaveContext.isBossRush) { + Actor_Spawn(&play->actorCtx, play, ACTOR_ITEM_B_HEART, this->actor.world.pos.x, + this->actor.world.pos.y, this->actor.world.pos.z, 0, 0, 0, 0, true); + } for (i = 2, sp7C = 2; i > 0; i--) { if (Math_Vec3f_DistXYZ(&sWarpPos[i], &player->actor.world.pos) < @@ -1660,8 +1665,13 @@ void BossVa_BodyDeath(BossVa* this, PlayState* play) { } } - Actor_Spawn(&play->actorCtx, play, ACTOR_EN_RU1, sWarpPos[sp7C].x, sWarpPos[sp7C].y, - sWarpPos[sp7C].z, 0, 0, 0, 0, true); + if (!gSaveContext.isBossRush) { + Actor_Spawn(&play->actorCtx, play, ACTOR_EN_RU1, sWarpPos[sp7C].x, sWarpPos[sp7C].y, + sWarpPos[sp7C].z, 0, 0, 0, 0, true); + } else { + Actor_Spawn(&play->actorCtx, play, ACTOR_DOOR_WARP1, sWarpPos[sp7C].x, sWarpPos[sp7C].y, + sWarpPos[sp7C].z, 0, 0, 0, WARP_DUNGEON_ADULT, false); + } } case DEATH_FINISH: Rand_CenteredFloat(0.5f); diff --git a/soh/src/overlays/actors/ovl_Demo_Sa/z_demo_sa.c b/soh/src/overlays/actors/ovl_Demo_Sa/z_demo_sa.c index 805e89f9f..9449086cf 100644 --- a/soh/src/overlays/actors/ovl_Demo_Sa/z_demo_sa.c +++ b/soh/src/overlays/actors/ovl_Demo_Sa/z_demo_sa.c @@ -8,6 +8,7 @@ #include "overlays/actors/ovl_En_Elf/z_en_elf.h" #include "overlays/actors/ovl_Door_Warp1/z_door_warp1.h" #include "objects/object_sa/object_sa.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #include "vt.h" @@ -253,11 +254,19 @@ void func_8098E960(DemoSa* this, PlayState* play) { if ((gSaveContext.chamberCutsceneNum == 0) && (gSaveContext.sceneSetupIndex < 4)) { player = GET_PLAYER(play); - this->action = 1; - play->csCtx.segment = D_8099010C; - gSaveContext.cutsceneTrigger = 2; - Item_Give(play, ITEM_MEDALLION_FOREST); - player->actor.world.rot.y = player->actor.shape.rot.y = this->actor.world.rot.y + 0x8000; + if (!gSaveContext.isBossRush) { + this->action = 1; + play->csCtx.segment = D_8099010C; + gSaveContext.cutsceneTrigger = 2; + Item_Give(play, ITEM_MEDALLION_FOREST); + player->actor.world.rot.y = player->actor.shape.rot.y = this->actor.world.rot.y + 0x8000; + } else { + this->action = 1; + if (gSaveContext.linkAge == LINK_AGE_CHILD) { + player->actor.world.rot.y = player->actor.shape.rot.y = -5461 + 0x8000; + } + BossRush_SpawnBlueWarps(play); + } } } diff --git a/soh/src/overlays/actors/ovl_Door_Warp1/z_door_warp1.c b/soh/src/overlays/actors/ovl_Door_Warp1/z_door_warp1.c index 965409ac2..e0e160518 100644 --- a/soh/src/overlays/actors/ovl_Door_Warp1/z_door_warp1.c +++ b/soh/src/overlays/actors/ovl_Door_Warp1/z_door_warp1.c @@ -1,6 +1,7 @@ #include "z_door_warp1.h" #include "objects/object_warp1/object_warp1.h" #include "soh/Enhancements/randomizer/randomizer_entrance.h" +#include "soh/Enhancements/boss-rush/BossRush.h" #define FLAGS 0 @@ -251,8 +252,13 @@ void DoorWarp1_SetupBlueCrystal(DoorWarp1* this, PlayState* play) { -255; } - play->envCtx.adjFogNear = -500; - this->warpTimer = 30; + if (!gSaveContext.isBossRush) { + play->envCtx.adjFogNear = -500; + this->warpTimer = 30; + } else { + play->envCtx.adjFogNear = 0; + this->warpTimer = 0; + } this->unk_1B8 = 4000; DoorWarp1_SetupAction(this, DoorWarp1_BlueCrystal); } @@ -293,7 +299,11 @@ void DoorWarp1_SetPlayerPos(DoorWarp1* this, PlayState* play) { player->actor.velocity.y = 0.0f; player->actor.world.pos.x = this->actor.world.pos.x; - player->actor.world.pos.y = this->actor.world.pos.y + 55.0f; + if (!gSaveContext.isBossRush) { + player->actor.world.pos.y = this->actor.world.pos.y + 55.0f; + } else { + player->actor.world.pos.y = this->actor.world.pos.y; + } player->actor.world.pos.z = this->actor.world.pos.z; } @@ -313,9 +323,13 @@ void func_80999214(DoorWarp1* this, PlayState* play) { Math_SmoothStepToF(&this->crystalAlpha, 255.0f, 0.2f, 5.0f, 0.1f); - darkness = (f32)(40 - this->warpTimer) / 40.0f; - darkness = CLAMP_MIN(darkness, 0); - + if (!gSaveContext.isBossRush) { + darkness = (f32)(40 - this->warpTimer) / 40.0f; + darkness = CLAMP_MIN(darkness, 0); + } else { + darkness = 0.0f; + } + for (i = 0; i < 3; i++) { play->envCtx.adjAmbientColor[i] = play->envCtx.adjFogColor[i] = play->envCtx.adjLight1Color[i] = -255.0f * darkness; @@ -352,7 +366,9 @@ void func_80999348(DoorWarp1* this, PlayState* play) { void DoorWarp1_FloatPlayer(DoorWarp1* this, PlayState* play) { Player* player = GET_PLAYER(play); - player->actor.gravity = -0.1f; + if (!gSaveContext.isBossRush) { + player->actor.gravity = -0.1f; + } } void DoorWarp1_PurpleCrystal(DoorWarp1* this, PlayState* play) { @@ -744,6 +760,11 @@ void DoorWarp1_AdultWarpIdle(DoorWarp1* this, PlayState* play) { Audio_PlayActorSound2(&this->actor, NA_SE_EV_WARP_HOLE - SFX_FLAG); if (DoorWarp1_PlayerInRange(this, play)) { + // Heal player in Boss Rush + if (gSaveContext.isBossRush) { + BossRush_HandleBlueWarpHeal(play); + } + player = GET_PLAYER(play); if (gSaveContext.n64ddFlag) { @@ -805,7 +826,9 @@ void DoorWarp1_AdultWarpOut(DoorWarp1* this, PlayState* play) { this->warpTimer++; if (this->warpTimer > sWarpTimerTarget && gSaveContext.nextCutsceneIndex == 0xFFEF) { - if (play->sceneNum == SCENE_MORIBOSSROOM) { + if (gSaveContext.isBossRush) { + BossRush_HandleBlueWarp(play, this->actor.world.pos.x, this->actor.world.pos.z); + } else if (play->sceneNum == SCENE_MORIBOSSROOM) { if (!(gSaveContext.eventChkInf[4] & 0x100)) { gSaveContext.eventChkInf[4] |= 0x100; Flags_SetRandomizerInf(RAND_INF_DUNGEONS_DONE_FOREST_TEMPLE); diff --git a/soh/src/overlays/actors/ovl_En_Box/z_en_box.c b/soh/src/overlays/actors/ovl_En_Box/z_en_box.c index 67938c0f1..927914371 100644 --- a/soh/src/overlays/actors/ovl_En_Box/z_en_box.c +++ b/soh/src/overlays/actors/ovl_En_Box/z_en_box.c @@ -202,6 +202,11 @@ void EnBox_Init(Actor* thisx, PlayState* play2) { if (play->sceneNum == SCENE_BMORI1 && this->dyna.actor.params == 10222) { this->movementFlags = ENBOX_MOVE_IMMOBILE; } + + // Delete chests in Boss Rush. Mainly for the chest in King Dodongo's boss room. + if (gSaveContext.isBossRush) { + EnBox_SetupAction(this, EnBox_Destroy); + } } void EnBox_Destroy(Actor* thisx, PlayState* play) { diff --git a/soh/src/overlays/actors/ovl_En_Kusa/z_en_kusa.c b/soh/src/overlays/actors/ovl_En_Kusa/z_en_kusa.c index 3f561ea5c..344691067 100644 --- a/soh/src/overlays/actors/ovl_En_Kusa/z_en_kusa.c +++ b/soh/src/overlays/actors/ovl_En_Kusa/z_en_kusa.c @@ -277,6 +277,11 @@ void EnKusa_Destroy(Actor* thisx, PlayState* play2) { } void EnKusa_SetupWaitObject(EnKusa* this) { + // Kill bushes in Boss Rush. Used in Gohma's arena. + if (gSaveContext.isBossRush) { + Actor_Kill(this); + } + EnKusa_SetupAction(this, EnKusa_WaitObject); } diff --git a/soh/src/overlays/actors/ovl_Obj_Tsubo/z_obj_tsubo.c b/soh/src/overlays/actors/ovl_Obj_Tsubo/z_obj_tsubo.c index 5d5757b6f..ddbba63ae 100644 --- a/soh/src/overlays/actors/ovl_Obj_Tsubo/z_obj_tsubo.c +++ b/soh/src/overlays/actors/ovl_Obj_Tsubo/z_obj_tsubo.c @@ -221,6 +221,11 @@ void ObjTsubo_WaterBreak(ObjTsubo* this, PlayState* play) { } void ObjTsubo_SetupWaitForObject(ObjTsubo* this) { + // Remove pots in Boss Rush. Present in Barinade's and Ganondorf's arenas. + if (gSaveContext.isBossRush) { + Actor_Kill(this); + } + this->actionFunc = ObjTsubo_WaitForObject; } diff --git a/soh/src/overlays/actors/ovl_player_actor/z_player.c b/soh/src/overlays/actors/ovl_player_actor/z_player.c index 794aa86bc..80867a56b 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -4437,7 +4437,8 @@ s32 func_80839800(Player* this, PlayState* play) { if ((this->doorType != PLAYER_DOORTYPE_NONE) && (!(this->stateFlags1 & PLAYER_STATE1_ITEM_OVER_HEAD) || ((this->heldActor != NULL) && (this->heldActor->id == ACTOR_EN_RU1)))) { - if (CHECK_BTN_ALL(sControlInput->press.button, BTN_A) || (func_8084F9A0 == this->func_674)) { + // Disable doors in Boss Rush so the player can't leave the boss rooms backwards. + if ((CHECK_BTN_ALL(sControlInput->press.button, BTN_A) || (func_8084F9A0 == this->func_674)) && !gSaveContext.isBossRush) { doorActor = this->doorActor; if (this->doorType <= PLAYER_DOORTYPE_AJAR) { diff --git a/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h b/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h index b926d7fd9..f4a23d38b 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h +++ b/soh/src/overlays/gamestates/ovl_file_choose/file_choose.h @@ -61,6 +61,10 @@ typedef enum { CM_START_QUEST_MENU, CM_QUEST_TO_MAIN, CM_NAME_ENTRY_TO_QUEST_MENU, + CM_ROTATE_TO_BOSS_RUSH_MENU, + CM_BOSS_RUSH_MENU, + CM_START_BOSS_RUSH_MENU, + CM_BOSS_RUSH_TO_QUEST, } ConfigMode; typedef enum { @@ -168,6 +172,13 @@ typedef enum { /* 99 */ FS_KBD_BTN_NONE = 99 } KeyboardButton; +typedef enum { + /* 00 */ FS_QUEST_NORMAL, + /* 01 */ FS_QUEST_MASTER, + /* 02 */ FS_QUEST_RANDOMIZER, + /* 03 */ FS_QUEST_BOSSRUSH, +} FileSelectQuest; + void FileChoose_SetupCopySource(GameState* thisx); void FileChoose_SelectCopySource(GameState* thisx); void FileChoose_SetupCopyDest1(GameState* thisx); diff --git a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c index 262500527..8b9f3f317 100644 --- a/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c +++ b/soh/src/overlays/gamestates/ovl_file_choose/z_file_choose.c @@ -1,4 +1,4 @@ -#include "file_choose.h" +#include "file_choose.h" #include @@ -11,28 +11,20 @@ #include "objects/gameplay_keep/gameplay_keep.h" #include "soh_assets.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" - +#include "soh/Enhancements/boss-rush/BossRush.h" #include "soh/Enhancements/custom-message/CustomMessageTypes.h" -#define NORMAL_QUEST 0 -#define MASTER_QUEST 1 -#define RANDOMIZER_QUEST 2 -#define MIN_QUEST (ResourceMgr_GameHasOriginal() ? NORMAL_QUEST : MASTER_QUEST) -u8 getMaxQuest() { - if ((strnlen(CVarGetString("gSpoilerLog", ""), 1) != 0)) { - return RANDOMIZER_QUEST; +#define MIN_QUEST (ResourceMgr_GameHasOriginal() ? FS_QUEST_NORMAL : FS_QUEST_MASTER) +#define MAX_QUEST FS_QUEST_BOSSRUSH +u8 hasRandomizerQuest() { + if (strnlen(CVarGetString("gSpoilerLog", ""), 1) != 0) { + return 1; } - - if (ResourceMgr_GameHasMasterQuest()) { - return MASTER_QUEST; - } - - return NORMAL_QUEST; + return 0; } -#define MAX_QUEST getMaxQuest() void FileChoose_DrawTextureI8(GraphicsContext* gfxCtx, const void* texture, s16 texWidth, s16 texHeight, s16 rectLeft, s16 rectTop, - s16 rectWidth, s16 rectHeight, u16 dsdx, u16 dtdy) { + s16 rectWidth, s16 rectHeight, s16 dsdx, s16 dtdy) { OPEN_DISPS(gfxCtx); gDPLoadTextureBlock(POLY_OPA_DISP++, texture, G_IM_FMT_I, G_IM_SIZ_8b, texWidth, texHeight, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); @@ -305,7 +297,7 @@ void DrawSeedHashSprites(FileChooseContext* this) { // Draw icons on the main menu, when a rando file is selected, and when quest selection is set to rando if ((this->configMode == CM_MAIN_MENU && (this->selectMode != SM_CONFIRM_FILE || Save_GetSaveMetaInfo(this->selectedFileIndex)->randoSave == 1)) || - (this->configMode == CM_QUEST_MENU && this->questType[this->buttonIndex] == RANDOMIZER_QUEST)) { + (this->configMode == CM_QUEST_MENU && this->questType[this->buttonIndex] == FS_QUEST_RANDOMIZER)) { if (this->fileInfoAlpha[this->selectedFileIndex] > 0) { // Use file info alpha to match fading @@ -415,24 +407,7 @@ void FileChoose_UpdateMainMenu(GameState* thisx) { if (!Save_GetSaveMetaInfo(this->buttonIndex)->valid) { Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); this->prevConfigMode = this->configMode; - if (MIN_QUEST != MAX_QUEST) { - this->configMode = CM_ROTATE_TO_QUEST_MENU; - } else { - this->configMode = CM_ROTATE_TO_NAME_ENTRY; - gSaveContext.isMasterQuest = MIN_QUEST == MASTER_QUEST; - this->questType[this->buttonIndex] = MIN_QUEST; - CVarSetInteger("gOnFileSelectNameEntry", 1); - this->kbdButton = FS_KBD_BTN_NONE; - this->charPage = FS_CHAR_PAGE_ENG; - this->kbdX = 0; - this->kbdY = 0; - this->charIndex = 0; - this->charBgAlpha = 0; - this->newFileNameCharCount = CVarGetInteger("gLinkDefaultName", 0) ? 4 : 0; - this->nameEntryBoxPosX = 120; - this->nameEntryBoxAlpha = 0; - memcpy(Save_GetSaveMetaInfo(this->buttonIndex)->playerName, CVarGetInteger("gLinkDefaultName", 0) ? &linkName : &emptyName, 8); - } + this->configMode = CM_ROTATE_TO_QUEST_MENU; this->logoAlpha = 0; } else if(!FileChoose_IsSaveCompatible(Save_GetSaveMetaInfo(this->buttonIndex))) { Audio_PlaySoundGeneral(NA_SE_SY_FSEL_ERROR, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); @@ -588,6 +563,19 @@ void FileChoose_StartQuestMenu(GameState* thisx) { } } +void FileChoose_StartBossRushMenu(GameState* thisx) { + FileChooseContext* this = (FileChooseContext*)thisx; + + this->logoAlpha -= 25; + this->bossRushUIAlpha = 0; + this->bossRushArrowOffset = 0; + + if (this->logoAlpha >= 0) { + this->logoAlpha = 0; + this->configMode = CM_BOSS_RUSH_MENU; + } +} + void FileChoose_UpdateQuestMenu(GameState* thisx) { static u8 emptyName[] = { 0x3E, 0x3E, 0x3E, 0x3E, 0x3E, 0x3E, 0x3E, 0x3E }; static u8 linkName[] = { 0x15, 0x2C, 0x31, 0x2E, 0x3E, 0x3E, 0x3E, 0x3E }; @@ -595,31 +583,30 @@ void FileChoose_UpdateQuestMenu(GameState* thisx) { FileChooseContext* this = (FileChooseContext*)thisx; Input* input = &this->state.input[0]; s8 i = 0; - bool dpad = CVarGetInteger("gDpadText", 0);(dpad && CHECK_BTN_ANY(input->press.button, BTN_DDOWN | BTN_DUP)); + bool dpad = CVarGetInteger("gDpadText", 0); FileChoose_UpdateRandomizer(); if (ABS(this->stickRelX) > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DLEFT | BTN_DRIGHT))) { if (this->stickRelX > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DRIGHT))) { this->questType[this->buttonIndex] += 1; - if (this->questType[this->buttonIndex] == MASTER_QUEST && !ResourceMgr_GameHasMasterQuest()) { - // the only case not handled by the MIN/MAX_QUEST logic below. This will either put it at - // above MAX_QUEST in which case it will wrap back around, or it will put it on MAX_QUEST - // in which case if MAX_QUEST even is that number it will be a valid selection that won't - // crash. + while ((this->questType[this->buttonIndex] == FS_QUEST_MASTER && !ResourceMgr_GameHasMasterQuest()) || + (this->questType[this->buttonIndex] == FS_QUEST_RANDOMIZER && !hasRandomizerQuest())) { + // If Master Quest is selected without a Master Quest OTR present or when Randomizer Quest is + // selected without a loaded Randomizer seed, skip past it. this->questType[this->buttonIndex] += 1; } } else if (this->stickRelX < -30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DLEFT))) { this->questType[this->buttonIndex] -= 1; - if (this->questType[this->buttonIndex] == MASTER_QUEST && !ResourceMgr_GameHasMasterQuest()) { - // the only case not handled by the MIN/MAX_QUEST logic below. This will either put it at - // below MIN_QUEST in which case it will wrap back around, or it will put it on MIN_QUEST - // in which case if MIN_QUEST even is that number it will be a valid selection that won't - // crash. + while ((this->questType[this->buttonIndex] == FS_QUEST_MASTER && !ResourceMgr_GameHasMasterQuest()) || + (this->questType[this->buttonIndex] == FS_QUEST_RANDOMIZER && !hasRandomizerQuest())) { + // If Master Quest is selected without a Master Quest OTR present or when Randomizer Quest is + // selected without a loaded Randomizer seed, skip past it. this->questType[this->buttonIndex] -= 1; } } + // If current buttonIndex is higher or lower than the min/max value, wrap around. if (this->questType[this->buttonIndex] > MAX_QUEST) { this->questType[this->buttonIndex] = MIN_QUEST; } else if (this->questType[this->buttonIndex] < MIN_QUEST) { @@ -630,25 +617,36 @@ void FileChoose_UpdateQuestMenu(GameState* thisx) { } if (CHECK_BTN_ALL(input->press.button, BTN_A)) { - Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); - gSaveContext.isMasterQuest = this->questType[this->buttonIndex] == MASTER_QUEST; - gSaveContext.n64ddFlag = this->questType[this->buttonIndex] == RANDOMIZER_QUEST; - osSyncPrintf("Selected Dungeon Quest: %d\n", gSaveContext.isMasterQuest); - this->prevConfigMode = this->configMode; - this->configMode = CM_ROTATE_TO_NAME_ENTRY; - this->logoAlpha = 0; - CVarSetInteger("gOnFileSelectNameEntry", 1); - this->kbdButton = FS_KBD_BTN_NONE; - this->charPage = FS_CHAR_PAGE_ENG; - this->kbdX = 0; - this->kbdY = 0; - this->charIndex = 0; - this->charBgAlpha = 0; - this->newFileNameCharCount = CVarGetInteger("gLinkDefaultName", 0) ? 4 : 0; - this->nameEntryBoxPosX = 120; - this->nameEntryBoxAlpha = 0; - memcpy(Save_GetSaveMetaInfo(this->buttonIndex)->playerName, CVarGetInteger("gLinkDefaultName", 0) ? &linkName : &emptyName, 8); - return; + + gSaveContext.isMasterQuest = this->questType[this->buttonIndex] == FS_QUEST_MASTER; + gSaveContext.n64ddFlag = this->questType[this->buttonIndex] == FS_QUEST_RANDOMIZER; + gSaveContext.isBossRush = this->questType[this->buttonIndex] == FS_QUEST_BOSSRUSH; + gSaveContext.isBossRushPaused = false; + + if (this->questType[this->buttonIndex] == FS_QUEST_BOSSRUSH) { + Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); + this->prevConfigMode = this->configMode; + this->configMode = CM_ROTATE_TO_BOSS_RUSH_MENU; + return; + } else { + Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); + osSyncPrintf("Selected Dungeon Quest: %d\n", gSaveContext.isMasterQuest); + this->prevConfigMode = this->configMode; + this->configMode = CM_ROTATE_TO_NAME_ENTRY; + this->logoAlpha = 0; + CVarSetInteger("gOnFileSelectNameEntry", 1); + this->kbdButton = FS_KBD_BTN_NONE; + this->charPage = FS_CHAR_PAGE_ENG; + this->kbdX = 0; + this->kbdY = 0; + this->charIndex = 0; + this->charBgAlpha = 0; + this->newFileNameCharCount = CVarGetInteger("gLinkDefaultName", 0) ? 4 : 0; + this->nameEntryBoxPosX = 120; + this->nameEntryBoxAlpha = 0; + memcpy(Save_GetSaveMetaInfo(this->buttonIndex)->playerName, CVarGetInteger("gLinkDefaultName", 0) ? &linkName : &emptyName, 8); + return; + } } if (CHECK_BTN_ALL(input->press.button, BTN_B)) { @@ -657,6 +655,93 @@ void FileChoose_UpdateQuestMenu(GameState* thisx) { } } +void FileChoose_UpdateBossRushMenu(GameState* thisx) { + FileChoose_UpdateStickDirectionPromptAnim(thisx); + FileChooseContext* this = (FileChooseContext*)thisx; + Input* input = &this->state.input[0]; + bool dpad = CVarGetInteger("gDpadText", 0); + + // Fade in elements after opening Boss Rush options menu + this->bossRushUIAlpha += 25; + if (this->bossRushUIAlpha > 255) { + this->bossRushUIAlpha = 255; + } + + // Animate up/down arrows. + this->bossRushArrowOffset += 1; + if (this->bossRushArrowOffset >= 30) { + this->bossRushArrowOffset = 0; + } + + // Move menu selection up or down. + if (ABS(this->stickRelY) > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DDOWN | BTN_DUP))) { + // Move down + if (this->stickRelY < -30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DDOWN))) { + // When selecting past the last option, cycle back to the first option. + if ((this->bossRushIndex + 1) > BOSSRUSH_OPTIONS_AMOUNT - 1) { + this->bossRushIndex = 0; + this->bossRushOffset = 0; + } else { + this->bossRushIndex++; + // When last visible option is selected when moving down, offset the list down by one. + if (this->bossRushIndex - this->bossRushOffset > BOSSRUSH_MAX_OPTIONS_ON_SCREEN - 1) { + this->bossRushOffset++; + } + } + } else if (this->stickRelY > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DUP))) { + // When selecting past the first option, cycle back to the last option and offset the list to view it properly. + if ((this->bossRushIndex - 1) < 0) { + this->bossRushIndex = BOSSRUSH_OPTIONS_AMOUNT - 1; + this->bossRushOffset = this->bossRushIndex - BOSSRUSH_MAX_OPTIONS_ON_SCREEN + 1; + } else { + // When first visible option is selected when moving up, offset the list up by one. + if (this->bossRushIndex - this->bossRushOffset == 0) { + this->bossRushOffset--; + } + this->bossRushIndex--; + } + } + + Audio_PlaySoundGeneral(NA_SE_SY_FSEL_CURSOR, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); + } + + // Cycle through choices for currently selected option. + if (ABS(this->stickRelX) > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DLEFT | BTN_DRIGHT))) { + if (this->stickRelX > 30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DRIGHT))) { + // If exceeding the amount of choices for the selected option, cycle back to the first. + if ((gSaveContext.bossRushOptions[this->bossRushIndex] + 1) == BossRush_GetSettingOptionsAmount(this->bossRushIndex)) { + gSaveContext.bossRushOptions[this->bossRushIndex] = 0; + } else { + gSaveContext.bossRushOptions[this->bossRushIndex]++; + } + } else if (this->stickRelX < -30 || (dpad && CHECK_BTN_ANY(input->press.button, BTN_DLEFT))) { + // If cycling back when already at the first choice for the selected option, cycle back to the last choice. + if ((gSaveContext.bossRushOptions[this->bossRushIndex] - 1) < 0) { + gSaveContext.bossRushOptions[this->bossRushIndex] = BossRush_GetSettingOptionsAmount(this->bossRushIndex) - 1; + } else { + gSaveContext.bossRushOptions[this->bossRushIndex]--; + } + } + + Audio_PlaySoundGeneral(NA_SE_SY_FSEL_CURSOR, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); + } + + if (CHECK_BTN_ALL(input->press.button, BTN_B)) { + this->configMode = CM_BOSS_RUSH_TO_QUEST; + return; + } + + // Load into the game. + if (CHECK_BTN_ALL(input->press.button, BTN_START) || CHECK_BTN_ALL(input->press.button, BTN_A)) { + Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); + this->buttonIndex = 0xFE; + this->menuMode = FS_MENU_MODE_SELECT; + this->selectMode = SM_FADE_OUT; + this->prevConfigMode = this->configMode; + return; + } +} + /** * Update function for `CM_UNUSED_31` */ @@ -689,10 +774,7 @@ void FileChoose_RotateToNameEntry(GameState* thisx) { this->windowRot += VREG(16); - if (MIN_QUEST == MAX_QUEST && this->windowRot >= 314.0f) { - this->windowRot = 314.0f; - this->configMode = CM_START_NAME_ENTRY; - } else if (this->windowRot >= 628.0f) { + if (this->windowRot >= 628.0f) { this->windowRot = 628.0f; this->configMode = CM_START_NAME_ENTRY; } @@ -719,8 +801,7 @@ void FileChoose_RotateToOptions(GameState* thisx) { */ void FileChoose_RotateToMain(GameState* thisx) { FileChooseContext* this = (FileChooseContext*)thisx; - if (this->configMode == CM_QUEST_TO_MAIN || (MIN_QUEST == MAX_QUEST && this->configMode == CM_NAME_ENTRY_TO_MAIN && this->prevConfigMode != CM_MAIN_MENU) || - this->configMode == CM_OPTIONS_TO_MAIN) { + if (this->configMode == CM_QUEST_TO_MAIN || this->configMode == CM_OPTIONS_TO_MAIN) { this->windowRot -= VREG(16); if (this->windowRot <= 0.0f) { @@ -732,7 +813,7 @@ void FileChoose_RotateToMain(GameState* thisx) { if (this->configMode == CM_NAME_ENTRY_TO_MAIN && this->prevConfigMode == CM_MAIN_MENU) { this->windowRot += VREG(16); - if (this->windowRot >= 942.0f || (MIN_QUEST == MAX_QUEST && this->windowRot >= 628.0f)) { + if (this->windowRot >= 942.0f) { this->windowRot = 0.0f; this->configMode = CM_MAIN_MENU; } @@ -742,7 +823,7 @@ void FileChoose_RotateToMain(GameState* thisx) { void FileChoose_RotateToQuest(GameState* thisx) { FileChooseContext* this = (FileChooseContext*)thisx; - if (this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU) { + if (this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU || this->configMode == CM_BOSS_RUSH_TO_QUEST) { this->windowRot -= VREG(16); if (this->windowRot <= 314.0f) { @@ -759,6 +840,17 @@ void FileChoose_RotateToQuest(GameState* thisx) { } } +void FileChoose_RotateToBossRush(GameState* thisx) { + FileChooseContext* this = (FileChooseContext*)thisx; + + this->windowRot += VREG(16); + + if (this->windowRot >= 628.0f) { + this->windowRot = 628.0f; + this->configMode = CM_START_BOSS_RUSH_MENU; + } +} + static void (*gConfigModeUpdateFuncs[])(GameState*) = { FileChoose_StartFadeIn, FileChoose_FinishFadeIn, FileChoose_UpdateMainMenu, FileChoose_SetupCopySource, @@ -783,6 +875,8 @@ static void (*gConfigModeUpdateFuncs[])(GameState*) = { FileChoose_UnusedCMDelay, FileChoose_RotateToQuest, FileChoose_UpdateQuestMenu, FileChoose_StartQuestMenu, FileChoose_RotateToMain, FileChoose_RotateToQuest, + FileChoose_RotateToBossRush, FileChoose_UpdateBossRushMenu, + FileChoose_StartBossRushMenu, FileChoose_RotateToQuest, }; /** @@ -1376,6 +1470,18 @@ const char* FileChoose_GetQuestChooseTitleTexName(Language lang) { } } +const char* FileChoose_GetBossRushOptionsTitleTexName(Language lang) { + switch (lang) { + case LANGUAGE_ENG: + default: + return gFileSelBossRushSettingsENGText; + case LANGUAGE_FRA: + return gFileSelBossRushSettingsFRAText; + case LANGUAGE_GER: + return gFileSelBossRushSettingsGERText; + } +} + /** * Draw most window contents including buttons, labels, and icons. * Does not include anything from the keyboard and settings windows. @@ -1388,11 +1494,26 @@ void FileChoose_DrawWindowContents(GameState* thisx) { s16 quadVtxIndex; s16 isActive; s16 pad; - char* tex = (this->configMode == CM_QUEST_MENU || this->configMode == CM_ROTATE_TO_NAME_ENTRY || - this->configMode == CM_START_QUEST_MENU || this->configMode == CM_QUEST_TO_MAIN || - this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU) - ? FileChoose_GetQuestChooseTitleTexName(gSaveContext.language) - : sTitleLabels[gSaveContext.language][this->titleLabel]; + char* tex; + + switch (this->configMode) { + case CM_QUEST_MENU: + case CM_ROTATE_TO_NAME_ENTRY: + case CM_START_QUEST_MENU: + case CM_QUEST_TO_MAIN: + case CM_NAME_ENTRY_TO_QUEST_MENU: + case CM_ROTATE_TO_BOSS_RUSH_MENU: + tex = FileChoose_GetQuestChooseTitleTexName(gSaveContext.language); + break; + case CM_BOSS_RUSH_MENU: + case CM_START_BOSS_RUSH_MENU: + case CM_BOSS_RUSH_TO_QUEST: + tex = FileChoose_GetBossRushOptionsTitleTexName(gSaveContext.language); + break; + default: + tex = sTitleLabels[gSaveContext.language][this->titleLabel]; + break; + } OPEN_DISPS(this->state.gfxCtx); @@ -1412,34 +1533,32 @@ void FileChoose_DrawWindowContents(GameState* thisx) { if ((this->configMode == CM_QUEST_MENU) || (this->configMode == CM_START_QUEST_MENU) || this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU) { // draw control stick prompts. - if (MIN_QUEST != MAX_QUEST) { - Gfx_SetupDL_39Opa(this->state.gfxCtx); - gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); - gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowCursorTex, G_IM_FMT_IA, G_IM_SIZ_8b, 16, 24, 0, - G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 4, G_TX_NOMASK, G_TX_NOLOD, - G_TX_NOLOD); - FileChoose_DrawTextRec(this->state.gfxCtx, this->stickLeftPrompt.arrowColorR, - this->stickLeftPrompt.arrowColorG, this->stickLeftPrompt.arrowColorB, - this->stickLeftPrompt.arrowColorA, this->stickLeftPrompt.arrowTexX, - this->stickLeftPrompt.arrowTexY, this->stickLeftPrompt.z, 0, 0, -1.0f, 1.0f); - FileChoose_DrawTextRec(this->state.gfxCtx, this->stickRightPrompt.arrowColorR, - this->stickRightPrompt.arrowColorG, this->stickRightPrompt.arrowColorB, - this->stickRightPrompt.arrowColorA, this->stickRightPrompt.arrowTexX, - this->stickRightPrompt.arrowTexY, this->stickRightPrompt.z, 0, 0, 1.0f, 1.0f); - gDPLoadTextureBlock(POLY_OPA_DISP++, gControlStickTex, G_IM_FMT_IA, G_IM_SIZ_8b, 16, 16, 0, - G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 4, G_TX_NOMASK, G_TX_NOLOD, - G_TX_NOLOD); - FileChoose_DrawTextRec(this->state.gfxCtx, this->stickLeftPrompt.stickColorR, - this->stickLeftPrompt.stickColorG, this->stickLeftPrompt.stickColorB, - this->stickLeftPrompt.stickColorA, this->stickLeftPrompt.stickTexX, - this->stickLeftPrompt.stickTexY, this->stickLeftPrompt.z, 0, 0, -1.0f, 1.0f); - FileChoose_DrawTextRec(this->state.gfxCtx, this->stickRightPrompt.stickColorR, - this->stickRightPrompt.stickColorG, this->stickRightPrompt.stickColorB, - this->stickRightPrompt.stickColorA, this->stickRightPrompt.stickTexX, - this->stickRightPrompt.stickTexY, this->stickRightPrompt.z, 0, 0, 1.0f, 1.0f); - } + Gfx_SetupDL_39Opa(this->state.gfxCtx); + gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); + gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowCursorTex, G_IM_FMT_IA, G_IM_SIZ_8b, 16, 24, 0, + G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 4, G_TX_NOMASK, G_TX_NOLOD, + G_TX_NOLOD); + FileChoose_DrawTextRec(this->state.gfxCtx, this->stickLeftPrompt.arrowColorR, + this->stickLeftPrompt.arrowColorG, this->stickLeftPrompt.arrowColorB, + this->stickLeftPrompt.arrowColorA, this->stickLeftPrompt.arrowTexX, + this->stickLeftPrompt.arrowTexY, this->stickLeftPrompt.z, 0, 0, -1.0f, 1.0f); + FileChoose_DrawTextRec(this->state.gfxCtx, this->stickRightPrompt.arrowColorR, + this->stickRightPrompt.arrowColorG, this->stickRightPrompt.arrowColorB, + this->stickRightPrompt.arrowColorA, this->stickRightPrompt.arrowTexX, + this->stickRightPrompt.arrowTexY, this->stickRightPrompt.z, 0, 0, 1.0f, 1.0f); + gDPLoadTextureBlock(POLY_OPA_DISP++, gControlStickTex, G_IM_FMT_IA, G_IM_SIZ_8b, 16, 16, 0, + G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 4, G_TX_NOMASK, G_TX_NOLOD, + G_TX_NOLOD); + FileChoose_DrawTextRec(this->state.gfxCtx, this->stickLeftPrompt.stickColorR, + this->stickLeftPrompt.stickColorG, this->stickLeftPrompt.stickColorB, + this->stickLeftPrompt.stickColorA, this->stickLeftPrompt.stickTexX, + this->stickLeftPrompt.stickTexY, this->stickLeftPrompt.z, 0, 0, -1.0f, 1.0f); + FileChoose_DrawTextRec(this->state.gfxCtx, this->stickRightPrompt.stickColorR, + this->stickRightPrompt.stickColorG, this->stickRightPrompt.stickColorB, + this->stickRightPrompt.stickColorA, this->stickRightPrompt.stickTexX, + this->stickRightPrompt.stickTexY, this->stickRightPrompt.z, 0, 0, 1.0f, 1.0f); switch (this->questType[this->buttonIndex]) { - case NORMAL_QUEST: + case FS_QUEST_NORMAL: default: gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, this->logoAlpha); FileChoose_DrawTextureI8(this->state.gfxCtx, gTitleTheLegendOfTextTex, 72, 8, 156, 108, 72, 8, 1024, 1024); @@ -1447,7 +1566,7 @@ void FileChoose_DrawWindowContents(GameState* thisx) { FileChoose_DrawImageRGBA32(this->state.gfxCtx, 160, 135, gTitleZeldaShieldLogoTex, 160, 160); break; - case MASTER_QUEST: + case FS_QUEST_MASTER: gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, this->logoAlpha); FileChoose_DrawTextureI8(this->state.gfxCtx, gTitleTheLegendOfTextTex, 72, 8, 156, 108, 72, 8, 1024, 1024); FileChoose_DrawTextureI8(this->state.gfxCtx, gTitleOcarinaOfTimeTMTextTex, 96, 8, 154, 163, 96, 8, 1024, 1024); @@ -1455,7 +1574,7 @@ void FileChoose_DrawWindowContents(GameState* thisx) { FileChoose_DrawImageRGBA32(this->state.gfxCtx, 182, 180, gTitleMasterQuestSubtitleTex, 128, 32); break; - case RANDOMIZER_QUEST: + case FS_QUEST_RANDOMIZER: DrawSeedHashSprites(this); gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, this->logoAlpha); FileChoose_DrawTextureI8(this->state.gfxCtx, gTitleTheLegendOfTextTex, 72, 8, 156, 108, 72, 8, 1024, 1024); @@ -1463,8 +1582,77 @@ void FileChoose_DrawWindowContents(GameState* thisx) { FileChoose_DrawImageRGBA32(this->state.gfxCtx, 160, 135, ResourceMgr_GameHasOriginal() ? gTitleZeldaShieldLogoTex : gTitleZeldaShieldLogoMQTex, 160, 160); FileChoose_DrawImageRGBA32(this->state.gfxCtx, 182, 180, gTitleRandomizerSubtitleTex, 128, 32); break; + + case FS_QUEST_BOSSRUSH: + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, this->logoAlpha); + FileChoose_DrawTextureI8(this->state.gfxCtx, gTitleTheLegendOfTextTex, 72, 8, 156, 108, 72, 8, 1024, 1024); + FileChoose_DrawTextureI8(this->state.gfxCtx, gTitleOcarinaOfTimeTMTextTex, 96, 8, 154, 163, 96, 8, 1024, 1024); + FileChoose_DrawImageRGBA32(this->state.gfxCtx, 160, 135, ResourceMgr_GameHasOriginal() ? gTitleZeldaShieldLogoTex : gTitleZeldaShieldLogoMQTex, 160, 160); + FileChoose_DrawImageRGBA32(this->state.gfxCtx, 182, 180, gTitleBossRushSubtitleTex, 128, 32); + break; } - } else if (this->configMode != CM_ROTATE_TO_NAME_ENTRY) { + } else if (this->configMode == CM_BOSS_RUSH_MENU) { + + uint8_t listOffset = this->bossRushOffset; + uint8_t textAlpha = this->bossRushUIAlpha; + + // Draw arrows to indicate that the list can scroll up or down. + // Arrow up + if (listOffset > 0) { + uint16_t arrowUpX = 140; + uint16_t arrowUpY = 76 - (this->bossRushArrowOffset / 10); + gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowUpTex, G_IM_FMT_IA, + G_IM_SIZ_16b, 16, 16, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, + G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + gSPWideTextureRectangle(POLY_OPA_DISP++, arrowUpX << 2, arrowUpY << 2, (arrowUpX + 8) << 2, + (arrowUpY + 8) << 2, G_TX_RENDERTILE, 0, 0, (1 << 11), (1 << 11)); + } + // Arrow down + if (BOSSRUSH_OPTIONS_AMOUNT - listOffset > BOSSRUSH_MAX_OPTIONS_ON_SCREEN) { + uint16_t arrowDownX = 140; + uint16_t arrowDownY = 181 + (this->bossRushArrowOffset / 10); + gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowDownTex, G_IM_FMT_IA, + G_IM_SIZ_16b, 16, 16, 0, + G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, + G_TX_NOLOD, G_TX_NOLOD); + gSPWideTextureRectangle(POLY_OPA_DISP++, arrowDownX << 2, arrowDownY << 2, (arrowDownX + 8) << 2, + (arrowDownY + 8) << 2, G_TX_RENDERTILE, 0, 0, (1 << 11), (1 << 11)); + } + + // Draw options. There's more options than what fits on the screen, so the visible options + // depend on the current offset of the list. Currently selected option pulses in + // color and has arrows surrounding the option. + for (uint8_t i = listOffset; i - listOffset < BOSSRUSH_MAX_OPTIONS_ON_SCREEN; i++) { + uint16_t textYOffset = (i - listOffset) * 16; + + // Option name. + Interface_DrawTextLine(this->state.gfxCtx, BossRush_GetSettingName(i, gSaveContext.language), + 65, (87 + textYOffset), 255, 255, 80, textAlpha, 0.8f, true); + + // Selected choice for option. + uint16_t finalKerning = Interface_DrawTextLine(this->state.gfxCtx, BossRush_GetSettingChoiceName(i, gSaveContext.bossRushOptions[i], gSaveContext.language), + 165, (87 + textYOffset), 255, 255, 255, textAlpha, 0.8f, true); + + // Draw arrows around selected option. + if (this->bossRushIndex == i) { + Gfx_SetupDL_39Opa(this->state.gfxCtx); + gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); + gDPLoadTextureBlock(POLY_OPA_DISP++, gArrowCursorTex, G_IM_FMT_IA, G_IM_SIZ_8b, 16, 24, 0, + G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, 4, G_TX_NOMASK, G_TX_NOLOD, + G_TX_NOLOD); + FileChoose_DrawTextRec(this->state.gfxCtx, this->stickLeftPrompt.arrowColorR, + this->stickLeftPrompt.arrowColorG, this->stickLeftPrompt.arrowColorB, + textAlpha, 160, (92 + textYOffset), 0.42f, 0, 0, -1.0f, + 1.0f); + FileChoose_DrawTextRec(this->state.gfxCtx, this->stickRightPrompt.arrowColorR, + this->stickRightPrompt.arrowColorG, this->stickRightPrompt.arrowColorB, + textAlpha, (171 + finalKerning), + (92 + textYOffset), 0.42f, 0, 0, 1.0f, 1.0f); + } + } + + } else if (this->configMode != CM_ROTATE_TO_NAME_ENTRY && this->configMode != CM_START_BOSS_RUSH_MENU && + this->configMode != CM_ROTATE_TO_BOSS_RUSH_MENU && this->configMode != CM_BOSS_RUSH_TO_QUEST) { gDPPipeSync(POLY_OPA_DISP++); gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, this->titleAlpha[1]); gDPLoadTextureBlock(POLY_OPA_DISP++, sTitleLabels[gSaveContext.language][this->nextTitleLabel], G_IM_FMT_IA, @@ -1702,7 +1890,7 @@ void FileChoose_ConfigModeDraw(GameState* thisx) { if (this->windowRot != 0) { if (this->configMode == CM_ROTATE_TO_QUEST_MENU || (this->configMode >= CM_MAIN_TO_OPTIONS && this->configMode <= CM_OPTIONS_TO_MAIN) || - MIN_QUEST == MAX_QUEST || this->configMode == CM_QUEST_TO_MAIN) { + this->configMode == CM_QUEST_TO_MAIN) { Matrix_RotateX(this->windowRot / 100.0f, MTXMODE_APPLY); } else { Matrix_RotateX((this->windowRot - 942.0f) / 100.0f, MTXMODE_APPLY); @@ -1736,11 +1924,7 @@ void FileChoose_ConfigModeDraw(GameState* thisx) { Matrix_Translate(0.0f, 0.0f, -93.6f, MTXMODE_NEW); Matrix_Scale(0.78f, 0.78f, 0.78f, MTXMODE_APPLY); - if (MIN_QUEST == MAX_QUEST) { - Matrix_RotateX((this->windowRot - 314.0f) / 100.0f, MTXMODE_APPLY); - } else { - Matrix_RotateX((this->windowRot - 628.0f) / 100.0f, MTXMODE_APPLY); - } + Matrix_RotateX((this->windowRot - 628.0f) / 100.0f, MTXMODE_APPLY); gSPMatrix(POLY_OPA_DISP++, MATRIX_NEWMTX(this->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); @@ -1790,7 +1974,7 @@ void FileChoose_ConfigModeDraw(GameState* thisx) { // draw quest menu if ((this->configMode == CM_QUEST_MENU) || (this->configMode == CM_ROTATE_TO_QUEST_MENU) || (this->configMode == CM_ROTATE_TO_NAME_ENTRY) || this->configMode == CM_QUEST_TO_MAIN || - this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU) { + this->configMode == CM_NAME_ENTRY_TO_QUEST_MENU || this->configMode == CM_ROTATE_TO_BOSS_RUSH_MENU) { // window gDPPipeSync(POLY_OPA_DISP++); gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); @@ -1819,6 +2003,36 @@ void FileChoose_ConfigModeDraw(GameState* thisx) { FileChoose_DrawWindowContents(&this->state); } + // Draw Boss Rush Options Menu + if (this->configMode == CM_BOSS_RUSH_MENU || this->configMode == CM_ROTATE_TO_BOSS_RUSH_MENU || + this->configMode == CM_START_BOSS_RUSH_MENU || this->configMode == CM_BOSS_RUSH_TO_QUEST) { + // window + gDPPipeSync(POLY_OPA_DISP++); + gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, this->windowColor[0], this->windowColor[1], this->windowColor[2], + this->windowAlpha); + gDPSetEnvColor(POLY_OPA_DISP++, 0, 0, 0, 0); + + Matrix_Translate(0.0f, 0.0f, -93.6f, MTXMODE_NEW); + Matrix_Scale(0.78f, 0.78f, 0.78f, MTXMODE_APPLY); + Matrix_RotateX((this->windowRot - 628.0f) / 100.0f, MTXMODE_APPLY); + + gSPMatrix(POLY_OPA_DISP++, MATRIX_NEWMTX(this->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + + gSPVertex(POLY_OPA_DISP++, &this->windowVtx[0], 32, 0); + gSPDisplayList(POLY_OPA_DISP++, gFileSelWindow1DL); + + gSPVertex(POLY_OPA_DISP++, &this->windowVtx[32], 32, 0); + gSPDisplayList(POLY_OPA_DISP++, gFileSelWindow2DL); + + gSPVertex(POLY_OPA_DISP++, &this->windowVtx[64], 16, 0); + gSPDisplayList(POLY_OPA_DISP++, gFileSelWindow3DL); + + gDPPipeSync(POLY_OPA_DISP++); + + FileChoose_DrawWindowContents(&this->state); + } + gDPPipeSync(POLY_OPA_DISP++); FileChoose_SetView(this, 0.0f, 0.0f, 64.0f); @@ -1920,6 +2134,8 @@ void FileChoose_ConfirmFile(GameState* thisx) { if (this->confirmButtonIndex == FS_BTN_CONFIRM_YES) { func_800AA000(300.0f, 180, 20, 100); Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); + // Reset Boss Rush because it's only ever saved in memory. + gSaveContext.isBossRush = 0; this->selectMode = SM_FADE_OUT; func_800F6964(0xF); } else { @@ -2033,26 +2249,28 @@ void FileChoose_LoadGame(GameState* thisx) { u16 swordEquipMask; s32 pad; + Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); + gSaveContext.fileNum = this->buttonIndex; + gSaveContext.gameMode = 0; + if ((this->buttonIndex == FS_BTN_SELECT_FILE_1 && CVarGetInteger("gDebugEnabled", 0)) || this->buttonIndex == 0xFF) { - Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); - gSaveContext.fileNum = this->buttonIndex; if (this->buttonIndex == 0xFF) { Sram_InitDebugSave(); } else { Sram_OpenSave(); } - gSaveContext.gameMode = 0; SET_NEXT_GAMESTATE(&this->state, Select_Init, SelectContext); - this->state.running = false; } else { - Audio_PlaySoundGeneral(NA_SE_SY_FSEL_DECIDE_L, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); - gSaveContext.fileNum = this->buttonIndex; - Sram_OpenSave(); - gSaveContext.gameMode = 0; + if (this->buttonIndex == 0xFE) { + Sram_InitBossRushSave(); + } else { + Sram_OpenSave(); + } SET_NEXT_GAMESTATE(&this->state, Play_Init, PlayState); - this->state.running = false; } + this->state.running = false; + Randomizer_LoadSettings(""); Randomizer_LoadHintLocations(""); Randomizer_LoadItemLocations("", true); @@ -2573,6 +2791,9 @@ void FileChoose_InitContext(GameState* thisx) { this->arrowAnimTween = 0; this->stickAnimTween = 0; + this->bossRushIndex = 0; + this->bossRushOffset = 0; + ShrinkWindow_SetVal(0); gSaveContext.skyboxTime = 0; diff --git a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c index 56afb768f..5f3a09b0a 100644 --- a/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c +++ b/soh/src/overlays/misc/ovl_kaleido_scope/z_kaleido_scope_PAL.c @@ -1508,8 +1508,14 @@ void KaleidoScope_DrawPages(PlayState* play, GraphicsContext* gfxCtx) { gDPSetCombineMode(POLY_KAL_DISP++, G_CC_MODULATEIA, G_CC_MODULATEIA); gDPSetPrimColor(POLY_KAL_DISP++, 0, 0, 255, 255, 255, pauseCtx->alpha); - POLY_KAL_DISP = - KaleidoScope_QuadTextureIA8(POLY_KAL_DISP, sPromptChoiceTexs[gSaveContext.language][0], 48, 16, 12); + if (!gSaveContext.isBossRush) { + POLY_KAL_DISP = KaleidoScope_QuadTextureIA8( + POLY_KAL_DISP, sPromptChoiceTexs[gSaveContext.language][0], 48, 16, 12); + } else { + // Show "No" twice in Boss Rush because the player can't save within it. + POLY_KAL_DISP = KaleidoScope_QuadTextureIA8( + POLY_KAL_DISP, sPromptChoiceTexs[gSaveContext.language][1], 48, 16, 12); + } POLY_KAL_DISP = KaleidoScope_QuadTextureIA8(POLY_KAL_DISP, sPromptChoiceTexs[gSaveContext.language][1], 48, 16, 16); @@ -3629,7 +3635,9 @@ void KaleidoScope_Update(PlayState* play) case 6: switch (pauseCtx->unk_1E4) { case 0: - if (CHECK_BTN_ALL(input->press.button, BTN_START)) { + // Boss Rush skips past the "Save?" window when pressing B while paused. + if (CHECK_BTN_ALL(input->press.button, BTN_START) || + (CHECK_BTN_ALL(input->press.button, BTN_B) && gSaveContext.isBossRush)) { if (CVarGetInteger("gCheatEasyPauseBufferEnabled", 0) || CVarGetInteger("gCheatEasyInputBufferingEnabled", 0)) { CVarSetInteger("gPauseBufferBlockInputFrame", 9); } @@ -4015,7 +4023,11 @@ void KaleidoScope_Update(PlayState* play) VREG(88) = 66; WREG(2) = 0; pauseCtx->alpha = 255; - pauseCtx->state = 0xE; + if (!gSaveContext.isBossRush) { + pauseCtx->state = 0xE; + } else { + pauseCtx->state = 0xF; + } gSaveContext.deaths++; if (gSaveContext.deaths > 999) { gSaveContext.deaths = 999; @@ -4059,7 +4071,7 @@ void KaleidoScope_Update(PlayState* play) case 0x10: if (CHECK_BTN_ALL(input->press.button, BTN_A) || CHECK_BTN_ALL(input->press.button, BTN_START)) { - if (pauseCtx->promptChoice == 0) { + if (pauseCtx->promptChoice == 0 && !gSaveContext.isBossRush) { Audio_PlaySoundGeneral(NA_SE_SY_PIECE_OF_HEART, &D_801333D4, 4, &D_801333E0, &D_801333E0, &D_801333E8); Play_SaveSceneFlags(play); @@ -4132,7 +4144,7 @@ void KaleidoScope_Update(PlayState* play) R_PAUSE_MENU_MODE = 0; func_800981B8(&play->objectCtx); func_800418D0(&play->colCtx, play); - if (pauseCtx->promptChoice == 0) { + if (pauseCtx->promptChoice == 0 && !gSaveContext.isBossRush) { Play_TriggerRespawn(play); gSaveContext.respawnFlag = -2; // In ER, handle death warp to last entrance from grottos @@ -4156,6 +4168,7 @@ void KaleidoScope_Update(PlayState* play) osSyncPrintf(VT_RST); } else { play->state.running = 0; + gSaveContext.isBossRush = false; SET_NEXT_GAMESTATE(&play->state, Opening_Init, OpeningContext); GameInteractor_ExecuteOnExitGame(gSaveContext.fileNum); }