From f5af415098a47a19dbc625e8be3f7ea499364982 Mon Sep 17 00:00:00 2001 From: Garrett Cox Date: Sat, 6 Dec 2025 10:45:43 -0600 Subject: [PATCH] Reindeer games additions (#33) * Remove hat from darunia * Update some stuffs * Remove custom collectible (in favor of CustomItem) * Roguelike Co-authored-by: Caladius Co-authored-by: Eblo <7004497+Eblo@users.noreply.github.com> * Fix a bunch of stuff * Persist roguelike data * Experimental anchor tweaks * Stop freeze from scrubs & fix pablo cvar --------- Co-authored-by: Caladius Co-authored-by: Eblo <7004497+Eblo@users.noreply.github.com> --- RogueLike.md | 38 + .../gTitleRogueLikeSubtitleTex.rgba32.png | Bin 0 -> 2988 bytes soh/assets/soh_assets.h | 3 + soh/include/z64item.h | 8 +- soh/include/z64save.h | 13 + soh/soh/Enhancements/Holiday/AGreenSpoon.cpp | 8 +- soh/soh/Enhancements/Holiday/Archez.cpp | 6 +- soh/soh/Enhancements/Holiday/Caladius.cpp | 91 +- soh/soh/Enhancements/Holiday/Fredomato.cpp | 93 +- soh/soh/Enhancements/Holiday/Grimey.cpp | 72 +- soh/soh/Enhancements/Holiday/LL.cpp | 7 +- soh/soh/Enhancements/Holiday/NotProxySaw.cpp | 937 +++++++++++++++++- soh/soh/Enhancements/Holiday/Pablo.cpp | 10 +- soh/soh/Enhancements/Holiday/ProxySaw.cpp | 312 +++++- soh/soh/Enhancements/Holiday/aMannus.cpp | 14 +- soh/soh/Enhancements/Holiday/lilDavid.cpp | 6 +- .../RogueLike/ActorBehavior/ActorBehavior.cpp | 80 ++ .../ActorBehavior/BusinessScrubs.cpp | 39 + .../RogueLike/ActorBehavior/Chests.cpp | 51 + .../RogueLike/ActorBehavior/Enemies.cpp | 59 ++ .../ActorBehavior/FreestandingItems.cpp | 72 ++ .../RogueLike/ActorBehavior/Grass.cpp | 38 + .../RogueLike/ActorBehavior/Pots.cpp | 34 + .../RogueLike/ActorBehavior/Rocks.cpp | 38 + .../RogueLike/ActorBehavior/Trees.cpp | 38 + soh/soh/Enhancements/RogueLike/Choices.hpp | 218 ++++ soh/soh/Enhancements/RogueLike/Difficulty.cpp | 115 +++ soh/soh/Enhancements/RogueLike/Difficulty.h | 24 + soh/soh/Enhancements/RogueLike/GUI/GUI.cpp | 511 ++++++++++ soh/soh/Enhancements/RogueLike/GUI/GUI.h | 59 ++ .../RogueLike/GUI/LevelUpWindow.cpp | 74 ++ .../RogueLike/GUI/StartingSelectionWindow.cpp | 71 ++ .../RogueLike/MiscBehavior/OnLoadGame.cpp | 21 + soh/soh/Enhancements/RogueLike/Quests.cpp | 681 +++++++++++++ soh/soh/Enhancements/RogueLike/Quests.h | 23 + soh/soh/Enhancements/RogueLike/RogueLike.cpp | 9 + soh/soh/Enhancements/RogueLike/RogueLike.h | 14 + soh/soh/Enhancements/RogueLike/Types.h | 45 + soh/soh/Enhancements/RogueLike/XP.cpp | 124 +++ soh/soh/Enhancements/RogueLike/XP.h | 27 + .../cosmetics/CosmeticsEditor.cpp | 25 - .../CustomItem.cpp} | 152 ++- .../CustomItem.h} | 13 +- .../custom-message/CustomMessageManager.cpp | 36 +- .../custom-message/CustomMessageManager.h | 20 + .../custom-message/CustomMessageTypes.h | 1 + .../game-interactor/GIEventQueue.cpp | 95 ++ .../game-interactor/GameInteractor.h | 22 + .../GameInteractor_HookTable.h | 1 + .../game-interactor/GameInteractor_Hooks.cpp | 9 +- .../game-interactor/GameInteractor_Hooks.h | 1 + .../vanilla-behavior/GIVanillaBehavior.h | 18 + .../randomizer/location_access.cpp | 2 +- .../Enhancements/randomizer/location_access.h | 2 + soh/soh/Network/Anchor/Anchor.cpp | 46 +- soh/soh/Network/Anchor/Anchor.h | 3 + soh/soh/Network/Network.cpp | 14 + soh/soh/Network/Network.h | 1 + soh/soh/OTRGlobals.cpp | 41 +- soh/soh/SaveManager.cpp | 44 + soh/soh/SaveManager.h | 2 + soh/soh/SohGui/SohMenu.cpp | 2 + soh/soh/z_scene_otr.cpp | 1 + soh/src/code/z_actor.c | 2 +- soh/src/code/z_collision_check.c | 8 + soh/src/code/z_draw.c | 5 +- soh/src/code/z_message_PAL.c | 2 + soh/src/code/z_parameter.c | 11 +- soh/src/code/z_play.c | 6 +- soh/src/code/z_player_lib.c | 2 +- .../actors/ovl_Boss_Dodongo/z_boss_dodongo.c | 2 +- .../actors/ovl_Boss_Ganon2/z_boss_ganon2.c | 4 +- .../ovl_Boss_Ganondrof/z_boss_ganondrof.c | 2 +- soh/src/overlays/actors/ovl_En_Am/z_en_am.c | 2 +- soh/src/overlays/actors/ovl_En_Ani/z_en_ani.c | 2 +- soh/src/overlays/actors/ovl_En_Bb/z_en_bb.c | 2 +- .../actors/ovl_En_Bom_Chu/z_en_bom_chu.c | 2 +- soh/src/overlays/actors/ovl_En_Box/z_en_box.c | 2 +- soh/src/overlays/actors/ovl_En_Cs/z_en_cs.c | 2 +- .../z_en_daiku_kakariko.c | 2 +- .../actors/ovl_En_Dekubaba/z_en_dekubaba.c | 2 +- soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c | 2 +- .../actors/ovl_En_Dodongo/z_en_dodongo.c | 2 +- soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c | 2 +- soh/src/overlays/actors/ovl_En_Ds/z_en_ds.c | 2 +- soh/src/overlays/actors/ovl_En_Du/z_en_du.c | 14 - soh/src/overlays/actors/ovl_En_Elf/z_en_elf.c | 2 +- soh/src/overlays/actors/ovl_En_Fu/z_en_fu.c | 2 +- .../actors/ovl_En_Heishi4/z_en_heishi4.c | 2 +- soh/src/overlays/actors/ovl_En_Hs2/z_en_hs2.c | 2 +- soh/src/overlays/actors/ovl_En_Hy/z_en_hy.c | 2 +- soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c | 6 +- soh/src/overlays/actors/ovl_En_In/z_en_in.c | 2 +- .../overlays/actors/ovl_En_Ishi/z_en_ishi.c | 3 +- soh/src/overlays/actors/ovl_En_Jj/z_en_jj.c | 2 +- soh/src/overlays/actors/ovl_En_Ma1/z_en_ma1.c | 2 +- soh/src/overlays/actors/ovl_En_Niw/z_en_niw.c | 2 +- .../actors/ovl_En_Niw_Girl/z_en_niw_girl.c | 2 +- .../actors/ovl_En_Niw_Lady/z_en_niw_lady.c | 2 +- .../actors/ovl_En_Nutsball/z_en_nutsball.c | 6 +- .../overlays/actors/ovl_En_Ossan/z_en_ossan.c | 4 +- .../actors/ovl_En_Po_Relay/z_en_po_relay.c | 2 +- .../actors/ovl_En_Shopnuts/z_en_shopnuts.c | 2 +- soh/src/overlays/actors/ovl_En_Skb/z_en_skb.c | 2 +- soh/src/overlays/actors/ovl_En_Sth/z_en_sth.c | 2 +- .../ovl_En_Syateki_Man/z_en_syateki_man.c | 2 +- soh/src/overlays/actors/ovl_En_Ta/z_en_ta.c | 2 +- .../overlays/actors/ovl_En_Test/z_en_test.c | 2 +- soh/src/overlays/actors/ovl_En_Tg/z_en_tg.c | 4 +- soh/src/overlays/actors/ovl_En_Tk/z_en_tk.c | 2 +- .../overlays/actors/ovl_En_Toryo/z_en_toryo.c | 2 +- soh/src/overlays/actors/ovl_En_Vm/z_en_vm.c | 2 +- soh/src/overlays/actors/ovl_En_Wf/z_en_wf.c | 2 +- .../actors/ovl_En_Wood02/z_en_wood02.c | 22 +- .../actors/ovl_player_actor/z_player.c | 31 +- .../ovl_file_choose/z_file_choose.c | 13 +- 116 files changed, 4375 insertions(+), 474 deletions(-) create mode 100644 RogueLike.md create mode 100644 soh/assets/custom/objects/object_mag/gTitleRogueLikeSubtitleTex.rgba32.png create mode 100644 soh/soh/Enhancements/RogueLike/ActorBehavior/ActorBehavior.cpp create mode 100644 soh/soh/Enhancements/RogueLike/ActorBehavior/BusinessScrubs.cpp create mode 100644 soh/soh/Enhancements/RogueLike/ActorBehavior/Chests.cpp create mode 100644 soh/soh/Enhancements/RogueLike/ActorBehavior/Enemies.cpp create mode 100644 soh/soh/Enhancements/RogueLike/ActorBehavior/FreestandingItems.cpp create mode 100644 soh/soh/Enhancements/RogueLike/ActorBehavior/Grass.cpp create mode 100644 soh/soh/Enhancements/RogueLike/ActorBehavior/Pots.cpp create mode 100644 soh/soh/Enhancements/RogueLike/ActorBehavior/Rocks.cpp create mode 100644 soh/soh/Enhancements/RogueLike/ActorBehavior/Trees.cpp create mode 100644 soh/soh/Enhancements/RogueLike/Choices.hpp create mode 100644 soh/soh/Enhancements/RogueLike/Difficulty.cpp create mode 100644 soh/soh/Enhancements/RogueLike/Difficulty.h create mode 100644 soh/soh/Enhancements/RogueLike/GUI/GUI.cpp create mode 100644 soh/soh/Enhancements/RogueLike/GUI/GUI.h create mode 100644 soh/soh/Enhancements/RogueLike/GUI/LevelUpWindow.cpp create mode 100644 soh/soh/Enhancements/RogueLike/GUI/StartingSelectionWindow.cpp create mode 100644 soh/soh/Enhancements/RogueLike/MiscBehavior/OnLoadGame.cpp create mode 100644 soh/soh/Enhancements/RogueLike/Quests.cpp create mode 100644 soh/soh/Enhancements/RogueLike/Quests.h create mode 100644 soh/soh/Enhancements/RogueLike/RogueLike.cpp create mode 100644 soh/soh/Enhancements/RogueLike/RogueLike.h create mode 100644 soh/soh/Enhancements/RogueLike/Types.h create mode 100644 soh/soh/Enhancements/RogueLike/XP.cpp create mode 100644 soh/soh/Enhancements/RogueLike/XP.h rename soh/soh/Enhancements/{custom-collectible/CustomCollectible.cpp => custom-item/CustomItem.cpp} (53%) rename soh/soh/Enhancements/{custom-collectible/CustomCollectible.h => custom-item/CustomItem.h} (75%) create mode 100644 soh/soh/Enhancements/game-interactor/GIEventQueue.cpp diff --git a/RogueLike.md b/RogueLike.md new file mode 100644 index 000000000..7a3ea13b7 --- /dev/null +++ b/RogueLike.md @@ -0,0 +1,38 @@ +# Game Design Document +The gist of this idea is you play through the game similar to a randomizer, fulfilling the objectives to reach and beat Ganon, but instead of getting items from checks, they are granted with a leveling/reward system. Among other things like buffs and debuffs. + +## Starting out UX +When beginning a game, you will have multiple randomly rolled selections to make, including a starting location, starting item, starting song + +## Gameplay +As you progress through the game, you will be rewarded XP for doing various actions throughout the world. When you have enough XP you will level up and you will be presented with a selection of buffs, items, or other rewards to choose from to enhance your character for the rest of the run. Actions include interacting with things, breaking things, killing enemies, talking with NPCs and completing their quests, etc. + +TODO: +- Health upgrades not implemented +- Defense doesn't do anything yet (maybe damage mitigation?) +- Speed doesn't do anything yet (will effect movement speed of bunny hood) +- Quests system in early stages, only two quests currently +- Don't have a global enemy spawning mechanism yet, just relying on existing enemy spawn points +- Not all checks have been replaced with XP rewards yet +- Not sure what to do about ammo yet +- Not sure what to do about shops yet +- Weighted list of XP amounts to grant for every check and enemy. For instance you shouldn't get that much XP for opening chests in Mido's house, but doing frog song minigame is huge etc + +## Difficulty Scaling +The main thing that will scale is enemy health & damage to link. Difficulty increases in levels, it will increase each time you kill a major boss, and there will also be a timer that eventually increases your difficulty based on time spent not gaining any experience. + +TODO: +- Damage to link scaling not implemented yet +- Defeating bosses to increase difficulty not implemented yet +- Hyper enemies / enemy size scaling could play into this? + +## Permanent progression +As you continue playing, the idea is you would very slowly gain a currency to grant you very small bonuses that will benefit you in every following run. Starting health is an example of a potential purchase-able thing, or starting with ocarina or bunny hood. It's likely that this currency would be earned from killing bosses and completing quests + +TODO: +- Permanent progression system not implemented yet + +## Ideas: +- Lock the player in certain areas to spawn specified waves of enemies before the player can proceed? i.e Hyrule Field or places without a Clear Room +- Utilize the trick from MM to hide/unhide Inventory Items based on what is earned? +- Ammo could have a timed regen based on upgrades? For instance you can only hold 1 stick, and if you break it, it regens after 5 minutes, unless you get an upgrade to hold 2 sticks, then it regens after 4 minutes, etc \ No newline at end of file diff --git a/soh/assets/custom/objects/object_mag/gTitleRogueLikeSubtitleTex.rgba32.png b/soh/assets/custom/objects/object_mag/gTitleRogueLikeSubtitleTex.rgba32.png new file mode 100644 index 0000000000000000000000000000000000000000..f64b6ba0c9a9dfc926be1e2d3b149032d77f1332 GIT binary patch literal 2988 zcmV;d3sdxoP)mouWxl~gqno^N#q`OKIDy6bhDT>WdR7{a1g&}De#^s!8Zq5wGEyEa`!+X~6tG$o$ zbpDumn8rEZ`#kScd+qi0TkCtj_5OZ)Cnd#$2M-=Rc<|uCg9i^DJb3Wn!Gi}69`6$4 zh$151;45M)v5;ulb*24byxmhyCpiKayV3bkL30rmG!GWT4N551-_?FhK}?7rVnXJ_ z!f0s8pILNQK@>5~3dbTma6HNjwK3kPjrDDPdV4US2=Vagach<*ZL!zEBbypsC*3b^mj?Q5Nfui#g$F5k}(D z_GnzrkHeM11Y9jjL}OMIddl|rsjG|pow#weO2moS_IWL&rKNxABqwq0(g4)%%EIBi zR3>yav8pn66JC7TfIH`^SnScCTXq+qcTd?@tp~I5ZG{krHb=0g&#LuR#wRJ^__3xK zw`+I9Y^d&=laMIWz5D$U!}Jm4;fne0lQFz~!h}SVjvPbcRt;4Z__|NQR9hR!_ULih zP*;18t(7?#)l~ZAvBVxSGTZD%jR2#Kx$*9f>kUkX>J^CNs(qF7-dI1D1ifuO%mBgO zGdNN%TR({?Xz!^fCnDpiJwmRhPFl|yYhi-%mgaD2xAhl7EfA^BQ+wF#k1e;TUk*a56^#!hv$wUammLlCsjkW zFYcYGz)z>k5liO)ifwclHR9D*)w}UyeJMnnqN#4BT9!M}#^S}rPlWjHm4KD+XL>C>~OE?C@0Fr@?ET<%fvXJX-;io zv0CfOkQuuY-!)W71Z2}lc@A^jCO>+4t&s$5#Lb%h$c$Nm+h>p9If=>EYNaWoyjw*5 zm_-Y!+eJLdCQ&SOnjSXAxoT)_Hg`=p-+l+;tZr4jQ)^S2U zAR+C^Ehx__A=Llez0>VNdOJI^*zmU-wz7+~2e@f0-VnuN<+T2_hT=@zK2?V5+!VI2H&Gmly?aT7TrrQEzM|Byg%Tl$ z=?v^lj0rIT-dC@c<>Jc&Lg@784uFo43p}n~CSfn_$wW?gFj`8|aVmE`Y|V#1rT3Zm z8npdg+ncZ)XY3g>Zo?sjw_AN8@nq?ic>Hi;KhEySVzIATuHHZaXRC}$ecV~x z$=bvaUmY&O-zy7gB6r|sc|L9&%ER@t?YLIjF67ENoC9qv-53ETWOH#E6X<0peV1+s znS;;rlTnu)&#qyI#ImG7FEnJYZ5J{#26Y+HFdQUc@6QllXsSC9M^nO3y7h$J}&6it=n<&g_A7|v3Io}L}BxgG{+gy z9+MC~+YwQ-CLnU=1VqfR$C#noSEN1!2QZ`4^PPr+xI?y@x+<962zJ_z3-xjTS!}CQ z$62wsjq~_X5?h_?!tJZ|WW$#W;+E4i7NA9x1r;U5CoYuFrjhNBtLM#Rd1+ex`aV0c zZ5_8iGFzqBoTy6jz0p*-5$6k1kQm4bA<`A7zdD*8g~n|OXvm32KV?NO*VfDsbz7ov zB0UnLjr3Wpg4ICWPgDM2jFEmR{cbd*zd9-t*)%Rj+#DC!80wre))v4>TMI^mw4kl7 zvP)i07W=~IWA7?o_)XyY+uECA#ml@=y!0c?wY7ks{m3(gi)LX*@JzT_4wD=tEe2~L z*WVSw1PY_*bpQr<(hj;xH#>tmZ%0b6o^{|7Un(}Lk zB`V1ExF6?ig%tPENOrS^?G3C3)Bx^v9-|W3h3@7-%W==K3C|r-}8hR!DNOgoa`-7Mmt7 zD^nY0XM_YNGlbh4L8)gC_Ta{AD)xLBV5N%~hhd1eH$=3ZA;QNQz}sX1^i}5ofr1>g6lBp|y4#y|{NF-< z&RCOEDPE44Wo3x@<4w=b9b=44veitQ$Us{&b}B#I)kX4ne@J&V8O-gE8>ZVJdD_{DHz}z*6pQQP+IaBb!Gi}6 i9z1yP;KAcBjDG_+@~wgiml(nT0000AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_1 }; SohGui::mSohMenu->AddWidget(path, "Evil Gossip Stone", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("EvilGossipStone")) - .Options(UIWidgets::CheckboxOptions().Tooltip("Don't you dare talk to them.")); + .Options(UIWidgets::CheckboxOptions().Tooltip("Gossip stones become hostile after being spoken to.")); } static RegisterShipInitFunc initFunc(OnConfigurationChanged, { CVAR("EvilGossipStone") }); diff --git a/soh/soh/Enhancements/Holiday/Archez.cpp b/soh/soh/Enhancements/Holiday/Archez.cpp index aaee3baf0..9bbf0ee0b 100644 --- a/soh/soh/Enhancements/Holiday/Archez.cpp +++ b/soh/soh/Enhancements/Holiday/Archez.cpp @@ -15,8 +15,7 @@ extern "C" { extern PlayState* gPlayState; } -#define AUTHOR "Archez" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v static bool sSkipNextLimb = false; static bool sSkipNextSkeleton = false; @@ -108,8 +107,7 @@ static void OnConfigurationChanged() { } static void RegisterMenu() { - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); + WidgetPath path = { "Holiday", "Visual", SECTION_COLUMN_1 }; SohGui::mSohMenu->AddWidget(path, "Snow Golems", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("SnowGolems")) .Callback([](WidgetInfo& info) { OnConfigurationChanged(); }) diff --git a/soh/soh/Enhancements/Holiday/Caladius.cpp b/soh/soh/Enhancements/Holiday/Caladius.cpp index 8a0b37df0..319eda149 100644 --- a/soh/soh/Enhancements/Holiday/Caladius.cpp +++ b/soh/soh/Enhancements/Holiday/Caladius.cpp @@ -6,6 +6,7 @@ #include "soh/Enhancements/custom-message/CustomMessageManager.h" #include "soh/Enhancements/randomizer/randomizer.h" #include "soh/frame_interpolation.h" +#include "soh/ObjectExtension/ActorListIndex.h" #include "soh_assets.h" extern "C" { @@ -16,8 +17,7 @@ extern PlayState* gPlayState; uint64_t GetUnixTimestamp(); } -#define AUTHOR "Caladius" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v bool isFeverDisabled = false; bool isExchangeDisabled = false; @@ -25,7 +25,8 @@ static float fontScale = 1.0f; extern GetItemEntry vanillaQueuedItemEntry; -std::vector boulderList = { ACTOR_OBJ_BOMBIWA, ACTOR_BG_ICE_SHELTER, ACTOR_EN_ISHI, ACTOR_OBJ_HAMISHI }; +std::vector boulderList = { ACTOR_OBJ_BOMBIWA, ACTOR_BG_ICE_SHELTER, ACTOR_EN_ISHI, ACTOR_EN_ISHI, + ACTOR_OBJ_HAMISHI }; std::string formatTimestampIceTrapFever(uint32_t value) { uint32_t sec = value / 10; @@ -69,17 +70,30 @@ s32 ActorSnapToFloor(Actor* refActor, PlayState* play, f32 arg2) { void RandomizeBoulder(Actor* refActor) { Actor* actor = (Actor*)refActor; - int16_t param = actor->params; + int16_t param = 0; int32_t yAdj = 0; - uint32_t roll = rand() % boulderList.size(); + + int32_t seed = gPlayState->sceneNum + actor->id + ((int32_t)(actor->world.pos.x * 10)) + + ((int32_t)(actor->world.pos.y * 10)) + ((int32_t)(actor->world.pos.z * 10)) + actor->params; + + uint32_t finalSeed = + ABS(seed) + (IS_RANDO ? Rando::Context::GetInstance()->GetSeed() : gSaveContext.ship.stats.fileCreatedAt); + Random_Init(finalSeed); + uint32_t roll = Random(0, boulderList.size()); + + u32 flag = actor->id == ACTOR_EN_ISHI ? ((actor->params >> 0xA) & 0x3C) | ((actor->params >> 6) & 3) + : actor->params & 0x3F; + if (boulderList[roll] == ACTOR_EN_ISHI) { - param = 3; + param = (Random(0, 2)) | ((flag & 0x3C) << 10) | ((flag & 3) << 6); + } else { + param = flag; } + yAdj = ActorSnapToFloor(actor, gPlayState, 0.0f); Actor_Spawn(&gPlayState->actorCtx, gPlayState, boulderList[roll], actor->world.pos.x, ActorSnapToFloor(actor, gPlayState, 0.0f), actor->world.pos.z, 0, 0, 0, param, false); - Actor_Kill(actor); } bool spawningPresents = false; @@ -216,21 +230,20 @@ static void OnPresentChange() { }); } +static bool isRandomizingBoulder = false; static void OnBlitzChange() { - COND_HOOK(OnSceneSpawnActors, CVarGetInteger(CVAR("Blitz.Enabled"), 0), []() { - if (!gPlayState) { + COND_HOOK(ShouldActorInit, CVarGetInteger(CVAR("Blitz.Enabled"), 0), [](void* actorRef, bool* should) { + if (isRandomizingBoulder) return; - } - ActorListEntry boulders = gPlayState->actorCtx.actorLists[ACTORCAT_PROP]; - Actor* currentActor = boulders.head; - if (currentActor != nullptr) { - while (currentActor != nullptr) { - for (auto& boulderActor : boulderList) { - if (currentActor->id == boulderActor) { - RandomizeBoulder(currentActor); - } - } - currentActor = currentActor->next; + + Actor* actor = (Actor*)actorRef; + for (auto& boulderActor : boulderList) { + if (actor->id == boulderActor) { + isRandomizingBoulder = true; + RandomizeBoulder(actor); + isRandomizingBoulder = false; + *should = false; + return; } } }); @@ -242,12 +255,6 @@ static void OnFeverConfigurationChanged() { if (fontScale < 1.0f) { fontScale = 1.0f; } - if (CVarGetInteger(CVAR("ExtendTimer"), 0) < 1) { - CVarSetInteger(CVAR("ExtendTimer"), 1); - } - if (CVarGetInteger(CVAR("StartTimer"), 0) < 1) { - CVarSetInteger(CVAR("StartTimer"), 1); - } } void CaladiusWindow::Draw() { @@ -271,18 +278,13 @@ void CaladiusWindow::Draw() { } static void RegisterMenu() { - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_2 }; SohGui::mSohMenu->AddWidget(path, "Holiday Fever", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("Fever.Enabled")) .Callback([](WidgetInfo& info) { OnFeverConfigurationChanged(); }) - .Options(UIWidgets::CheckboxOptions().Tooltip("Can you beat your objective before the Fever sets in?\n" - "- Obtaining Ice Traps extends your timer.")); - SohGui::mSohMenu->AddWidget(path, "Font: %.1fx", WIDGET_CVAR_SLIDER_FLOAT) - .CVar(CVAR("FontScale")) - .Callback([](WidgetInfo& info) { OnFeverConfigurationChanged(); }) - .PreFunc([](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("Fever.Enabled"), 0); }) - .Options(UIWidgets::FloatSliderOptions().DefaultValue(1.0f).Min(1.0f).Max(5.0f)); + .Options(UIWidgets::CheckboxOptions().Tooltip( + "Death will come for you when the timer runs out? Obtaining Ice Traps extends your timer. \n\nShould be " + "enabled before starting a new file, won't work well with existing files.")); SohGui::mSohMenu->AddWidget(path, "Starting Timer: %d minutes", WIDGET_CVAR_SLIDER_INT) .CVar(CVAR("StartTimer")) .Callback([](WidgetInfo& info) { OnFeverConfigurationChanged(); }) @@ -294,24 +296,13 @@ static void RegisterMenu() { .PreFunc([](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("Fever.Enabled"), 0); }) .Options(UIWidgets::IntSliderOptions().DefaultValue(5).Min(1).Max(10)); - SohGui::mSohMenu->AddWidget(path, "BoulderBlitzSect", WIDGET_SEPARATOR); - SohGui::mSohMenu->AddWidget(path, "Boulder Blitz", WIDGET_CVAR_CHECKBOX) + path.column = SECTION_COLUMN_1; + + SohGui::mSohMenu->AddWidget(path, "Shuffle Boulders & Ice", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("Blitz.Enabled")) .Callback([](WidgetInfo& info) { OnBlitzChange(); }) - .Options(UIWidgets::CheckboxOptions().Tooltip("Boulders will randomly be replaced with other boulder types.")); - - SohGui::mSohMenu->AddWidget(path, "OrnamentExchSect", WIDGET_SEPARATOR); - SohGui::mSohMenu->AddWidget(path, "Ornament Exchange", WIDGET_CVAR_CHECKBOX) - .CVar(CVAR("OrnExch.Enabled")) - .Callback([](WidgetInfo& info) { OnPresentChange(); }) .Options(UIWidgets::CheckboxOptions().Tooltip( - "See Malon as Young Link in Lon Lon Ranch to exchange Gifts for Ornaments!")); - - SohGui::mSohMenu->AddWidget(path, "Gifts Required: %d Gifts", WIDGET_CVAR_SLIDER_INT) - .CVar(CVAR("OrnExch.Amount")) - .Callback([](WidgetInfo& info) { OnFeverConfigurationChanged(); }) - .PreFunc([](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("OrnExch.Enabled"), 0); }) - .Options(UIWidgets::IntSliderOptions().DefaultValue(15).Min(5).Max(30)); + "Boulders & Ice will randomly be replaced with other boulders & ice when the scene loads.")); } static void RegisterMod() { diff --git a/soh/soh/Enhancements/Holiday/Fredomato.cpp b/soh/soh/Enhancements/Holiday/Fredomato.cpp index 86957255f..83778d9ed 100644 --- a/soh/soh/Enhancements/Holiday/Fredomato.cpp +++ b/soh/soh/Enhancements/Holiday/Fredomato.cpp @@ -7,8 +7,8 @@ #include "soh/Enhancements/randomizer/3drando/random.hpp" #include "soh/Enhancements/randomizer/location_access.h" #include "soh/Enhancements/randomizer/entrance.h" -#include "soh/Enhancements/custom-collectible/CustomCollectible.h" #include "soh/Notification/Notification.h" +#include "soh/Enhancements/custom-item/CustomItem.h" #include "soh/Enhancements/nametag.h" #include "objects/gameplay_field_keep/gameplay_field_keep.h" @@ -28,8 +28,7 @@ void DoorAna_GrabPlayer(DoorAna* doorAna, PlayState* play); } extern GetItemEntry vanillaQueuedItemEntry; -#define AUTHOR "Fredomato" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v static CollisionPoly snowballPoly; static f32 raycastResult; @@ -165,20 +164,6 @@ static void SpawnRandomGrotto() { } void SpawnStick(Vec3f pos) { - CustomCollectible::Spawn( - pos.x, pos.y + 150.0f, pos.z, 0, CustomCollectible::KILL_ON_TOUCH | CustomCollectible::TOSS_ON_SPAWN, 0, - [](Actor* actor, PlayState* play) { - FredsQuestWoodOnHand++; - Audio_PlaySoundGeneral(NA_SE_SY_METRONOME, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale, - &gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb); - }, - [](Actor* actor, PlayState* play) { - Matrix_Scale(40.0f, 40.0f, 40.0f, MTXMODE_APPLY); - for (int i = 4; i < 7; i++) { - Matrix_RotateZYX(800 * i, 0, 800 * i, MTXMODE_APPLY); - GetItem_Draw(play, GID_STICK); - } - }); } Actor* specialTree = nullptr; @@ -269,8 +254,6 @@ void DrawCrazyTaxiArrow(Actor* actor, PlayState* play) { } void SpawnCrazyTaxiArrow() { - EnItem00* arrow = CustomCollectible::Spawn(0, 0, 0, 0, CustomCollectible::KEEP_ON_PLAYER, 0, NULL, NULL); - arrow->actor.draw = DrawCrazyTaxiArrow; } void CollectionPoint_Update(Actor* actor, PlayState* play) { @@ -339,13 +322,6 @@ void CollectionPoint_Draw(Actor* actor, PlayState* play) { } void SpawnCollectionPoint() { - EnItem00* collectionPoint = CustomCollectible::Spawn(859.0f, 347.0f, 5185.0f, 0xB000, 0, 0, NULL, NULL); - collectionPoint->actor.update = CollectionPoint_Update; - collectionPoint->actor.draw = CollectionPoint_Draw; - collectionPoint->actor.flags |= ACTOR_FLAG_DRAW_CULLING_DISABLED; - SkelAnime_InitFlex(gPlayState, &collectionPointSkelAnime, (FlexSkeletonHeader*)&object_toryo_Skel_007150, - (AnimationHeader*)&object_toryo_Anim_000E50, collectionPointJointTable, - collectionPointMorphTable, 17); } void RandomTrap_Update(Actor* actor, PlayState* play) { @@ -398,8 +374,7 @@ void RandomTrap_Draw(Actor* actor, PlayState* play) { void SpawnRandomTrap() { Vec3f pos = FindValidPos(2000.0f); - EnItem00* randomTrap = - CustomCollectible::Spawn(pos.x, pos.y, pos.z, 0, CustomCollectible::TOSS_ON_SPAWN, 0, NULL, NULL); + EnItem00* randomTrap = CustomItem::Spawn(pos.x, pos.y, pos.z, 0, CustomItem::TOSS_ON_SPAWN, 0, NULL, NULL); SoundSource_PlaySfxAtFixedWorldPos(gPlayState, &randomTrap->actor.world.pos, 20, NA_SE_EV_LIGHTNING); randomTrap->actor.update = RandomTrap_Update; randomTrap->actor.draw = RandomTrap_Draw; @@ -448,67 +423,13 @@ static void OnConfigurationChanged() { } static void RegisterMenu() { + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_2 }; - // UIWidgets::EnhancementSliderFloat("Xfloat", "Xfloat", CVAR("tmpxf"), 0.0f, 10.0f, "%.2f", 1.0f, false); - // UIWidgets::EnhancementSliderFloat("Yfloat", "Yfloat", CVAR("tmpyf"), 0.0f, 10.0f, "%.2f", 1.0f, false); - // UIWidgets::EnhancementSliderFloat("Zfloat", "Zfloat", CVAR("tmpzf"), 0.0f, 10.0f, "%.2f", 1.0f, false); - // UIWidgets::EnhancementSliderInt("Xs", "Xs", CVAR("tmpxs"), 0, UINT16_MAX, "%d", 1, false); - // UIWidgets::EnhancementSliderInt("Ys", "Ys", CVAR("tmpys"), 0, UINT16_MAX, "%d", 1, false); - // UIWidgets::EnhancementSliderInt("Zs", "Zs", CVAR("tmpzs"), 0, UINT16_MAX, "%d", 1, false); - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); - - SohGui::mSohMenu->AddWidget(path, "Fred's Quest", WIDGET_CVAR_CHECKBOX) - .CVar(CVAR("FredsQuest.Enabled")) - .Options(UIWidgets::CheckboxOptions().Tooltip( - "Collect wood and bring it to the collection point in Hyrule Field for a small reward.")); - SohGui::mSohMenu->AddWidget(path, "Crazy Taxi Arrow", WIDGET_CVAR_CHECKBOX) - .CVar(CVAR("FredsQuest.CrazyTaxiArrow")) - .PreFunc( - [](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("FredsQuest.Enabled"), 0); }) - .Options(UIWidgets::CheckboxOptions().Tooltip( - "Collect wood and bring it to the collection point in Hyrule Field for a small reward.")); - - SohGui::mSohMenu->AddWidget(path, "Wood Needed", WIDGET_CVAR_SLIDER_INT) - .CVar(CVAR("FredsQuest.WoodNeeded")) - .Callback([](WidgetInfo& info) { OnConfigurationChanged(); }) - .PreFunc( - [](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("FredsQuest.Enabled"), 0); }) - .Options(UIWidgets::IntSliderOptions().DefaultValue(300).Min(0).Max(1000)); - - SohGui::mSohMenu->AddWidget(path, "Tree Bonk Drop Rate", WIDGET_CVAR_SLIDER_INT) - .CVar(CVAR("FredsQuest.TreeBonkDropRate")) - .Callback([](WidgetInfo& info) { OnConfigurationChanged(); }) - .PreFunc( - [](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("FredsQuest.Enabled"), 0); }) - .Options(UIWidgets::IntSliderOptions().DefaultValue(1).Min(0).Max(10)); - - SohGui::mSohMenu->AddWidget(path, "Tree Break Drop Rate", WIDGET_CVAR_SLIDER_INT) - .CVar(CVAR("FredsQuest.TreeBreakDropRate")) - .Callback([](WidgetInfo& info) { OnConfigurationChanged(); }) - .PreFunc( - [](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("FredsQuest.Enabled"), 0); }) - .Options(UIWidgets::IntSliderOptions().DefaultValue(3).Min(0).Max(50)); - - SohGui::mSohMenu->AddWidget(path, "Special Break Drop Rate", WIDGET_CVAR_SLIDER_INT) - .CVar(CVAR("FredsQuest.SpecialBreakDropRate")) - .Callback([](WidgetInfo& info) { OnConfigurationChanged(); }) - .PreFunc( - [](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("FredsQuest.Enabled"), 0); }) - .Options(UIWidgets::IntSliderOptions().DefaultValue(10).Min(0).Max(50)); - - SohGui::mSohMenu->AddWidget(path, "Encumbered Threshold", WIDGET_CVAR_SLIDER_INT) - .CVar(CVAR("FredsQuest.EncumberedThreshold")) - .Callback([](WidgetInfo& info) { OnConfigurationChanged(); }) - .PreFunc( - [](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("FredsQuest.Enabled"), 0); }) - .Options(UIWidgets::IntSliderOptions().DefaultValue(60).Min(0).Max(200).Tooltip( - "If you have more than this many sticks, you will be encumbered and run slower. 0 for disabled")); - - SohGui::mSohMenu->AddWidget(path, "Random Traps", WIDGET_CVAR_CHECKBOX) + SohGui::mSohMenu->AddWidget(path, "Chasing Knockback Spikes", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("RandomTraps.Enabled")) .Options(UIWidgets::CheckboxOptions().Tooltip( - "Random traps will spawn around you at a configurable rate. (Currently only knockback)")); + "Random spikes will spawn around you at a configurable rate, chasing you for a short time before " + "disappearing. If they touch you, you get knocked back.")); SohGui::mSohMenu->AddWidget(path, "Trap Lifetime (Seconds)", WIDGET_CVAR_SLIDER_INT) .CVar(CVAR("RandomTraps.Lifetime")) diff --git a/soh/soh/Enhancements/Holiday/Grimey.cpp b/soh/soh/Enhancements/Holiday/Grimey.cpp index 1ebf9cac8..d8f26cacf 100644 --- a/soh/soh/Enhancements/Holiday/Grimey.cpp +++ b/soh/soh/Enhancements/Holiday/Grimey.cpp @@ -15,10 +15,10 @@ void func_80ABBBA8(EnNutsball* nut, PlayState* play); void EnNutsball_Draw(Actor* nut, PlayState* play); } -#define AUTHOR "Grimey" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v static bool spawningPenguins = false; +static u32 hailstormActiveTimer = 0; typedef enum { PENGUIN_STATE_IDLE, @@ -103,46 +103,31 @@ void Penguin_Destroy(Actor* actor, PlayState* play) { static void OnConfigurationChanged() { COND_HOOK(OnPlayerUpdate, CVarGetInteger(CVAR("Hailstorm"), 0), []() { - // Every frame has a 1/500 chance of spawning close hail - if (rand() % 500 == 0) { - int spawned = 0; - while (spawned < 1) { - Vec3f pos = GET_PLAYER(gPlayState)->actor.world.pos; - pos.x += (float)Random(0, 50) - 25.0f; - pos.z += (float)Random(0, 50) - 25.0f; - pos.y += 200.0f; - - Actor* actor = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_NUTSBALL, pos.x, pos.y, pos.z, 0, - 0, 0, 0, false); - EnNutsball* nut = (EnNutsball*)actor; - nut->actor.draw = EnNutsball_Draw; - nut->actor.shape.rot.y = 0; - nut->timer = 0; - nut->actionFunc = func_80ABBBA8; - nut->actor.speedXZ = 0.0f; - nut->actor.gravity = -2.0f; - spawned++; - } + // Every frame has a 1/1000 chance to start a hailstorm if there isn't one already + if (hailstormActiveTimer == 0 && rand() % 1000 == 0) { + hailstormActiveTimer = 20 * 10; // Lasts for 20 seconds } - // Every frame has a 1/50 chance of spawning far hail - if (rand() % 50 == 0) { - int spawned = 0; - while (spawned < 1) { - Vec3f pos = GET_PLAYER(gPlayState)->actor.world.pos; - pos.x += (float)Random(0, 500) - 250.0f; - pos.z += (float)Random(0, 500) - 250.0f; - pos.y += 200.0f; + if (hailstormActiveTimer > 0) { + hailstormActiveTimer--; + if (rand() % 2 == 0) { + int spawned = 0; + while (spawned < 1) { + Vec3f pos = GET_PLAYER(gPlayState)->actor.world.pos; + pos.x += (float)Random(0, 500) - 250.0f; + pos.z += (float)Random(0, 500) - 250.0f; + pos.y += 500.0f; - Actor* actor = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_NUTSBALL, pos.x, pos.y, pos.z, 0, - 0, 0, 0, false); - EnNutsball* nut = (EnNutsball*)actor; - nut->actor.draw = EnNutsball_Draw; - nut->actor.shape.rot.y = 0; - nut->timer = 0; - nut->actionFunc = func_80ABBBA8; - nut->actor.speedXZ = 0.0f; - nut->actor.gravity = -2.0f; - spawned++; + Actor* actor = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_NUTSBALL, pos.x, pos.y, + pos.z, 0, 0, 0, 0, false); + EnNutsball* nut = (EnNutsball*)actor; + nut->actor.draw = EnNutsball_Draw; + nut->actor.shape.rot.y = 0; + nut->timer = 0; + nut->actionFunc = func_80ABBBA8; + nut->actor.speedXZ = 0.0f; + nut->actor.gravity = -2.0f; + spawned++; + } } } }); @@ -209,12 +194,7 @@ static void OnConfigurationChanged() { } static void RegisterMenu() { - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); - - SohGui::mSohMenu->AddWidget(path, "Penguins", WIDGET_CVAR_CHECKBOX) - .CVar(CVAR("Penguins")) - .Options(UIWidgets::CheckboxOptions().Tooltip("Penguins will spawn in huddles throughout hyrule")); + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_1 }; SohGui::mSohMenu->AddWidget(path, "Hailstorm", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("Hailstorm")) diff --git a/soh/soh/Enhancements/Holiday/LL.cpp b/soh/soh/Enhancements/Holiday/LL.cpp index 5b3cd2ec6..477947992 100644 --- a/soh/soh/Enhancements/Holiday/LL.cpp +++ b/soh/soh/Enhancements/Holiday/LL.cpp @@ -10,9 +10,7 @@ extern PlayState* gPlayState; // TODO: Include anything you need here from C land } -// TODO: Change this to YourName -#define AUTHOR "LL" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v static ImVec4 customColorZero = RAINBOW_PRESETS[0][0]; static ImVec4 customColorOne = RAINBOW_PRESETS[0][1]; @@ -54,8 +52,7 @@ static void OnConfigurationChanged() { } static void RegisterMenu() { - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_1 }; SohGui::mSohMenu->AddWidget(path, "Custom Rainbows", WIDGET_CVAR_CHECKBOX).CVar(CVAR("EnableCustomRainbows")); diff --git a/soh/soh/Enhancements/Holiday/NotProxySaw.cpp b/soh/soh/Enhancements/Holiday/NotProxySaw.cpp index 704579312..a76875bb0 100644 --- a/soh/soh/Enhancements/Holiday/NotProxySaw.cpp +++ b/soh/soh/Enhancements/Holiday/NotProxySaw.cpp @@ -12,8 +12,7 @@ extern "C" { extern PlayState* gPlayState; } -#define AUTHOR "NotProxySaw" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v int dialogIndex = 0; int affection = 0; @@ -35,40 +34,838 @@ const std::vector dialogs = { { // 0 { "I'm here to end this... peacefully.", 1, 2 }, { "I respect a man with ambition.", -1, 3 }, } }, - { // 1 + { // 1 - Loneliness path "Lonely? Power's my only ally.", { { "There's more to you than that.", 3, 4 }, - { "I get it more than you think.", 5, 4 }, - { "Why not let someone in?", 2, 4 }, + { "I get it more than you think.", 5, 6 }, + { "Why not let someone in?", 2, 5 }, } }, - { // 2 + { // 2 - Peace path "Peace? It's meaningless here.", { - { "Maybe you've forgotten peace.", 2, 4 }, - { "Power isn't everything.", 4, 4 }, - { "Ally, not enemy.. that's my goal.", 1, 4 }, + { "Maybe you've forgotten peace.", 2, 7 }, + { "Power isn't everything.", 4, 8 }, + { "Ally, not enemy.. that's my goal.", 1, 9 }, } }, - { // 3 + { // 3 - Respect path "Respect? From you?", { - { "We're not so different.", 3, 4 }, - { "Maybe I admire your strength.", 5, 4 }, - { "True power must be earned.", -1, 4 }, + { "We're not so different.", 3, 10 }, + { "Maybe I admire your strength.", 5, 11 }, + { "True power must be earned.", -1, 12 }, } }, - { // 4 + { // 4 - Early convergence "You're... different than I thought.", { - { "Together, we'd be unstoppable.", 5, 5 }, - { "Power won't bring fulfillment.", 3, 5 }, - { "Let's change the world.", 4, 5 }, + { "Together, we'd be unstoppable.", 4, 13 }, + { "Power won't bring fulfillment.", 3, 14 }, + { "Let's change the world.", 5, 15 }, } }, - { // 5 - "What if I trusted you with power?", + { // 5 - Vulnerability path + "No one has ever... understood before.", { - { "I'll protect it. And you.", 10, -1 }, - { "Together, we're unstoppable.", 5, -1 }, - { "Our way. Our history.", -3, -1 }, + { "I want to understand you.", 5, 16 }, + { "We all need someone.", 4, 17 }, + { "Your guard can come down with me.", 3, 18 }, + } }, + { // 6 - Shared experience path + "You think you know my struggle?", + { + { "I've faced darkness too.", 4, 19 }, + { "We're both outcasts.", 5, 20 }, + { "Tell me your story.", 3, 16 }, + } }, + { // 7 - Memory path + "Peace... I barely remember it.", + { + { "Let me remind you.", 5, 21 }, + { "We can create new memories.", 4, 22 }, + { "The desert remembers peace.", 2, 23 }, + } }, + { // 8 - Philosophy path + "Then what IS everything?", + { + { "Connection. Trust. Love.", 5, 24 }, + { "Finding your true purpose.", 3, 25 }, + { "Balance between power and heart.", 4, 26 }, + } }, + { // 9 - Alliance path + "An ally? What could you offer me?", + { + { "Companionship you've never known.", 5, 27 }, + { "A kingdom rebuilt together.", 3, 28 }, + { "Freedom from this endless cycle.", 4, 29 }, + } }, + { // 10 - Similarity path + "Similar? Explain yourself.", + { + { "We both fight for what we believe.", 3, 30 }, + { "We've both been betrayed.", 5, 31 }, + { "We're both stronger than we show.", 4, 32 }, + } }, + { // 11 - Admiration path + "Admire? This isn't a trick?", + { + { "Your determination is inspiring.", 5, 33 }, + { "You've overcome so much.", 4, 34 }, + { "I see the man behind the power.", 6, 35 }, + } }, + { // 12 - Challenge path (lower affection) + "Then prove you've earned it!", + { + { "I've earned the right to stand here.", 2, 36 }, + { "I'm not here to prove anything to you.", -2, 37 }, + { "My actions speak louder than words.", 3, 38 }, + } }, + { // 13 - Partnership power path + "Unstoppable... together?", + { + { "Your strength and my courage.", 5, 39 }, + { "We'd reshape Hyrule as equals.", 4, 40 }, + { "Imagine what we could achieve.", 3, 41 }, + } }, + { // 14 - Fulfillment path + "What could fulfill me then?", + { + { "Someone who sees you, truly.", 6, 42 }, + { "A purpose beyond conquest.", 3, 43 }, + { "The warmth of genuine connection.", 5, 44 }, + } }, + { // 15 - Revolution path + "Change the world... how?", + { + { "End this cycle of hatred.", 4, 45 }, + { "Build something beautiful together.", 5, 46 }, + { "Rewrite our destinies.", 6, 47 }, + } }, + { // 16 - Understanding convergence + "No one has ever tried to understand...", + { + { "I'm not no one. I'm here.", 6, 48 }, + { "Your past shaped you, not defined you.", 4, 49 }, + { "Let me be the first.", 5, 50 }, + } }, + { // 17 - Need path + "Need? I've survived alone this long.", + { + { "Surviving isn't living.", 5, 51 }, + { "You don't have to anymore.", 6, 52 }, + { "Even the strongest need support.", 4, 53 }, + } }, + { // 18 - Guard down path + "Why would I lower my guard?", + { + { "Because I'm lowering mine first.", 6, 54 }, + { "Trust has to start somewhere.", 4, 55 }, + { "You're safe with me.", 5, 56 }, + } }, + { // 19 - Darkness shared path + "Your darkness... tell me.", + { + { "I've fought my own demons.", 5, 57 }, + { "We all have shadows within us.", 4, 58 }, + { "But I chose light. You can too.", 3, 59 }, + } }, + { // 20 - Outcast bond path + "Outcasts... yes. We are.", + { + { "Then let's find belonging in each other.", 7, 60 }, + { "We don't have to be alone.", 5, 61 }, + { "Our shared pain connects us.", 4, 62 }, + } }, + { // 21 - Reminder path + "How could you remind me?", + { + { "Close your eyes. Remember the desert wind.", 5, 63 }, + { "Through moments like this one.", 6, 64 }, + { "Let me show you, not tell you.", 4, 65 }, + } }, + { // 22 - New memories path + "New memories... with you?", + { + { "Every ending is a new beginning.", 6, 66 }, + { "Yes. Better ones than you've known.", 5, 67 }, + { "We write our own story from here.", 4, 68 }, + } }, + { // 23 - Desert memories path + "The desert... my home once.", + { + { "It can be again, differently.", 4, 69 }, + { "Home is where the heart is.", 5, 70 }, + { "I'd like to see it with you.", 6, 71 }, + } }, + { // 24 - Love path (high affection) + "Love? You speak of... love?", + { + { "Is that so impossible?", 6, 72 }, + { "I think I'm falling for you.", 8, 73 }, + { "Love is the strongest power.", 5, 74 }, + } }, + { // 25 - Purpose path + "My purpose was always power.", + { + { "You're meant for more.", 5, 75 }, + { "Purpose can evolve.", 4, 76 }, + { "Find purpose in protecting, not destroying.", 3, 77 }, + } }, + { // 26 - Balance path + "Balance... I've never had that.", + { + { "I'll help you find it.", 6, 78 }, + { "We can balance each other.", 7, 79 }, + { "It's not too late to learn.", 4, 80 }, + } }, + { // 27 - Companionship offer + "Companionship? What does that mean?", + { + { "It means you're not alone anymore.", 7, 81 }, + { "Someone to share victories and defeats.", 5, 82 }, + { "A partner in every sense.", 6, 83 }, + } }, + { // 28 - Kingdom rebuild path + "Rebuild? After all I've destroyed?", + { + { "Redemption is always possible.", 5, 84 }, + { "We build on lessons learned.", 4, 85 }, + { "Your past doesn't define your future.", 6, 86 }, + } }, + { // 29 - Freedom path + "Freedom from the cycle...", + { + { "Break free with me.", 7, 87 }, + { "We choose our own fate.", 6, 88 }, + { "This doesn't have to be our story.", 5, 89 }, + } }, + { // 30 - Belief path + "You fight for Hyrule. I fight for...", + { + { "For recognition. I see you.", 6, 90 }, + { "We can fight for each other now.", 7, 91 }, + { "What we fight for can change.", 4, 92 }, + } }, + { // 31 - Betrayal bond path + "Betrayed... yes. You know that pain?", + { + { "More than you realize.", 6, 93 }, + { "But I won't betray you.", 7, 94 }, + { "We can heal together.", 5, 95 }, + } }, + { // 32 - Hidden strength path + "Stronger than I show?", + { + { "Your vulnerability is strength.", 6, 96 }, + { "True strength is opening your heart.", 7, 97 }, + { "I see both your power and pain.", 5, 98 }, + } }, + { // 33 - Inspiration path + "You're... inspired by me?", + { + { "Your resolve never wavered.", 6, 99 }, + { "Despite everything, you stood tall.", 7, 100 }, + { "That kind of strength is rare.", 5, 101 }, + } }, + { // 34 - Overcome path + "I've overcome... so much pain.", + { + { "And you don't have to face more alone.", 7, 102 }, + { "Let me help carry that burden.", 6, 103 }, + { "Your journey shaped a remarkable person.", 5, 104 }, + } }, + { // 35 - True sight path (very high affection) + "You... you see me? The real me?", + { + { "Every part of you.", 8, 105 }, + { "Behind the armor and anger, yes.", 7, 106 }, + { "And I want to know you more.", 6, 107 }, + } }, + { // 36 - Earned respect path + "Perhaps you have earned something...", + { + { "Your respect means everything.", 5, 108 }, + { "I've earned a chance.", 4, 109 }, + { "Maybe even your trust?", 6, 110 }, + } }, + { // 37 - Confrontation path (recovery possible) + "Arrogant! Just like I thought!", + { + { "Wait, I didn't mean it like that.", 3, 111 }, + { "You're right, I'm sorry.", 4, 112 }, + { "Let me start over.", 2, 113 }, + } }, + { // 38 - Action path + "Actions... like coming here unarmed?", + { + { "I'm armed with only honesty.", 6, 114 }, + { "My sword isn't drawn.", 5, 115 }, + { "I chose words over weapons.", 7, 116 }, + } }, + { // 39 - Combined strength + "My strength and your courage...", + { + { "We'd be legendary.", 6, 117 }, + { "Nothing could stand in our way.", 5, 118 }, + { "But more than that, we'd have each other.", 8, 119 }, + } }, + { // 40 - Equals path + "Equals? You'd see me as equal?", + { + { "Never as less than.", 7, 81 }, + { "Partners in every way.", 8, 83 }, + { "Two halves of a greater whole.", 6, 79 }, + } }, + { // 41 - Achievement dreams + "I've imagined... but never with another.", + { + { "Dream with me now.", 7, 66 }, + { "Our achievements, together.", 6, 82 }, + { "Let's make it real.", 8, 117 }, + } }, + { // 42 - True sight deep + "Someone who sees me truly...", + { + { "I see your scars and your dreams.", 8, 105 }, + { "Every layer, every truth.", 7, 106 }, + { "And I'm not afraid.", 9, 54 }, + } }, + { // 43 - New purpose + "A purpose beyond conquest...", + { + { "Building instead of destroying.", 6, 84 }, + { "Creating a legacy of hope.", 7, 86 }, + { "Finding joy in creation.", 5, 76 }, + } }, + { // 44 - Connection warmth + "Warmth... I've been cold so long.", + { + { "Let me warm your heart.", 9, 65 }, + { "You don't have to be cold anymore.", 8, 52 }, + { "I'll be your warmth.", 10, 56 }, + } }, + { // 45 - End hatred + "End the hatred... can it be done?", + { + { "With you by my side, yes.", 8, 88 }, + { "We start by choosing love.", 9, 74 }, + { "Together, we break the cycle.", 7, 87 }, + } }, + { // 46 - Build beauty + "Something beautiful... with you?", + { + { "The most beautiful thing.", 9, 73 }, + { "A future worth living for.", 8, 86 }, + { "Our love could heal Hyrule.", 10, 95 }, + } }, + { // 47 - Rewrite destiny + "Rewrite our destinies... together...", + { + { "Our story, our way.", 9, 68 }, + { "No prophecy can stop us.", 8, 88 }, + { "Destiny is what we make it.", 10, 119 }, + } }, + { // 48 - Here for you + "You're here... for me?", + { + { "Only for you.", 9, 73 }, + { "I chose you, Ganondorf.", 10, 94 }, + { "And I'm not leaving.", 8, 52 }, + } }, + { // 49 - Past vs present + "Shaped but not defined...", + { + { "You define yourself now.", 8, 76 }, + { "Your future is unwritten.", 7, 86 }, + { "Define yourself through love.", 9, 74 }, + } }, + { // 50 - First to understand + "The first to try...", + { + { "And I'll never stop trying.", 10, 103 }, + { "You deserve understanding.", 8, 95 }, + { "Let me be your first and last.", 9, 61 }, + } }, + { // 51 - Living vs surviving + "Living... what is that like?", + { + { "Let me show you.", 9, 65 }, + { "It's everything you've missed.", 8, 64 }, + { "We'll discover it together.", 10, 66 }, + } }, + { // 52 - No more alone + "I don't... have to be alone?", + { + { "Never again.", 10, 81 }, + { "I'm here, always.", 9, 102 }, + { "We'll face everything together.", 8, 53 }, + } }, + { // 53 - Strongest need support + "Even the strongest...", + { + { "Especially the strongest.", 8, 103 }, + { "Let me be your strength.", 9, 97 }, + { "We're stronger together.", 10, 119 }, + } }, + { // 54 - Mutual vulnerability + "You'd... lower yours first?", + { + { "I already have.", 10, 94 }, + { "My heart is open to you.", 9, 97 }, + { "I trust you completely.", 11, 110 }, + } }, + { // 55 - Trust begins + "Trust... starting here?", + { + { "Right here, right now.", 9, 64 }, + { "With us, with this moment.", 10, 94 }, + { "I trust you with everything.", 8, 110 }, + } }, + { // 56 - Safe haven + "Safe... with you?", + { + { "Always. I promise.", 11, 94 }, + { "I'll protect your heart.", 10, 103 }, + { "You're safe in my arms.", 12, 60 }, + } }, + { // 57 - Shared demons + "Your demons... like mine?", + { + { "We can fight them together.", 9, 53 }, + { "They don't control us anymore.", 8, 59 }, + { "Love conquers all darkness.", 10, 74 }, + } }, + { // 58 - Universal shadows + "Shadows within us all...", + { + { "But you bring light to mine.", 10, 63 }, + { "Let's be each other's light.", 11, 79 }, + { "Together we shine brighter.", 9, 118 }, + } }, + { // 59 - Choose light + "I could... choose light?", + { + { "You already are, by listening.", 9, 48 }, + { "Choose me, choose light.", 10, 88 }, + { "It's never too late.", 8, 80 }, + } }, + { // 60 - Belonging together + "Belonging... in each other...", + { + { "You belong with me.", 12, 83 }, + { "We're home in each other.", 11, 119 }, + { "My heart is your home.", 13, 119 }, + } }, + { // 61 - Together forever + "Not alone... anymore...", + { + { "Never, ever alone.", 11, 102 }, + { "I'll always be here.", 10, 119 }, + { "Forever together.", 12, 119 }, + } }, + { // 62 - Connected pain + "Our shared pain connects us...", + { + { "Now let joy connect us.", 10, 117 }, + { "Pain brought us here, love keeps us.", 11, 95 }, + { "From pain to paradise.", 9, 51 }, + } }, + { // 63 - Desert meditation + "The desert wind... I remember...", + { + { "Hold onto that feeling.", 9, 64 }, + { "Peace can return.", 10, 69 }, + { "I'll be your peace.", 11, 56 }, + } }, + { // 64 - This moment + "This moment... it feels...", + { + { "Like coming home?", 10, 79 }, + { "Like the start of something beautiful?", 11, 66 }, + { "Right. It feels right.", 12, 79 }, + } }, + { // 65 - Show not tell + "Show me... how?", + { + { "Through actions, through devotion.", 10, 114 }, + { "Every day, every moment.", 11, 102 }, + { "Let me love you.", 12, 73 }, + } }, + { // 66 - New beginning + "A new beginning... with you...", + { + { "Our greatest adventure.", 11, 117 }, + { "Better than any ending.", 10, 117 }, + { "The best is yet to come.", 12, 89 }, + } }, + { // 67 - Better memories + "Better memories... I want that.", + { + { "Then take my hand.", 12, 119 }, + { "We'll make them together.", 11, 68 }, + { "Starting right now.", 10, 64 }, + } }, + { // 68 - Write our story + "Our story... from here...", + { + { "The greatest story ever told.", 11, 117 }, + { "Written in love, not war.", 12, 116 }, + { "A story for the ages.", 10, 89 }, + } }, + { // 69 - Home differently + "Home, differently... explain.", + { + { "With love, not loneliness.", 10, 119 }, + { "Together, not alone.", 11, 119 }, + { "Our home, our rules.", 9, 71 }, + } }, + { // 70 - Heart is home + "Where the heart is...", + { + { "My heart is with you.", 12, 83 }, + { "You are my home.", 13, 119 }, + { "Home is in your eyes.", 11, 105 }, + } }, + { // 71 - See together + "You'd... visit my homeland with me?", + { + { "Anywhere you go, I go.", 12, 61 }, + { "I want to see your world.", 11, 107 }, + { "Your past is part of our future.", 10, 86 }, + } }, + { // 72 - Love not impossible + "Not impossible... but unexpected.", + { + { "The best things are unexpected.", 11, 117 }, + { "Sometimes fate surprises us.", 10, 88 }, + { "I love you, Ganondorf.", 13, 73 }, + } }, + { // 73 - Falling confession + "You're... falling for me?", + { + { "I've already fallen.", 14, -1 }, + { "Completely, utterly, truly.", 13, -1 }, + { "Catch me?", 12, -1 }, + } }, + { // 74 - Love strongest power + "Love... the strongest power...", + { + { "And it's ours to share.", 12, 119 }, + { "More powerful than the Triforce.", 13, -1 }, + { "Let love be our strength.", 11, 97 }, + } }, + { // 75 - Meant for more + "Meant for more... perhaps...", + { + { "I know you are.", 10, 76 }, + { "You're meant for greatness and love.", 11, 101 }, + { "Let me prove it to you.", 9, 65 }, + } }, + { // 76 - Purpose evolves + "Purpose can evolve...", + { + { "Like we evolve together.", 11, 78 }, + { "Grow with me.", 10, 117 }, + { "Our purpose is each other.", 12, 83 }, + } }, + { // 77 - Protect not destroy + "Protect... instead of destroy...", + { + { "Protect what we build together.", 10, 84 }, + { "Protect each other's hearts.", 11, 103 }, + { "I'll teach you, if you teach me.", 9, 78 }, + } }, + { // 78 - Help find balance + "You'll help me find it?", + { + { "Every step of the way.", 11, 102 }, + { "We'll find it together.", 10, 79 }, + { "I'm already helping, see?", 12, 64 }, + } }, + { // 79 - Balance each other + "Balance each other...", + { + { "Perfect harmony.", 12, 117 }, + { "Yin and yang.", 11, 119 }, + { "Two souls, one heart.", 13, 119 }, + } }, + { // 80 - Never too late + "Never too late... truly?", + { + { "For love? Never.", 12, 119 }, + { "You're here now, that's what matters.", 11, 64 }, + { "This is your moment.", 10, 88 }, + } }, + { // 81 - Not alone anymore + "Not alone... anymore...", + { + { "You have me, always.", 12, 102 }, + { "We have each other.", 13, 119 }, + { "Forever and always.", 11, 119 }, + } }, + { // 82 - Share everything + "Share victories and defeats...", + { + { "Everything, together.", 11, 119 }, + { "In good times and bad.", 12, 83 }, + { "Till the end of time.", 10, 119 }, + } }, + { // 83 - Partner every sense + "Partner in every sense...", + { + { "In battle and in love.", 12, 91 }, + { "Soul mates.", 13, 119 }, + { "My other half.", 14, -1 }, + } }, + { // 84 - Redemption possible + "Redemption... for me?", + { + { "For everyone. Especially you.", 11, 95 }, + { "I believe in you.", 12, 109 }, + { "Love redeems all.", 10, 74 }, + } }, + { // 85 - Build on lessons + "Lessons learned... yes...", + { + { "Wisdom through experience.", 10, 76 }, + { "Our past guides our future.", 11, 86 }, + { "Together we're wiser.", 9, 78 }, + } }, + { // 86 - Future undefined + "My future... undefined...", + { + { "Let's define it together.", 12, 89 }, + { "A blank canvas for us to paint.", 11, 68 }, + { "Our future is love.", 13, 119 }, + } }, + { // 87 - Break free together + "Break free... with you...", + { + { "Hand in hand.", 13, 119 }, + { "Into a new dawn.", 12, 117 }, + { "Freedom through love.", 14, -1 }, + } }, + { // 88 - Choose fate + "We choose... our own fate...", + { + { "And I choose you.", 14, -1 }, + { "Our fate is our love.", 13, 119 }, + { "Destiny be damned.", 12, 89 }, + } }, + { // 89 - Not our story + "This doesn't have to be our story...", + { + { "We write a better one.", 13, 117 }, + { "Our love story.", 14, -1 }, + { "A story of hope.", 12, 117 }, + } }, + { // 90 - Recognition found + "You... see me...", + { + { "I see you, I love you.", 13, 105 }, + { "All of you.", 12, 98 }, + { "And I always will.", 14, -1 }, + } }, + { // 91 - Fight for each other + "Fight for each other...", + { + { "With each other.", 13, 119 }, + { "Side by side.", 12, 117 }, + { "Because of each other.", 14, -1 }, + } }, + { // 92 - What we fight changes + "What we fight for... changes...", + { + { "Love changes everything.", 13, 95 }, + { "You've changed me.", 12, 104 }, + { "We've changed each other.", 14, -1 }, + } }, + { // 93 - Know betrayal pain + "You know that pain...", + { + { "But we heal together.", 12, 95 }, + { "No more betrayal, only trust.", 13, 110 }, + { "Our bond is unbreakable.", 11, 94 }, + } }, + { // 94 - Won't betray promise + "You won't... betray me?", + { + { "Never. I swear it.", 14, -1 }, + { "You have my word, my heart.", 13, 119 }, + { "I'd sooner die.", 12, 102 }, + } }, + { // 95 - Heal together + "Heal... together...", + { + { "Our love is the remedy.", 13, 119 }, + { "Time and trust heal all.", 12, 110 }, + { "I'll help you heal.", 14, -1 }, + } }, + { // 96 - Vulnerability strength + "Vulnerability... is strength?", + { + { "The greatest strength.", 12, 101 }, + { "It takes courage to be vulnerable.", 13, 99 }, + { "With me, you can be vulnerable.", 14, -1 }, + } }, + { // 97 - Open heart strength + "Opening my heart...", + { + { "Is the bravest thing you'll do.", 13, 99 }, + { "I'll cherish it always.", 14, -1 }, + { "Your heart is safe with me.", 15, -1 }, + } }, + { // 98 - Power and pain + "My power and pain...", + { + { "Both make you who you are.", 12, 104 }, + { "I love all of you.", 14, -1 }, + { "Let me ease your pain.", 13, 95 }, + } }, + { // 99 - Unwavering resolve + "My resolve... you noticed?", + { + { "How could I not?", 12, 100 }, + { "It's magnificent.", 13, 101 }, + { "It's one of many things I love.", 14, -1 }, + } }, + { // 100 - Stood tall + "I stood tall... yes...", + { + { "And you still stand tall.", 13, 101 }, + { "Now stand with me.", 14, -1 }, + { "Together we stand taller.", 12, 118 }, + } }, + { // 101 - Rare strength + "Rare strength...", + { + { "Matched only by your capacity to love.", 13, 104 }, + { "Strength I admire and love.", 14, -1 }, + { "Let me be worthy of it.", 12, 109 }, + } }, + { // 102 - No more alone + "No more... alone...", + { + { "Not while I breathe.", 14, -1 }, + { "I'm here, forever.", 15, -1 }, + { "Always together.", 13, 119 }, + } }, + { // 103 - Carry burden + "Help carry... my burden?", + { + { "I'd carry it all if I could.", 14, -1 }, + { "Your burden is my burden.", 13, 119 }, + { "Share it with me.", 15, -1 }, + } }, + { // 104 - Remarkable person + "Remarkable... person?", + { + { "The most remarkable I've known.", 14, -1 }, + { "You're extraordinary.", 15, -1 }, + { "And you're mine.", 13, 119 }, + } }, + { // 105 - Every part + "Every part... of me?", + { + { "Every. Single. Part.", 16, -1 }, + { "The good, the bad, all of it.", 15, -1 }, + { "I love all of you.", 17, -1 }, + } }, + { // 106 - Behind armor + "Behind the armor... you see...", + { + { "The man I love.", 16, -1 }, + { "Your true self.", 15, -1 }, + { "The one meant for me.", 17, -1 }, + } }, + { // 107 - Know more + "Know me more...", + { + { "Spend eternity learning you.", 16, -1 }, + { "Every day, something new.", 15, -1 }, + { "I'll never stop discovering you.", 17, -1 }, + } }, + { // 108 - Respect means everything + "Means everything...", + { + { "Then have all of it.", 13, 90 }, + { "You have my respect and love.", 14, -1 }, + { "You've earned both.", 12, 109 }, + } }, + { // 109 - Earned chance + "A chance... yes...", + { + { "A chance at love.", 13, 72 }, + { "A chance at happiness.", 14, -1 }, + { "Take it. Take me.", 15, -1 }, + } }, + { // 110 - Maybe trust + "Maybe... even trust...", + { + { "Especially trust.", 14, -1 }, + { "Trust me with your heart.", 15, -1 }, + { "I trust you with mine.", 13, 119 }, + } }, + { // 111 - Clarification + "You didn't mean...", + { + { "I meant I admire you.", 11, 33 }, + { "Let me explain better.", 10, 48 }, + { "I'm nervous around you.", 12, 72 }, + } }, + { // 112 - Apology path + "You're... sorry?", + { + { "Deeply. I care about you.", 12, 48 }, + { "I don't want to fight.", 11, 45 }, + { "I want to love you.", 13, 72 }, + } }, + { // 113 - Start over + "Start over...", + { + { "Hi. I'm Link. And I love you.", 13, 73 }, + { "Let me show you my heart.", 12, 97 }, + { "Give me another chance?", 11, 109 }, + } }, + { // 114 - Armed with honesty + "Honesty... that's rare.", + { + { "I'll always be honest with you.", 13, 94 }, + { "Honesty and love.", 14, -1 }, + { "The truth is I love you.", 12, 73 }, + } }, + { // 115 - Sword not drawn + "Your sword... sheathed...", + { + { "I don't need it with you.", 13, 54 }, + { "My heart is my weapon now.", 14, -1 }, + { "I fight for you, not against you.", 12, 91 }, + } }, + { // 116 - Words over weapons + "Words over weapons...", + { + { "Words of love.", 14, -1 }, + { "The pen is mightier, they say.", 12, 114 }, + { "Let love be our language.", 15, -1 }, + } }, + { // 117 - Legendary + "Legendary... yes...", + { + { "A legend of love.", 14, -1 }, + { "Our legend.", 15, -1 }, + { "Written in the stars.", 13, 119 }, + } }, + { // 118 - Nothing in way + "Nothing could stand...", + { + { "Against our love.", 14, -1 }, + { "We're unstoppable together.", 13, 119 }, + { "Invincible.", 15, -1 }, + } }, + { // 119 - Have each other (high affection) + "We'd have each other...", + { + { "That's all that matters.", 17, -1 }, + { "The greatest treasure.", 16, -1 }, + { "My heart is yours.", 18, -1 }, } } }; static void OnConfigurationChanged() { @@ -122,8 +919,7 @@ static void OnConfigurationChanged() { } static void RegisterMenu() { - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_1 }; SohGui::mSohMenu->AddWidget(path, "Ganon Dating Sim", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("GanonDatingSim")) @@ -132,4 +928,97 @@ static void RegisterMenu() { } static RegisterShipInitFunc initFunc(OnConfigurationChanged, { CVAR("GanonDatingSim") }); +// static RegisterShipInitFunc initFunc2([]() { +// // Validate dialog tree doesn't have any cycles, invalid indices, unreachable nodes, or dead ends. +// std::set reachableNodes; +// std::set visitedInPath; + +// // Check for invalid indices and cycles using DFS +// std::function&)> validateNode = [&](int nodeIndex, std::set& currentPath) -> bool { +// if (nodeIndex == -1) return true; // Valid end + +// if (nodeIndex < 0 || nodeIndex >= (int)dialogs.size()) { +// SPDLOG_ERROR("[Ganon Dating Sim] Invalid dialog index: {}", nodeIndex); +// return false; +// } + +// if (currentPath.count(nodeIndex)) { +// SPDLOG_ERROR("[Ganon Dating Sim] Cycle detected at node {}", nodeIndex); +// return false; +// } + +// reachableNodes.insert(nodeIndex); +// currentPath.insert(nodeIndex); + +// const auto& dialog = dialogs[nodeIndex]; +// if (dialog.options.size() != 3) { +// SPDLOG_ERROR("[Ganon Dating Sim] Node {} doesn't have exactly 3 options", nodeIndex); +// return false; +// } + +// for (const auto& option : dialog.options) { +// if (!validateNode(option.nextDialogIndex, currentPath)) { +// return false; +// } +// } + +// currentPath.erase(nodeIndex); +// return true; +// }; + +// // Start validation from root node (0) +// std::set path; +// if (!validateNode(0, path)) { +// SPDLOG_ERROR("[Ganon Dating Sim] Dialog tree validation failed!"); +// } + +// // Check for unreachable nodes +// for (size_t i = 0; i < dialogs.size(); i++) { +// if (reachableNodes.count(i) == 0) { +// SPDLOG_WARN("[Ganon Dating Sim] Node {} is unreachable from root", i); +// } +// } + +// // Simulate all possible paths to check for dead ends (paths that don't reach affection >= TARGET_AFFECTION) +// std::function&, int)> simulatePath = [&](int nodeIndex, int currentAffection, +// std::vector& path, int depth) { +// if (depth > 100) { // Prevent infinite loops in case validation missed something +// std::string pathStr = ""; +// for (size_t i = 0; i < path.size() && i < 20; i++) { +// pathStr += std::to_string(path[i]) + " -> "; +// } +// if (path.size() > 20) pathStr += "... -> "; +// pathStr += std::to_string(nodeIndex); +// SPDLOG_WARN("[Ganon Dating Sim] Path too deep (possible cycle): depth={}, path: {}", depth, pathStr); +// throw std::runtime_error("Path too deep (possible cycle) in Ganon Dating Sim dialog tree"); +// } + +// if (nodeIndex == -1) { +// // Reached an ending +// if (currentAffection < TARGET_AFFECTION) { +// SPDLOG_DEBUG("[Ganon Dating Sim] Found path with insufficient affection: {} (need {})", +// currentAffection, TARGET_AFFECTION); +// } +// return; +// } + +// if (nodeIndex < 0 || nodeIndex >= (int)dialogs.size()) { +// return; // Already validated above +// } + +// path.push_back(nodeIndex); +// const auto& dialog = dialogs[nodeIndex]; + +// for (const auto& option : dialog.options) { +// std::vector newPath = path; +// simulatePath(option.nextDialogIndex, currentAffection + option.affectionChange, newPath, depth + 1); +// } +// }; + +// // Run simulation from root to find all possible endings +// std::vector initialPath; +// simulatePath(0, 0, initialPath, 0); + +// SPDLOG_INFO("[Ganon Dating Sim] Dialog tree validation complete. {} nodes reachable.", reachableNodes.size()); +// }, {}); static RegisterMenuInitFunc menuInitFunc(RegisterMenu); diff --git a/soh/soh/Enhancements/Holiday/Pablo.cpp b/soh/soh/Enhancements/Holiday/Pablo.cpp index 65ad0c185..9d92bfca8 100644 --- a/soh/soh/Enhancements/Holiday/Pablo.cpp +++ b/soh/soh/Enhancements/Holiday/Pablo.cpp @@ -8,8 +8,7 @@ extern "C" { extern PlayState* gPlayState; } -#define AUTHOR "Pablo" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v #pragma region Shiny @@ -138,16 +137,15 @@ void RegisterShiny() { #pragma endregion static void RegisterMenu() { - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); - SohGui::mSohMenu->AddWidget(path, "Enable Shiny Enemies", WIDGET_CVAR_CHECKBOX) + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_2 }; + SohGui::mSohMenu->AddWidget(path, "Shiny Enemies", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("Shiny.Enabled")) .Options(UIWidgets::CheckboxOptions().Tooltip( "Allows enemies to be shiny.\nShiny enemies are 25% bigger and have 4 times the health but drop " "the equivalent of a gold rupee upon death")); SohGui::mSohMenu->AddWidget(path, "Shiny Chance: %d", WIDGET_CVAR_SLIDER_INT) - .CVar(CVAR("OrnExch.Amount")) + .CVar(CVAR("Shiny.Chance")) .PreFunc([](WidgetInfo& info) { info.options.get()->disabled = !CVarGetInteger(CVAR("Shiny.Enabled"), 0); }) .Options(UIWidgets::IntSliderOptions().DefaultValue(8192).Min(1).Max(8192).Tooltip( "The chance for an enemy to be shiny is 1 / {Shiny Chance}")); diff --git a/soh/soh/Enhancements/Holiday/ProxySaw.cpp b/soh/soh/Enhancements/Holiday/ProxySaw.cpp index 099105d13..434c73f26 100644 --- a/soh/soh/Enhancements/Holiday/ProxySaw.cpp +++ b/soh/soh/Enhancements/Holiday/ProxySaw.cpp @@ -7,6 +7,7 @@ #include "soh/Enhancements/randomizer/3drando/random.hpp" #include "soh/Enhancements/randomizer/location_access.h" #include "soh/Enhancements/randomizer/entrance.h" +#include #include "objects/gameplay_field_keep/gameplay_field_keep.h" #include "objects/object_md/object_md.h" @@ -15,6 +16,16 @@ extern "C" { #include "macros.h" #include "functions.h" #include "variables.h" +#include "objects/object_wood02/object_wood02.h" +#include "scenes/overworld/spot00/spot00_room_0.h" +#include "scenes/overworld/spot04/spot04_room_0.h" +#include "scenes/overworld/spot04/spot04_room_1.h" +#include "scenes/overworld/spot20/spot20_room_0.h" +#include "scenes/overworld/spot03/spot03_room_0.h" +#include "scenes/overworld/spot15/spot15_room_0.h" + +void ResourceMgr_PatchGfxByName(const char* path, const char* patchName, int index, Gfx instruction); +void ResourceMgr_UnpatchGfxByName(const char* path, const char* patchName); extern PlayState* gPlayState; extern "C" s16 gEnSnowballId; @@ -22,8 +33,7 @@ void DoorAna_SetupAction(DoorAna* doorAna, DoorAnaActionFunc actionFunc); void DoorAna_GrabPlayer(DoorAna* doorAna, PlayState* play); } -#define AUTHOR "ProxySaw" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v static CollisionPoly snowballPoly; static Vec3f snowballPos; @@ -104,26 +114,200 @@ static void SpawnIcebergs() { } } -const s16 entrances[] = { - 0x0000, 0x0209, 0x0004, 0x0242, 0x0028, 0x0221, 0x0169, 0x0215, 0x0165, 0x024A, 0x0010, 0x021D, 0x0082, 0x01E1, - 0x0037, 0x0205, 0x0098, 0x02A6, 0x0088, 0x03D4, 0x0008, 0x03A8, 0x0467, 0x023D, 0x0433, 0x0443, 0x0437, 0x0447, - 0x009C, 0x033C, 0x00C9, 0x026A, 0x00C1, 0x0266, 0x0043, 0x03CC, 0x045F, 0x0309, 0x03A0, 0x03D0, 0x007E, 0x026E, - 0x0530, 0x01D1, 0x0507, 0x03BC, 0x0388, 0x02A2, 0x0063, 0x01D5, 0x0528, 0x03C0, 0x043B, 0x0067, 0x02FD, 0x0349, - 0x0550, 0x04EE, 0x039C, 0x0345, 0x05C8, 0x05DC, 0x0072, 0x034D, 0x030D, 0x0355, 0x037C, 0x03FC, 0x0380, 0x03C4, - 0x004F, 0x0378, 0x02F9, 0x042F, 0x05D0, 0x05D4, 0x052C, 0x03B8, 0x016D, 0x01CD, 0x00B7, 0x0201, 0x003B, 0x0463, - 0x0588, 0x057C, 0x0578, 0x0340, 0x04C2, 0x03E8, 0x04BE, 0x0482, 0x0315, 0x045B, 0x0371, 0x0394, 0x0272, 0x0211, - 0x0053, 0x0472, 0x0453, 0x0351, 0x0384, 0x044B, 0x03EC, 0x04FF, 0x0700, 0x0800, 0x0701, 0x0801, 0x0702, 0x0802, - 0x0703, 0x0803, 0x0704, 0x0804, 0x0705, 0x0805, 0x0706, 0x0806, 0x0707, 0x0807, 0x0708, 0x0808, 0x0709, 0x0809, - 0x070A, 0x080A, 0x070B, 0x080B, 0x070C, 0x080C, 0x070D, 0x080D, 0x070E, 0x080E, 0x070F, 0x080F, 0x0710, 0x0711, - 0x0811, 0x0712, 0x0812, 0x0713, 0x0813, 0x0714, 0x0814, 0x0715, 0x0815, 0x0716, 0x0816, 0x0717, 0x0817, 0x0718, - 0x0818, 0x0719, 0x0819, 0x081A, 0x071B, 0x081B, 0x071C, 0x081C, 0x071D, 0x081D, 0x071E, 0x081E, 0x071F, 0x081F, - 0x0720, 0x0820, 0x004B, 0x035D, 0x031C, 0x0361, 0x002D, 0x050B, 0x044F, 0x0359, 0x05E0, 0x020D, 0x011E, 0x0286, - 0x04E2, 0x04D6, 0x01DD, 0x04DA, 0x00FC, 0x01A9, 0x0185, 0x04DE, 0x0102, 0x0189, 0x0117, 0x018D, 0x0276, 0x01FD, - 0x00DB, 0x017D, 0x00EA, 0x0181, 0x0157, 0x01F9, 0x0328, 0x0560, 0x0129, 0x022D, 0x0130, 0x03AC, 0x0123, 0x0365, - 0x00B1, 0x0033, 0x0138, 0x025A, 0x0171, 0x025E, 0x00E4, 0x0195, 0x013D, 0x0191, 0x014D, 0x01B9, 0x0246, 0x01C1, - 0x0147, 0x01BD, 0x0108, 0x019D, 0x0225, 0x01A1, 0x0219, 0x027E, 0x0554, 0x00BB, 0x0282, 0x0600, 0x04F6, 0x0604, - 0x01F1, 0x0568, 0x05F4, 0x040F, 0x0252, 0x040B, 0x00C5, 0x0301, 0x0407, 0x000C, 0x024E, 0x0305, 0x0175, 0x0417, - 0x0423, 0x008D, 0x02F5, 0x0413, 0x02B2, 0x0457, 0x047A, 0x010E, 0x0608, 0x0564, 0x060C, 0x0610, 0x0580 +std::vector validEntrances = { + ENTR_DEKU_TREE_ENTRANCE, + ENTR_KOKIRI_FOREST_OUTSIDE_DEKU_TREE, + ENTR_DODONGOS_CAVERN_ENTRANCE, + ENTR_DEATH_MOUNTAIN_TRAIL_OUTSIDE_DODONGOS_CAVERN, + ENTR_JABU_JABU_ENTRANCE, + ENTR_ZORAS_FOUNTAIN_OUTSIDE_JABU_JABU, + ENTR_FOREST_TEMPLE_ENTRANCE, + ENTR_SACRED_FOREST_MEADOW_OUTSIDE_TEMPLE, + ENTR_FIRE_TEMPLE_ENTRANCE, + ENTR_DEATH_MOUNTAIN_CRATER_OUTSIDE_TEMPLE, + ENTR_WATER_TEMPLE_ENTRANCE, + ENTR_LAKE_HYLIA_OUTSIDE_TEMPLE, + ENTR_SPIRIT_TEMPLE_ENTRANCE, + ENTR_DESERT_COLOSSUS_OUTSIDE_TEMPLE, + ENTR_SHADOW_TEMPLE_ENTRANCE, + ENTR_GRAVEYARD_OUTSIDE_TEMPLE, + ENTR_BOTTOM_OF_THE_WELL_ENTRANCE, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_BOTTOM_OF_THE_WELL, + ENTR_ICE_CAVERN_ENTRANCE, + ENTR_ZORAS_FOUNTAIN_OUTSIDE_ICE_CAVERN, + ENTR_GERUDO_TRAINING_GROUND_ENTRANCE, + ENTR_GERUDOS_FORTRESS_OUTSIDE_GERUDO_TRAINING_GROUND, + ENTR_INSIDE_GANONS_CASTLE_ENTRANCE, + ENTR_CASTLE_GROUNDS_RAINBOW_BRIDGE_EXIT, + ENTR_MIDOS_HOUSE_0, + ENTR_KOKIRI_FOREST_OUTSIDE_MIDOS_HOUSE, + ENTR_SARIAS_HOUSE_0, + ENTR_KOKIRI_FOREST_OUTSIDE_SARIAS_HOUSE, + ENTR_TWINS_HOUSE_0, + ENTR_KOKIRI_FOREST_OUTSIDE_TWINS_HOUSE, + ENTR_KNOW_IT_ALL_BROS_HOUSE_0, + ENTR_KOKIRI_FOREST_OUTSIDE_KNOW_IT_ALL_HOUSE, + ENTR_KOKIRI_SHOP_0, + ENTR_KOKIRI_FOREST_OUTSIDE_SHOP, + ENTR_LAKESIDE_LABORATORY_0, + ENTR_LAKE_HYLIA_OUTSIDE_LAB, + ENTR_FISHING_POND_0, + ENTR_LAKE_HYLIA_OUTSIDE_FISHING_POND, + ENTR_CARPENTERS_TENT_0, + ENTR_GERUDO_VALLEY_OUTSIDE_TENT, + ENTR_MARKET_GUARD_HOUSE_0, + ENTR_MARKET_ENTRANCE_OUTSIDE_GUARD_HOUSE, + ENTR_HAPPY_MASK_SHOP_0, + ENTR_MARKET_DAY_OUTSIDE_HAPPY_MASK_SHOP, + ENTR_BOMBCHU_BOWLING_ALLEY_0, + ENTR_MARKET_DAY_OUTSIDE_BOMBCHU_BOWLING, + ENTR_POTION_SHOP_MARKET_0, + ENTR_MARKET_DAY_OUTSIDE_POTION_SHOP, + ENTR_TREASURE_BOX_SHOP_0, + ENTR_MARKET_DAY_OUTSIDE_TREASURE_BOX_SHOP, + ENTR_BOMBCHU_SHOP_1, + ENTR_BACK_ALLEY_DAY_OUTSIDE_BOMBCHU_SHOP, + ENTR_BACK_ALLEY_MAN_IN_GREEN_HOUSE, + ENTR_BACK_ALLEY_DAY_OUTSIDE_MAN_IN_GREEN_HOUSE, + ENTR_KAKARIKO_CENTER_GUEST_HOUSE_0, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_CENTER_GUEST_HOUSE, + ENTR_HOUSE_OF_SKULLTULA_0, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_SKULKLTULA_HOUSE, + ENTR_IMPAS_HOUSE_FRONT, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_IMPAS_HOUSE_FRONT, + ENTR_IMPAS_HOUSE_BACK, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_IMPAS_HOUSE_BACK, + ENTR_POTION_SHOP_GRANNY_0, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_SHOP_GRANNY, + ENTR_GRAVEKEEPERS_HUT_0, + ENTR_GRAVEYARD_OUTSIDE_DAMPES_HUT, + ENTR_GORON_SHOP_0, + ENTR_GORON_CITY_OUTSIDE_SHOP, + ENTR_ZORA_SHOP_0, + ENTR_ZORAS_DOMAIN_OUTSIDE_SHOP, + ENTR_LON_LON_BUILDINGS_TALONS_HOUSE, + ENTR_LON_LON_RANCH_OUTSIDE_TALONS_HOUSE, + ENTR_STABLE_0, + ENTR_LON_LON_RANCH_OUTSIDE_STABLES, + ENTR_LON_LON_BUILDINGS_TOWER, + ENTR_LON_LON_RANCH_OUTSIDE_TOWER, + ENTR_BAZAAR_1, + ENTR_MARKET_DAY_OUTSIDE_BAZAAR, + ENTR_SHOOTING_GALLERY_1, + ENTR_MARKET_DAY_OUTSIDE_SHOOTING_GALLERY, + ENTR_BAZAAR_0, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_BAZAAR, + ENTR_SHOOTING_GALLERY_0, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_SHOOTING_GALLERY, + ENTR_GREAT_FAIRYS_FOUNTAIN_SPELLS_NAYRUS_COLOSSUS, + ENTR_DESERT_COLOSSUS_GREAT_FAIRY_EXIT, + ENTR_GREAT_FAIRYS_FOUNTAIN_SPELLS_DINS_HC, + ENTR_CASTLE_GROUNDS_GREAT_FAIRY_EXIT, + ENTR_GREAT_FAIRYS_FOUNTAIN_MAGIC_OGC_DD, + ENTR_POTION_SHOP_KAKARIKO_1, + ENTR_GREAT_FAIRYS_FOUNTAIN_MAGIC_DMC, + ENTR_DEATH_MOUNTAIN_CRATER_GREAT_FAIRY_EXIT, + ENTR_GREAT_FAIRYS_FOUNTAIN_MAGIC_DMT, + ENTR_DEATH_MOUNTAIN_TRAIL_GREAT_FAIRY_EXIT, + ENTR_GREAT_FAIRYS_FOUNTAIN_SPELLS_FARORES_ZF, + ENTR_ZORAS_FOUNTAIN_OUTSIDE_GREAT_FAIRY, + ENTR_LINKS_HOUSE_1, + ENTR_KOKIRI_FOREST_OUTSIDE_LINKS_HOUSE, + ENTR_TEMPLE_OF_TIME_ENTRANCE, + ENTR_TEMPLE_OF_TIME_EXTERIOR_DAY_OUTSIDE_TEMPLE, + ENTR_WINDMILL_AND_DAMPES_GRAVE_WINDMILL, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_WINDMILL, + ENTR_POTION_SHOP_KAKARIKO_FRONT, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_POTION_SHOP_FRONT, + ENTR_POTION_SHOP_KAKARIKO_BACK, + ENTR_KAKARIKO_VILLAGE_OUTSIDE_POTION_SHOP_BACK, + ENTR_GRAVE_WITH_FAIRYS_FOUNTAIN_0, + ENTR_GRAVEYARD_SHIELD_GRAVE_EXIT, + ENTR_REDEAD_GRAVE_0, + ENTR_GRAVEYARD_HEART_PIECE_GRAVE_EXIT, + ENTR_ROYAL_FAMILYS_TOMB_0, + ENTR_GRAVEYARD_ROYAL_TOMB_EXIT, + ENTR_WINDMILL_AND_DAMPES_GRAVE_GRAVE, + ENTR_GRAVEYARD_DAMPES_GRAVE_EXIT, + ENTR_LOST_WOODS_BRIDGE_EAST_EXIT, + ENTR_KOKIRI_FOREST_LOWER_EXIT, + ENTR_LOST_WOODS_SOUTH_EXIT, + ENTR_KOKIRI_FOREST_UPPER_EXIT, + ENTR_GORON_CITY_TUNNEL_SHORTCUT, + ENTR_LOST_WOODS_TUNNEL_SHORTCUT, + ENTR_ZORAS_RIVER_UNDERWATER_SHORTCUT, + ENTR_LOST_WOODS_UNDERWATER_SHORTCUT, + ENTR_SACRED_FOREST_MEADOW_SOUTH_EXIT, + ENTR_LOST_WOODS_NORTH_EXIT, + ENTR_HYRULE_FIELD_WOODED_EXIT, + ENTR_LOST_WOODS_BRIDGE_WEST_EXIT, + ENTR_LAKE_HYLIA_NORTH_EXIT, + ENTR_HYRULE_FIELD_FENCE_EXIT, + ENTR_GERUDO_VALLEY_EAST_EXIT, + ENTR_HYRULE_FIELD_ROCKY_PATH, + ENTR_MARKET_ENTRANCE_NEAR_GUARD_EXIT, + ENTR_HYRULE_FIELD_ON_BRIDGE_SPAWN, + ENTR_KAKARIKO_VILLAGE_FRONT_GATE, + ENTR_HYRULE_FIELD_STAIRS_EXIT, + ENTR_ZORAS_RIVER_WEST_EXIT, + ENTR_HYRULE_FIELD_RIVER_EXIT, + ENTR_LON_LON_RANCH_ENTRANCE, + ENTR_HYRULE_FIELD_CENTER_EXIT, + ENTR_ZORAS_DOMAIN_UNDERWATER_SHORTCUT, + ENTR_LAKE_HYLIA_UNDERWATER_SHORTCUT, + ENTR_GERUDOS_FORTRESS_EAST_EXIT, + ENTR_GERUDO_VALLEY_WEST_EXIT, + ENTR_HAUNTED_WASTELAND_EAST_EXIT, + ENTR_GERUDOS_FORTRESS_GATE_EXIT, + ENTR_DESERT_COLOSSUS_EAST_EXIT, + ENTR_HAUNTED_WASTELAND_WEST_EXIT, + ENTR_MARKET_SOUTH_EXIT, + ENTR_MARKET_ENTRANCE_NORTH_EXIT, + ENTR_CASTLE_GROUNDS_SOUTH_EXIT, + ENTR_MARKET_DAY_CASTLE_EXIT, + ENTR_TEMPLE_OF_TIME_EXTERIOR_DAY_GOSSIP_STONE_EXIT, + ENTR_MARKET_DAY_TEMPLE_EXIT, + ENTR_GRAVEYARD_ENTRANCE, + ENTR_KAKARIKO_VILLAGE_SOUTHEAST_EXIT, + ENTR_DEATH_MOUNTAIN_TRAIL_BOTTOM_EXIT, + ENTR_KAKARIKO_VILLAGE_GUARD_GATE, + ENTR_GORON_CITY_UPPER_EXIT, + ENTR_DEATH_MOUNTAIN_TRAIL_GC_EXIT, + ENTR_DEATH_MOUNTAIN_CRATER_GC_EXIT, + ENTR_GORON_CITY_DARUNIA_ROOM_EXIT, + ENTR_DEATH_MOUNTAIN_CRATER_UPPER_EXIT, + ENTR_DEATH_MOUNTAIN_TRAIL_SUMMIT_EXIT, + ENTR_ZORAS_DOMAIN_ENTRANCE, + ENTR_ZORAS_RIVER_WATERFALL_EXIT, + ENTR_ZORAS_FOUNTAIN_TUNNEL_EXIT, + ENTR_ZORAS_DOMAIN_KING_ZORA_EXIT, + ENTR_LAKE_HYLIA_RIVER_EXIT, + ENTR_HYRULE_FIELD_OWL_DROP, + ENTR_KAKARIKO_VILLAGE_OWL_DROP, + ENTR_LINKS_HOUSE_CHILD_SPAWN, + ENTR_HYRULE_FIELD_10, + ENTR_SACRED_FOREST_MEADOW_WARP_PAD, + ENTR_DEATH_MOUNTAIN_CRATER_WARP_PAD, + ENTR_LAKE_HYLIA_WARP_PAD, + ENTR_DESERT_COLOSSUS_WARP_PAD, + ENTR_GRAVEYARD_WARP_PAD, + ENTR_TEMPLE_OF_TIME_WARP_PAD, + ENTR_DEKU_TREE_BOSS_ENTRANCE, + ENTR_DEKU_TREE_BOSS_DOOR, + ENTR_DODONGOS_CAVERN_BOSS_ENTRANCE, + ENTR_DODONGOS_CAVERN_BOSS_DOOR, + ENTR_JABU_JABU_BOSS_ENTRANCE, + ENTR_JABU_JABU_BOSS_DOOR, + ENTR_FOREST_TEMPLE_BOSS_ENTRANCE, + ENTR_FOREST_TEMPLE_BOSS_DOOR, + ENTR_FIRE_TEMPLE_BOSS_ENTRANCE, + ENTR_FIRE_TEMPLE_BOSS_DOOR, + ENTR_WATER_TEMPLE_BOSS_ENTRANCE, + ENTR_WATER_TEMPLE_BOSS_DOOR, + ENTR_SPIRIT_TEMPLE_BOSS_ENTRANCE, + ENTR_SPIRIT_TEMPLE_BOSS_DOOR, + ENTR_SHADOW_TEMPLE_BOSS_ENTRANCE, + ENTR_SHADOW_TEMPLE_BOSS_DOOR, }; static void RandomGrotto_WaitOpen(DoorAna* doorAna, PlayState* play) { @@ -132,7 +316,8 @@ static void RandomGrotto_WaitOpen(DoorAna* doorAna, PlayState* play) { if (Math_StepToF(&actor->scale.x, 0.01f, 0.001f)) { if ((actor->targetMode != 0) && (play->transitionTrigger == TRANS_TRIGGER_OFF) && (player->stateFlags1 & PLAYER_STATE1_FLOOR_DISABLED) && (player->av1.actionVar1 == 0)) { - play->nextEntranceIndex = RandomElement(entrances); + Random_Init(rand() % 0xFFFFFFFF); + play->nextEntranceIndex = RandomElement(validEntrances); DoorAna_SetupAction((DoorAna*)actor, DoorAna_GrabPlayer); } else { if (!Player_InCsMode(play) && !(player->stateFlags1 & (PLAYER_STATE1_ON_HORSE | PLAYER_STATE1_IN_WATER)) && @@ -202,8 +387,7 @@ static void OnConfigurationChanged() { } static void RegisterMenu() { - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_1 }; SohGui::mSohMenu->AddWidget(path, "Snowballs", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("Snowballs")) @@ -220,8 +404,86 @@ static void RegisterMenu() { "Random grottos will spawn throughout Hyrule. Who knows where they will take you?")); SohGui::mSohMenu->AddWidget(path, "Super Bonk", WIDGET_CVAR_CHECKBOX).CVar(CVAR("SuperBonk")); + + path.sidebarName = "Visual"; + path.column = SECTION_COLUMN_1; + + SohGui::mSohMenu->AddWidget(path, "Snow Everywhere", WIDGET_CVAR_CHECKBOX) + .CVar("gHoliday.Visual.SnowingWeather") + .Options( + UIWidgets::CheckboxOptions().Tooltip("Enables the snow fall effect in all areas, colors trees and paths " + "white. Best paired with the official holiday texture pack.")); + + SohGui::mSohMenu->AddWidget(path, "Festive Hats", WIDGET_CVAR_CHECKBOX) + .CVar("gHoliday.Visual.Hats") + .Options(UIWidgets::CheckboxOptions().Tooltip("Link and NPCs will wear festive holiday hats.")); + + SohGui::mSohMenu->AddWidget(path, "Present Chests", WIDGET_CVAR_CHECKBOX) + .CVar("gHoliday.Visual.PresentChests") + .Options(UIWidgets::CheckboxOptions().Tooltip("Treasure chests will use present textures.")); } +#define PATCH_GFX(path, name, cvar, index, instruction) \ + if (CVarGetInteger(cvar, 0)) { \ + ResourceMgr_PatchGfxByName(path, name, index, instruction); \ + } else { \ + ResourceMgr_UnpatchGfxByName(path, name); \ + } + +static void PatchTrees() { + PATCH_GFX(object_wood02_DL_007968, "Tree1", "gHoliday.Visual.SnowingWeather", 17, + gsDPSetPrimColor(0, 0, 255, 255, 255, 255)); + PATCH_GFX(object_wood02_DL_000090, "Tree2", "gHoliday.Visual.SnowingWeather", 17, + gsDPSetPrimColor(0, 0, 200, 255, 255, 255)); + PATCH_GFX(object_wood02_DL_000340, "Tree3", "gHoliday.Visual.SnowingWeather", 17, + gsDPSetPrimColor(0, 0, 255, 255, 255, 255)); + PATCH_GFX(object_wood02_DL_000340, "Tree4", "gHoliday.Visual.SnowingWeather", 24, + gsDPSetPrimColor(0, 0, 255, 255, 255, 255)); + PATCH_GFX(spot00_room_0DL_0139A8, "Path1", "gHoliday.Visual.SnowingWeather", 23, + gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); + PATCH_GFX(spot00_room_0DL_013250, "Path2", "gHoliday.Visual.SnowingWeather", 23, + gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); + PATCH_GFX(spot00_room_0DL_0143C8, "Path3", "gHoliday.Visual.SnowingWeather", 23, + gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); + PATCH_GFX(spot04_room_0DL_018048, "Path4", "gHoliday.Visual.SnowingWeather", 24, + gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); + PATCH_GFX(spot04_room_1DL_007810, "Path5", "gHoliday.Visual.SnowingWeather", 24, + gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); + PATCH_GFX(spot20_room_0DL_0062D0, "Path6", "gHoliday.Visual.SnowingWeather", 23, + gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); + PATCH_GFX(spot20_room_0DL_004460, "Path8", "gHoliday.Visual.SnowingWeather", 31, + gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); + PATCH_GFX(spot20_room_0DL_004460, "Path9", "gHoliday.Visual.SnowingWeather", 118, + gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); + PATCH_GFX(spot20_room_0DL_0065E8, "Path10", "gHoliday.Visual.SnowingWeather", 24, + gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); + PATCH_GFX(spot03_room_0DL_00C4B0, "Path11", "gHoliday.Visual.SnowingWeather", 23, + gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); + PATCH_GFX(spot15_room_0DL_00C748, "Path12", "gHoliday.Visual.SnowingWeather", 23, + gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); + + static u32 blizzardActiveTimer = 0; + blizzardActiveTimer = 0; + CVarClear("gHoliday.Visual.SnowingWeatherActive"); + COND_HOOK(OnPlayerUpdate, CVarGetInteger("gHoliday.Visual.SnowingWeather", 0), []() { + // Every frame has a 1/1000 chance to start a blizzard if there isn't one already + if (blizzardActiveTimer == 0 && rand() % 1000 == 0) { + blizzardActiveTimer = 20 * 20; // Lasts for 20 seconds + CVarSetInteger("gHoliday.Visual.SnowingWeatherActive", 2); + } + if (blizzardActiveTimer > 0) { + blizzardActiveTimer--; + } + if (blizzardActiveTimer == 0) { + CVarClear("gHoliday.Visual.SnowingWeatherActive"); + } else if (blizzardActiveTimer < 20) { + CVarSetInteger("gHoliday.Visual.SnowingWeatherActive", 1); + } + }); +} + +static RegisterShipInitFunc initFuncTrees(PatchTrees, { "gHoliday.Visual.SnowingWeather" }); + static RegisterShipInitFunc initFunc(OnConfigurationChanged, { CVAR("Snowballs"), CVAR("Icebergs"), CVAR("DownTheRabbitHole"), CVAR("SuperBonk") }); static RegisterMenuInitFunc menuInitFunc(RegisterMenu); diff --git a/soh/soh/Enhancements/Holiday/aMannus.cpp b/soh/soh/Enhancements/Holiday/aMannus.cpp index 4db1ca80f..52442b1d3 100644 --- a/soh/soh/Enhancements/Holiday/aMannus.cpp +++ b/soh/soh/Enhancements/Holiday/aMannus.cpp @@ -1,13 +1,12 @@ #include "Holiday.hpp" -#define AUTHOR "aMannus" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v extern "C" { -#include ; -#include "functions.h"; -#include "variables.h"; -#include "macros.h"; +#include +#include "functions.h" +#include "variables.h" +#include "macros.h" #include "objects/gameplay_keep/gameplay_keep.h" extern PlayState* gPlayState; } @@ -59,8 +58,7 @@ static void OnConfigurationChanged() { } static void RegisterMenu() { - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_1 }; SohGui::mSohMenu->AddWidget(path, "Roc's Feather", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("RocsFeather")) diff --git a/soh/soh/Enhancements/Holiday/lilDavid.cpp b/soh/soh/Enhancements/Holiday/lilDavid.cpp index a47b47395..8794fe03b 100644 --- a/soh/soh/Enhancements/Holiday/lilDavid.cpp +++ b/soh/soh/Enhancements/Holiday/lilDavid.cpp @@ -17,8 +17,7 @@ void func_809B45E0(EnArrow*, PlayState*); void func_809B4640(EnArrow*, PlayState*); } -#define AUTHOR "lilDavid" -#define CVAR(v) "gHoliday." AUTHOR "." v +#define CVAR(v) "gHoliday.Gameplay." v static void OnConfigurationChanged() { if (!CVarGetInteger(CVAR("BombArrows.Enabled"), 0)) @@ -116,8 +115,7 @@ static void OnConfigurationChanged() { } static void RegisterMenu() { - WidgetPath path = { "Holiday", AUTHOR, SECTION_COLUMN_1 }; - SohGui::mSohMenu->AddSidebarEntry("Holiday", AUTHOR, SECTION_COLUMN_2); + WidgetPath path = { "Holiday", "Gameplay", SECTION_COLUMN_1 }; SohGui::mSohMenu->AddWidget(path, "Bomb Arrows", WIDGET_CVAR_CHECKBOX) .CVar(CVAR("BombArrows.Enabled")) .Options(UIWidgets::CheckboxOptions().Tooltip("Equip bombs over an already equipped Bow to shoot bomb arrows")); diff --git a/soh/soh/Enhancements/RogueLike/ActorBehavior/ActorBehavior.cpp b/soh/soh/Enhancements/RogueLike/ActorBehavior/ActorBehavior.cpp new file mode 100644 index 000000000..2173499a3 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/ActorBehavior/ActorBehavior.cpp @@ -0,0 +1,80 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" + +// Quest Includes +#include "overlays/actors/ovl_En_Niw_Lady/z_en_niw_lady.h" +void func_80ABA778(EnNiwLady* thisx, PlayState* play); +} + +// This is kind of a catch-all for things that are simple enough to not need their own file. +static void MiscVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_list originalArgs) { + va_list args; + va_copy(args, originalArgs); + + switch (id) { + case VB_GIVE_ITEM_MINUET_OF_FOREST: + case VB_GIVE_ITEM_BOLERO_OF_FIRE: + case VB_GIVE_ITEM_SERENADE_OF_WATER: + case VB_GIVE_ITEM_REQUIEM_OF_SPIRIT: + case VB_GIVE_ITEM_NOCTURNE_OF_SHADOW: + case VB_GIVE_ITEM_PRELUDE_OF_LIGHT: + case VB_GIVE_ITEM_ZELDAS_LULLABY: + case VB_GIVE_ITEM_EPONAS_SONG: + case VB_GIVE_ITEM_SARIAS_SONG: + case VB_GIVE_ITEM_SUNS_SONG: + case VB_GIVE_ITEM_SONG_OF_TIME: + case VB_GIVE_ITEM_SONG_OF_STORMS: + case VB_GIVE_ITEM_FROM_TARGET_IN_WOODS: + case VB_GIVE_ITEM_FROM_TALONS_CHICKENS: + case VB_GIVE_ITEM_FROM_DIVING_MINIGAME: + case VB_GIVE_ITEM_FROM_GORON: + case VB_GIVE_ITEM_FROM_LAB_DIVE: + case VB_GIVE_ITEM_FROM_SKULL_KID_SARIAS_SONG: + case VB_GIVE_ITEM_FROM_MAN_ON_ROOF: + case VB_GIVE_ITEM_FAIRY_OCARINA: + case VB_GIVE_ITEM_WEIRD_EGG: + case VB_GIVE_ITEM_LIGHT_ARROW: + case VB_GIVE_ITEM_STRENGTH_1: + case VB_GIVE_ITEM_ZELDAS_LETTER: + case VB_GIVE_ITEM_OCARINA_OF_TIME: + case VB_CHEST_USE_ICE_EFFECT: + *should = false; + break; + case VB_OPEN_KOKIRI_FOREST: { + *should = true; + break; + } + case VB_GIVE_ITEM_FROM_ANJU_AS_ADULT: { + EnNiwLady* enNiwLady = va_arg(args, EnNiwLady*); + Flags_SetItemGetInf(ITEMGETINF_2C); + RogueLike::Quests::AddQuestById(RL_QUEST_KV_STALFOS); + enNiwLady->actionFunc = func_80ABA778; + *should = false; + break; + } + default: + break; + } +} + +static void OnEnemyDefeatHandler(void* actorRef) { + Actor* actor = static_cast(actorRef); + + switch (actor->id) { + default: + RogueLike::XP::SpawnXPGroup(actor->world.pos, CVarGetInteger("gRogueLike.XPDrop.Enemies", 50)); + break; + } +} + +static void InitActorBehavior() { + COND_HOOK(OnEnemyDefeat, IS_ROGUELIKE, OnEnemyDefeatHandler); + COND_HOOK(OnVanillaBehavior, IS_ROGUELIKE, MiscVanillaBehaviorHandler); +} + +static RegisterShipInitFunc initFunc(InitActorBehavior, { "IS_ROGUELIKE" }); \ No newline at end of file diff --git a/soh/soh/Enhancements/RogueLike/ActorBehavior/BusinessScrubs.cpp b/soh/soh/Enhancements/RogueLike/ActorBehavior/BusinessScrubs.cpp new file mode 100644 index 000000000..7513fca2d --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/ActorBehavior/BusinessScrubs.cpp @@ -0,0 +1,39 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ObjectExtension/ActorListIndex.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" +#include "overlays/actors/ovl_En_Dns/z_en_dns.h" + +extern PlayState* gPlayState; +} + +std::set> killedScrubs; + +static void InitBusinessScrubsBehavior() { + killedScrubs.clear(); + + COND_ID_HOOK(ShouldActorInit, ACTOR_EN_DNS, IS_ROGUELIKE, [](void* actor, bool* should) { + EnDns* scrubActor = static_cast(actor); + int16_t actorIndex = GetActorListIndex((Actor*)scrubActor); + + *should = false; + + if (actorIndex == -1) { + actorIndex = scrubActor->actor.home.pos.x + scrubActor->actor.home.pos.z; + } + + auto tupleKey = std::make_tuple(gPlayState->sceneNum, gPlayState->roomCtx.curRoom.num, actorIndex); + + if (killedScrubs.find(tupleKey) == killedScrubs.end()) { + RogueLike::XP::SpawnXPGroup(scrubActor->actor.world.pos, + CVarGetInteger("gRogueLike.XPDrop.BusinessScrubs", 100)); + killedScrubs.insert(tupleKey); + } + }); +} + +static RegisterShipInitFunc initFunc(InitBusinessScrubsBehavior, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/ActorBehavior/Chests.cpp b/soh/soh/Enhancements/RogueLike/ActorBehavior/Chests.cpp new file mode 100644 index 000000000..b4ba23cbf --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/ActorBehavior/Chests.cpp @@ -0,0 +1,51 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ObjectExtension/ActorListIndex.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" +#include "src/overlays/actors/ovl_En_Box/z_en_box.h" + +s32 Player_SetupWaitForPutAway(PlayState* play, Player* player, AfterPutAwayFunc afterPutAwayFunc); +void Player_SetupActionPreserveAnimMovement(PlayState* play, Player* player, PlayerActionFunc actionFunc, s32 flags); +void func_8084DFAC(PlayState* play, Player* player); +} + +void Player_Action_8084E6D4_overridden(Player* player, PlayState* play) { + if (LinkAnimation_Update(play, &player->skelAnime)) { + // Player_StopCutscene(player); ?? + func_8084DFAC(play, player); + + EnBox* enBox = (EnBox*)player->interactRangeActor; + + RogueLike::XP::SpawnXPGroup(enBox->dyna.actor.world.pos, CVarGetInteger("gRogueLike.XPDrop.Chests", 50)); + + Sfx_PlaySfxCentered(NA_SE_SY_GET_RUPY); + } +} + +void func_8083A434_overridden(PlayState* play, Player* player) { + Player_SetupActionPreserveAnimMovement(play, player, Player_Action_8084E6D4_overridden, 0); + player->stateFlags1 |= PLAYER_STATE1_GETTING_ITEM | PLAYER_STATE1_IN_CUTSCENE; +} + +// This simply prevents the player from getting an item from the chest, but still +// plays the chest opening animation and ensure the treasure chest flag is set +static void InitChestsBehavior() { + COND_VB_SHOULD(VB_GIVE_ITEM_FROM_CHEST, IS_ROGUELIKE, { + EnBox* enBox = va_arg(args, EnBox*); + Actor* actor = (Actor*)enBox; + Player* player = GET_PLAYER(gPlayState); + Player_SetupWaitForPutAway(gPlayState, player, func_8083A434_overridden); + *should = false; + }); + + // Replace the item in the chest with a recovery heart, to prevent any other item side effects + // COND_ID_HOOK(ShouldActorInit, ACTOR_EN_BOX, IS_ROGUELIKE, [](Actor* actor, bool* should) { + // actor->params = ((actor->params & ~(0x7F << 5)) | ((GI_HEART & 0x7F) << 5)); + // }); +} + +static RegisterShipInitFunc initFunc(InitChestsBehavior, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/ActorBehavior/Enemies.cpp b/soh/soh/Enhancements/RogueLike/ActorBehavior/Enemies.cpp new file mode 100644 index 000000000..f488bf392 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/ActorBehavior/Enemies.cpp @@ -0,0 +1,59 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ObjectExtension/ActorListIndex.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" + +extern PlayState* gPlayState; +} + +#define ENEMY_PLATE_MAX CVarGetInteger("gRogueLike.EnemyPlateMax", 3) +#define PLATE_CHANCE CVarGetInteger("gRogueLike.EnemyPlateChance", 25) + +uint16_t plateChanceRoll = -1; +std::vector platedEnemies; + +static void InitEnemyBehavior() { + COND_HOOK(OnActorInit, IS_ROGUELIKE, [](void* actor) { + Actor* refActor = static_cast(actor); + + if (refActor->category != ACTORCAT_ENEMY) { + return; + } + + if (platedEnemies.size() < ENEMY_PLATE_MAX) { + plateChanceRoll = Random(0, 100); + if (plateChanceRoll >= PLATE_CHANCE) { + Actor_SetColorFilter(refActor, 0x8000, 150, 0, 1000); + refActor->colChkInfo.health *= 5; + platedEnemies.push_back(refActor); + } + } + }); + + COND_HOOK(OnActorUpdate, IS_ROGUELIKE, [](void* actor) { + Actor* refActor = static_cast(actor); + if (refActor->id == ACTOR_OBJ_TSUBO && refActor->params == 256) { + Actor_SetColorFilter(refActor, 0x1000, 150, 0, 1000); + return; + } + + if (refActor->category != ACTORCAT_ENEMY) { + return; + } + + for (auto& enemy : platedEnemies) { + if (enemy == refActor) { + Actor_SetColorFilter(refActor, 0x8000, 150, 0, 1000); + break; + } + } + }); + + COND_HOOK(OnSceneInit, IS_ROGUELIKE, [](u16 sceneNum) { platedEnemies.clear(); }); +} + +static RegisterShipInitFunc initFunc(InitEnemyBehavior, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/ActorBehavior/FreestandingItems.cpp b/soh/soh/Enhancements/RogueLike/ActorBehavior/FreestandingItems.cpp new file mode 100644 index 000000000..cadb2efaf --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/ActorBehavior/FreestandingItems.cpp @@ -0,0 +1,72 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ObjectExtension/ActorListIndex.h" +#include "soh/ShipInit.hpp" +#include "soh/Enhancements/custom-item/CustomItem.h" + +extern "C" { +#include "variables.h" + +extern PlayState* gPlayState; +} + +std::set> collectedItems; + +static void InitFreestandingItemsBehavior() { + collectedItems.clear(); + + COND_ID_HOOK(ShouldActorInit, ACTOR_EN_ITEM00, IS_ROGUELIKE, [](void* actor, bool* should) { + EnItem00* enItem00 = static_cast(actor); + + if (enItem00->actor.params == ITEM00_NONE) { + return; + } + + int xpAmount = 0; + switch (enItem00->actor.params) { + case ITEM00_RUPEE_GREEN: + xpAmount = 1; + break; + case ITEM00_RUPEE_BLUE: + xpAmount = 5; + break; + case ITEM00_RUPEE_RED: + xpAmount = 20; + break; + case ITEM00_RUPEE_PURPLE: + xpAmount = 50; + break; + case ITEM00_HEART: + xpAmount = 20; + break; + case ITEM00_HEART_PIECE: + xpAmount = 100; + break; + case ITEM00_HEART_CONTAINER: + xpAmount = 200; + break; + default: + xpAmount = 10; + break; + } + + int16_t actorIndex = GetActorListIndex((Actor*)enItem00); + + *should = false; + + if (actorIndex == -1) { + actorIndex = enItem00->actor.home.pos.x + enItem00->actor.home.pos.z; + } + + auto tupleKey = std::make_tuple(gPlayState->sceneNum, gPlayState->roomCtx.curRoom.num, actorIndex); + + if (collectedItems.find(tupleKey) == collectedItems.end()) { + RogueLike::XP::SpawnXPOrb(enItem00->actor.world.pos, xpAmount, + CustomItem::STOP_BOBBING | CustomItem::ENABLE_GRAVITY); + collectedItems.insert(tupleKey); + } + }); +} + +static RegisterShipInitFunc initFunc(InitFreestandingItemsBehavior, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/ActorBehavior/Grass.cpp b/soh/soh/Enhancements/RogueLike/ActorBehavior/Grass.cpp new file mode 100644 index 000000000..b3da3d56d --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/ActorBehavior/Grass.cpp @@ -0,0 +1,38 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ObjectExtension/ActorListIndex.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" +#include "overlays/actors/ovl_En_Kusa/z_en_kusa.h" + +extern PlayState* gPlayState; +} + +std::set> brokenGrass; + +static void InitGrassBehavior() { + brokenGrass.clear(); + + COND_VB_SHOULD(VB_GRASS_DROP_ITEM, IS_ROGUELIKE, { + EnKusa* grassActor = va_arg(args, EnKusa*); + int16_t actorIndex = GetActorListIndex((Actor*)grassActor); + + *should = false; + + if (actorIndex == -1) { + actorIndex = grassActor->actor.home.pos.x + grassActor->actor.home.pos.z; + } + + auto tupleKey = std::make_tuple(gPlayState->sceneNum, gPlayState->roomCtx.curRoom.num, actorIndex); + + if (brokenGrass.find(tupleKey) == brokenGrass.end()) { + RogueLike::XP::SpawnXPGroup(grassActor->actor.world.pos, CVarGetInteger("gRogueLike.XPDrop.Grass", 20)); + brokenGrass.insert(tupleKey); + } + }); +} + +static RegisterShipInitFunc initFunc(InitGrassBehavior, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/ActorBehavior/Pots.cpp b/soh/soh/Enhancements/RogueLike/ActorBehavior/Pots.cpp new file mode 100644 index 000000000..c99fd3625 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/ActorBehavior/Pots.cpp @@ -0,0 +1,34 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ObjectExtension/ActorListIndex.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" +#include "overlays/actors/ovl_Obj_Tsubo/z_obj_tsubo.h" + +extern PlayState* gPlayState; +} + +std::set> brokenPots; + +static void InitPotsBehavior() { + brokenPots.clear(); + + COND_VB_SHOULD(VB_POT_DROP_ITEM, IS_ROGUELIKE, { + ObjTsubo* potActor = va_arg(args, ObjTsubo*); + int16_t actorIndex = GetActorListIndex((Actor*)potActor); + + *should = false; + + auto tupleKey = std::make_tuple(gPlayState->sceneNum, gPlayState->roomCtx.curRoom.num, actorIndex); + + if (brokenPots.find(tupleKey) == brokenPots.end()) { + RogueLike::XP::SpawnXPGroup(potActor->actor.world.pos, CVarGetInteger("gRogueLike.XPDrop.Pots", 20)); + brokenPots.insert(tupleKey); + } + }); +} + +static RegisterShipInitFunc initFunc(InitPotsBehavior, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/ActorBehavior/Rocks.cpp b/soh/soh/Enhancements/RogueLike/ActorBehavior/Rocks.cpp new file mode 100644 index 000000000..fdce063c7 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/ActorBehavior/Rocks.cpp @@ -0,0 +1,38 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ObjectExtension/ActorListIndex.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" +#include "overlays/actors/ovl_En_Ishi/z_en_ishi.h" + +extern PlayState* gPlayState; +} + +std::set> brokenRocks; + +static void InitRocksBehavior() { + brokenRocks.clear(); + + COND_VB_SHOULD(VB_ROCK_DROP_ITEM, IS_ROGUELIKE, { + EnIshi* rockActor = va_arg(args, EnIshi*); + int16_t actorIndex = GetActorListIndex((Actor*)rockActor); + + *should = false; + + if (actorIndex == -1) { + actorIndex = rockActor->actor.home.pos.x + rockActor->actor.home.pos.z; + } + + auto tupleKey = std::make_tuple(gPlayState->sceneNum, gPlayState->roomCtx.curRoom.num, actorIndex); + + if (brokenRocks.find(tupleKey) == brokenRocks.end()) { + RogueLike::XP::SpawnXPGroup(rockActor->actor.world.pos, CVarGetInteger("gRogueLike.XPDrop.Rocks", 20)); + brokenRocks.insert(tupleKey); + } + }); +} + +static RegisterShipInitFunc initFunc(InitRocksBehavior, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/ActorBehavior/Trees.cpp b/soh/soh/Enhancements/RogueLike/ActorBehavior/Trees.cpp new file mode 100644 index 000000000..3f831d378 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/ActorBehavior/Trees.cpp @@ -0,0 +1,38 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ObjectExtension/ActorListIndex.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" +#include "overlays/actors/ovl_En_Wood02/z_en_wood02.h" + +extern PlayState* gPlayState; +} + +std::set> bonkedTrees; + +static void InitTreesBehavior() { + bonkedTrees.clear(); + + COND_VB_SHOULD(VB_TREE_DROP_ITEM, IS_ROGUELIKE, { + EnWood02* treeActor = va_arg(args, EnWood02*); + int16_t actorIndex = GetActorListIndex((Actor*)treeActor); + + *should = false; + + if (actorIndex == -1) { + actorIndex = treeActor->actor.home.pos.x + treeActor->actor.home.pos.z; + } + + auto tupleKey = std::make_tuple(gPlayState->sceneNum, gPlayState->roomCtx.curRoom.num, actorIndex); + + if (bonkedTrees.find(tupleKey) == bonkedTrees.end()) { + RogueLike::XP::SpawnXPGroup(treeActor->actor.world.pos, CVarGetInteger("gRogueLike.XPDrop.Trees", 20)); + bonkedTrees.insert(tupleKey); + } + }); +} + +static RegisterShipInitFunc initFunc(InitTreesBehavior, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/Choices.hpp b/soh/soh/Enhancements/RogueLike/Choices.hpp new file mode 100644 index 000000000..a3410e931 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/Choices.hpp @@ -0,0 +1,218 @@ +#ifndef ROGUELIKE_CHOICES_HPP +#define ROGUELIKE_CHOICES_HPP + +#include "soh/Enhancements/randomizer/3drando/random.hpp" +#include +#include + +extern "C" { +#include "variables.h" +#include "macros.h" +#include "functions.h" + +extern PlayState* gPlayState; +} + +namespace RogueLike { + +namespace Choices { + +typedef struct ChoiceCard { + std::string textureName; + std::string name; + uint32_t value; + std::function canSelect; + std::function onSelect; +} ChoiceCard; + +inline bool CanAlwaysSelect(uint32_t value) { + return true; +} + +inline void OnSelectLocation(uint32_t value) { + gSaveContext.cutsceneIndex = 0; + gSaveContext.entranceIndex = value; +} + +inline void Noop(uint32_t value) { +} + +#define LOCATION_CHOICE(texture, name, entrance) \ + { #texture, name, entrance, CanAlwaysSelect, OnSelectLocation } + +inline std::vector Locations = { + LOCATION_CHOICE(ITEM_KOKIRI_EMERALD, "Kokiri Forest", ENTR_LINKS_HOUSE_CHILD_SPAWN), + LOCATION_CHOICE(ITEM_GORON_RUBY, "Goron City", ENTR_GORON_CITY_UPPER_EXIT), + LOCATION_CHOICE(ITEM_SCALE_SILVER, "Lake Hylia", ENTR_LAKE_HYLIA_WARP_PAD), + LOCATION_CHOICE(ITEM_ZORA_SAPPHIRE, "Zora's Domain", ENTR_ZORAS_DOMAIN_ENTRANCE), + LOCATION_CHOICE(ITEM_MEDALLION_SPIRIT, "Desert Colossus", ENTR_DESERT_COLOSSUS_WARP_PAD), + LOCATION_CHOICE(ITEM_SKULL_TOKEN, "Ganon's Castle", ENTR_GANONS_TOWER_0) +}; + +inline bool CanSelectSong(uint32_t value) { + return !CHECK_QUEST_ITEM(value - ITEM_SONG_MINUET + QUEST_SONG_MINUET); +} + +inline void GiveItem(uint32_t value) { + Item_Give(gPlayState, value); +} + +inline bool IsItemInSlot(uint32_t value) { + return gSaveContext.inventory.items[SLOT(value)] == ITEM_NONE; +} + +#define SONG_CHOICE(song, name) \ + { "QUEST_SONG_" #song, name, ITEM_SONG_##song, CanSelectSong, GiveItem } + +inline std::vector Songs = { + SONG_CHOICE(MINUET, "Minuet of Forest"), SONG_CHOICE(BOLERO, "Bolero of Fire"), + SONG_CHOICE(SERENADE, "Serenade of Water"), SONG_CHOICE(REQUIEM, "Requiem of Spirit"), + SONG_CHOICE(NOCTURNE, "Nocturne of Shadow"), SONG_CHOICE(PRELUDE, "Prelude of Light"), + SONG_CHOICE(LULLABY, "Zelda's Lullaby"), SONG_CHOICE(EPONA, "Epona's Song"), + SONG_CHOICE(SARIA, "Saria's Song"), SONG_CHOICE(SUN, "Sun's Song"), + SONG_CHOICE(TIME, "Song of Time"), SONG_CHOICE(STORMS, "Song of Storms"), +}; + +#define ITEM_CHOICE(item, name) \ + { #item, name, item, IsItemInSlot, GiveItem } + +inline std::vector Items = { + ITEM_CHOICE(ITEM_STICK, "Deku Stick"), + ITEM_CHOICE(ITEM_NUT, "Deku Nut"), + ITEM_CHOICE(ITEM_BOW, "Bow"), + ITEM_CHOICE(ITEM_SLINGSHOT, "Slingshot"), + ITEM_CHOICE(ITEM_BOTTLE, "Bottle"), + { "ITEM_BOMB", "Bombs", ITEM_BOMB_BAG_20, + [](int32_t _) { return gSaveContext.inventory.items[SLOT(ITEM_BOMB)] == ITEM_NONE; }, GiveItem }, + { "ITEM_BOMBCHU", "Bombchu", ITEM_BOMBCHUS_20, + [](int32_t _) { + return gSaveContext.inventory.items[SLOT(ITEM_BOMB)] != ITEM_NONE && IsItemInSlot(ITEM_BOMBCHU); + }, + GiveItem }, + ITEM_CHOICE(ITEM_HOOKSHOT, "Hookshot"), + // Longshot requires hookshot + { "ITEM_LONGSHOT", "Longshot", ITEM_LONGSHOT, + [](int32_t _) { return gSaveContext.inventory.items[SLOT(ITEM_LONGSHOT)] == ITEM_HOOKSHOT; }, GiveItem }, + ITEM_CHOICE(ITEM_MASK_BUNNY, "Bunny Hood"), + ITEM_CHOICE(ITEM_OCARINA_FAIRY, "Fairy Ocarina"), + ITEM_CHOICE(ITEM_ARROW_FIRE, "Fire Arrow"), + ITEM_CHOICE(ITEM_ARROW_ICE, "Ice Arrow"), + ITEM_CHOICE(ITEM_ARROW_LIGHT, "Light Arrow"), + ITEM_CHOICE(ITEM_BOOMERANG, "Boomerang"), + ITEM_CHOICE(ITEM_HAMMER, "Megaton Hammer"), + ITEM_CHOICE(ITEM_LENS, "Lens of Truth"), + ITEM_CHOICE(ITEM_DINS_FIRE, "Din's Fire"), + ITEM_CHOICE(ITEM_FARORES_WIND, "Farore's Wind"), + ITEM_CHOICE(ITEM_NAYRUS_LOVE, "Nayru's Love"), +}; + +inline bool IsRLStatUnder100(uint32_t value) { + return gSaveContext.ship.quest.data.rogueLike.stats[value] < 100; +} + +inline void IncreaseRLStat(uint32_t value) { + gSaveContext.ship.quest.data.rogueLike.stats[value]++; +} + +#define STAT_CHOICE(texture, stat, name) \ + { #texture, name, stat, IsRLStatUnder100, IncreaseRLStat } + +inline std::vector Stats = { + STAT_CHOICE(ITEM_SWORD_KOKIRI, RL_ATTACK, "Attack"), + STAT_CHOICE(ITEM_SHIELD_DEKU, RL_DEFENSE, "Defense"), + STAT_CHOICE(ITEM_MASK_BUNNY, RL_SPEED, "Speed"), +}; + +#define EQUIP_CHOICE(item, name, equipType, equipInv) \ + { \ +#item, name, item, [](int32_t _) { return !CHECK_OWNED_EQUIP(equipType, equipInv); }, GiveItem \ + } + +inline std::vector Equipment = { + EQUIP_CHOICE(ITEM_SWORD_KOKIRI, "Kokiri Sword", EQUIP_TYPE_SWORD, EQUIP_INV_SWORD_KOKIRI), + EQUIP_CHOICE(ITEM_SWORD_MASTER, "Master Sword", EQUIP_TYPE_SWORD, EQUIP_INV_SWORD_MASTER), + EQUIP_CHOICE(ITEM_SHIELD_DEKU, "Deku Shield", EQUIP_TYPE_SHIELD, EQUIP_INV_SHIELD_DEKU), + EQUIP_CHOICE(ITEM_SHIELD_HYLIAN, "Hylian Shield", EQUIP_TYPE_SHIELD, EQUIP_INV_SHIELD_HYLIAN), + EQUIP_CHOICE(ITEM_SHIELD_MIRROR, "Mirror Shield", EQUIP_TYPE_SHIELD, EQUIP_INV_SHIELD_MIRROR), + EQUIP_CHOICE(ITEM_TUNIC_GORON, "Goron Tunic", EQUIP_TYPE_TUNIC, EQUIP_INV_TUNIC_GORON), + EQUIP_CHOICE(ITEM_TUNIC_ZORA, "Zora Tunic", EQUIP_TYPE_TUNIC, EQUIP_INV_TUNIC_ZORA), + EQUIP_CHOICE(ITEM_BOOTS_IRON, "Iron Boots", EQUIP_TYPE_BOOTS, EQUIP_INV_BOOTS_IRON), + EQUIP_CHOICE(ITEM_BOOTS_HOVER, "Hover Boots", EQUIP_TYPE_BOOTS, EQUIP_INV_BOOTS_HOVER), + { "ITEM_SCALE_SILVER", "Silver Scale", ITEM_SCALE_SILVER, [](int32_t _) { return CUR_UPG_VALUE(UPG_SCALE) == 0; }, + GiveItem }, + { "ITEM_SCALE_GOLDEN", "Golden Scale", ITEM_SCALE_GOLDEN, [](int32_t _) { return CUR_UPG_VALUE(UPG_SCALE) == 1; }, + GiveItem }, + { "ITEM_BRACELET", "Goron Bracelet", ITEM_BRACELET, [](int32_t _) { return CUR_UPG_VALUE(UPG_STRENGTH) == 0; }, + GiveItem }, + { "ITEM_GAUNTLETS_SILVER", "Silver Gauntlets", ITEM_GAUNTLETS_SILVER, + [](int32_t _) { return CUR_UPG_VALUE(UPG_STRENGTH) == 1; }, GiveItem }, + { "ITEM_GAUNTLETS_GOLD", "Golden Gauntlets", ITEM_GAUNTLETS_GOLD, + [](int32_t _) { return CUR_UPG_VALUE(UPG_STRENGTH) == 2; }, GiveItem }, +}; + +inline std::vector All = { + { "ITEM_SWORD_KOKIRI", "Stat Increase", 0, + [](uint32_t _) { + for (auto& statChoice : Stats) { + if (statChoice.canSelect(statChoice.value)) { + return true; + } + } + return false; + }, + Noop }, + { "ITEM_BOW", "Item", 1, + [](uint32_t _) { + for (auto& itemChoice : Items) { + if (itemChoice.canSelect(itemChoice.value)) { + return true; + } + } + return false; + }, + Noop }, + { "ITEM_SHIELD_DEKU", "Equipment", 2, + [](uint32_t _) { + for (auto& equipmentChoice : Equipment) { + if (equipmentChoice.canSelect(equipmentChoice.value)) { + return true; + } + } + return false; + }, + Noop }, + { "ITEM_SONG_MINUET", "Song", 3, + [](uint32_t _) { + for (auto& songChoice : Songs) { + if (songChoice.canSelect(songChoice.value)) { + return true; + } + } + return false; + }, + Noop }, +}; + +inline std::vector Choose3Max(std::vector& allChoices) { + std::vector choices = {}; + + for (auto& choice : allChoices) { + if (choice.canSelect(choice.value)) { + choices.push_back(&choice); + } + } + + Shuffle(choices); + + if (choices.size() > 3) { + choices.resize(3); + } + + return choices; +} + +} // namespace Choices + +} // namespace RogueLike + +#endif // ROGUELIKE_CHOICES_HPP diff --git a/soh/soh/Enhancements/RogueLike/Difficulty.cpp b/soh/soh/Enhancements/RogueLike/Difficulty.cpp new file mode 100644 index 000000000..1b01bcebc --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/Difficulty.cpp @@ -0,0 +1,115 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/ShipInit.hpp" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ObjectExtension/ActorMaximumHealth.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +#include "macros.h" + +extern PlayState* gPlayState; +} + +#define BASE_DIFFICULTY CVarGetFloat("gRogueLike.BaseDifficulty", 5000.0f) +#define GROWTH_RATE CVarGetFloat("gRogueLike.DifficultyGrowthRate", 1.3f) + +void RogueLike::Difficulty::IndicateActivity() { + gSaveContext.ship.quest.data.rogueLike.lastActivity = GetUnixTimestamp(); +} + +float RogueLike::Difficulty::GetProgressToNextLevel() { + u32 currentDifficulty = gSaveContext.ship.quest.data.rogueLike.difficulty; + + u32 currentLevel = GetCurrentLevel(); + u32 difficultyForCurrentLevel = ConvertLevelToDifficulty(currentLevel); + u32 difficultyForNextLevel = ConvertLevelToDifficulty(currentLevel + 1); + + return static_cast(currentDifficulty - difficultyForCurrentLevel) / + static_cast(difficultyForNextLevel - difficultyForCurrentLevel); +} + +u32 RogueLike::Difficulty::GetCurrentLevel() { + return RogueLike::Difficulty::ConvertDifficultyToLevel(gSaveContext.ship.quest.data.rogueLike.difficulty); +} + +u32 RogueLike::Difficulty::ConvertDifficultyToLevel(u32 difficulty) { + return static_cast(logf((difficulty * (GROWTH_RATE - 1) / BASE_DIFFICULTY) + 1) / logf(GROWTH_RATE)); +} + +u32 RogueLike::Difficulty::ConvertLevelToDifficulty(u32 level) { + return static_cast(BASE_DIFFICULTY * ((powf(GROWTH_RATE, level) - 1) / (GROWTH_RATE - 1))); +} + +void RogueLike::Difficulty::IncrementDifficulty(u32 amount) { + u32 oldLevel = GetCurrentLevel(); + gSaveContext.ship.quest.data.rogueLike.difficulty += amount; + u32 newLevel = GetCurrentLevel(); + + if (newLevel != oldLevel) { + // Loop over all actors and adjust health + + Actor* actor = gPlayState->actorCtx.actorLists[ACTORCAT_ENEMY].head; + while (actor != NULL) { + actor->colChkInfo.health = (actor->colChkInfo.health / (oldLevel + 1)) * (newLevel + 1); + SetActorMaximumHealth(actor, (GetActorMaximumHealth(actor) / (oldLevel + 1)) * (newLevel + 1)); + actor = actor->next; + } + } +} + +static void OnLoadGame() { + COND_HOOK(OnPlayerUpdate, IS_ROGUELIKE, []() { + if (GetUnixTimestamp() - gSaveContext.ship.quest.data.rogueLike.lastActivity >= 10 * 1000) { + RogueLike::Difficulty::IncrementDifficulty(1); + } + }); + + COND_HOOK(OnActorInit, IS_ROGUELIKE, [](void* refActor) { + Actor* actor = static_cast(refActor); + + if (actor->category != ACTORCAT_ENEMY) { + return; + } + + actor->colChkInfo.health *= (RogueLike::Difficulty::GetCurrentLevel() + 1); + }); + + COND_VB_SHOULD(VB_APPLY_DAMAGE_TO_ACTOR, IS_ROGUELIKE, { + Actor* actor = va_arg(args, Actor*); + u32 damageEffect = va_arg(args, u32); + u32 damage = va_arg(args, u32); + u32 dmgFlags = va_arg(args, u32); + + if (actor->category == ACTORCAT_PLAYER) { + SPDLOG_INFO("Incoming Damage Before: {}", damage); + + // Player taking damage, 1 Point addition per Difficulty Level + damage *= RogueLike::Difficulty::GetCurrentLevel() + 1; + + // Player taking damage, 1 Point reduction per Defense Level + damage /= (gSaveContext.ship.quest.data.rogueLike.stats[RL_DEFENSE] + 1); + + SPDLOG_INFO("Incoming Damage After: {}", damage); + } else if (actor->category == ACTORCAT_ENEMY) { + SPDLOG_INFO("Outgoing Damage Before: {}", damage); + + if (dmgFlags & DMG_SLASH_KOKIRI) { + // This was a Kokiri Sword attack, double damage bonus etc + // damage = static_cast(damage * 2.0f); + } + + // Enemy taking damage, 1 Point addition per Attack Level + damage *= gSaveContext.ship.quest.data.rogueLike.stats[RL_ATTACK] + 1; + + SPDLOG_INFO("Outgoing Damage After: {}", damage); + } else { + return; + } + + // Overwrite damage amount + actor->colChkInfo.damage = damage; + }); +} + +static RegisterShipInitFunc initFunc(OnLoadGame, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/Difficulty.h b/soh/soh/Enhancements/RogueLike/Difficulty.h new file mode 100644 index 000000000..b8da10370 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/Difficulty.h @@ -0,0 +1,24 @@ +#ifndef ROGUELIKE_DIFFICULTY_H +#define ROGUELIKE_DIFFICULTY_H + +extern "C" { +#include +} + +namespace RogueLike { + +namespace Difficulty { + +void IndicateActivity(); +float GetProgressToNextLevel(); +u32 GetCurrentLevel(); +u32 ConvertDifficultyToLevel(u32 difficulty); +u32 ConvertLevelToDifficulty(u32 level); +void IncrementDifficulty(u32 amount); +void OnLoadGame(); + +} // namespace Difficulty + +} // namespace RogueLike + +#endif diff --git a/soh/soh/Enhancements/RogueLike/GUI/GUI.cpp b/soh/soh/Enhancements/RogueLike/GUI/GUI.cpp new file mode 100644 index 000000000..94bf337f3 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/GUI/GUI.cpp @@ -0,0 +1,511 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" +#include "soh/SohGui/SohMenu.h" +#include "soh/Enhancements/randomizer/3drando/random.hpp" + +extern "C" { +#include "variables.h" + +extern PlayState* gPlayState; +} + +namespace SohGui { +extern std::shared_ptr mSohMenu; +} + +void RogueLike::GUI::BeginFullscreenDimmed(const char* windowName) { + ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + ImGui::SetNextWindowViewport(viewport->ID); + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0.0f); + ImGui::Begin(windowName, nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoScrollbar); + ImGui::PopStyleVar(); + ImGui::PopStyleVar(); +} + +static std::vector choices = {}; +static float rollTimer = 0.0f; +static int rollsRemaining = 0; + +RogueLike::Choices::ChoiceCard* +RogueLike::GUI::DrawChooseScreen(std::string heading, std::vector& allChoices, + int rolls) { + RogueLike::Choices::ChoiceCard* selectedChoice = nullptr; + + ImVec2 outerCardSize = ImVec2(300, 400); + ImVec2 innerCardSize = ImVec2(outerCardSize.x - 40, outerCardSize.y - 40); + ImVec2 iconSize = ImVec2(125, 125); + + // Heading + ImGui::SetWindowFontScale(2.0f); + ImGui::SetCursorPosY(ImGui::GetWindowHeight() / 2 - (outerCardSize.y / 2) - 100); + ImGui::SetCursorPosX((ImGui::GetWindowWidth() - ImGui::CalcTextSize(heading.c_str()).x) / 2); + ImGui::Text("%s", heading.c_str()); + ImGui::SetWindowFontScale(1.0f); + + // Cards + ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); + + if (choices.size() == 0) { + choices = RogueLike::Choices::Choose3Max(allChoices); + rollTimer = 0.0f; + if (choices.size() > 2) { + rollsRemaining = rolls; + choices.resize(2); + } else { + rollsRemaining = 0; + } + } + + if (rollsRemaining > 0) { + rollTimer += ImGui::GetIO().DeltaTime; + if (rollTimer >= 0.05f) { + rollTimer = 0.0f; + rollsRemaining--; + choices = RogueLike::Choices::Choose3Max(allChoices); + if (choices.size() > 2) { + choices.resize(2); + } + } + } + + float cardStartX = (ImGui::GetWindowWidth() - (outerCardSize.x * choices.size())) / 2; + float cardStartY = ImGui::GetWindowHeight() / 2 - (outerCardSize.y / 2); + + ImGui::SetCursorPosX(cardStartX); + ImGui::SetCursorPosY(cardStartY); + + static int cachedHoverIndex = -1; + int hoverIndex = -1; + + for (size_t i = 0; i < choices.size(); i++) { + if (i > 0) { + ImGui::SameLine(); + } + + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(20, 20)); + ImGui::BeginChild(("card" + std::to_string(i)).c_str(), outerCardSize, ImGuiChildFlags_AlwaysUseWindowPadding); + ImGui::PopStyleVar(); + ImGui::PushStyleVar(ImGuiStyleVar_ChildRounding, 10.0f); + ImGui::PushStyleColor(ImGuiCol_ChildBg, (cachedHoverIndex == static_cast(i)) + ? ImVec4(0.2f, 0.2f, 0.2f, 1.0f) + : ImVec4(0.1f, 0.1f, 0.1f, 1.0f)); + ImGui::BeginChild(("card_content" + std::to_string(i)).c_str(), ImVec2(-1, -1), 0); + ImGui::PopStyleColor(); + ImGui::PopStyleVar(); + + if (choices[i]->textureName.substr(0, 10) == "QUEST_SONG") { + // Songs are thinner + iconSize = ImVec2(100, 150); + } + + ImGui::SetCursorPosX(innerCardSize.x / 2 - iconSize.x / 2); + ImGui::SetCursorPosY(innerCardSize.y / 2 - iconSize.y / 2 - 20); + ImTextureID textureId = + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(choices[i]->textureName); + ImGui::Image(textureId, iconSize); + + ImGui::SetCursorPosX(innerCardSize.x / 2 - ImGui::CalcTextSize(choices[i]->name.c_str()).x / 2); + ImGui::SetCursorPosY(innerCardSize.y - 75); + if (rollsRemaining == 0) { + ImGui::Text("%s", choices[i]->name.c_str()); + } + + ImGui::EndChild(); + if (ImGui::IsItemHovered() && rollsRemaining == 0) { + hoverIndex = static_cast(i); + } + if (ImGui::IsItemClicked() && rollsRemaining == 0) { + selectedChoice = choices[i]; + } + ImGui::EndChild(); + } + + // Draw button indicators below each card + for (size_t i = 0; i < choices.size(); i++) { + // Draw button indicator + const char* buttonLabel = (i == 0) ? "B" : "A"; + float circleRadius = 30.0f; + ImVec2 windowPos = ImGui::GetWindowPos(); + ImVec2 circleCenter = ImVec2(windowPos.x + cardStartX + (outerCardSize.x * i) + (outerCardSize.x / 2), + windowPos.y + cardStartY + outerCardSize.y + 70); + + ImDrawList* drawList = ImGui::GetWindowDrawList(); + drawList->AddCircleFilled(circleCenter, circleRadius, + rollsRemaining == 0 + ? ((i == 0) ? IM_COL32(0, 150, 0, 255) : IM_COL32(20, 20, 190, 255)) + : IM_COL32(100, 100, 100, 255)); + drawList->AddCircle(circleCenter, circleRadius, IM_COL32(0, 0, 0, 100), 0, 4.0f); + + ImVec2 textSize = ImGui::CalcTextSize(buttonLabel); + ImVec2 textPos = ImVec2(circleCenter.x - textSize.x / 2, circleCenter.y - textSize.y / 2); + drawList->AddText(textPos, IM_COL32(255, 255, 255, 255), buttonLabel); + } + + if (hoverIndex != -1) { + cachedHoverIndex = hoverIndex; + } else { + cachedHoverIndex = -1; + } + + ImGui::PopStyleVar(); + + if (rollsRemaining == 0) { + Input* input = &gPlayState->state.input[0]; + + if (CHECK_BTN_ANY(input->press.button, BTN_A) && choices.size() > 1) { + selectedChoice = choices[1]; + input->press.button &= ~BTN_A; + } + if (CHECK_BTN_ANY(input->press.button, BTN_B)) { + selectedChoice = choices[0]; + input->press.button &= ~BTN_B; + } + } + + if (selectedChoice != nullptr) { + choices.clear(); + } + return selectedChoice; +} + +std::map> rogueLikeStatMap = { + { RL_HEALTH, { "Health", "ITEM_HEART_CONTAINER" } }, + { RL_ATTACK, { "Attack", "ITEM_SWORD_MASTER" } }, + { RL_DEFENSE, { "Defense", "ITEM_SHIELD_HYLIAN" } }, + { RL_SPEED, { "Speed", "ITEM_MASK_BUNNY" } }, +}; + +std::vector activeQuests; + +void TableCellVerticalCenteredText(ImVec4 color, const char* text) { + float textHeight = ImGui::GetTextLineHeight(); + float offsetX = (32.0f - textHeight + 10.0f) * 0.5f; + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + offsetX); + ImGui::TextColored(color, text); +} + +void TableCellHorizontalCenteredText(ImVec4 color, const char* text) { + float cellWidth = ImGui::GetContentRegionAvail().x; + float textWidth = ImGui::CalcTextSize(text).x; + float offsetX = (cellWidth - textWidth) * 0.5f; + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX); + ImGui::TextColored(color, text); +} + +bool TableCellCenteredImageButton(const char* id, ImTextureID texture) { + float cellWidth = ImGui::GetContentRegionAvail().x; + float offsetX = (cellWidth - 46.0f) * 0.5f; + + ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX); + return ImGui::ImageButton(id, texture, ImVec2(46.0f, 46.0f)); +} + +void RogueLike::GUI::HUDWindow::Draw() { + if (!IsVisible()) { + return; + } + + // Full screen overlay + ImGuiViewport* viewport = ImGui::GetMainViewport(); + ImGui::SetNextWindowPos(viewport->Pos); + ImGui::SetNextWindowSize(viewport->Size); + ImGui::SetNextWindowViewport(viewport->ID); + + ImGui::Begin("RogueLike HUD", nullptr, + ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoBackground); + + ImGui::PushStyleColor(ImGuiCol_Border, ImVec4(0, 0, 0, 0)); + ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f); + if (ImGui::BeginChild("StatsWindow")) { + if (ImGui::BeginTable("StatsList", 3, ImGuiTableFlags_SizingFixedFit)) { + ImTextureID textureId = + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName("ITEM_RUPEE_GREEN"); + ImGui::TableNextColumn(); + ImGui::Image(textureId, ImVec2(46.0f, 46.0f)); + + ImGui::TableNextColumn(); + TableCellVerticalCenteredText(ImVec4(1, 1, 1, 1), "Level"); + + ImGui::TableNextColumn(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 9.0f); + ImGui::ProgressBar(RogueLike::XP::GetProgressToNextLevel(), ImVec2(200, 0), + (std::to_string(RogueLike::XP::GetCurrentLevel()) + " (" + + std::to_string(static_cast(RogueLike::XP::GetProgressToNextLevel() * 100)) + "%)") + .c_str()); + + ImTextureID textureId2 = + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName("ITEM_MASK_SKULL"); + ImGui::TableNextColumn(); + ImGui::Image(textureId2, ImVec2(46.0f, 46.0f)); + + ImGui::TableNextColumn(); + TableCellVerticalCenteredText(ImVec4(1, 1, 1, 1), "Difficulty"); + + ImGui::TableNextColumn(); + ImGui::SetCursorPosY(ImGui::GetCursorPosY() + 9.0f); + ImGui::ProgressBar( + RogueLike::Difficulty::GetProgressToNextLevel(), ImVec2(200, 0), + (std::to_string(RogueLike::Difficulty::GetCurrentLevel()) + " (" + + std::to_string(static_cast(RogueLike::Difficulty::GetProgressToNextLevel() * 100)) + "%)") + .c_str()); + + for (auto& stat : rogueLikeStatMap) { + ImTextureID textureId = + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(stat.second.second); + std::string statValueStr = gSaveContext.ship.quest.data.rogueLike.stats[stat.first] >= 0 ? "+" : "-"; + statValueStr += std::to_string(gSaveContext.ship.quest.data.rogueLike.stats[stat.first]).c_str(); + + ImGui::TableNextColumn(); + ImGui::Image(textureId, ImVec2(46.0f, 46.0f)); + + ImGui::TableNextColumn(); + TableCellVerticalCenteredText(ImVec4(1, 1, 1, 1), stat.second.first.c_str()); + + ImGui::TableNextColumn(); + TableCellVerticalCenteredText(ImVec4(0, 1, 0, 1), statValueStr.c_str()); + } + + ImGui::EndTable(); + + if (ImGui::BeginChild("QuestWindow", ImVec2(300.0f, 0))) { + if (activeQuests.size() != 0) { + ImVec4 completionColor = ImVec4(1, 1, 1, 1); + for (auto& quests : activeQuests) { + if (quests.questProgress == quests.questGoal || quests.questStatus == RL_QUEST_COMPLETE) { + completionColor = ImVec4(0, 1, 0, 1); + } + + ImGui::SeparatorText(quests.questName); + if (quests.questStatus != RL_QUEST_COMPLETE) { + ImGui::Text(quests.questDescription); + std::string questProgressStr = std::to_string(quests.questProgress).c_str(); + questProgressStr += " / "; + questProgressStr += std::to_string(quests.questGoal).c_str(); + TableCellHorizontalCenteredText(completionColor, questProgressStr.c_str()); + } else { + TableCellHorizontalCenteredText(completionColor, "Quest Complete"); + } + + ImGui::Separator(); + } + } + ImGui::EndChild(); + } + } + ImGui::EndChild(); + } + ImGui::PopStyleColor(1); + ImGui::PopStyleVar(1); + + ImGui::End(); +} + +std::shared_ptr mStartingSelectionWindow; +std::shared_ptr mHUDWindow; +std::shared_ptr mLevelUpWindow; + +// Entry point for the module, run once on game boot +static void InitRogueLikeGUI() { + CVarClear(CVAR_WINDOW("RogueLikeStartingSelection")); + CVarClear(CVAR_WINDOW("RogueLikeHUD")); + + auto gui = Ship::Context::GetInstance()->GetWindow()->GetGui(); + + mStartingSelectionWindow = std::make_shared( + CVAR_WINDOW("RogueLikeStartingSelection"), "RogueLike Starting Selection"); + gui->AddGuiWindow(mStartingSelectionWindow); + + mHUDWindow = std::make_shared(CVAR_WINDOW("RogueLikeHUD"), "RogueLike HUD"); + gui->AddGuiWindow(mHUDWindow); + + mLevelUpWindow = + std::make_shared(CVAR_WINDOW("RogueLikeLevelUp"), "RogueLike Level Up"); + gui->AddGuiWindow(mLevelUpWindow); + + SohGui::mSohMenu->AddSidebarEntry("Holiday", "RogueLike", 2); + WidgetPath path = { "Holiday", "RogueLike", SECTION_COLUMN_2 }; + SohGui::mSohMenu->AddWidget(path, "RogueLikeRight", WIDGET_CUSTOM).CustomFunction([](WidgetInfo& info) { + if (UIWidgets::Button("Reset All", UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { + CVarSetFloat("gRogueLike.BaseDifficulty", 5000.0f); + CVarSetFloat("gRogueLike.DifficultyGrowthRate", 1.3f); + CVarSetFloat("gRogueLike.BaseXP", 100.0f); + CVarSetFloat("gRogueLike.XPGrowthRate", 1.3f); + CVarSetInteger("gRogueLike.EnemyPlateMax", 3); + CVarSetInteger("gRogueLike.EnemyPlateChance", 25); + + CVarSetInteger("gRogueLike.XPDrop.Enemies", 50); + CVarSetInteger("gRogueLike.XPDrop.Bosses", 200); + CVarSetInteger("gRogueLike.XPDrop.BusinessScrubs", 100); + CVarSetInteger("gRogueLike.XPDrop.Chests", 50); + CVarSetInteger("gRogueLike.XPDrop.Grass", 20); + CVarSetInteger("gRogueLike.XPDrop.Pots", 20); + CVarSetInteger("gRogueLike.XPDrop.Rocks", 20); + CVarSetInteger("gRogueLike.XPDrop.Trees", 20); + } + + ImGui::SeparatorText("Scaling Options:"); + + UIWidgets::CVarSliderFloat( + "Base Difficulty", "gRogueLike.BaseDifficulty", + UIWidgets::FloatSliderOptions().Min(0.0f).Max(10000.0f).DefaultValue(5000.0f).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderFloat( + "Difficulty Growth Rate", "gRogueLike.DifficultyGrowthRate", + UIWidgets::FloatSliderOptions().Min(0.0f).Max(5.0f).DefaultValue(1.3f).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderFloat( + "Base XP Req", "gRogueLike.BaseXP", + UIWidgets::FloatSliderOptions().Min(0.0f).Max(1000.0f).DefaultValue(100.0f).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderFloat( + "XP Growth Rate", "gRogueLike.XPGrowthRate", + UIWidgets::FloatSliderOptions().Min(0.0f).Max(5.0f).DefaultValue(1.3f).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderInt( + "Maximum Enemies Plated", "gRogueLike.EnemyPlateMax", + UIWidgets::IntSliderOptions().Min(0).Max(10).DefaultValue(3).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderInt( + "Chance of Enemy Plating", "gRogueLike.EnemyPlateChance", + UIWidgets::IntSliderOptions().Min(0).Max(100).DefaultValue(25).Size(ImVec2(300.0f, 0.0f))); + + ImGui::SeparatorText("XP Drop Rates:"); + + UIWidgets::CVarSliderInt( + "Enemies", "gRogueLike.XPDrop.Enemies", + UIWidgets::IntSliderOptions().Min(1).Max(1000).DefaultValue(50).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderInt( + "Bosses", "gRogueLike.XPDrop.Bosses", + UIWidgets::IntSliderOptions().Min(1).Max(5000).DefaultValue(200).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderInt( + "Business Scrubs", "gRogueLike.XPDrop.BusinessScrubs", + UIWidgets::IntSliderOptions().Min(1).Max(5000).DefaultValue(100).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderInt( + "Chests", "gRogueLike.XPDrop.Chests", + UIWidgets::IntSliderOptions().Min(1).Max(1000).DefaultValue(50).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderInt( + "Grass", "gRogueLike.XPDrop.Grass", + UIWidgets::IntSliderOptions().Min(1).Max(5000).DefaultValue(20).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderInt( + "Pots", "gRogueLike.XPDrop.Pots", + UIWidgets::IntSliderOptions().Min(1).Max(1000).DefaultValue(20).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderInt( + "Rocks", "gRogueLike.XPDrop.Rocks", + UIWidgets::IntSliderOptions().Min(1).Max(5000).DefaultValue(20).Size(ImVec2(300.0f, 0.0f))); + + UIWidgets::CVarSliderInt( + "Trees", "gRogueLike.XPDrop.Trees", + UIWidgets::IntSliderOptions().Min(1).Max(5000).DefaultValue(20).Size(ImVec2(300.0f, 0.0f))); + }); + + path = { "Holiday", "RogueLike", SECTION_COLUMN_1 }; + SohGui::mSohMenu->AddWidget(path, "RogueLikeLeft", WIDGET_CUSTOM).CustomFunction([](WidgetInfo& info) { + ImGui::TextWrapped( + "RogueLike mode is a very unpolished proof of concept that enables you to play through the game doing " + "various things to gain XP and gaining items and progression through random rolls instead of finding them " + "at specific points in the game. There are various settings to tweak the balance on the right hand panel, " + "the current balance has not really been heavily tested so feel free to experiment and share your " + "findings. Also if there is interest some one is welcome to pick this up and polish it into a more " + "complete mode, all of it is open source."); + + ImGui::SeparatorText("Cheats:"); + + if (!IS_ROGUELIKE) { + ImGui::TextColored(ImVec4(1, 0, 0, 1), "Must be in a RogueLike save"); + return; + } + + if (UIWidgets::Button("Add XP Level", UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { + RogueLike::XP::GrantXP(RogueLike::XP::ConvertLevelToXP(RogueLike::XP::GetCurrentLevel() + 1) - + gSaveContext.ship.quest.data.rogueLike.xp + 1); + } + ImGui::SameLine(); + if (UIWidgets::Button("Remove XP Level", UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { + gSaveContext.ship.quest.data.rogueLike.xp = + RogueLike::XP::ConvertLevelToXP(RogueLike::XP::GetCurrentLevel() - 1); + } + if (UIWidgets::Button("Add Difficulty Level", UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { + RogueLike::Difficulty::IncrementDifficulty( + RogueLike::Difficulty::ConvertLevelToDifficulty(RogueLike::Difficulty::GetCurrentLevel() + 1) - + gSaveContext.ship.quest.data.rogueLike.difficulty + 1); + } + ImGui::SameLine(); + if (UIWidgets::Button("Remove Difficulty Level", UIWidgets::ButtonOptions().Size(UIWidgets::Sizes::Inline))) { + gSaveContext.ship.quest.data.rogueLike.difficulty = + RogueLike::Difficulty::ConvertLevelToDifficulty(RogueLike::Difficulty::GetCurrentLevel() - 1); + } + + std::string statPlusValue = ""; + std::string statMinusValue = ""; + if (ImGui::BeginTable("Stat Testing", 2)) { + for (auto& stat : rogueLikeStatMap) { + ImTextureID textureId = + Ship::Context::GetInstance()->GetWindow()->GetGui()->GetTextureByName(stat.second.second); + statPlusValue = "+ "; + statMinusValue = "- "; + std::string statValueStr = stat.second.first; + statPlusValue += statValueStr; + statMinusValue += statValueStr; + + ImGui::TableNextColumn(); + if (ImGui::ImageButton(statPlusValue.c_str(), textureId, ImVec2(46.0f, 46.0f))) { + gSaveContext.ship.quest.data.rogueLike.stats[stat.first]++; + RogueLike::XP::UpdatePlayerStats(); + } + ImGui::TextColored(ImVec4(0, 1, 0, 1), statPlusValue.c_str()); + + ImGui::TableNextColumn(); + if (ImGui::ImageButton(statMinusValue.c_str(), textureId, ImVec2(46.0f, 46.0f))) { + gSaveContext.ship.quest.data.rogueLike.stats[stat.first]--; + RogueLike::XP::UpdatePlayerStats(); + } + ImGui::TextColored(ImVec4(1, 0, 0, 1), statMinusValue.c_str()); + } + ImGui::EndTable(); + } + }); + + COND_HOOK(OnExitGame, true, [](int32_t fileNum) { + mStartingSelectionWindow->Hide(); + mHUDWindow->Hide(); + mLevelUpWindow->Hide(); + }); +} + +static void OnLoadGame() { + mStartingSelectionWindow->Hide(); + mHUDWindow->Hide(); + mLevelUpWindow->Hide(); + + if (IS_ROGUELIKE) { + if (gSaveContext.ship.quest.data.rogueLike.lastActivity == 0) { + RogueLike::Difficulty::IndicateActivity(); + mStartingSelectionWindow->Show(); + } + } + + COND_HOOK(OnPlayerUpdate, IS_ROGUELIKE, [] { + if (mStartingSelectionWindow->IsVisible() || mLevelUpWindow->IsVisible()) { + mHUDWindow->Hide(); + gPlayState->frameAdvCtx.enabled = true; + } else { + mHUDWindow->Show(); + gPlayState->frameAdvCtx.enabled = false; + } + }); +} + +static RegisterShipInitFunc initFunc(InitRogueLikeGUI, {}); +static RegisterShipInitFunc initFunc2(OnLoadGame, { "IS_ROGUELIKE" }); diff --git a/soh/soh/Enhancements/RogueLike/GUI/GUI.h b/soh/soh/Enhancements/RogueLike/GUI/GUI.h new file mode 100644 index 000000000..33cc68c3a --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/GUI/GUI.h @@ -0,0 +1,59 @@ +#ifndef ROGUELIKE_GUI_H +#define ROGUELIKE_GUI_H + +#include +#include +#include +#include "soh/Enhancements/RogueLike/Choices.hpp" + +namespace RogueLike { + +namespace GUI { + +void BeginFullscreenDimmed(const char* windowName); +RogueLike::Choices::ChoiceCard* DrawChooseScreen(std::string heading, + std::vector& allChoices, int rolls); + +class StartingSelectionWindow final : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + void Draw() override; + ~StartingSelectionWindow(){}; + + protected: + void InitElement() override{}; + void DrawElement() override{}; + void UpdateElement() override{}; +}; + +class HUDWindow final : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + void Draw() override; + ~HUDWindow(){}; + + protected: + void InitElement() override{}; + void DrawElement() override{}; + void UpdateElement() override{}; +}; + +class LevelUpWindow final : public Ship::GuiWindow { + public: + using GuiWindow::GuiWindow; + void Draw() override; + ~LevelUpWindow(){}; + + protected: + void InitElement() override{}; + void DrawElement() override{}; + void UpdateElement() override{}; +}; + +extern std::shared_ptr mLevelUpWindow; + +} // namespace GUI + +} // namespace RogueLike + +#endif diff --git a/soh/soh/Enhancements/RogueLike/GUI/LevelUpWindow.cpp b/soh/soh/Enhancements/RogueLike/GUI/LevelUpWindow.cpp new file mode 100644 index 000000000..8b3790847 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/GUI/LevelUpWindow.cpp @@ -0,0 +1,74 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" + +extern "C" { +#include "variables.h" +#include "macros.h" +#include "functions.h" + +extern PlayState* gPlayState; +} + +typedef enum { + WINDOW_STATE_CHOOSE_TYPE, + WINDOW_STATE_CHOOSE_CARD, + WINDOW_STATE_APPLY, +} LevelUpWindowState; + +static LevelUpWindowState state = WINDOW_STATE_CHOOSE_TYPE; +static RogueLike::Choices::ChoiceCard* typeChoice = nullptr; +static RogueLike::Choices::ChoiceCard* cardChoice = nullptr; + +void RogueLike::GUI::LevelUpWindow::Draw() { + if (!IsVisible()) { + return; + } + + BeginFullscreenDimmed("RogueLike Level Up"); + + switch (state) { + case WINDOW_STATE_CHOOSE_TYPE: { + typeChoice = DrawChooseScreen("Choose.", RogueLike::Choices::All, 10); + if (typeChoice != nullptr) { + state = WINDOW_STATE_CHOOSE_CARD; + } + } break; + case WINDOW_STATE_CHOOSE_CARD: { + switch (typeChoice->value) { + case 0: // Stat + cardChoice = DrawChooseScreen("Choose.", RogueLike::Choices::Stats, 10); + break; + case 1: // Item + cardChoice = DrawChooseScreen("Choose.", RogueLike::Choices::Items, 10); + break; + case 2: // Equipment + cardChoice = DrawChooseScreen("Choose.", RogueLike::Choices::Equipment, 10); + break; + case 3: // Song + cardChoice = DrawChooseScreen("Choose.", RogueLike::Choices::Songs, 10); + break; + default: + break; + } + + if (cardChoice != nullptr) { + state = WINDOW_STATE_APPLY; + } + } break; + default: { + // Apply choices + cardChoice->onSelect(cardChoice->value); + RogueLike::XP::UpdatePlayerStats(); + + // Close window and continue game + this->Hide(); + gPlayState->frameAdvCtx.enabled = false; + + // Reset state + state = WINDOW_STATE_CHOOSE_TYPE; + typeChoice = nullptr; + cardChoice = nullptr; + } break; + } + + ImGui::End(); +} diff --git a/soh/soh/Enhancements/RogueLike/GUI/StartingSelectionWindow.cpp b/soh/soh/Enhancements/RogueLike/GUI/StartingSelectionWindow.cpp new file mode 100644 index 000000000..9e51ba4dc --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/GUI/StartingSelectionWindow.cpp @@ -0,0 +1,71 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" + +extern "C" { +#include "variables.h" +#include "macros.h" +#include "functions.h" + +extern PlayState* gPlayState; +} + +typedef enum { + WINDOW_STATE_LOCATION, + WINDOW_STATE_ITEM, + WINDOW_STATE_SONG, + WINDOW_STATE_APPLY, +} StartingSelectionWindowState; + +static StartingSelectionWindowState state = WINDOW_STATE_LOCATION; +static RogueLike::Choices::ChoiceCard* locationChoice = nullptr; +static RogueLike::Choices::ChoiceCard* itemChoice = nullptr; +static RogueLike::Choices::ChoiceCard* songChoice = nullptr; + +void RogueLike::GUI::StartingSelectionWindow::Draw() { + if (!IsVisible()) { + return; + } + + BeginFullscreenDimmed("RogueLike Starting Selection"); + + switch (state) { + case WINDOW_STATE_LOCATION: { + locationChoice = DrawChooseScreen("Where will you begin your journey?", RogueLike::Choices::Locations, 20); + if (locationChoice != nullptr) { + state = WINDOW_STATE_ITEM; + } + } break; + case WINDOW_STATE_ITEM: { + itemChoice = DrawChooseScreen("What will you take?", RogueLike::Choices::Items, 20); + + if (itemChoice != nullptr) { + state = WINDOW_STATE_SONG; + } + } break; + case WINDOW_STATE_SONG: { + songChoice = DrawChooseScreen("What is your tune of choice?", RogueLike::Choices::Songs, 20); + + if (songChoice != nullptr) { + state = WINDOW_STATE_APPLY; + } + } break; + default: { + // Apply choices + locationChoice->onSelect(locationChoice->value); + itemChoice->onSelect(itemChoice->value); + songChoice->onSelect(songChoice->value); + + // Close window and reload game state + this->Hide(); + SET_NEXT_GAMESTATE(&gPlayState->state, Play_Init, PlayState); + gPlayState->state.running = false; + + // Reset state + state = WINDOW_STATE_LOCATION; + locationChoice = nullptr; + itemChoice = nullptr; + songChoice = nullptr; + } break; + } + + ImGui::End(); +} diff --git a/soh/soh/Enhancements/RogueLike/MiscBehavior/OnLoadGame.cpp b/soh/soh/Enhancements/RogueLike/MiscBehavior/OnLoadGame.cpp new file mode 100644 index 000000000..f264e35aa --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/MiscBehavior/OnLoadGame.cpp @@ -0,0 +1,21 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" + +extern "C" { +#include "variables.h" +#include "functions.h" +} + +static void OnLoadGame(int32_t fileNum) { + ShipInit::Init("IS_ROGUELIKE"); + + if (IS_ROGUELIKE) { + Flags_SetEventChkInf(EVENTCHKINF_OPENED_THE_DOOR_OF_TIME); + Flags_SetEventChkInf(EVENTCHKINF_SHOWED_MIDO_SWORD_SHIELD); + Flags_SetEventChkInf(EVENTCHKINF_SPOKE_TO_MIDO_AFTER_DEKU_TREES_DEATH); + } +} + +static RegisterShipInitFunc + initFunc([]() { GameInteractor::Instance->RegisterGameHook(OnLoadGame); }, {}); diff --git a/soh/soh/Enhancements/RogueLike/Quests.cpp b/soh/soh/Enhancements/RogueLike/Quests.cpp new file mode 100644 index 000000000..c1fc1a637 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/Quests.cpp @@ -0,0 +1,681 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" +#include "soh/ShipInit.hpp" +#include "soh/ActorDB.h" +#include "soh/Enhancements/custom-message/CustomMessageManager.h" +#include "soh/Notification/Notification.h" + +extern "C" { +#include "variables.h" +#include +#include + +#include "overlays/actors/ovl_Bg_Mjin/z_bg_mjin.h" +#include "overlays/actors/ovl_En_Vm/z_en_vm.h" + +extern PlayState* gPlayState; +s32 Object_Spawn(ObjectContext* objectCtx, s16 objectId); +} + +// clang-format off +std::vector rogueLikeQuestList = { + { RL_QUEST_HF_TRIAL_A, "Ganon's Fury I", "Watch out!", RL_QUEST_ACTIVE, 0, 1 }, + { RL_QUEST_HF_TRIAL_B, "Ganon's Fury II", "Get to Gerudo Valley\nbefore time runs out!", RL_QUEST_ACTIVE, 0, 1 }, + { RL_QUEST_KF_HOPOFFAITH, "Hop of Faith", "Sidehop from the fence\nabove the waterfall and land\non the middle platform.", RL_QUEST_ACTIVE, 0, 1}, + { RL_QUEST_KF_STRONGMAN, "Toe Crushers", "Mido likes rock, show them\nthat we don't!", RL_QUEST_ACTIVE, 0, 11 }, + { RL_QUEST_KV_POTHUNT, "The Pot Thickens", "A magical pot with extra lives?\nFind out how many!", RL_QUEST_ACTIVE, 0, 4 }, + { RL_QUEST_KV_STALFOS, "Stal-Not-So-Child", "The Stalchild in Hyrule Field have\ngotten bigger, take them out!", RL_QUEST_ACTIVE, 0, 5 }, + { RL_QUEST_ZD_POTTERY, "A Smashing View", "Toss a pot off the edge\nof the waterfall.", RL_QUEST_ACTIVE, 0, 1 }, +}; + +std::vector potHuntLocations = { + { 150.378f, 300.0f, 1166.648f }, + { 6.151f, 755.0f, -91.802f }, + { 1760.740f, 542.62f, 534.236f }, + { -381.964f, 240.0f, 1597.884f }, +}; + +const std::vector> trialAActorSpawnList = { + { { 563.838f, -0, 3059.409f }, -27275 }, + { { 678.031f, -0, 2563.164f }, -10891 }, + { { 185.132f, -0, 2404.207f }, -27275 }, + { { 36.108f, -0, 2925.440f }, -10891 }, + { { 335.571f, 20.0f, 2677.854f }, 0 }, +}; +// clang-format on + +Actor* trialActorSlot = NULL; +extern std::vector activeQuests; +static std::vector potHuntAvailability; +static std::vector currentTrialActorList; +static bool potHuntActorSpawned = false; +static bool sendConditionMessage = true; + +bool CheckActiveQuestById(u8 questId) { + for (auto& quest : activeQuests) { + if (quest.questId == questId) { + return true; + } + } + return false; +} + +bool CheckQuestGoalCompleteById(u8 questId) { + for (auto& quest : activeQuests) { + if (quest.questId == questId) { + return (quest.questProgress == quest.questGoal); + } + } + return false; +} + +bool CheckQuestCompletedById(u8 questId) { + for (auto& quest : activeQuests) { + if (quest.questId == questId) { + return (quest.questStatus); + } + } + return false; +} + +void SendQuestConditionMessage(u8 questId) { + if (!sendConditionMessage) { + return; + } + + std::string message = ""; + ImVec4 color = ImVec4(1, 1, 1, 1); + + switch (questId) { + case RL_QUEST_KV_POTHUNT: + message = "A Magical Pot has appeared nearby."; + color = ImVec4(0, 0.25f, 0.75f, 1); + break; + case RL_QUEST_HF_TRIAL_A: + message = "Come back when you have a sword..."; + color = ImVec4(1, 0, 0, 1); + break; + case RL_QUEST_HF_TRIAL_B: + message = "Come back when you're faster..."; + color = ImVec4(1, 0, 0, 1); + break; + default: + return; + } + Notification::Emit({ + .message = message, + .messageColor = color, + }); + sendConditionMessage = false; +} + +void RogueLike::Quests::CompleteQuestById(u8 questId) { + for (auto& quest : activeQuests) { + if (quest.questId == questId) { + quest.questStatus = RL_QUEST_COMPLETE; + break; + } + } +} + +void RogueLike::Quests::RemoveQuestById(u8 questId) { + int index = -1; + for (int i = 0; i < activeQuests.size(); i++) { + if (activeQuests[i].questId == questId) { + index = i; + break; + } + } + + if (index != -1) { + activeQuests.erase(activeQuests.begin() + index); + } +} + +void RogueLike::Quests::AddQuestById(u8 questId) { + if (CheckActiveQuestById(questId)) { + return; + } + + activeQuests.push_back(rogueLikeQuestList[questId]); +} + +u16 GetQuestProgress(u8 questId) { + if (!CheckActiveQuestById(questId)) { + return 0; + } + + for (auto& quest : activeQuests) { + if (quest.questId == questId) { + return quest.questProgress; + } + } + + return 0; +} + +u16 GetQuestGoal(u8 questId) { + if (!CheckActiveQuestById(questId)) { + return 0; + } + + for (auto& quest : activeQuests) { + if (quest.questId == questId) { + return quest.questGoal; + } + } + + return 0; +} + +u16 DetermineInitialQuestProgress(u8 questId, int16_t progressActor) { + ActorListEntry actorList = gPlayState->actorCtx.actorLists[ACTORCAT_PROP]; + u16 initialProgress = GetQuestGoal(questId); + if (questId == RL_QUEST_KF_STRONGMAN) { + initialProgress++; + } + + Actor* currentActor = actorList.head; + while (currentActor != nullptr) { + if (currentActor->id == progressActor) { + initialProgress--; + } + currentActor = currentActor->next; + } + + return initialProgress; +} + +void RogueLike::Quests::UpdateQuestProgress(u8 questId) { + if (!CheckActiveQuestById(questId)) { + return; + } + + for (auto& quest : activeQuests) { + if (quest.questId == questId) { + quest.questProgress++; + break; + } + } +} + +void RogueLike::Quests::SetQuestProgress(u8 questId, u16 progress) { + if (!CheckActiveQuestById(questId)) { + return; + } + + for (auto& quest : activeQuests) { + if (quest.questId == questId) { + quest.questProgress = progress; + break; + } + } +} + +void RogueLike::Quests::ResetQuestProgress(u8 questId) { + if (!CheckActiveQuestById(questId)) { + return; + } + + if (CheckQuestGoalCompleteById(questId)) { + return; + } + + for (auto& quest : activeQuests) { + if (quest.questId == questId) { + if (questId == RL_QUEST_KV_POTHUNT) { + potHuntAvailability.clear(); + } + quest.questProgress = 0; + break; + } + } +} + +// StartQuest() - Used for Quest Specific Functions. +void StartQuest(u8 questId) { + uint32_t index = 0; + switch (questId) { + case RL_QUEST_HF_TRIAL_A: + if (!CheckQuestGoalCompleteById(RL_QUEST_HF_TRIAL_A)) { + GameInteractor::RawAction::SetWeatherStorm(true); + for (auto& slot : trialAActorSpawnList) { + if (slot.first.y != 20.0f) { + trialActorSlot = + Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_BG_HIDAN_FWBIG, slot.first.x, + slot.first.y, slot.first.z, 0, slot.second, 0, 0, false); + currentTrialActorList.push_back(trialActorSlot); + } else { + EnVm* beamosActor = + (EnVm*)Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_VM, slot.first.x, + slot.first.y, slot.first.z, 0, slot.second, 0, 0, false); + beamosActor->beamSightRange = 400.0f; + currentTrialActorList.push_back(&beamosActor->actor); + } + index++; + } + Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_ZF, 255.359f, -0, 3134.437f, 0, 0, 0, -2, + false); + } else { + RogueLike::Quests::CompleteQuestById(RL_QUEST_HF_TRIAL_A); + uint32_t reward = RogueLike::XP::ConvertLevelToXP(RogueLike::XP::GetCurrentLevel() + 1); + RogueLike::XP::GrantXP(reward); + for (auto& kill : currentTrialActorList) { + Actor_Kill(kill); + kill = NULL; + } + currentTrialActorList.clear(); + GameInteractor::RawAction::SetWeatherStorm(false); + } + break; + case RL_QUEST_HF_TRIAL_B: + if (!CheckQuestGoalCompleteById(RL_QUEST_HF_TRIAL_B)) { + GameInteractor::RawAction::SetWeatherStorm(true); + gSaveContext.timerState = 6; + gSaveContext.timerSeconds = 70; + } else { + RogueLike::Quests::CompleteQuestById(RL_QUEST_HF_TRIAL_B); + uint32_t reward = RogueLike::XP::ConvertLevelToXP(RogueLike::XP::GetCurrentLevel() + 1); + gSaveContext.timerState = 0; + gSaveContext.timerSeconds = 0; + GameInteractor::RawAction::SetWeatherStorm(false); + } + break; + case RL_QUEST_KV_POTHUNT: + if (!CheckQuestGoalCompleteById(RL_QUEST_KV_POTHUNT)) { + if (potHuntAvailability.size() == 0) { + potHuntAvailability = potHuntLocations; + } + u8 potRoll = rand() % potHuntAvailability.size(); + Vec3f spawnPoint = potHuntAvailability[potRoll]; + Actor* potActor = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_OBJ_TSUBO, spawnPoint.x, + spawnPoint.y, spawnPoint.z, 0, 0, 0, 256, false); + Actor_SetColorFilter(potActor, 0x1000, 150, 0, 1000); + potHuntAvailability.erase(potHuntAvailability.begin() + potRoll); + sendConditionMessage = true; + SendQuestConditionMessage(RL_QUEST_KV_POTHUNT); + } + break; + case RL_QUEST_KV_STALFOS: + if (!CheckQuestGoalCompleteById(RL_QUEST_KV_STALFOS)) { + float centerX = 2475.3f; + float centerZ = 496.3f; + float radius = 100.0f; + + for (int i = 0; i < GetQuestGoal(RL_QUEST_KV_STALFOS); i++) { + float angle = i * (2 * M_PI / 5); + float spawnX = centerX + radius * cosf(angle); + float spawnZ = centerZ + radius * sinf(angle); + + if (ActorDB::Instance->RetrieveEntry(ACTOR_EN_TEST).entry.valid) { + Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_TEST, spawnX, -4.7f, spawnZ, 0, 0, 0, 1, + 0); + } + } + } + break; + default: + break; + } +} + +RogueLikeQuest FindTrialByLocation(Actor* trialActor) { + switch (gPlayState->sceneNum) { + case SCENE_HYRULE_FIELD: + if (trialActor->world.pos.x == 335.571f && trialActor->world.pos.z == 2677.854f) { + if (LINK_IS_ADULT && ((EQUIP_FLAG_SWORD_MASTER & gSaveContext.inventory.equipment) || + (EQUIP_FLAG_SWORD_BGS & gSaveContext.inventory.equipment))) { + return RL_QUEST_HF_TRIAL_A; + } else { + SendQuestConditionMessage(RL_QUEST_HF_TRIAL_A); + } + } + if (trialActor->world.pos.x == 1490.550f && trialActor->world.pos.z == 8760.643f) { + if (LINK_IS_ADULT) { + return RL_QUEST_HF_TRIAL_B; + } + } + break; + default: + break; + } + + return RL_QUEST_ID_MAX; +} + +static void InitRogueLikeQuests() { + activeQuests.clear(); + currentTrialActorList.clear(); +} + +static void OnLoadGame() { + activeQuests.clear(); + for (auto& load : gSaveContext.ship.quest.data.rogueLike.quests) { + if (load.questDescription != NULL) { + activeQuests.push_back(load); + } + } + + COND_HOOK(OnPlayerUpdate, IS_ROGUELIKE, []() { + Player* player = GET_PLAYER(gPlayState); + RogueLikeQuest questId = RL_QUEST_ID_MAX; + static bool hopOfFaithStart = false; + static bool trialTimerInit = false; + static uint32_t trialTimer = 0; + + if (trialTimerInit == false) { + trialTimer = gPlayState->gameplayFrames; + trialTimerInit = true; + } + + if (CheckActiveQuestById(RL_QUEST_HF_TRIAL_B) && !CheckQuestGoalCompleteById(RL_QUEST_HF_TRIAL_B)) { + if (gSaveContext.timerState == 6) { + if (gSaveContext.timerSeconds > 0) { + if (trialTimer <= gPlayState->gameplayFrames - 20) { + gSaveContext.timerSeconds--; + trialTimer = gPlayState->gameplayFrames; + } + } else if (gSaveContext.timerSeconds <= 0) { + gSaveContext.health = 4; + gSaveContext.timerState = 0; + gSaveContext.timerSeconds = 0; + RogueLike::Quests::RemoveQuestById(RL_QUEST_HF_TRIAL_B); + sendConditionMessage = true; + SendQuestConditionMessage(RL_QUEST_HF_TRIAL_B); + } + } + } + + if (CheckActiveQuestById(RL_QUEST_KF_HOPOFFAITH) && !CheckQuestGoalCompleteById(RL_QUEST_KF_HOPOFFAITH)) { + bool isHopping = (player->stateFlags2 & PLAYER_STATE2_HOPPING); + if (!hopOfFaithStart && isHopping && player->actor.world.pos.y >= 360.0f) { + hopOfFaithStart = true; + } + if (hopOfFaithStart && !isHopping) { + hopOfFaithStart = false; + if ((player->actor.world.pos.x >= 318.0f && player->actor.world.pos.x <= 418.0f) && + (player->actor.world.pos.z >= -227.6f && player->actor.world.pos.z <= -126.5f)) { + RogueLike::Quests::UpdateQuestProgress(RL_QUEST_KF_HOPOFFAITH); + } + } + } + if (gPlayState->sceneNum == SCENE_HYRULE_FIELD) { + Actor* trialActor = + Actor_FindNearby(gPlayState, &GET_PLAYER(gPlayState)->actor, ACTOR_BG_MJIN, ACTORCAT_BG, 45.0f); + if (trialActor != NULL) { + questId = FindTrialByLocation(trialActor); + if (questId != RL_QUEST_ID_MAX) { + if (!CheckActiveQuestById(questId)) { + RogueLike::Quests::AddQuestById(questId); + StartQuest(questId); + } + } + } + } + }); + + COND_HOOK(OnSceneInit, IS_ROGUELIKE, [](u16 sceneNum) { + if (CheckActiveQuestById(RL_QUEST_HF_TRIAL_B) && gSaveContext.timerState == 6 && + gPlayState->sceneNum == SCENE_GERUDO_VALLEY) { + if (gSaveContext.timerSeconds > 0) { + RogueLike::Quests::UpdateQuestProgress(RL_QUEST_HF_TRIAL_B); + } + } + for (auto& quest : activeQuests) { + RogueLike::Quests::ResetQuestProgress(quest.questId); + } + sendConditionMessage = true; + }); + + COND_HOOK(OnRoomInit, IS_ROGUELIKE, [](u16 roomNum) { + if (gPlayState->sceneNum == SCENE_KOKIRI_FOREST) { + if (CheckActiveQuestById(RL_QUEST_KF_STRONGMAN)) { + if (!CheckQuestCompletedById(RL_QUEST_KF_STRONGMAN)) { + RogueLike::Quests::ResetQuestProgress(RL_QUEST_KF_STRONGMAN); + } + } + } + }); + + COND_HOOK(OnSceneSpawnActors, IS_ROGUELIKE, []() { + if (gPlayState->sceneNum == SCENE_HYRULE_FIELD) { + if (CheckActiveQuestById(RL_QUEST_KV_STALFOS)) { + StartQuest(RL_QUEST_KV_STALFOS); + } + Object_Spawn(&gPlayState->objectCtx, OBJECT_MJIN); + Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_BG_MJIN, 335.571f, -0.0f, 2677.854f, 0, 0, 0, 1, + false); + Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_BG_MJIN, 1490.550f, -135.0f, 8760.643f, 0, 0, 0, 1, + false); + } + }); + + COND_HOOK(OnActorKill, IS_ROGUELIKE, [](void* actor) { + Actor* refActor = (Actor*)actor; + + switch (refActor->id) { + case ACTOR_EN_TEST: + if (CheckActiveQuestById(RL_QUEST_KV_STALFOS) && gPlayState->sceneNum == SCENE_HYRULE_FIELD) { + if (!CheckQuestGoalCompleteById(RL_QUEST_KV_STALFOS)) { + RogueLike::Quests::UpdateQuestProgress(RL_QUEST_KV_STALFOS); + } + } + break; + case ACTOR_OBJ_TSUBO: + if (gPlayState->sceneNum == SCENE_ZORAS_DOMAIN) { + if (CheckActiveQuestById(RL_QUEST_ZD_POTTERY)) { + if (!(CheckQuestGoalCompleteById(RL_QUEST_ZD_POTTERY) && + CheckQuestCompletedById(RL_QUEST_ZD_POTTERY))) { + if (GET_PLAYER(gPlayState)->actor.world.pos.y >= 830.0f && + refActor->world.pos.y <= 800.0f) { + RogueLike::Quests::UpdateQuestProgress(RL_QUEST_ZD_POTTERY); + } + } + } + } else if (gPlayState->sceneNum == SCENE_KAKARIKO_VILLAGE) { + if (CheckActiveQuestById(RL_QUEST_KV_POTHUNT) && refActor->params == 256) { + if (!(CheckQuestGoalCompleteById(RL_QUEST_KV_POTHUNT) && + CheckQuestCompletedById(RL_QUEST_KV_POTHUNT))) { + RogueLike::Quests::UpdateQuestProgress(RL_QUEST_KV_POTHUNT); + StartQuest(RL_QUEST_KV_POTHUNT); + } + } + } + break; + case ACTOR_EN_ISHI: + if (refActor->world.pos.x == refActor->home.pos.x && refActor->world.pos.z == refActor->home.pos.z) { + return; + } + if (CheckActiveQuestById(RL_QUEST_KF_STRONGMAN) && gPlayState->sceneNum == SCENE_KOKIRI_FOREST && + gPlayState->roomCtx.curRoom.num == 0) { + if (!CheckQuestGoalCompleteById(RL_QUEST_KF_STRONGMAN)) { + RogueLike::Quests::UpdateQuestProgress(RL_QUEST_KF_STRONGMAN); + } + } + break; + case ACTOR_EN_ZF: + if (gPlayState->sceneNum == SCENE_HYRULE_FIELD) { + if (CheckActiveQuestById(RL_QUEST_HF_TRIAL_A)) { + if (!CheckQuestGoalCompleteById(RL_QUEST_HF_TRIAL_A)) { + RogueLike::Quests::UpdateQuestProgress(RL_QUEST_HF_TRIAL_A); + StartQuest(RL_QUEST_HF_TRIAL_A); + } + } + } + break; + default: + break; + } + }); + + // RL_QUEST_KV_STALFOS + COND_ID_HOOK(OnOpenText, 0x503e, IS_ROGUELIKE, [](u16* textId, bool* loadFromMessageTable) { + auto oldEntry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + std::string endOfMessage = oldEntry.GetEnglish().substr(oldEntry.GetEnglish().size() - 4); + auto messageEntry = CustomMessage("Stalfos have been attacking our Cucco's, please help us!" + endOfMessage); + messageEntry.AutoFormat(); + messageEntry.LoadIntoFont(); + *loadFromMessageTable = false; + }); + + COND_ID_HOOK(OnOpenText, 0x5042, IS_ROGUELIKE, [](u16* textId, bool* loadFromMessageTable) { + if (CheckQuestGoalCompleteById(RL_QUEST_KV_STALFOS) && !CheckQuestCompletedById(RL_QUEST_KV_STALFOS)) { + auto oldEntry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + std::string endOfMessage = oldEntry.GetEnglish().substr(oldEntry.GetEnglish().size() - 2); + auto messageEntry = CustomMessage("Thank you for saving our cucco's!" + endOfMessage); + messageEntry.AutoFormat(); + messageEntry.LoadIntoFont(); + + RogueLike::XP::SpawnXPGroup(GET_PLAYER(gPlayState)->actor.world.pos, 20); + RogueLike::Quests::CompleteQuestById(RL_QUEST_KV_STALFOS); + + *loadFromMessageTable = false; + } + }); + + // RL_QUEST_ZD_POTTERY + COND_ID_HOOK(OnOpenText, 0x4006, IS_ROGUELIKE, [](u16* textId, bool* loadFromMessageTable) { + auto oldEntry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + std::string endOfMessage = oldEntry.GetEnglish().substr(oldEntry.GetEnglish().size() - 2); + auto messageEntry = CustomMessage("Have you ever heard the sound of ceramic shattering against the water? You " + "should give it a try, it's beautiful!" + + endOfMessage); + messageEntry.AutoFormat(); + messageEntry.LoadIntoFont(); + + RogueLike::Quests::AddQuestById(RL_QUEST_ZD_POTTERY); + + *loadFromMessageTable = false; + }); + + COND_ID_HOOK(OnOpenText, 0x4007, IS_ROGUELIKE, [](u16* textId, bool* loadFromMessageTable) { + auto oldEntry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + std::string endOfMessage = oldEntry.GetEnglish().substr(oldEntry.GetEnglish().size() - 2); + auto messageEntry = CustomMessage("" + endOfMessage); + + if (!CheckQuestGoalCompleteById(RL_QUEST_ZD_POTTERY)) { + messageEntry = CustomMessage( + "Need a tip? Take one of the Pots outside of the Shop and toss it over the top of the waterfall." + + endOfMessage); + } else { + messageEntry = CustomMessage( + "The sound, wasn't that exhilarating? There may be a few more pots if you fancy another go." + + endOfMessage); + RogueLike::XP::SpawnXPGroup(GET_PLAYER(gPlayState)->actor.world.pos, 10); + RogueLike::Quests::CompleteQuestById(RL_QUEST_ZD_POTTERY); + } + + messageEntry.AutoFormat(); + messageEntry.LoadIntoFont(); + *loadFromMessageTable = false; + }); + + // RL_QUEST_KF_STRONGMAN + COND_ID_HOOK(OnOpenText, 0x1004, IS_ROGUELIKE, [](u16* textId, bool* loadFromMessageTable) { + auto oldEntry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + std::string endOfMessage = oldEntry.GetEnglish().substr(oldEntry.GetEnglish().size() - 2); + auto messageEntry = CustomMessage("" + endOfMessage); + + if (!CheckQuestGoalCompleteById(RL_QUEST_KF_STRONGMAN)) { + messageEntry = CustomMessage( + "Stupid Mido likes these stupid rocks! Don't just stand there, help me smash them!" + endOfMessage); + if (!CheckActiveQuestById(RL_QUEST_KF_STRONGMAN)) { + RogueLike::Quests::AddQuestById(RL_QUEST_KF_STRONGMAN); + RogueLike::Quests::SetQuestProgress( + RL_QUEST_KF_STRONGMAN, DetermineInitialQuestProgress(RL_QUEST_KF_STRONGMAN, ACTOR_EN_ISHI)); + } + } else { + messageEntry = CustomMessage("Thanks for being one of the good guys! Oh, this one? Don't worry, I'll have " + "it smashed by your 17th birthday." + + endOfMessage); + RogueLike::XP::SpawnXPGroup(GET_PLAYER(gPlayState)->actor.world.pos, 10); + RogueLike::Quests::CompleteQuestById(RL_QUEST_KF_STRONGMAN); + } + + messageEntry.AutoFormat(); + messageEntry.LoadIntoFont(); + *loadFromMessageTable = false; + }); + + // RL_QUEST_KF_HOPOFFAITH + COND_ID_HOOK(OnOpenText, 0x10d7, IS_ROGUELIKE, [](u16* textId, bool* loadFromMessageTable) { + auto oldEntry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + std::string endOfMessage = oldEntry.GetEnglish().substr(oldEntry.GetEnglish().size() - 4); + auto messageEntry = CustomMessage("" + endOfMessage); + + if (!CheckActiveQuestById(RL_QUEST_KF_HOPOFFAITH)) { + messageEntry = + CustomMessage("Wow, you came all the way to see me? Not afraid of heights I see." + endOfMessage); + } else { + Message_ContinueTextbox(gPlayState, 0x10d8); + } + + messageEntry.AutoFormat(); + messageEntry.LoadIntoFont(); + *loadFromMessageTable = false; + }); + + COND_ID_HOOK(OnOpenText, 0x10d8, IS_ROGUELIKE, [](u16* textId, bool* loadFromMessageTable) { + auto oldEntry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + std::string endOfMessage = oldEntry.GetEnglish().substr(oldEntry.GetEnglish().size() - 2); + auto messageEntry = CustomMessage("" + endOfMessage); + + if (!CheckActiveQuestById(RL_QUEST_KF_HOPOFFAITH)) { + messageEntry = + CustomMessage("If we're going to continue meeting like this, let's see what you got!" + endOfMessage); + RogueLike::Quests::AddQuestById(RL_QUEST_KF_HOPOFFAITH); + } else { + if (!CheckQuestGoalCompleteById(RL_QUEST_KF_HOPOFFAITH)) { + messageEntry = CustomMessage("Can't read the Quest List, huh. See that fence above the waterfall? " + "Sidehop off of it and land on the middle platform below." + + endOfMessage); + } else { + messageEntry = + CustomMessage("Hey, nice distance! They say you can unload doors doing that." + endOfMessage); + if (!CheckQuestCompletedById(RL_QUEST_KF_HOPOFFAITH)) { + RogueLike::XP::SpawnXPGroup(GET_PLAYER(gPlayState)->actor.world.pos, 10); + RogueLike::Quests::CompleteQuestById(RL_QUEST_KF_HOPOFFAITH); + } + } + } + + messageEntry.AutoFormat(); + messageEntry.LoadIntoFont(); + *loadFromMessageTable = false; + }); + + // RL_QUEST_KV_POTHUNT + COND_ID_HOOK(OnOpenText, 0x5036, IS_ROGUELIKE, [](u16* textId, bool* loadFromMessageTable) { + auto oldEntry = CustomMessage::LoadVanillaMessageTableEntry(*textId); + std::string endOfMessage = oldEntry.GetEnglish().substr(oldEntry.GetEnglish().size() - 2); + auto messageEntry = CustomMessage("" + endOfMessage); + + if (!CheckActiveQuestById(RL_QUEST_KV_POTHUNT)) { + messageEntry = CustomMessage("I've noticed you have a knack for smashing things. I developed a special Pot " + "that could use testing, it's hidden around here somewhere." + + endOfMessage); + RogueLike::Quests::AddQuestById(RL_QUEST_KV_POTHUNT); + StartQuest(RL_QUEST_KV_POTHUNT); + } else { + if (!CheckQuestGoalCompleteById(RL_QUEST_KV_POTHUNT)) { + messageEntry = CustomMessage("It's hidden around here somewhere, get hunting!" + endOfMessage); + } else { + if (!CheckQuestCompletedById(RL_QUEST_KV_POTHUNT)) { + messageEntry = + CustomMessage("Nicely done! It's not much but here's something for your help." + endOfMessage); + RogueLike::XP::SpawnXPGroup(GET_PLAYER(gPlayState)->actor.world.pos, 10); + RogueLike::Quests::CompleteQuestById(RL_QUEST_KV_POTHUNT); + } else { + messageEntry = CustomMessage("Thanks for your help." + endOfMessage); + } + } + } + + messageEntry.AutoFormat(); + messageEntry.LoadIntoFont(); + *loadFromMessageTable = false; + }); +} + +static RegisterShipInitFunc initFunc(InitRogueLikeQuests, {}); +static RegisterShipInitFunc initFunc2(OnLoadGame, { "IS_ROGUELIKE" }); \ No newline at end of file diff --git a/soh/soh/Enhancements/RogueLike/Quests.h b/soh/soh/Enhancements/RogueLike/Quests.h new file mode 100644 index 000000000..90fd4b227 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/Quests.h @@ -0,0 +1,23 @@ +#ifndef ROGUELIKE_QUESTS_H +#define ROGUELIKE_QUESTS_H + +extern "C" { +#include +} + +namespace RogueLike { + +namespace Quests { + +void CompleteQuestById(u8 questId); +void AddQuestById(u8 questId); +void RemoveQuestById(u8 questId); +void UpdateQuestProgress(u8 questId); +void SetQuestProgress(u8 questId, u16 progress); +void ResetQuestProgress(u8 questId); + +} // namespace Quests + +} // namespace RogueLike + +#endif \ No newline at end of file diff --git a/soh/soh/Enhancements/RogueLike/RogueLike.cpp b/soh/soh/Enhancements/RogueLike/RogueLike.cpp new file mode 100644 index 000000000..1c876ab08 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/RogueLike.cpp @@ -0,0 +1,9 @@ +#include "RogueLike.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/ShipInit.hpp" + +// Entry point for the module, run once on game boot +static void InitRogueLike() { +} + +static RegisterShipInitFunc initFunc(InitRogueLike, {}); diff --git a/soh/soh/Enhancements/RogueLike/RogueLike.h b/soh/soh/Enhancements/RogueLike/RogueLike.h new file mode 100644 index 000000000..1e7370401 --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/RogueLike.h @@ -0,0 +1,14 @@ +#ifndef ROGUELIKE_H +#define ROGUELIKE_H + +#include "GUI/GUI.h" +#include "XP.h" +#include "Difficulty.h" +#include "Quests.h" +#include "Types.h" + +extern std::vector activeQuests; + +namespace RogueLike {} // namespace RogueLike + +#endif // ROGUELIKE_H diff --git a/soh/soh/Enhancements/RogueLike/Types.h b/soh/soh/Enhancements/RogueLike/Types.h new file mode 100644 index 000000000..693a3f38f --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/Types.h @@ -0,0 +1,45 @@ +#ifndef ROGUELIKE_TYPES_H +#define ROGUELIKE_TYPES_H + +#ifdef __cplusplus +extern "C" { +#endif + +typedef enum { + RL_HEALTH, + RL_ATTACK, + RL_DEFENSE, + RL_SPEED, + RL_MAX, +} RoguelikeStats; + +typedef enum { + RL_QUEST_ACTIVE, + RL_QUEST_COMPLETE, +} RogueLikeQuestStatus; + +typedef enum { + RL_QUEST_HF_TRIAL_A, + RL_QUEST_HF_TRIAL_B, + RL_QUEST_KF_HOPOFFAITH, + RL_QUEST_KF_STRONGMAN, + RL_QUEST_KV_POTHUNT, + RL_QUEST_KV_STALFOS, + RL_QUEST_ZD_POTTERY, + RL_QUEST_ID_MAX, +} RogueLikeQuest; + +typedef struct { + u8 questId; + const char* questName; + const char* questDescription; + u8 questStatus; + u16 questProgress; + u16 questGoal; +} RogueLikeQuestObject; + +#ifdef __cplusplus +} +#endif + +#endif // ROGUELIKE_TYPES_H diff --git a/soh/soh/Enhancements/RogueLike/XP.cpp b/soh/soh/Enhancements/RogueLike/XP.cpp new file mode 100644 index 000000000..042e60a9f --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/XP.cpp @@ -0,0 +1,124 @@ +#include "soh/Enhancements/RogueLike/RogueLike.h" +#include "soh/Enhancements/custom-item/CustomItem.h" + +extern "C" { +#include "variables.h" +#include "functions.h" +#include "macros.h" + +extern PlayState* gPlayState; +} + +extern std::shared_ptr mLevelUpWindow; +extern std::map> rogueLikeStatMap; + +#define BASE_XP CVarGetFloat("gRogueLike.BaseXP", 100.0f) +#define GROWTH_RATE CVarGetFloat("gRogueLike.XPGrowthRate", 1.3f) + +float RogueLike::XP::GetProgressToNextLevel() { + u32 currentXP = gSaveContext.ship.quest.data.rogueLike.xp; + + u32 currentLevel = GetCurrentLevel(); + u32 xpForCurrentLevel = ConvertLevelToXP(currentLevel); + u32 xpForNextLevel = ConvertLevelToXP(currentLevel + 1); + + return static_cast(currentXP - xpForCurrentLevel) / static_cast(xpForNextLevel - xpForCurrentLevel); +} + +u32 RogueLike::XP::GetCurrentLevel() { + return RogueLike::XP::ConvertXPToLevel(gSaveContext.ship.quest.data.rogueLike.xp); +} + +u32 RogueLike::XP::ConvertXPToLevel(u32 xp) { + return static_cast(logf((xp * (GROWTH_RATE - 1) / BASE_XP) + 1) / logf(GROWTH_RATE)); +} + +u32 RogueLike::XP::ConvertLevelToXP(u32 level) { + return static_cast(BASE_XP * ((powf(GROWTH_RATE, level) - 1) / (GROWTH_RATE - 1))); +} + +void RogueLike::XP::GrantXP(u32 amount) { + + u32 oldLevel = GetCurrentLevel(); + gSaveContext.ship.quest.data.rogueLike.xp += amount; + RogueLike::Difficulty::IndicateActivity(); + u32 newLevel = GetCurrentLevel(); + + if (newLevel != oldLevel) { + Sfx_PlaySfxCentered(NA_SE_SY_CORRECT_CHIME); + mLevelUpWindow->Show(); + } +} + +void RogueLike::XP::SpawnXPOrb(Vec3f spawnPos, u32 amount, int16_t flags) { + CustomItem::Spawn( + spawnPos.x, spawnPos.y + 10.0f, spawnPos.z, 0, flags, amount, + [](Actor* actor, PlayState* play) { + RogueLike::XP::GrantXP(CUSTOM_ITEM_PARAM); + Sfx_PlaySfxCentered(NA_SE_SY_RUPY_COUNT); + }, + [](Actor* actor, PlayState* play) { + Matrix_Scale(15.0f, 15.0f, 15.0f, MTXMODE_APPLY); + Matrix_Translate(0.0f, -40.0f, 0.0f, MTXMODE_APPLY); + + u32 amount = CUSTOM_ITEM_PARAM; + GetItemDrawID drawId = GID_RUPEE_GREEN; + + if (amount >= 200) { + drawId = GID_RUPEE_GOLD; + } else if (amount >= 50) { + drawId = GID_RUPEE_PURPLE; + } else if (amount >= 20) { + drawId = GID_RUPEE_RED; + } else if (amount >= 5) { + drawId = GID_RUPEE_BLUE; + } + + GetItem_Draw(play, drawId); + + // Slowly move towards the player + Player* player = GET_PLAYER(play); + + // Don't magnet till it hits the ground + if (actor->bgCheckFlags & 1 && !Player_InBlockingCsMode(gPlayState, player)) { + if (actor->xzDistToPlayer < 100.0f) { + s16 targetYaw = Actor_WorldYawTowardActor(actor, &player->actor); + actor->world.rot.y = targetYaw; + + // the further away, the slower it moves + const f32 desiredSpeed = ((actor->xzDistToPlayer - 10.0f) / 90.0f) * (0.01f - 3.0f) + 3.0f; + actor->speedXZ = desiredSpeed; + } + + if (actor->xzDistToPlayer < 10.0f) { + CUSTOM_ITEM_FLAGS |= CustomItem::KILL_ON_TOUCH; + } + } + }); +} + +void RogueLike::XP::SpawnXPGroup(Vec3f spawnPos, u32 amount) { + u32 remainingAmount = amount; + + while (remainingAmount > 0) { + std::vector orbSizes = { 1 }; + + if (remainingAmount >= 200) + orbSizes.push_back(200); + if (remainingAmount >= 50) + orbSizes.push_back(50); + if (remainingAmount >= 20) + orbSizes.push_back(20); + if (remainingAmount >= 5) + orbSizes.push_back(5); + + u32 orbAmount = orbSizes[Rand_ZeroOne() * orbSizes.size()]; + + SpawnXPOrb(spawnPos, orbAmount); + remainingAmount -= orbAmount; + } +} + +void RogueLike::XP::UpdatePlayerStats() { + gSaveContext.healthCapacity = 0x30 + (gSaveContext.ship.quest.data.rogueLike.stats[RL_HEALTH] * 0x10); +} diff --git a/soh/soh/Enhancements/RogueLike/XP.h b/soh/soh/Enhancements/RogueLike/XP.h new file mode 100644 index 000000000..bfc9013ee --- /dev/null +++ b/soh/soh/Enhancements/RogueLike/XP.h @@ -0,0 +1,27 @@ +#ifndef ROGUELIKE_XP_H +#define ROGUELIKE_XP_H + +#include "soh/Enhancements/custom-item/CustomItem.h" + +extern "C" { +#include +} + +namespace RogueLike { + +namespace XP { + +float GetProgressToNextLevel(); +u32 GetCurrentLevel(); +u32 ConvertXPToLevel(u32 xp); +u32 ConvertLevelToXP(u32 level); +void GrantXP(u32 amount); +void SpawnXPOrb(Vec3f spawnPos, u32 amount, int16_t flags = CustomItem::STOP_BOBBING | CustomItem::TOSS_ON_SPAWN); +void SpawnXPGroup(Vec3f spawnPos, u32 amount); +void UpdatePlayerStats(); + +} // namespace XP + +} // namespace RogueLike + +#endif diff --git a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp index c0ac6bbc1..bd4c55c67 100644 --- a/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp +++ b/soh/soh/Enhancements/cosmetics/CosmeticsEditor.cpp @@ -615,23 +615,6 @@ void CosmeticsUpdateTick() { 5. GFX Command: The GFX command you want to insert */ void ApplyOrResetCustomGfxPatches(bool manualChange) { - if (manualChange) { - PATCH_GFX(object_wood02_DL_007968, "Tree1", "gLetItSnow", 17, gsDPSetPrimColor(0, 0, 255, 255, 255, 255)); - PATCH_GFX(object_wood02_DL_000090, "Tree2", "gLetItSnow", 17, gsDPSetPrimColor(0, 0, 200, 255, 255, 255)); - PATCH_GFX(object_wood02_DL_000340, "Tree3", "gLetItSnow", 17, gsDPSetPrimColor(0, 0, 255, 255, 255, 255)); - PATCH_GFX(object_wood02_DL_000340, "Tree4", "gLetItSnow", 24, gsDPSetPrimColor(0, 0, 255, 255, 255, 255)); - PATCH_GFX(spot00_room_0DL_0139A8, "Path1", "gLetItSnow", 23, gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); - PATCH_GFX(spot00_room_0DL_013250, "Path2", "gLetItSnow", 23, gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); - PATCH_GFX(spot00_room_0DL_0143C8, "Path3", "gLetItSnow", 23, gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); - PATCH_GFX(spot04_room_0DL_018048, "Path4", "gLetItSnow", 24, gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); - PATCH_GFX(spot04_room_1DL_007810, "Path5", "gLetItSnow", 24, gsDPSetPrimColor(0, 0, 100, 150, 255, 60)); - PATCH_GFX(spot20_room_0DL_0062D0, "Path6", "gLetItSnow", 23, gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); - PATCH_GFX(spot20_room_0DL_004460, "Path8", "gLetItSnow", 31, gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); - PATCH_GFX(spot20_room_0DL_004460, "Path9", "gLetItSnow", 118, gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); - PATCH_GFX(spot20_room_0DL_0065E8, "Path10", "gLetItSnow", 24, gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); - PATCH_GFX(spot03_room_0DL_00C4B0, "Path11", "gLetItSnow", 23, gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); - PATCH_GFX(spot15_room_0DL_00C748, "Path12", "gLetItSnow", 23, gsDPSetPrimColor(0, 0, 200, 230, 255, 30)); - } static CosmeticOption& magicFaroresPrimary = cosmeticOptions.at("Magic.FaroresPrimary"); if (manualChange || CVarGetInteger(magicFaroresPrimary.rainbowCvar, 0)) { Color_RGBA8 color = CVarGetColor(magicFaroresPrimary.valuesCvar, magicFaroresPrimary.defaultColor); @@ -1940,14 +1923,6 @@ void DrawSillyTab() { UIWidgets::Separator(true, true, 2.0f, 2.0f); - UIWidgets::CVarCheckbox("Let It Snow", CVAR_GENERAL("LetItSnow"), - UIWidgets::CheckboxOptions() - .Color(THEME_COLOR) - .Tooltip("Makes snow fall, changes chest texture colors to red and green, etc, for " - "December holidays.\nWill reset on restart outside of December 23-25.")); - - UIWidgets::Separator(true, true, 2.0f, 2.0f); - if (UIWidgets::CVarSliderFloat("Link Body Size", CVAR_COSMETIC("Link.BodySize.Value"), UIWidgets::FloatSliderOptions() .Format("%.3f") diff --git a/soh/soh/Enhancements/custom-collectible/CustomCollectible.cpp b/soh/soh/Enhancements/custom-item/CustomItem.cpp similarity index 53% rename from soh/soh/Enhancements/custom-collectible/CustomCollectible.cpp rename to soh/soh/Enhancements/custom-item/CustomItem.cpp index a6ecaf1ab..d3bde51e3 100644 --- a/soh/soh/Enhancements/custom-collectible/CustomCollectible.cpp +++ b/soh/soh/Enhancements/custom-item/CustomItem.cpp @@ -1,27 +1,48 @@ -#include "CustomCollectible.h" -#include +#include "CustomItem.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" -#include "soh/Enhancements/randomizer/3drando/random.hpp" -#include "soh/frame_interpolation.h" -#include "soh/Enhancements/custom-message/CustomMessageManager.h" extern "C" { #include "z64actor.h" #include "functions.h" #include "variables.h" #include "macros.h" -#include "objects/object_md/object_md.h" extern PlayState* gPlayState; } -EnItem00* CustomCollectible::Spawn(f32 posX, f32 posY, f32 posZ, s16 rot, s16 flags, s16 params, ActorFunc actionFunc, - ActorFunc drawFunc) { +// #region These were copied from z_en_item00.c +static ColliderCylinderInit sCylinderInit = { + { + COLTYPE_NONE, + AT_NONE, + AC_ON | AT_TYPE_PLAYER, + OC1_NONE, + OC2_NONE, + COLSHAPE_CYLINDER, + }, + { + ELEMTYPE_UNK0, + { 0x00000000, 0x00, 0x00 }, + { 0x00000010, 0x00, 0x00 }, + TOUCH_NONE | TOUCH_SFX_NORMAL, + BUMP_ON, + OCELEM_NONE, + }, + { 10, 30, 0, { 0, 0, 0 } }, +}; + +static InitChainEntry sInitChain[] = { + ICHAIN_F32(targetArrowOffset, 2000, ICHAIN_STOP), +}; +// #endregion + +EnItem00* CustomItem::Spawn(f32 posX, f32 posY, f32 posZ, s16 rot, s16 flags, s16 params, ActorFunc actionFunc, + ActorFunc drawFunc) { if (!gPlayState) { return nullptr; } Actor* actor = Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_ITEM00, posX, posY, posZ, flags, rot, params, - ITEM00_NONE, 0); + ITEM00_NONE, false); EnItem00* enItem00 = (EnItem00*)actor; if (actionFunc != NULL) { @@ -35,88 +56,104 @@ EnItem00* CustomCollectible::Spawn(f32 posX, f32 posY, f32 posZ, s16 rot, s16 fl return enItem00; } -void CustomCollectible_Init(Actor* actor, PlayState* play) { +void CustomItem_Init(Actor* actor, PlayState* play) { EnItem00* enItem00 = (EnItem00*)actor; - if (CUSTOM_ITEM_FLAGS & CustomCollectible::STOP_BOBBING) { + if (CUSTOM_ITEM_FLAGS & CustomItem::STOP_BOBBING) { actor->shape.yOffset = 1250.0f; } else { actor->shape.yOffset = (Math_SinS(actor->shape.rot.y) * 150.0f) + 1250.0f; } - if (CUSTOM_ITEM_FLAGS & CustomCollectible::HIDE_TILL_OVERHEAD) { + if (CUSTOM_ITEM_FLAGS & CustomItem::HIDE_TILL_OVERHEAD) { Actor_SetScale(actor, 0.0f); } else { Actor_SetScale(actor, 0.015f); } - if (CUSTOM_ITEM_FLAGS & CustomCollectible::KEEP_ON_PLAYER) { + if (CUSTOM_ITEM_FLAGS & CustomItem::KEEP_ON_PLAYER) { Math_Vec3f_Copy(&actor->world.pos, &GET_PLAYER(play)->actor.world.pos); } - if (CUSTOM_ITEM_FLAGS & CustomCollectible::TOSS_ON_SPAWN) { + if (CUSTOM_ITEM_FLAGS & CustomItem::TOSS_ON_SPAWN) { actor->velocity.y = 8.0f; actor->speedXZ = 2.0f; - actor->gravity = -1.4f; actor->world.rot.y = Rand_ZeroOne() * 40000.0f; } + if (CUSTOM_ITEM_FLAGS & CustomItem::TOSS_ON_SPAWN || CUSTOM_ITEM_FLAGS & CustomItem::ENABLE_GRAVITY) { + actor->gravity = -1.4f; + } + + Actor_ProcessInitChain(actor, sInitChain); + Collider_InitCylinder(play, &enItem00->collider); + Collider_SetCylinder(play, &enItem00->collider, actor, &sCylinderInit); enItem00->unk_15A = -1; } // By default this will just assume the GID was passed in as the rot z, if you want different functionality you should // override the draw -void CustomCollectible_Draw(Actor* actor, PlayState* play) { +void CustomItem_Draw(Actor* actor, PlayState* play) { Matrix_Scale(30.0f, 30.0f, 30.0f, MTXMODE_APPLY); GetItem_Draw(play, CUSTOM_ITEM_PARAM); } -void CustomCollectible_Update(Actor* actor, PlayState* play) { +// Once the item is touched we need to clear movement vars so the item doesn't sink in the players hands/above head +void CustomItem_ItemTouched(Actor* actor, PlayState* play) { + actor->speedXZ = 0.0f; + actor->velocity.y = 0.0f; + actor->gravity = 0.0f; + actor->shape.yOffset = 1250.0f; +} + +void CustomItem_Update(Actor* actor, PlayState* play) { EnItem00* enItem00 = (EnItem00*)actor; Player* player = GET_PLAYER(play); - if (!(CUSTOM_ITEM_FLAGS & CustomCollectible::STOP_SPINNING)) { + if (!(CUSTOM_ITEM_FLAGS & CustomItem::STOP_SPINNING)) { actor->shape.rot.y += 960; } - if (CUSTOM_ITEM_FLAGS & CustomCollectible::STOP_BOBBING) { - actor->shape.yOffset = 1250.0f; - } else { + if (!(CUSTOM_ITEM_FLAGS & CustomItem::STOP_BOBBING)) { actor->shape.yOffset = (Math_SinS(actor->shape.rot.y) * 150.0f) + 1250.0f; } - if (CUSTOM_ITEM_FLAGS & CustomCollectible::HIDE_TILL_OVERHEAD) { + if (CUSTOM_ITEM_FLAGS & CustomItem::HIDE_TILL_OVERHEAD) { Actor_SetScale(actor, 0.0f); } - if (CUSTOM_ITEM_FLAGS & CustomCollectible::KEEP_ON_PLAYER) { - actor->gravity = 0.0f; + if (CUSTOM_ITEM_FLAGS & CustomItem::KEEP_ON_PLAYER) { Math_Vec3f_Copy(&actor->world.pos, &GET_PLAYER(play)->actor.world.pos); } - if (CUSTOM_ITEM_FLAGS & CustomCollectible::KILL_ON_TOUCH) { + // Player range check accounting for goron rolling behavior. Matches EnItem00 range check. + bool playerInRangeOfPickup = (actor->xzDistToPlayer <= 30.0f) && (fabsf(actor->yDistToPlayer) <= fabsf(50.0f)); + + if (CUSTOM_ITEM_FLAGS & CustomItem::KILL_ON_TOUCH) { // Pretty self explanatory, if the player is within range, kill the actor and call the action function - if ((actor->xzDistToPlayer <= 50.0f) && (fabsf(actor->yDistToPlayer) <= fabsf(20.0f))) { + if (playerInRangeOfPickup) { if (enItem00->actionFunc != NULL) { enItem00->actionFunc(enItem00, play); - CUSTOM_ITEM_FLAGS |= CustomCollectible::CALLED_ACTION; + CUSTOM_ITEM_FLAGS |= CustomItem::CALLED_ACTION; } Actor_Kill(actor); } - } else if (CUSTOM_ITEM_FLAGS & CustomCollectible::GIVE_OVERHEAD) { + } else if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_OVERHEAD) { // If the item hasn't been picked up (unk_15A == -1) and the player is within range - if (enItem00->unk_15A == -1 && (actor->xzDistToPlayer <= 50.0f) && - (fabsf(actor->yDistToPlayer) <= fabsf(20.0f))) { + if (enItem00->unk_15A == -1 && playerInRangeOfPickup) { // Fire the action function if (enItem00->actionFunc != NULL) { enItem00->actionFunc(enItem00, play); - CUSTOM_ITEM_FLAGS |= CustomCollectible::CALLED_ACTION; + CUSTOM_ITEM_FLAGS |= CustomItem::CALLED_ACTION; } Sfx_PlaySfxCentered(NA_SE_SY_GET_ITEM); // Set the unk_15A to 15, this indicates the item has been picked up and will start the overhead animation enItem00->unk_15A = 15; - CUSTOM_ITEM_FLAGS |= CustomCollectible::STOP_BOBBING; - CUSTOM_ITEM_FLAGS |= CustomCollectible::KEEP_ON_PLAYER; + CUSTOM_ITEM_FLAGS |= CustomItem::STOP_BOBBING; + CUSTOM_ITEM_FLAGS |= CustomItem::KEEP_ON_PLAYER; + CustomItem_ItemTouched(actor, play); + // Move to player right away on this frame + Math_Vec3f_Copy(&actor->world.pos, &GET_PLAYER(play)->actor.world.pos); } // If the item has been picked up @@ -128,8 +165,7 @@ void CustomCollectible_Update(Actor* actor, PlayState* play) { enItem00->unk_15A--; // Account for the different heights of the player forms - f32 height = 45.0f; - // TODO: Check for adult? + f32 height = LINK_IS_ADULT ? 60.0f : 45.0f; // Bob the item up and down actor->world.pos.y += (height + (Math_SinS(enItem00->unk_15A * 15000) * (enItem00->unk_15A * 0.3f))); @@ -139,15 +175,20 @@ void CustomCollectible_Update(Actor* actor, PlayState* play) { if (enItem00->unk_15A == 0) { Actor_Kill(actor); } - } else if (CUSTOM_ITEM_FLAGS & CustomCollectible::GIVE_ITEM_CUTSCENE) { + } else if (CUSTOM_ITEM_FLAGS & CustomItem::GIVE_ITEM_CUTSCENE) { // If the item hasn't been picked up and the player is within range + if (!Actor_HasParent(actor, play) && enItem00->unk_15A == -1) { - Actor_OfferGetItem(actor, play, GI_SHIP, 50.0f, 20.0f); + Actor_OfferGetItem(actor, play, GI_SHIP, 50.0f, 80.0f); } else { if (enItem00->unk_15A == -1) { - CUSTOM_ITEM_FLAGS |= CustomCollectible::STOP_BOBBING; - CUSTOM_ITEM_FLAGS |= CustomCollectible::KEEP_ON_PLAYER; - CUSTOM_ITEM_FLAGS |= CustomCollectible::HIDE_TILL_OVERHEAD; + // actor->shape.yOffset = 1250.0f; + CUSTOM_ITEM_FLAGS |= CustomItem::STOP_BOBBING; + // Math_Vec3f_Copy(&actor->world.pos, &GET_PLAYER(play)->actor.world.pos); + CUSTOM_ITEM_FLAGS |= CustomItem::KEEP_ON_PLAYER; + // Actor_SetScale(actor, 0.0f); + CUSTOM_ITEM_FLAGS |= CustomItem::HIDE_TILL_OVERHEAD; + CustomItem_ItemTouched(actor, play); } // Begin incrementing the unk_15A, indicating the item has been picked up @@ -158,14 +199,14 @@ void CustomCollectible_Update(Actor* actor, PlayState* play) { // After the first 20 frames, show the item and call the action function if (enItem00->unk_15A == 20 && enItem00->actionFunc != NULL) { enItem00->actionFunc(enItem00, play); - CUSTOM_ITEM_FLAGS |= CustomCollectible::CALLED_ACTION; + CUSTOM_ITEM_FLAGS |= CustomItem::CALLED_ACTION; } // Override the bobbing animation to be a fixed height actor->shape.yOffset = 900.0f; Actor_SetScale(actor, 0.007f); - f32 height = 45.0f; - // TODO: Check for adult? + // Account for the different heights of the player forms + f32 height = LINK_IS_ADULT ? 60.0f : 45.0f; actor->world.pos.y += height; } @@ -186,11 +227,19 @@ void CustomCollectible_Update(Actor* actor, PlayState* play) { actor->speedXZ = 0.0f; } - Collider_UpdateCylinder(actor, &enItem00->collider); - CollisionCheck_SetAC(play, &play->colChkCtx, &enItem00->collider.base); + if (CUSTOM_ITEM_FLAGS & CustomItem::ABLE_TO_BOOMERANG) { + Collider_UpdateCylinder(actor, &enItem00->collider); + CollisionCheck_SetAC(play, &play->colChkCtx, &enItem00->collider.base); + } } -void CustomCollectible::RegisterHooks() { +void CustomItem_Destroy(Actor* actor, PlayState* play) { + EnItem00* enItem00 = (EnItem00*)actor; + + Collider_DestroyCylinder(play, &enItem00->collider); +} + +void CustomItem::RegisterHooks() { GameInteractor::Instance->RegisterGameHookForID( ACTOR_EN_ITEM00, [](void* actorRef, bool* should) { Actor* actor = (Actor*)actorRef; @@ -198,13 +247,16 @@ void CustomCollectible::RegisterHooks() { return; } - actor->init = CustomCollectible_Init; - actor->update = CustomCollectible_Update; - actor->draw = CustomCollectible_Draw; - actor->destroy = NULL; + actor->init = CustomItem_Init; + actor->update = CustomItem_Update; + actor->draw = CustomItem_Draw; + actor->destroy = CustomItem_Destroy; // Set the rotX/rotZ back to 0, the original values can be accessed from actor->home actor->world.rot.x = 0; actor->world.rot.z = 0; + actor->shape.rot.x = 0; + actor->shape.rot.y = 0; + actor->shape.rot.z = 0; }); } diff --git a/soh/soh/Enhancements/custom-collectible/CustomCollectible.h b/soh/soh/Enhancements/custom-item/CustomItem.h similarity index 75% rename from soh/soh/Enhancements/custom-collectible/CustomCollectible.h rename to soh/soh/Enhancements/custom-item/CustomItem.h index ff2549b0f..6292a124b 100644 --- a/soh/soh/Enhancements/custom-collectible/CustomCollectible.h +++ b/soh/soh/Enhancements/custom-item/CustomItem.h @@ -1,3 +1,6 @@ +#ifndef CUSTOM_ITEM_H +#define CUSTOM_ITEM_H + extern "C" { #include "z64actor.h" } @@ -5,9 +8,9 @@ extern "C" { #define CUSTOM_ITEM_FLAGS (actor->home.rot.x) #define CUSTOM_ITEM_PARAM (actor->home.rot.z) -namespace CustomCollectible { +namespace CustomItem { -enum CustomCollectibleFlags : int16_t { +enum CustomItemFlags : int16_t { KILL_ON_TOUCH = 1 << 0, // 0000 0000 0000 0001 GIVE_OVERHEAD = 1 << 1, // 0000 0000 0000 0010 GIVE_ITEM_CUTSCENE = 1 << 2, // 0000 0000 0000 0100 @@ -17,8 +20,12 @@ enum CustomCollectibleFlags : int16_t { STOP_SPINNING = 1 << 6, // 0000 0000 0100 0000 CALLED_ACTION = 1 << 7, // 0000 0000 1000 0000 TOSS_ON_SPAWN = 1 << 8, // 0000 0001 0000 0000 + ABLE_TO_BOOMERANG = 1 << 9, // 0000 0010 0000 0000 + ENABLE_GRAVITY = 1 << 10, // 0000 0100 0000 0000 }; void RegisterHooks(); EnItem00* Spawn(f32 posX, f32 posY, f32 posZ, s16 rot, s16 flags, s16 params, ActorFunc actionFunc = NULL, ActorFunc drawFunc = NULL); -}; // namespace CustomCollectible +}; // namespace CustomItem + +#endif // CUSTOM_ITEM_H diff --git a/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp b/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp index 987b93e1a..9009024cd 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp +++ b/soh/soh/Enhancements/custom-message/CustomMessageManager.cpp @@ -1,5 +1,7 @@ #include "CustomMessageManager.h" #include "CustomMessageInterfaceAddon.h" +#include "CustomMessageTypes.h" +#include "soh/Enhancements/game-interactor/GameInteractor.h" #include #include #include @@ -10,6 +12,8 @@ #include "soh/util.h" extern "C" { +#include "functions.h" + extern PlayState* gPlayState; } @@ -223,8 +227,20 @@ void CustomMessage::LoadIntoFont() { Font* font = &msgCtx->font; char* buffer = font->msgBuf; const int maxBufferSize = sizeof(font->msgBuf); + font->charTexBuf[0] = (type << 4) | position; - msgCtx->msgLength = font->msgLength = SohUtils::CopyStringToCharBuffer(GetEnglish(MF_RAW), buffer, maxBufferSize); + + std::string content = GetEnglish(MF_RAW); + switch (gSaveContext.language) { + case LANGUAGE_FRA: + content = GetFrench(MF_RAW); + break; + case LANGUAGE_GER: + content = GetGerman(MF_RAW); + break; + } + + msgCtx->msgLength = font->msgLength = SohUtils::CopyStringToCharBuffer(buffer, content, maxBufferSize); } CustomMessage CustomMessage::operator+(const CustomMessage& right) const { @@ -836,3 +852,21 @@ bool CustomMessageManager::AddCustomMessageTable(std::string tableID) { CustomMessageTable newMessageTable; return messageTables.emplace(tableID, newMessageTable).second; } + +void CustomMessageManager::SetActiveCustomMessage(CustomMessage message) { + activeCustomMessage = message; +} + +void CustomMessageManager::StartTextbox(CustomMessage message) { + activeCustomMessage = message; + + Message_StartTextbox(gPlayState, TEXT_CUSTOM_MESSAGE, &GET_PLAYER(gPlayState)->actor); +} + +void CustomMessageManager::RegisterHooks() { + GameInteractor::Instance->RegisterGameHookForID( + TEXT_CUSTOM_MESSAGE, [&](u16* textId, bool* loadFromMessageTable) { + *loadFromMessageTable = false; + activeCustomMessage.LoadIntoFont(); + }); +} diff --git a/soh/soh/Enhancements/custom-message/CustomMessageManager.h b/soh/soh/Enhancements/custom-message/CustomMessageManager.h index 9ae39e42a..c72fdc163 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageManager.h +++ b/soh/soh/Enhancements/custom-message/CustomMessageManager.h @@ -247,11 +247,15 @@ class CustomMessageManager { bool InsertCustomMessage(std::string tableID, uint16_t textID, CustomMessage message); + CustomMessage activeCustomMessage; + public: static CustomMessageManager* Instance; CustomMessageManager() = default; + void RegisterHooks(); + /** * @brief Formats the provided Custom Message Entry and inserts it into the table with the provided tableID, * with the provided giid (getItemID) as its key. This function also inserts the icon corresponding to @@ -310,6 +314,22 @@ class CustomMessageManager { * already exists.) */ bool AddCustomMessageTable(std::string tableID); + + /** + * @brief Sets the active custom message, which will be used the next time + * TEXT_CUSTOM_MESSAGE is used for a text box. + * + * @param message the message to set as active + */ + void SetActiveCustomMessage(CustomMessage message); + + /** + * @brief Displays a custom message in a textbox. This is the same as calling + * SetActiveCustomMessage and then beginning a textbox with TEXT_CUSTOM_MESSAGE. + * + * @param message the message to set as active + */ + void StartTextbox(CustomMessage message); }; class MessageNotFoundException : public std::exception { diff --git a/soh/soh/Enhancements/custom-message/CustomMessageTypes.h b/soh/soh/Enhancements/custom-message/CustomMessageTypes.h index 86d0d4856..f87c27a54 100644 --- a/soh/soh/Enhancements/custom-message/CustomMessageTypes.h +++ b/soh/soh/Enhancements/custom-message/CustomMessageTypes.h @@ -14,6 +14,7 @@ typedef enum { TEXT_SKULLTULA_PEOPLE_MAKE_YOU_VERY_RICH = 0x0027, TEXT_SKULLTULA_PEOPLE_CURSE_HAS_BEEN_BROKEN = 0x0028, TEXT_SKULLTULA_PEOPLE_SAVING_MY_KIDS = 0x0029, + TEXT_CUSTOM_MESSAGE = 0x004B, // Unused TEXT_ITEM_KEY_SMALL = 0x0060, TEXT_ITEM_DUNGEON_MAP = 0x0066, TEXT_CHEST_GAME_REAL_GAMBLER = 0x006E, diff --git a/soh/soh/Enhancements/game-interactor/GIEventQueue.cpp b/soh/soh/Enhancements/game-interactor/GIEventQueue.cpp new file mode 100644 index 000000000..0872ceded --- /dev/null +++ b/soh/soh/Enhancements/game-interactor/GIEventQueue.cpp @@ -0,0 +1,95 @@ +#include "GameInteractor.h" +#include "soh/Enhancements/custom-item/CustomItem.h" + +extern "C" { +#include "variables.h" +#include "macros.h" +#include "functions.h" + +extern SaveContext gSaveContext; +extern PlayState* gPlayState; +} + +void ProcessEvents() { + Player* player = GET_PLAYER(gPlayState); + + // If the player has a message active, stop + if (gPlayState->msgCtx.msgMode != 0) { + return; + } + + // If the player is in a blocking cutscene, stop + if (Player_InBlockingCsMode(gPlayState, player)) { + return; + } + + // If player is dead, stop + if (player->stateFlags1 & PLAYER_STATE1_DEAD) { + return; + } + + // If there is an event active, stop + const auto& currentEvent = GameInteractor::Instance->currentEvent; + if (auto e = std::get_if(¤tEvent)) { + // no-op + } else { + return; + } + + // If there are no events that need to happen, stop + if (GameInteractor::Instance->events.empty()) { + return; + } + + GameInteractor::Instance->currentEvent = GameInteractor::Instance->events.front(); + const auto& nextEvent = GameInteractor::Instance->currentEvent; + + if (auto e = std::get_if(&nextEvent)) { + EnItem00* enItem00; + // If the player is climbing or in the air, deliver the item without a cutscene but freeze the player + if (!e->showGetItemCutscene || + (player->stateFlags1 & + (PLAYER_STATE1_CHARGING_SPIN_ATTACK | PLAYER_STATE1_HANGING_OFF_LEDGE | PLAYER_STATE1_CLIMBING_LEDGE | + PLAYER_STATE1_JUMPING | PLAYER_STATE1_FREEFALL | PLAYER_STATE1_FIRST_PERSON | + PLAYER_STATE1_CLIMBING_LADDER | PLAYER_STATE1_IN_WATER)) || + (Player_GetExplosiveHeld(player) > -1)) { + enItem00 = CustomItem::Spawn( + player->actor.world.pos.x, player->actor.world.pos.y, player->actor.world.pos.z, 0, + CustomItem::GIVE_OVERHEAD | CustomItem::HIDE_TILL_OVERHEAD | CustomItem::KEEP_ON_PLAYER, e->param, + [](Actor* actor, PlayState* play) { + Player* player = GET_PLAYER(gPlayState); + const auto& nextEvent = GameInteractor::Instance->currentEvent; + if (auto e = std::get_if(&nextEvent)) { + e->giveItem(actor, play); + if (e->showGetItemCutscene) { + player->actor.freezeTimer = 30; + } + } + }, + e->drawItem); + } else { + enItem00 = CustomItem::Spawn( + player->actor.world.pos.x, player->actor.world.pos.y, player->actor.world.pos.z, 0, + CustomItem::GIVE_ITEM_CUTSCENE | CustomItem::HIDE_TILL_OVERHEAD | CustomItem::KEEP_ON_PLAYER, e->param, + e->giveItem, e->drawItem); + } + enItem00->actor.destroy = [](Actor* actor, PlayState* play) { + if (!(CUSTOM_ITEM_FLAGS & CustomItem::CALLED_ACTION)) { + // Event was not handled, requeue it + GameInteractor::Instance->events.push_back(GameInteractor::Instance->currentEvent); + } + + GameInteractor::Instance->currentEvent = GIEventNone{}; + }; + } + + GameInteractor::Instance->events.erase(GameInteractor::Instance->events.begin()); +} + +void GameInteractor::RegisterOwnHooks() { + // Cleanup all hooks at the start of each frame + GameInteractor::Instance->RegisterGameHook( + []() { GameInteractor::Instance->RemoveAllQueuedHooks(); }); + + GameInteractor::Instance->RegisterGameHook(ProcessEvents); +} diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor.h b/soh/soh/Enhancements/game-interactor/GameInteractor.h index 772fdca1e..4856a4178 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor.h @@ -109,6 +109,22 @@ void GameInteractor_SetTriforceHuntCreditsWarpActive(uint8_t state); #pragma message("Compiling without support, the Hook Debugger will not be available") #endif +struct GIEventNone {}; + +struct GIEventGiveItem { + // Whether or not to show the get item cutscene. If true and the player is in the air, the + // player will instead be frozen for a few seconds. If this is true you _must_ call + // CustomMessage::SetActiveCustomMessage in the giveItem function otherwise you'll just see a blank message. + bool showGetItemCutscene; + // Arbitrary s16 that can be accessed from within the give/draw functions with CUSTOM_ITEM_PARAM + s16 param; + // These are run in the context of an item00 actor. This isn't super important but can be useful in some cases + ActorFunc giveItem; + ActorFunc drawItem; +}; + +typedef std::variant GIEvent; + typedef uint32_t HOOK_ID; enum HookType { @@ -221,6 +237,8 @@ class GameInteractor { public: static GameInteractor* Instance; + void RegisterOwnHooks(); + // Game State class State { public: @@ -253,6 +271,10 @@ class GameInteractor { static GameInteractionEffectQueryResult ApplyEffect(GameInteractionEffectBase* effect); static GameInteractionEffectQueryResult RemoveEffect(RemovableGameInteractionEffect* effect); + // EventQueue + std::vector events = {}; + GIEvent currentEvent = GIEventNone(); + // Game Hooks HOOK_ID nextHookId = 1; diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h index f779001c1..8153734b5 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_HookTable.h @@ -17,6 +17,7 @@ DEFINE_HOOK(OnEquipmentDelete, (int16_t equipmentType, uint16_t equipValue)); DEFINE_HOOK(OnSaleEnd, (GetItemEntry itemEntry)); DEFINE_HOOK(OnTransitionEnd, (int16_t sceneNum)); DEFINE_HOOK(OnSceneInit, (int16_t sceneNum)); +DEFINE_HOOK(OnRoomInit, (int16_t roomNum)); DEFINE_HOOK(AfterSceneCommands, (int16_t sceneNum)); DEFINE_HOOK(OnSceneFlagSet, (int16_t sceneNum, int16_t flagType, int16_t flag)); DEFINE_HOOK(OnSceneFlagUnset, (int16_t sceneNum, int16_t flagType, int16_t flag)); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp index 87c59c071..ddac6b273 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.cpp @@ -23,9 +23,6 @@ void GameInteractor_ExecuteOnExitGame(int32_t fileNum) { } void GameInteractor_ExecuteOnGameStateMainStart() { - // Cleanup all hooks at the start of each frame - GameInteractor::Instance->RemoveAllQueuedHooks(); - GameInteractor::Instance->ExecuteHooks(); } @@ -60,6 +57,12 @@ void GameInteractor_ExecuteOnSceneInit(int16_t sceneNum) { GameInteractor::Instance->ExecuteHooksForFilter(sceneNum); } +void GameInteractor_ExecuteOnRoomInit(int16_t roomNum) { + GameInteractor::Instance->ExecuteHooks(roomNum); + GameInteractor::Instance->ExecuteHooksForID(roomNum, roomNum); + GameInteractor::Instance->ExecuteHooksForFilter(roomNum); +} + void GameInteractor_ExecuteAfterSceneCommands(int16_t sceneNum) { GameInteractor::Instance->ExecuteHooks(sceneNum); GameInteractor::Instance->ExecuteHooksForID(sceneNum, sceneNum); diff --git a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h index f3ef7c2a3..e095befff 100644 --- a/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h +++ b/soh/soh/Enhancements/game-interactor/GameInteractor_Hooks.h @@ -20,6 +20,7 @@ void GameInteractor_ExecuteOnEquipmentDelete(int16_t equipmentType, uint16_t equ void GameInteractor_ExecuteOnSaleEndHooks(GetItemEntry itemEntry); void GameInteractor_ExecuteOnTransitionEndHooks(int16_t sceneNum); void GameInteractor_ExecuteOnSceneInit(int16_t sceneNum); +void GameInteractor_ExecuteOnRoomInit(int16_t roomNum); void GameInteractor_ExecuteAfterSceneCommands(int16_t sceneNum); void GameInteractor_ExecuteOnSceneFlagSet(int16_t sceneNum, int16_t flagType, int16_t flag); void GameInteractor_ExecuteOnSceneFlagUnset(int16_t sceneNum, int16_t flagType, int16_t flag); diff --git a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h index 928565338..4af4fef0a 100644 --- a/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h +++ b/soh/soh/Enhancements/game-interactor/vanilla-behavior/GIVanillaBehavior.h @@ -1761,6 +1761,14 @@ typedef enum { // - `*ObjTsubo` VB_POT_DROP_ITEM, + // #### `result` + // ```c + // actor.params & 1) == ROCK_SMALL + // ``` + // #### `args` + // - `*EnIshi` + VB_ROCK_DROP_ITEM, + // #### `result` // ```c // true @@ -2252,6 +2260,16 @@ typedef enum { // - `*Actor` VB_RECIEVE_FALL_DAMAGE, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `*Actor` + // - `u8` (damageEffect) + // - `u8` (damage) + VB_APPLY_DAMAGE_TO_ACTOR, + // #### `result` // ```c // true diff --git a/soh/soh/Enhancements/randomizer/location_access.cpp b/soh/soh/Enhancements/randomizer/location_access.cpp index 0f517aa3f..738c311a2 100644 --- a/soh/soh/Enhancements/randomizer/location_access.cpp +++ b/soh/soh/Enhancements/randomizer/location_access.cpp @@ -946,7 +946,7 @@ std::string CleanCheckConditionString(std::string condition) { } namespace Regions { -auto GetAllRegions() { +std::array GetAllRegions() { static const size_t regionCount = RR_MAX - (RR_NONE + 1); static std::array allRegions = {}; diff --git a/soh/soh/Enhancements/randomizer/location_access.h b/soh/soh/Enhancements/randomizer/location_access.h index d55928eea..c567df781 100644 --- a/soh/soh/Enhancements/randomizer/location_access.h +++ b/soh/soh/Enhancements/randomizer/location_access.h @@ -4,6 +4,7 @@ #include #include #include +#include #include "soh/Enhancements/randomizer/randomizerTypes.h" #include "soh/Enhancements/randomizer/context.h" @@ -237,6 +238,7 @@ extern void AccessReset(); extern void ResetAllLocations(); extern bool HasTimePassAccess(uint8_t age); extern void DumpWorldGraph(std::string str); +extern std::array GetAllRegions(); } // namespace Regions void RegionTable_Init(); diff --git a/soh/soh/Network/Anchor/Anchor.cpp b/soh/soh/Network/Anchor/Anchor.cpp index 5852e62e0..755f40e59 100644 --- a/soh/soh/Network/Anchor/Anchor.cpp +++ b/soh/soh/Network/Anchor/Anchor.cpp @@ -40,6 +40,26 @@ void Anchor::OnDisconnected() { RegisterHooks(); } +void Anchor::ProcessOutgoingPackets() { + // Copy all queued packets while holding the lock, then send them after releasing + std::queue packetsToSend; + { + std::lock_guard lock(outgoingPacketQueueMutex); + packetsToSend.swap(outgoingPacketQueue); + } + + // Send packets without holding the lock + while (!packetsToSend.empty()) { + nlohmann::json payload = packetsToSend.front(); + packetsToSend.pop(); + + if (!payload.contains("quiet")) { + SPDLOG_DEBUG("[Anchor] Sending payload:\n{}", payload.dump()); + } + Network::SendJsonToRemote(payload); + } +} + void Anchor::SendJsonToRemote(nlohmann::json payload) { if (!isConnected) { return; @@ -47,9 +67,17 @@ void Anchor::SendJsonToRemote(nlohmann::json payload) { payload["clientId"] = ownClientId; if (!payload.contains("quiet")) { - SPDLOG_DEBUG("[Anchor] Sending payload:\n{}", payload.dump()); + SPDLOG_DEBUG("[Anchor] Queuing payload:\n{}", payload.dump()); } - Network::SendJsonToRemote(payload); + + if (payload["type"] == HANDSHAKE) { + Network::SendJsonToRemote(payload); + return; + } + + // Queue the packet to be sent on the network thread + std::lock_guard lock(outgoingPacketQueueMutex); + outgoingPacketQueue.push(payload); } void Anchor::OnIncomingJson(nlohmann::json payload) { @@ -87,11 +115,17 @@ void Anchor::OnIncomingJson(nlohmann::json payload) { } void Anchor::ProcessIncomingPacketQueue() { - std::lock_guard lock(incomingPacketQueueMutex); + // Copy all queued packets while holding the lock, then process them after releasing + std::queue packetsToProcess; + { + std::lock_guard lock(incomingPacketQueueMutex); + packetsToProcess.swap(incomingPacketQueue); + } - while (!incomingPacketQueue.empty()) { - nlohmann::json payload = incomingPacketQueue.front(); - incomingPacketQueue.pop(); + // Process packets without holding the lock + while (!packetsToProcess.empty()) { + nlohmann::json payload = packetsToProcess.front(); + packetsToProcess.pop(); std::string packetType = payload["type"].get(); diff --git a/soh/soh/Network/Anchor/Anchor.h b/soh/soh/Network/Anchor/Anchor.h index 9eb103230..cce5e397a 100644 --- a/soh/soh/Network/Anchor/Anchor.h +++ b/soh/soh/Network/Anchor/Anchor.h @@ -76,6 +76,8 @@ class Anchor : public Network { bool isProcessingIncomingPacket = false; std::queue incomingPacketQueue; std::mutex incomingPacketQueueMutex; + std::queue outgoingPacketQueue; + std::mutex outgoingPacketQueueMutex; nlohmann::json PrepClientState(); nlohmann::json PrepRoomState(); @@ -143,6 +145,7 @@ class Anchor : public Network { void OnIncomingJson(nlohmann::json payload); void OnConnected(); void OnDisconnected(); + void ProcessOutgoingPackets(); void DrawMenu(); void ProcessIncomingPacketQueue(); void SendJsonToRemote(nlohmann::json packet); diff --git a/soh/soh/Network/Network.cpp b/soh/soh/Network/Network.cpp index d9edb5550..6eae1f3f1 100644 --- a/soh/soh/Network/Network.cpp +++ b/soh/soh/Network/Network.cpp @@ -46,6 +46,9 @@ void Network::OnConnected() { void Network::OnDisconnected() { } +void Network::ProcessOutgoingPackets() { +} + void Network::SendDataToRemote(const char* payload) { #ifdef ENABLE_REMOTE_CONTROL SPDLOG_DEBUG("[Network] Sending data: {}", payload); @@ -68,6 +71,7 @@ void Network::ReceiveFromServer() { if (networkSocket) { isConnected = true; + receivedData.clear(); SPDLOG_INFO("[Network] Connection to server established!"); OnConnected(); @@ -90,7 +94,11 @@ void Network::ReceiveFromServer() { break; } + // Always process outgoing packets + ProcessOutgoingPackets(); + if (socketsReady == 0) { + // No incoming data continue; } @@ -119,9 +127,15 @@ void Network::ReceiveFromServer() { } } + if (socketSet) { + SDLNet_FreeSocketSet(socketSet); + } + if (isConnected) { SDLNet_TCP_Close(networkSocket); + networkSocket = nullptr; isConnected = false; + receivedData.clear(); OnDisconnected(); SPDLOG_INFO("[Network] Ending receiving thread..."); } diff --git a/soh/soh/Network/Network.h b/soh/soh/Network/Network.h index a04ece71e..f1ec42c02 100644 --- a/soh/soh/Network/Network.h +++ b/soh/soh/Network/Network.h @@ -44,6 +44,7 @@ class Network { virtual void OnIncomingJson(nlohmann::json payload); virtual void OnConnected(); virtual void OnDisconnected(); + virtual void ProcessOutgoingPackets(); void SendDataToRemote(const char* payload); virtual void SendJsonToRemote(nlohmann::json packet); }; diff --git a/soh/soh/OTRGlobals.cpp b/soh/soh/OTRGlobals.cpp index 8b0cce20c..612158582 100644 --- a/soh/soh/OTRGlobals.cpp +++ b/soh/soh/OTRGlobals.cpp @@ -82,7 +82,6 @@ #include "Enhancements/mods.h" #include "Enhancements/game-interactor/GameInteractor.h" #include "Enhancements/randomizer/draw.h" -#include "Enhancements/custom-collectible/CustomCollectible.h" #include #include #include @@ -129,6 +128,7 @@ #include "soh/config/ConfigUpdaters.h" #include "soh/ShipInit.hpp" +#include "soh/Enhancements/custom-item/CustomItem.h" extern "C" { #include "src/overlays/actors/ovl_En_Dns/z_en_dns.h" @@ -769,9 +769,9 @@ extern "C" void VanillaItemTable_Init() { GET_ITEM(ITEM_NUT_UPGRADE_30, OBJECT_GI_NUTS, GID_NUTS, 0xA7, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_LESSER, MOD_NONE, GI_NUT_UPGRADE_30), GET_ITEM(ITEM_NUT_UPGRADE_40, OBJECT_GI_NUTS, GID_NUTS, 0xA8, 0x80, CHEST_ANIM_SHORT, ITEM_CATEGORY_LESSER, MOD_NONE, GI_NUT_UPGRADE_40), GET_ITEM(ITEM_BULLET_BAG_50, OBJECT_GI_DEKUPOUCH, GID_BULLET_BAG_50, 0x6C, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_LESSER, MOD_NONE, GI_BULLET_BAG_50), - GET_ITEM(ITEM_SHIP, OBJECT_UNSET_16E, GID_MAXIMUM, 0x00, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_LESSER, MOD_NONE, GI_SHIP), GET_ITEM_NONE, GET_ITEM_NONE, + GET_ITEM(ITEM_SHIP, OBJECT_UNSET_16E, GID_MAXIMUM,TEXT_CUSTOM_MESSAGE, 0x80, CHEST_ANIM_LONG, ITEM_CATEGORY_JUNK, MOD_NONE, GI_SHIP), GET_ITEM_NONE // GI_MAX - if you need to add to this table insert it before this entry. // clang-format on }; @@ -1282,6 +1282,22 @@ extern "C" void InitOTR(int argc, char* argv[]) { conf->RegisterVersionUpdater(std::make_shared()); conf->RunVersionUpdates(); + CVarRegisterInteger(CVAR_SETTING("AltAssets"), 1); + CVarRegisterInteger(CVAR_GENERAL("LetItSnow"), 1); + CVarRegisterInteger("gHoliday.Visual.SnowingWeather", 1); + CVarRegisterInteger("gHoliday.Visual.Hats", 1); + CVarRegisterInteger("gHoliday.Gameplay.Snowballs", 1); + CVarRegisterInteger(CVAR_COSMETIC("Hud.AButton.Changed"), 1); + CVarRegisterColor(CVAR_COSMETIC("Hud.AButton.Value"), Color_RGBA8{ 255, 255, 255, 255 }); + CVarRegisterInteger(CVAR_COSMETIC("Hud.BButton.Changed"), 1); + CVarRegisterColor(CVAR_COSMETIC("Hud.BButton.Value"), Color_RGBA8{ 255, 255, 255, 255 }); + CVarRegisterInteger(CVAR_COSMETIC("Hud.CButtons.Changed"), 1); + CVarRegisterColor(CVAR_COSMETIC("Hud.CButtons.Value"), Color_RGBA8{ 255, 255, 255, 255 }); + CVarRegisterInteger(CVAR_COSMETIC("Consumable.Hearts.Changed"), 1); + CVarRegisterColor(CVAR_COSMETIC("Consumable.Hearts.Value"), Color_RGBA8{ 255, 158, 0, 255 }); + CVarRegisterInteger(CVAR_COSMETIC("Consumable.Magic.Changed"), 1); + CVarRegisterColor(CVAR_COSMETIC("Consumable.Magic.Value"), Color_RGBA8{ 255, 0, 0, 255 }); + SohGui::SetupGuiElements(); ShipInit::InitAll(); @@ -1309,9 +1325,11 @@ extern "C" void InitOTR(int argc, char* argv[]) { OTRExtScanner(); VanillaItemTable_Init(); DebugConsole_Init(); + CustomMessageManager::Instance->RegisterHooks(); + GameInteractor::Instance->RegisterOwnHooks(); + CustomItem::RegisterHooks(); InitMods(); - CustomCollectible::RegisterHooks(); ActorDB::AddBuiltInCustomActors(); // #region SOH [Randomizer] TODO: Remove these and refactor spoiler file handling for randomizer CVarClear(CVAR_GENERAL("RandomizerNewFileDropped")); @@ -1324,23 +1342,6 @@ extern "C" void InitOTR(int argc, char* argv[]) { time_t now = time(NULL); tm* tm_now = localtime(&now); - // if (tm_now->tm_mon == 11 && tm_now->tm_mday >= 24 && tm_now->tm_mday <= 25) { - // CVarRegisterInteger(CVAR_GENERAL("LetItSnow"), 1); - // } else { - // CVarClear(CVAR_GENERAL("LetItSnow")); - // } - - CVarRegisterInteger(CVAR_GENERAL("LetItSnow"), 1); - CVarRegisterInteger(CVAR_COSMETIC("Hud.AButton.Changed"), 1); - CVarRegisterColor(CVAR_COSMETIC("Hud.AButton.Value"), Color_RGBA8{ 255, 255, 255, 255 }); - CVarRegisterInteger(CVAR_COSMETIC("Hud.BButton.Changed"), 1); - CVarRegisterColor(CVAR_COSMETIC("Hud.BButton.Value"), Color_RGBA8{ 255, 255, 255, 255 }); - CVarRegisterInteger(CVAR_COSMETIC("Hud.CButtons.Changed"), 1); - CVarRegisterColor(CVAR_COSMETIC("Hud.CButtons.Value"), Color_RGBA8{ 255, 255, 255, 255 }); - CVarRegisterInteger(CVAR_COSMETIC("Consumable.Hearts.Changed"), 1); - CVarRegisterColor(CVAR_COSMETIC("Consumable.Hearts.Value"), Color_RGBA8{ 255, 158, 0, 255 }); - CVarRegisterInteger(CVAR_COSMETIC("Consumable.Magic.Changed"), 1); - CVarRegisterColor(CVAR_COSMETIC("Consumable.Magic.Value"), Color_RGBA8{ 255, 0, 0, 255 }); srand(now); #ifdef ENABLE_REMOTE_CONTROL diff --git a/soh/soh/SaveManager.cpp b/soh/soh/SaveManager.cpp index f0d6e06c4..07ef1f9c8 100644 --- a/soh/soh/SaveManager.cpp +++ b/soh/soh/SaveManager.cpp @@ -114,6 +114,7 @@ SaveManager::SaveManager() { coreSectionIDsByName["scenes"] = SECTION_ID_SCENES; coreSectionIDsByName["trackerData"] = SECTION_ID_TRACKER_DATA; coreSectionIDsByName["archipelagoData"] = SECTION_ID_ARCHIPELAGO; + coreSectionIDsByName["rogueLike"] = SECTION_ID_ROGUELIKE; AddLoadFunction("base", 1, LoadBaseVersion1); AddLoadFunction("base", 2, LoadBaseVersion2); AddLoadFunction("base", 3, LoadBaseVersion3); @@ -122,6 +123,8 @@ SaveManager::SaveManager() { AddLoadFunction("randomizer", 1, LoadRandomizer); AddSaveFunction("randomizer", 1, SaveRandomizer, true, SECTION_PARENT_NONE); + AddLoadFunction("rogueLike", 1, LoadRogueLike); + AddSaveFunction("rogueLike", 1, SaveRogueLike, true, SECTION_PARENT_NONE); AddInitFunction(InitFileImpl); @@ -412,6 +415,36 @@ void SaveManager::SaveRandomizer(SaveContext* saveContext, int sectionID, bool f }); } +void SaveManager::LoadRogueLike() { + if (gSaveContext.ship.quest.id != QUEST_ROGUELIKE) { + return; + } + + SaveManager::Instance->LoadData("difficulty", gSaveContext.ship.quest.data.rogueLike.difficulty); + SaveManager::Instance->LoadData("lastActivity", gSaveContext.ship.quest.data.rogueLike.lastActivity); + SaveManager::Instance->LoadData("xp", gSaveContext.ship.quest.data.rogueLike.xp); + + SaveManager::Instance->LoadArray("stats", ARRAY_COUNT(gSaveContext.ship.quest.data.rogueLike.stats), [&](size_t i) { + u32 value = 0; + SaveManager::Instance->LoadData("", value); + gSaveContext.ship.quest.data.rogueLike.stats[i] = value; + }); +} + +void SaveManager::SaveRogueLike(SaveContext* saveContext, int sectionID, bool fullSave) { + if (saveContext->ship.quest.id != QUEST_ROGUELIKE) { + return; + } + + SaveManager::Instance->SaveData("difficulty", saveContext->ship.quest.data.rogueLike.difficulty); + SaveManager::Instance->SaveData("lastActivity", saveContext->ship.quest.data.rogueLike.lastActivity); + SaveManager::Instance->SaveData("xp", saveContext->ship.quest.data.rogueLike.xp); + + SaveManager::Instance->SaveArray("stats", ARRAY_COUNT(saveContext->ship.quest.data.rogueLike.stats), [&](size_t i) { + SaveManager::Instance->SaveData("", saveContext->ship.quest.data.rogueLike.stats[i]); + }); +} + // Init() here is an extension of InitSram, and thus not truly an initializer for SaveManager itself. don't put any // class initialization stuff here void SaveManager::Init() { @@ -623,6 +656,11 @@ void SaveManager::InitMeta(int fileNum) { fileMetaInfo[fileNum].requiresOriginal = !IS_MASTER_QUEST && (!IS_RANDO || randoContext->GetDungeons()->CountMQ() < 12); + if (IS_ROGUELIKE) { // IDK + fileMetaInfo[fileNum].requiresMasterQuest = false; + fileMetaInfo[fileNum].requiresOriginal = false; + } + fileMetaInfo[fileNum].buildVersionMajor = gSaveContext.ship.stats.buildVersionMajor; fileMetaInfo[fileNum].buildVersionMinor = gSaveContext.ship.stats.buildVersionMinor; fileMetaInfo[fileNum].buildVersionPatch = gSaveContext.ship.stats.buildVersionPatch; @@ -2148,6 +2186,9 @@ void SaveManager::LoadBaseVersion4() { SaveManager::Instance->LoadData("dogParams", gSaveContext.dogParams); SaveManager::Instance->LoadData("filenameLanguage", gSaveContext.ship.filenameLanguage); SaveManager::Instance->LoadData("maskMemory", gSaveContext.ship.maskMemory); + + // Ugh.. + SaveManager::Instance->LoadData("questId", gSaveContext.ship.quest.id); } void SaveManager::SaveBase(SaveContext* saveContext, int sectionID, bool fullSave) { @@ -2316,6 +2357,9 @@ void SaveManager::SaveBase(SaveContext* saveContext, int sectionID, bool fullSav SaveManager::Instance->SaveData("dogParams", saveContext->dogParams); SaveManager::Instance->SaveData("filenameLanguage", saveContext->ship.filenameLanguage); SaveManager::Instance->SaveData("maskMemory", saveContext->ship.maskMemory); + + // Ugh.. + SaveManager::Instance->SaveData("questId", gSaveContext.ship.quest.id); } // Load a string into a char array based on size and ensuring it is null terminated when overflowed diff --git a/soh/soh/SaveManager.h b/soh/soh/SaveManager.h index 5ec76ce01..a3639b9cd 100644 --- a/soh/soh/SaveManager.h +++ b/soh/soh/SaveManager.h @@ -174,6 +174,8 @@ class SaveManager { static void LoadRandomizer(); static void SaveRandomizer(SaveContext* saveContext, int sectionID, bool fullSave); + static void LoadRogueLike(); + static void SaveRogueLike(SaveContext* saveContext, int sectionID, bool fullSave); static void LoadBaseVersion1(); static void LoadBaseVersion2(); diff --git a/soh/soh/SohGui/SohMenu.cpp b/soh/soh/SohGui/SohMenu.cpp index 9300f44bb..5852ad1e2 100644 --- a/soh/soh/SohGui/SohMenu.cpp +++ b/soh/soh/SohGui/SohMenu.cpp @@ -87,6 +87,8 @@ void SohMenu::InitElement() { AddMenuEnhancements(); AddMenuRandomizer(); AddMenuEntry("Holiday", CVAR_SETTING("Menu.HolidaySidebarSection")); + AddSidebarEntry("Holiday", "Gameplay", 2); + AddSidebarEntry("Holiday", "Visual", 2); AddMenuNetwork(); AddMenuDevTools(); diff --git a/soh/soh/z_scene_otr.cpp b/soh/soh/z_scene_otr.cpp index 1da9d729a..bdfee23da 100644 --- a/soh/soh/z_scene_otr.cpp +++ b/soh/soh/z_scene_otr.cpp @@ -517,6 +517,7 @@ extern "C" s32 OTRfunc_8009728C(PlayState* play, RoomContext* roomCtx, s32 roomN roomCtx->unk_30 ^= 1; SPDLOG_INFO("Room Init - curRoom.num: {0:#x}", roomCtx->curRoom.num); + GameInteractor_ExecuteOnRoomInit(roomCtx->curRoom.num); return 1; } diff --git a/soh/src/code/z_actor.c b/soh/src/code/z_actor.c index 451a75a9c..b83edfc10 100644 --- a/soh/src/code/z_actor.c +++ b/soh/src/code/z_actor.c @@ -3353,7 +3353,7 @@ Actor* Actor_Spawn(ActorContext* actorCtx, PlayState* play, s16 actorId, f32 pos objBankIndex = Object_GetIndex(&gPlayState->objectCtx, dbEntry->objectId); if (objBankIndex < 0 && (!gMapLoading || CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || - CVarGetInteger("gHoliday.Caladius.Blitz.Enabled", 0))) { + CVarGetInteger("gHoliday.Gameplay.Blitz.Enabled", 0))) { objBankIndex = 0; } diff --git a/soh/src/code/z_collision_check.c b/soh/src/code/z_collision_check.c index aa466e412..af5e6585e 100644 --- a/soh/src/code/z_collision_check.c +++ b/soh/src/code/z_collision_check.c @@ -2,6 +2,7 @@ #include "vt.h" #include "overlays/effects/ovl_Effect_Ss_HitMark/z_eff_ss_hitmark.h" #include "soh/Enhancements/game-interactor/GameInteractor.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include typedef s32 (*ColChkResetFunc)(PlayState*, Collider*); @@ -3030,6 +3031,13 @@ void CollisionCheck_ApplyDamage(PlayState* play, CollisionCheckContext* colChkCt if (CVarGetInteger(CVAR_ENHANCEMENT("IvanCoopModeEnabled"), 0)) { collider->actor->colChkInfo.damage *= GET_PLAYER(play)->ivanDamageMultiplier; } + + if (!GameInteractor_Should(VB_APPLY_DAMAGE_TO_ACTOR, true, collider->actor, + collider->actor->colChkInfo.damageEffect, collider->actor->colChkInfo.damage, + info->acHitInfo->toucher.dmgFlags)) { + collider->actor->colChkInfo.damageEffect = 0; + collider->actor->colChkInfo.damage = 0; + } } /** diff --git a/soh/src/code/z_draw.c b/soh/src/code/z_draw.c index 66ea16878..67cca0267 100644 --- a/soh/src/code/z_draw.c +++ b/soh/src/code/z_draw.c @@ -399,9 +399,10 @@ DrawItemTableEntry sDrawItemTable[] = { * Calls the corresponding draw function for the given draw ID */ void GetItem_Draw(PlayState* play, s16 drawId) { - if (drawId < 0 || drawId >= GID_MAXIMUM) { + // SoH [Enhancements] Prevent any UB here, GID_MAXIMUM useful for overriding GI draws + if (drawId < 0 || drawId >= GID_MAXIMUM) return; - } + sDrawItemTable[drawId].drawFunc(play, drawId); } diff --git a/soh/src/code/z_message_PAL.c b/soh/src/code/z_message_PAL.c index 7a59e4409..e698dd09a 100644 --- a/soh/src/code/z_message_PAL.c +++ b/soh/src/code/z_message_PAL.c @@ -2716,6 +2716,8 @@ void Message_OpenText(PlayState* play, u16 textId) { bool loadFromMessageTable = true; GameInteractor_ExecuteOnOpenText(&textId, &loadFromMessageTable); + sDisplayNextMessageAsEnglish = false; + if (msgCtx->msgMode == MSGMODE_NONE) { gSaveContext.unk_13EE = gSaveContext.unk_13EA; } diff --git a/soh/src/code/z_parameter.c b/soh/src/code/z_parameter.c index 0e9373b43..a94134ae3 100644 --- a/soh/src/code/z_parameter.c +++ b/soh/src/code/z_parameter.c @@ -1873,11 +1873,9 @@ u8 Return_Item(u8 itemID, ModIndex modId, ItemID returnItem) { * @return u8 */ u8 Item_Give(PlayState* play, u8 item) { - // TODO: Add ShouldItemGive - // if (!GameInteractor_ShouldItemGive(item) || item == ITEM_SHIP) { - if (item == ITEM_SHIP) { + // SoH [Enhancements] Ignore ITEM_SHIP, used for CustomItem + if (item == ITEM_SHIP) return ITEM_NONE; - } // prevents getting sticks without the bag in case something got missed if (IS_RANDO && (item == ITEM_STICK || item == ITEM_STICKS_5 || item == ITEM_STICKS_10) && @@ -2479,10 +2477,9 @@ u8 Item_CheckObtainability(u8 item) { s16 slot = SLOT(item); s32 temp; - // SOH [Enhancements] Added to enable custom item gives - if (item == ITEM_SHIP) { + // SoH [Enhancements] Ignore ITEM_SHIP, used for CustomItem + if (item == ITEM_SHIP) return ITEM_NONE; - } if (item >= ITEM_STICKS_5) { slot = SLOT(sExtraItemBases[item - ITEM_STICKS_5]); diff --git a/soh/src/code/z_play.c b/soh/src/code/z_play.c index eb999d72d..5ebe23fb8 100644 --- a/soh/src/code/z_play.c +++ b/soh/src/code/z_play.c @@ -1603,6 +1603,10 @@ void Play_Draw(PlayState* play) { if ((HREG(80) != 10) || (HREG(88) != 0)) { if (play->envCtx.sandstormState != SANDSTORM_OFF) { Environment_DrawSandstorm(play, play->envCtx.sandstormState); + } else if (CVarGetInteger("gHoliday.Visual.SnowingWeatherActive", 0) == 1) { + Environment_DrawSandstorm(play, SANDSTORM_DISSIPATE); + } else if (CVarGetInteger("gHoliday.Visual.SnowingWeatherActive", 0) == 2) { + Environment_DrawSandstorm(play, SANDSTORM_ACTIVE); } } @@ -1699,7 +1703,7 @@ time_t Play_GetRealTime() { void Play_Main(GameState* thisx) { PlayState* play = (PlayState*)thisx; - if (play->envCtx.unk_EE[2] == 0 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (play->envCtx.unk_EE[2] == 0 && CVarGetInteger("gHoliday.Visual.SnowingWeather", 0)) { play->envCtx.unk_EE[3] = 64; Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_OBJECT_KANKYO, 0, 0, 0, 0, 0, 0, 3, 0); } diff --git a/soh/src/code/z_player_lib.c b/soh/src/code/z_player_lib.c index 9c6dbc182..4a1845c4c 100644 --- a/soh/src/code/z_player_lib.c +++ b/soh/src/code/z_player_lib.c @@ -1814,7 +1814,7 @@ void Player_PostLimbDrawGameplay(PlayState* play, s32 limbIndex, Gfx** dList, Ve Matrix_MultVec3f(&sZeroVec, D_80160000); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0) && !(this->stateFlags1 & PLAYER_STATE1_FIRST_PERSON) && + if (CVarGetInteger("gHoliday.Visual.Hats", 0) && !(this->stateFlags1 & PLAYER_STATE1_FIRST_PERSON) && !(this->stateFlags2 & PLAYER_STATE2_CRAWLING)) { if (limbIndex == PLAYER_LIMB_HEAD) { OPEN_DISPS(play->state.gfxCtx); 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 daffc865e..bc145eca3 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 @@ -1359,7 +1359,7 @@ void BossDodongo_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s } Collider_UpdateSpheres(limbIndex, &this->collider); - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 7) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); 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 01fb2b855..95549b09a 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 @@ -2650,7 +2650,7 @@ void BossGanon2_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* } } - if (CVarGetInteger("gLetItSnow", 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 16) { Matrix_Push(); Matrix_RotateZYX(5977, 4649, 18154, MTXMODE_APPLY); @@ -2782,7 +2782,7 @@ void BossGanon2_PostLimbDraw2(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s Matrix_MultVec3f(&D_80907164, &this->unk_1B8); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 11) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); 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 15874fe87..a67636d06 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 @@ -1436,7 +1436,7 @@ void BossGanondrof_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec Matrix_MultVec3f(&zeroVec, &this->bodyPartsPos[limbIndex - 1]); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0) && this->deathState == NOT_DEAD) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0) && this->deathState == NOT_DEAD) { if (limbIndex == 15) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Am/z_en_am.c b/soh/src/overlays/actors/ovl_En_Am/z_en_am.c index ea191f249..e00d12926 100644 --- a/soh/src/overlays/actors/ovl_En_Am/z_en_am.c +++ b/soh/src/overlays/actors/ovl_En_Am/z_en_am.c @@ -956,7 +956,7 @@ void EnAm_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, EnAm_TransformSwordHitbox(&this->dyna.actor, play); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 4) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Ani/z_en_ani.c b/soh/src/overlays/actors/ovl_En_Ani/z_en_ani.c index 98efae143..9ee8ff570 100644 --- a/soh/src/overlays/actors/ovl_En_Ani/z_en_ani.c +++ b/soh/src/overlays/actors/ovl_En_Ani/z_en_ani.c @@ -320,7 +320,7 @@ void EnAni_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Matrix_MultVec3f(&sMultVec, &this->actor.focus.pos); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 15) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Bb/z_en_bb.c b/soh/src/overlays/actors/ovl_En_Bb/z_en_bb.c index 1f764de70..764042be9 100644 --- a/soh/src/overlays/actors/ovl_En_Bb/z_en_bb.c +++ b/soh/src/overlays/actors/ovl_En_Bb/z_en_bb.c @@ -1274,7 +1274,7 @@ void EnBb_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, BodyBreak_SetInfo(&this->bodyBreak, limbIndex, 4, 15, 15, dList, BODYBREAK_OBJECT_DEFAULT); - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 15) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Bom_Chu/z_en_bom_chu.c b/soh/src/overlays/actors/ovl_En_Bom_Chu/z_en_bom_chu.c index 99487e337..8e8c1b8fa 100644 --- a/soh/src/overlays/actors/ovl_En_Bom_Chu/z_en_bom_chu.c +++ b/soh/src/overlays/actors/ovl_En_Bom_Chu/z_en_bom_chu.c @@ -528,7 +528,7 @@ void EnBomChu_Draw(Actor* thisx, PlayState* play) { gSPMatrix(POLY_OPA_DISP++, MATRIX_NEWMTX(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); gSPDisplayList(POLY_OPA_DISP++, gBombchuDL); - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { Matrix_Push(); Matrix_RotateZYX(0, -3100, 17047, MTXMODE_APPLY); Matrix_Translate(445.946f, -27.027f, 608.108f, MTXMODE_APPLY); 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 f52b4a458..aca91bb2f 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 @@ -674,7 +674,7 @@ void EnBox_UpdateSizeAndTexture(EnBox* this, PlayState* play) { } } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0) && hasChristmasChestTexturesAvailable && + if (CVarGetInteger("gHoliday.Visual.PresentChests", 0) && hasChristmasChestTexturesAvailable && hasCreatedRandoChestTextures && !hasCustomChestDLs) { if (this->dyna.actor.scale.x == 0.01f) { this->boxBodyDL = gChristmasRedTreasureChestChestFrontDL; diff --git a/soh/src/overlays/actors/ovl_En_Cs/z_en_cs.c b/soh/src/overlays/actors/ovl_En_Cs/z_en_cs.c index 86fa46e42..331faa589 100644 --- a/soh/src/overlays/actors/ovl_En_Cs/z_en_cs.c +++ b/soh/src/overlays/actors/ovl_En_Cs/z_en_cs.c @@ -524,7 +524,7 @@ void EnCs_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Matrix_Get(&this->spookyMaskMtx); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 15) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Daiku_Kakariko/z_en_daiku_kakariko.c b/soh/src/overlays/actors/ovl_En_Daiku_Kakariko/z_en_daiku_kakariko.c index dfc217053..33ad1c757 100644 --- a/soh/src/overlays/actors/ovl_En_Daiku_Kakariko/z_en_daiku_kakariko.c +++ b/soh/src/overlays/actors/ovl_En_Daiku_Kakariko/z_en_daiku_kakariko.c @@ -539,7 +539,7 @@ void EnDaikuKakariko_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, V gSPDisplayList(POLY_OPA_DISP++, carpenterHeadDLists[this->actor.params & 3]); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 15) { Matrix_Push(); switch (this->actor.params) { diff --git a/soh/src/overlays/actors/ovl_En_Dekubaba/z_en_dekubaba.c b/soh/src/overlays/actors/ovl_En_Dekubaba/z_en_dekubaba.c index 1c354c59c..81af2e1b6 100644 --- a/soh/src/overlays/actors/ovl_En_Dekubaba/z_en_dekubaba.c +++ b/soh/src/overlays/actors/ovl_En_Dekubaba/z_en_dekubaba.c @@ -1278,7 +1278,7 @@ void EnDekubaba_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* Collider_UpdateSpheres(limbIndex, &this->collider); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 4) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c b/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c index b3d80414d..d9cea2e79 100644 --- a/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c +++ b/soh/src/overlays/actors/ovl_En_Dns/z_en_dns.c @@ -494,7 +494,7 @@ void EnDns_Update(Actor* thisx, PlayState* play) { void EnDns_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { EnDns* this = (EnDns*)thisx; - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 17) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Dodongo/z_en_dodongo.c b/soh/src/overlays/actors/ovl_En_Dodongo/z_en_dodongo.c index 3efe190fd..5a9932be1 100644 --- a/soh/src/overlays/actors/ovl_En_Dodongo/z_en_dodongo.c +++ b/soh/src/overlays/actors/ovl_En_Dodongo/z_en_dodongo.c @@ -916,7 +916,7 @@ void EnDodongo_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* } } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 7) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c b/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c index 5cc221f09..b7a0190a6 100644 --- a/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c +++ b/soh/src/overlays/actors/ovl_En_Dog/z_en_dog.c @@ -497,7 +497,7 @@ s32 EnDog_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f* p } void EnDog_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 4) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Ds/z_en_ds.c b/soh/src/overlays/actors/ovl_En_Ds/z_en_ds.c index 6374a1b88..31980667c 100644 --- a/soh/src/overlays/actors/ovl_En_Ds/z_en_ds.c +++ b/soh/src/overlays/actors/ovl_En_Ds/z_en_ds.c @@ -291,7 +291,7 @@ void EnDs_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Matrix_MultVec3f(&sMultVec, &this->actor.focus.pos); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 5) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Du/z_en_du.c b/soh/src/overlays/actors/ovl_En_Du/z_en_du.c index d5e638881..9bb1049cf 100644 --- a/soh/src/overlays/actors/ovl_En_Du/z_en_du.c +++ b/soh/src/overlays/actors/ovl_En_Du/z_en_du.c @@ -656,20 +656,6 @@ void EnDu_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, if (limbIndex == 16) { Matrix_MultVec3f(&D_809FF40C, &this->actor.focus.pos); } - - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { - if (limbIndex == 17) { - OPEN_DISPS(play->state.gfxCtx); - Matrix_Push(); - Matrix_RotateZYX(13062, -1329, -15499, MTXMODE_APPLY); - Matrix_Translate(945.946f, -297.297f, 608.108f, MTXMODE_APPLY); - Matrix_Scale(1.217f, 1.217f, 1.217f, MTXMODE_APPLY); - gSPMatrix(POLY_OPA_DISP++, MATRIX_NEWMTX(play->state.gfxCtx), G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); - gSPDisplayList(POLY_OPA_DISP++, gSantaHatGenericDL); - Matrix_Pop(); - CLOSE_DISPS(play->state.gfxCtx); - } - } } void EnDu_Draw(Actor* thisx, PlayState* play) { diff --git a/soh/src/overlays/actors/ovl_En_Elf/z_en_elf.c b/soh/src/overlays/actors/ovl_En_Elf/z_en_elf.c index 3d3985b48..e656ddcae 100644 --- a/soh/src/overlays/actors/ovl_En_Elf/z_en_elf.c +++ b/soh/src/overlays/actors/ovl_En_Elf/z_en_elf.c @@ -1501,7 +1501,7 @@ s32 EnElf_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f* p s32 EnElf_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { EnElf* this = (EnElf*)thisx; - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 2) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Fu/z_en_fu.c b/soh/src/overlays/actors/ovl_En_Fu/z_en_fu.c index 005313f4e..b4ad830dc 100644 --- a/soh/src/overlays/actors/ovl_En_Fu/z_en_fu.c +++ b/soh/src/overlays/actors/ovl_En_Fu/z_en_fu.c @@ -296,7 +296,7 @@ void EnFu_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Matrix_MultVec3f(&sMtxSrc, &this->actor.focus.pos); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 14) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Heishi4/z_en_heishi4.c b/soh/src/overlays/actors/ovl_En_Heishi4/z_en_heishi4.c index cf10b2d56..b24c43093 100644 --- a/soh/src/overlays/actors/ovl_En_Heishi4/z_en_heishi4.c +++ b/soh/src/overlays/actors/ovl_En_Heishi4/z_en_heishi4.c @@ -428,7 +428,7 @@ s32 EnHeishi_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f s32 EnHeishi4_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { EnHeishi4* this = (EnHeishi4*)thisx; - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 16) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Hs2/z_en_hs2.c b/soh/src/overlays/actors/ovl_En_Hs2/z_en_hs2.c index 7f272093e..bbcd7b686 100644 --- a/soh/src/overlays/actors/ovl_En_Hs2/z_en_hs2.c +++ b/soh/src/overlays/actors/ovl_En_Hs2/z_en_hs2.c @@ -170,7 +170,7 @@ void EnHs2_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Matrix_MultVec3f(&D_80A6F4CC, &this->actor.focus.pos); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 9) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Hy/z_en_hy.c b/soh/src/overlays/actors/ovl_En_Hy/z_en_hy.c index 8023d0236..9027163bd 100644 --- a/soh/src/overlays/actors/ovl_En_Hy/z_en_hy.c +++ b/soh/src/overlays/actors/ovl_En_Hy/z_en_hy.c @@ -1206,7 +1206,7 @@ void EnHy_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Matrix_MultVec3f(&sp3C, &this->actor.focus.pos); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 15) { Matrix_Push(); switch (this->actor.params) { diff --git a/soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c b/soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c index 04bebae06..e12581ee4 100644 --- a/soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c +++ b/soh/src/overlays/actors/ovl_En_Ik/z_en_ik.c @@ -955,7 +955,7 @@ void EnIk_PostLimbDraw3(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, break; } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 11) { Matrix_Push(); Matrix_RotateZYX(0, 0, -15056, MTXMODE_APPLY); @@ -1249,7 +1249,7 @@ void EnIk_PostLimbDraw2(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, } break; } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 11) { Matrix_Push(); Matrix_RotateZYX(0, 0, -15056, MTXMODE_APPLY); @@ -1408,7 +1408,7 @@ void EnIk_PostLimbDraw1(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, break; } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 11) { Matrix_Push(); Matrix_RotateZYX(0, 0, -15056, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_In/z_en_in.c b/soh/src/overlays/actors/ovl_En_In/z_en_in.c index 8678cf2c8..1bf643854 100644 --- a/soh/src/overlays/actors/ovl_En_In/z_en_in.c +++ b/soh/src/overlays/actors/ovl_En_In/z_en_in.c @@ -993,7 +993,7 @@ void EnIn_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, gSPDisplayList(POLY_OPA_DISP++, gIngoChildEraPitchForkDL); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 16) { Matrix_Push(); Matrix_RotateZYX(-8192, -222, -11513, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Ishi/z_en_ishi.c b/soh/src/overlays/actors/ovl_En_Ishi/z_en_ishi.c index 37dc1fa9d..d2e2e273b 100644 --- a/soh/src/overlays/actors/ovl_En_Ishi/z_en_ishi.c +++ b/soh/src/overlays/actors/ovl_En_Ishi/z_en_ishi.c @@ -8,6 +8,7 @@ #include "overlays/effects/ovl_Effect_Ss_Kakera/z_eff_ss_kakera.h" #include "objects/gameplay_field_keep/gameplay_field_keep.h" #include "soh/OTRGlobals.h" +#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h" #include "vt.h" @@ -251,7 +252,7 @@ void EnIshi_SpawnDustLarge(EnIshi* this, PlayState* play) { void EnIshi_DropCollectible(EnIshi* this, PlayState* play) { s16 dropParams; - if ((this->actor.params & 1) == ROCK_SMALL) { + if (GameInteractor_Should(VB_ROCK_DROP_ITEM, (this->actor.params & 1) == ROCK_SMALL, this)) { dropParams = (this->actor.params >> 8) & 0xF; if (dropParams >= 0xD) { diff --git a/soh/src/overlays/actors/ovl_En_Jj/z_en_jj.c b/soh/src/overlays/actors/ovl_En_Jj/z_en_jj.c index 09b4575b1..e1db32870 100644 --- a/soh/src/overlays/actors/ovl_En_Jj/z_en_jj.c +++ b/soh/src/overlays/actors/ovl_En_Jj/z_en_jj.c @@ -313,7 +313,7 @@ void EnJj_Update(Actor* thisx, PlayState* play) { s32 EnJj_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { EnJj* this = (EnJj*)thisx; - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 13) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Ma1/z_en_ma1.c b/soh/src/overlays/actors/ovl_En_Ma1/z_en_ma1.c index 9d8f2689b..c8606d5db 100644 --- a/soh/src/overlays/actors/ovl_En_Ma1/z_en_ma1.c +++ b/soh/src/overlays/actors/ovl_En_Ma1/z_en_ma1.c @@ -469,7 +469,7 @@ void EnMa1_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Matrix_MultVec3f(&vec, &this->actor.focus.pos); } - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 15) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Niw/z_en_niw.c b/soh/src/overlays/actors/ovl_En_Niw/z_en_niw.c index c15ba5323..3a9a6905c 100644 --- a/soh/src/overlays/actors/ovl_En_Niw/z_en_niw.c +++ b/soh/src/overlays/actors/ovl_En_Niw/z_en_niw.c @@ -1137,7 +1137,7 @@ s32 EnNiw_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3f* p s32 EnNiw_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { EnNiw* this = (EnNiw*)thisx; - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 15) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Niw_Girl/z_en_niw_girl.c b/soh/src/overlays/actors/ovl_En_Niw_Girl/z_en_niw_girl.c index f95de6a6f..6eeeaa22e 100644 --- a/soh/src/overlays/actors/ovl_En_Niw_Girl/z_en_niw_girl.c +++ b/soh/src/overlays/actors/ovl_En_Niw_Girl/z_en_niw_girl.c @@ -251,7 +251,7 @@ static Vec3f sConstVec3f = { 0.2f, 0.2f, 0.2f }; s32 EnNiwGirl_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { EnNiwGirl* this = (EnNiwGirl*)thisx; - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 4) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Niw_Lady/z_en_niw_lady.c b/soh/src/overlays/actors/ovl_En_Niw_Lady/z_en_niw_lady.c index b21f783e5..b888f1845 100644 --- a/soh/src/overlays/actors/ovl_En_Niw_Lady/z_en_niw_lady.c +++ b/soh/src/overlays/actors/ovl_En_Niw_Lady/z_en_niw_lady.c @@ -590,7 +590,7 @@ s32 EnNiwLady_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3 s32 EnNiwLady_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { EnNiwLady* this = (EnNiwLady*)thisx; - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 15) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); diff --git a/soh/src/overlays/actors/ovl_En_Nutsball/z_en_nutsball.c b/soh/src/overlays/actors/ovl_En_Nutsball/z_en_nutsball.c index ad59ec1d0..eae576a81 100644 --- a/soh/src/overlays/actors/ovl_En_Nutsball/z_en_nutsball.c +++ b/soh/src/overlays/actors/ovl_En_Nutsball/z_en_nutsball.c @@ -48,7 +48,7 @@ static ColliderCylinderInit sCylinderInit = { }, { ELEMTYPE_UNK0, - { 0xFFCFFFFF, 0x02, 0x08 }, + { 0xFFCFFFFF, 0x00, 0x08 }, { 0xFFCFFFFF, 0x00, 0x00 }, TOUCH_ON | TOUCH_SFX_WOOD, BUMP_ON, @@ -73,12 +73,10 @@ void EnNutsball_Init(Actor* thisx, PlayState* play) { EnNutsball* this = (EnNutsball*)thisx; s32 pad; - this->collider.info.toucher.effect = 2; - ActorShape_Init(&this->actor.shape, 400.0f, ActorShadow_DrawCircle, 13.0f); Collider_InitCylinder(play, &this->collider); Collider_SetCylinder(play, &this->collider, &this->actor, &sCylinderInit); - if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || CVarGetInteger("gLetItSnow", 0)) { + if (CVarGetInteger(CVAR_ENHANCEMENT("RandomizedEnemies"), 0) || CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { this->objBankIndex = 0; } else { this->objBankIndex = Object_GetIndex(&play->objectCtx, sObjectIDs[this->actor.params]); diff --git a/soh/src/overlays/actors/ovl_En_Ossan/z_en_ossan.c b/soh/src/overlays/actors/ovl_En_Ossan/z_en_ossan.c index 31cc9a633..a809be282 100644 --- a/soh/src/overlays/actors/ovl_En_Ossan/z_en_ossan.c +++ b/soh/src/overlays/actors/ovl_En_Ossan/z_en_ossan.c @@ -2429,7 +2429,7 @@ void EnOssan_DrawStickDirectionPrompts(PlayState* play, EnOssan* this) { s32 EnOssan_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { EnOssan* this = (EnOssan*)thisx; - if (CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { if (limbIndex == 8) { switch (this->actor.params) { case 4: { @@ -2529,7 +2529,7 @@ s32 EnOssan_OverrideLimbDrawKokiriShopkeeper(PlayState* play, s32 limbIndex, Gfx gSPSegment(POLY_OPA_DISP++, 0x0A, SEGMENTED_TO_VIRTUAL(sKokiriShopkeeperEyeTextures[this->eyeTextureIdx])); } - if (limbIndex == 15 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 15 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { Matrix_Push(); Matrix_RotateZYX(14169, -2215, 0, MTXMODE_APPLY); Matrix_Translate(1810.811f, -351.351f, -94.595f, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Po_Relay/z_en_po_relay.c b/soh/src/overlays/actors/ovl_En_Po_Relay/z_en_po_relay.c index 5b64a4355..b71ac4740 100644 --- a/soh/src/overlays/actors/ovl_En_Po_Relay/z_en_po_relay.c +++ b/soh/src/overlays/actors/ovl_En_Po_Relay/z_en_po_relay.c @@ -433,7 +433,7 @@ void EnPoRelay_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* CLOSE_DISPS(play->state.gfxCtx); } - if (limbIndex == 16 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 16 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(10627, 3321, -13727, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Shopnuts/z_en_shopnuts.c b/soh/src/overlays/actors/ovl_En_Shopnuts/z_en_shopnuts.c index fdbe2fd9a..aec5aa47e 100644 --- a/soh/src/overlays/actors/ovl_En_Shopnuts/z_en_shopnuts.c +++ b/soh/src/overlays/actors/ovl_En_Shopnuts/z_en_shopnuts.c @@ -308,7 +308,7 @@ void EnShopnuts_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* CLOSE_DISPS(play->state.gfxCtx); } - if (limbIndex == 17 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 17 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(0, 0, 17490, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Skb/z_en_skb.c b/soh/src/overlays/actors/ovl_En_Skb/z_en_skb.c index 9bc10c242..a653f6085 100644 --- a/soh/src/overlays/actors/ovl_En_Skb/z_en_skb.c +++ b/soh/src/overlays/actors/ovl_En_Skb/z_en_skb.c @@ -556,7 +556,7 @@ void EnSkb_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, BodyBreak_SetInfo(&this->bodyBreak, limbIndex, 0, 18, 18, dList, BODYBREAK_OBJECT_DEFAULT); } - if (limbIndex == 11 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 11 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(0, 0, -2215, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Sth/z_en_sth.c b/soh/src/overlays/actors/ovl_En_Sth/z_en_sth.c index 3d80eb38e..a92571e41 100644 --- a/soh/src/overlays/actors/ovl_En_Sth/z_en_sth.c +++ b/soh/src/overlays/actors/ovl_En_Sth/z_en_sth.c @@ -400,7 +400,7 @@ void EnSth_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, } } - if (limbIndex == 15 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 15 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(-4207, -665, -4650, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c b/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c index 5f4515ff8..825125088 100644 --- a/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c +++ b/soh/src/overlays/actors/ovl_En_Syateki_Man/z_en_syateki_man.c @@ -525,7 +525,7 @@ s32 EnSyatekiMan_OverrideLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, V s32 EnSyatekiMan_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, void* thisx) { EnSyatekiMan* this = (EnSyatekiMan*)thisx; - if (limbIndex == 8 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 8 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(2214, 3985, -7750, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Ta/z_en_ta.c b/soh/src/overlays/actors/ovl_En_Ta/z_en_ta.c index fc1e48dfe..23ab6219e 100644 --- a/soh/src/overlays/actors/ovl_En_Ta/z_en_ta.c +++ b/soh/src/overlays/actors/ovl_En_Ta/z_en_ta.c @@ -1217,7 +1217,7 @@ void EnTa_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Matrix_MultVec3f(&D_80B16E7C, &this->actor.focus.pos); } - if (limbIndex == 15 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 15 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(11955, -1993, 221, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Test/z_en_test.c b/soh/src/overlays/actors/ovl_En_Test/z_en_test.c index dcf48095e..e41f843e1 100644 --- a/soh/src/overlays/actors/ovl_En_Test/z_en_test.c +++ b/soh/src/overlays/actors/ovl_En_Test/z_en_test.c @@ -1958,7 +1958,7 @@ void EnTest_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot } } - if (limbIndex == 11 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 11 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(-10849, 0, -5314, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Tg/z_en_tg.c b/soh/src/overlays/actors/ovl_En_Tg/z_en_tg.c index 6421ea571..a0b4c17a4 100644 --- a/soh/src/overlays/actors/ovl_En_Tg/z_en_tg.c +++ b/soh/src/overlays/actors/ovl_En_Tg/z_en_tg.c @@ -168,7 +168,7 @@ void EnTg_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, if (limbIndex == 9) { // Place the target point at the guy's head instead of the center of the actor Matrix_MultVec3f(&targetOffset, &this->actor.focus.pos); - if (CVAR_GENERAL("LetItSnow")) { + if (CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(-3100, 1992, 2435, MTXMODE_APPLY); @@ -180,7 +180,7 @@ void EnTg_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, Matrix_Pop(); CLOSE_DISPS(play->state.gfxCtx); } - } else if (limbIndex == 20 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + } else if (limbIndex == 20 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(-2657, -1550, 1549, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Tk/z_en_tk.c b/soh/src/overlays/actors/ovl_En_Tk/z_en_tk.c index 5492cb866..3f558df2a 100644 --- a/soh/src/overlays/actors/ovl_En_Tk/z_en_tk.c +++ b/soh/src/overlays/actors/ovl_En_Tk/z_en_tk.c @@ -730,7 +730,7 @@ void EnTk_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, func_80B1D200(play); } - if (limbIndex == 16 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 16 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(10627, 3321, -13727, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Toryo/z_en_toryo.c b/soh/src/overlays/actors/ovl_En_Toryo/z_en_toryo.c index 9f21fa36a..570b2a44f 100644 --- a/soh/src/overlays/actors/ovl_En_Toryo/z_en_toryo.c +++ b/soh/src/overlays/actors/ovl_En_Toryo/z_en_toryo.c @@ -423,7 +423,7 @@ void EnToryo_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* ro break; } - if (limbIndex == 15 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 15 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(-23691, 664, -2879, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Vm/z_en_vm.c b/soh/src/overlays/actors/ovl_En_Vm/z_en_vm.c index e7de92354..db8c33dee 100644 --- a/soh/src/overlays/actors/ovl_En_Vm/z_en_vm.c +++ b/soh/src/overlays/actors/ovl_En_Vm/z_en_vm.c @@ -523,7 +523,7 @@ void EnVm_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, &this->colliderQuad2.dim.quad[3]); } - if (limbIndex == 6 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 6 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(19704, -1329, 11734, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Wf/z_en_wf.c b/soh/src/overlays/actors/ovl_En_Wf/z_en_wf.c index 1502ec510..bf6916d51 100644 --- a/soh/src/overlays/actors/ovl_En_Wf/z_en_wf.c +++ b/soh/src/overlays/actors/ovl_En_Wf/z_en_wf.c @@ -1430,7 +1430,7 @@ void EnWf_PostLimbDraw(PlayState* play, s32 limbIndex, Gfx** dList, Vec3s* rot, } } - if (limbIndex == 17 && CVarGetInteger(CVAR_GENERAL("LetItSnow"), 0)) { + if (limbIndex == 17 && CVarGetInteger("gHoliday.Visual.Hats", 0)) { OPEN_DISPS(play->state.gfxCtx); Matrix_Push(); Matrix_RotateZYX(0, 0, -18377, MTXMODE_APPLY); diff --git a/soh/src/overlays/actors/ovl_En_Wood02/z_en_wood02.c b/soh/src/overlays/actors/ovl_En_Wood02/z_en_wood02.c index e02a147ba..e6589e677 100644 --- a/soh/src/overlays/actors/ovl_En_Wood02/z_en_wood02.c +++ b/soh/src/overlays/actors/ovl_En_Wood02/z_en_wood02.c @@ -460,24 +460,22 @@ void EnWood02_Draw(Actor* thisx, PlayState* play) { if ((type == WOOD_TREE_OVAL_GREEN_SPAWNER) || (type == WOOD_TREE_OVAL_GREEN_SPAWNED) || (type == WOOD_TREE_OVAL_GREEN) || (type == WOOD_LEAF_GREEN)) { - // red = 50; - // green = 170; - // blue = 70; - red = 255; - green = 255; - blue = 255; + red = 50; + green = 170; + blue = 70; } else if ((type == WOOD_TREE_OVAL_YELLOW_SPAWNER) || (type == WOOD_TREE_OVAL_YELLOW_SPAWNED) || (type == WOOD_LEAF_YELLOW)) { - // red = 180; - // green = 155; - // blue = 0; - red = 255; - green = 255; - blue = 255; + red = 180; + green = 155; + blue = 0; } else { red = green = blue = 255; } + if (CVarGetInteger("gHoliday.Visual.SnowingWeather", 0)) { + red = green = blue = 255; + } + Gfx_SetupDL_25Xlu(gfxCtx); if (GameInteractor_Should(VB_TREE_SETUP_DRAW, (this->actor.params == WOOD_LEAF_GREEN) || (this->actor.params == WOOD_LEAF_YELLOW), 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 442a2dfc3..55f80385a 100644 --- a/soh/src/overlays/actors/ovl_player_actor/z_player.c +++ b/soh/src/overlays/actors/ovl_player_actor/z_player.c @@ -7135,9 +7135,13 @@ void func_8083DFE0(Player* this, f32* arg1, s16* arg2) { if (this->meleeWeaponState == 0) { float maxSpeed = R_RUN_SPEED_LIMIT / 100.0f; - if (CVarGetInteger(CVAR_ENHANCEMENT("MMBunnyHood"), BUNNY_HOOD_VANILLA) == BUNNY_HOOD_FAST_AND_JUMP && - this->currentMask == PLAYER_MASK_BUNNY) { - maxSpeed *= 1.5f; + if (this->currentMask == PLAYER_MASK_BUNNY) { + if (IS_ROGUELIKE) { + maxSpeed *= 1.0f + (((f32)gSaveContext.ship.quest.data.rogueLike.stats[RL_SPEED]) / 10.0f) * 1.5f; + } else if (CVarGetInteger(CVAR_ENHANCEMENT("MMBunnyHood"), BUNNY_HOOD_VANILLA) == + BUNNY_HOOD_FAST_AND_JUMP) { + maxSpeed *= 1.5f; + } } if (CVarGetInteger(CVAR_SETTING("WalkModifier.Enabled"), 0) && @@ -8886,9 +8890,13 @@ void Player_Action_80842180(Player* this, PlayState* play) { if (!func_8083C484(this, &sp2C, &sp2A)) { - if (CVarGetInteger(CVAR_ENHANCEMENT("MMBunnyHood"), BUNNY_HOOD_VANILLA) != BUNNY_HOOD_VANILLA && - this->currentMask == PLAYER_MASK_BUNNY) { - sp2C *= 1.5f; + if (this->currentMask == PLAYER_MASK_BUNNY) { + if (IS_ROGUELIKE) { + sp2C *= 1.0f + (((f32)gSaveContext.ship.quest.data.rogueLike.stats[RL_SPEED]) / 10.0f) * 1.5f; + } else if (CVarGetInteger(CVAR_ENHANCEMENT("MMBunnyHood"), BUNNY_HOOD_VANILLA) == + BUNNY_HOOD_FAST_AND_JUMP) { + sp2C *= 1.5f; + } } if (CVarGetInteger(CVAR_SETTING("WalkModifier.Enabled"), 0)) { @@ -12835,9 +12843,14 @@ s16 func_8084ABD8(PlayState* play, Player* this, s32 arg2, s16 arg3) { if (CVarGetInteger(CVAR_SETTING("MoveInFirstPerson"), 0)) { f32 movementSpeed = LINK_IS_ADULT ? 9.0f : 8.25f; - if (CVarGetInteger(CVAR_ENHANCEMENT("MMBunnyHood"), BUNNY_HOOD_VANILLA) != BUNNY_HOOD_VANILLA && - this->currentMask == PLAYER_MASK_BUNNY) { - movementSpeed *= 1.5f; + + if (this->currentMask == PLAYER_MASK_BUNNY) { + if (IS_ROGUELIKE) { + movementSpeed *= 1.0f + (((f32)gSaveContext.ship.quest.data.rogueLike.stats[RL_SPEED]) / 10.0f) * 1.5f; + } else if (CVarGetInteger(CVAR_ENHANCEMENT("MMBunnyHood"), BUNNY_HOOD_VANILLA) == + BUNNY_HOOD_FAST_AND_JUMP) { + movementSpeed *= 1.5f; + } } f32 relX = (sControlInput->rel.stick_x / 10 * -invertXAxisMulti); 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 7d747012c..d28e03b26 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 @@ -704,7 +704,7 @@ static void DrawMoreInfo(FileChooseContext* this, s16 fileIndex, u8 alpha) { } #define MIN_QUEST (ResourceMgr_GameHasOriginal() ? QUEST_NORMAL : QUEST_MASTER) -#define MAX_QUEST QUEST_ARCHIPELAGO +#define MAX_QUEST QUEST_ROGUELIKE void Sram_InitDebugSave(void); void Sram_InitBossRushSave(); @@ -2745,6 +2745,17 @@ void FileChoose_DrawWindowContents(GameState* thisx) { ResourceMgr_GameHasOriginal() ? gTitleZeldaShieldLogoTex : gTitleZeldaShieldLogoMQTex, 160, 160); FileChoose_DrawImageRGBA32(this->state.gfxCtx, 182, 180, gTitleArchipelagoSubtitleTex, 128, 32); break; + case QUEST_ROGUELIKE: + 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, gTitleRogueLikeSubtitleTex, 128, 32); + break; } } else if (this->configMode == CM_BOSS_RUSH_MENU) { FileChoose_DrawBossRushMenuWindowContents(this);