Add consumable Nayru's Love (#7365)

This commit is contained in:
Chris
2026-10-12 20:02:47 -04:00
committed by GitHub
parent 96be197fd2
commit a6be06874f
15 changed files with 186 additions and 31 deletions
+7
View File
@@ -1495,6 +1495,13 @@
"The jump can also be used when already in mid-air. Roc's Feather is not considered by logic."
]
},
"nayrus_love_uses": {
"name": "Nayru's Love Uses",
"description": [
"Limits Nayru's Love to a set number of casts. Each Nayru's Love you find adds one use, and this sets ",
"how many are placed in the item pool. Starting with Nayru's Love counts as one. 0 keeps it unlimited."
]
},
"slingbow_break_beehives": {
"name": "Slingshot/Bow Can Break Beehives",
"description": "Allows Slingshot and Bow to break beehives when Beehive Shuffle is turned on."
+1
View File
@@ -227,6 +227,7 @@ typedef struct {
typedef struct ShipRandomizerSaveContextData {
u8 triforcePiecesCollected;
u8 bombchuUpgradeLevel;
u8 nayrusLoveUses;
s8 silverShadowBlades;
s8 silverShadowPit;
s8 silverShadowSpikes;
@@ -702,6 +702,11 @@ typedef enum {
// ```c
// true
// ```
// In `ItemInSlotUsesAmmo` (pause menu, with Better Ammo Rendering):
// ```c
// item == ITEM_STICK || item == ITEM_NUT || item == ITEM_BOMB || item == ITEM_BOW ||
// item == ITEM_SLINGSHOT || item == ITEM_BOMBCHU || item == ITEM_BEAN
// ```
// #### `args`
// - `*int16_t` (item id)
VB_DRAW_AMMO_COUNT,
@@ -2025,6 +2030,16 @@ typedef enum {
// - `*EnKo`
VB_OPEN_KOKIRI_FOREST,
// #### `result`
// In `Interface_DrawAmmoCount` and `KaleidoScope_DrawAmmoCount`:
// ```c
// true
// ```
// #### `args`
// - `*int16_t` (item id)
// - `*int16_t` (ammo count)
VB_OVERRIDE_AMMO_COUNT,
// #### `result`
// ```c
// false
@@ -175,7 +175,12 @@ void GenerateItemPool() {
if (!ctx->GetOption(RSK_STARTING_LIGHT_ARROWS)) AddItemToPool(RG_LIGHT_ARROWS, 2, 1, 1, 1);
if (!ctx->GetOption(RSK_STARTING_DINS_FIRE)) AddItemToPool(RG_DINS_FIRE, 2, 1, 1, 1);
if (!ctx->GetOption(RSK_STARTING_FARORES_WIND)) AddItemToPool(RG_FARORES_WIND, 2, 1, 1, 0);
if (!ctx->GetOption(RSK_STARTING_NAYRUS_LOVE)) AddItemToPool(RG_NAYRUS_LOVE, 2, 1, 1, 0);
if (ctx->GetOption(RSK_NAYRUS_LOVE_USES).Get() > 0) {
AddFixedItemToPool(RG_NAYRUS_LOVE, ctx->GetOption(RSK_NAYRUS_LOVE_USES).Get() -
(ctx->GetOption(RSK_STARTING_NAYRUS_LOVE) ? 1 : 0));
} else if (!ctx->GetOption(RSK_STARTING_NAYRUS_LOVE)) {
AddItemToPool(RG_NAYRUS_LOVE, 2, 1, 1, 0);
}
AddItemToPool(RG_GREG_RUPEE, 1, 1, 1, 1);
AddFixedItemToPool(RG_PROGRESSIVE_HOOKSHOT, 2 - ctx->GetOption(RSK_STARTING_HOOKSHOT).Get());
if (!ctx->GetOption(RSK_STARTING_HYLIAN_SHIELD)) AddItemToPool(RG_HYLIAN_SHIELD, 1, 1, 1, 1);
@@ -0,0 +1,82 @@
#include "soh/Enhancements/game-interactor/GameInteractor.h"
#include "soh/Enhancements/custom-message/CustomMessageManager.h"
#include "soh/Enhancements/randomizer/SeedContext.h"
#include "soh/ShipInit.hpp"
extern "C" {
#include <z64.h>
#include "functions.h"
#include "macros.h"
#include "variables.h"
}
#define NAYRUS_LOVE_USES_MAX 99
#define TEXT_GET_NAYRUS_LOVE 0xAF
static u8& NayrusLoveUses() {
return gSaveContext.ship.quest.data.randomizer.nayrusLoveUses;
}
// The text opens before Item_Give runs, so the stored count doesn't include this item yet
static void BuildNayrusLoveMessage(uint16_t* textId, bool* loadFromMessageTable) {
CustomMessage msg = CustomMessage("You got %bNayru's Love%w!&You now have %b[[d]]%w |use|uses| of it!",
"Du erhältst %bNayrus Umarmung%w!&Du kannst sie nun %b[[d]]%w-mal einsetzen!",
"Vous obtenez l'%bAmour de Nayru%w!&Vous pouvez l'utiliser %b[[d]]%w fois!");
uint8_t uses = NayrusLoveUses() < NAYRUS_LOVE_USES_MAX ? NayrusLoveUses() + 1 : NAYRUS_LOVE_USES_MAX;
msg.InsertNumber(uses);
msg.AutoFormat(ITEM_NAYRUS_LOVE);
msg.LoadIntoFont();
*loadFromMessageTable = false;
}
void RegisterConsumableNayrusLove() {
bool shouldRegister = IS_RANDO && RAND_GET_OPTION(RSK_NAYRUS_LOVE_USES).Get() > 0;
COND_HOOK(OnItemReceive, shouldRegister, [](GetItemEntry itemEntry) {
if (itemEntry.modIndex != MOD_NONE || itemEntry.itemId != ITEM_NAYRUS_LOVE) {
return;
}
if (NayrusLoveUses() < NAYRUS_LOVE_USES_MAX) {
NayrusLoveUses()++;
}
});
COND_VB_SHOULD(VB_CHANGE_HELD_ITEM_AND_USE_ITEM, shouldRegister, {
int32_t usedItem = va_arg(args, int32_t);
if (usedItem == ITEM_NAYRUS_LOVE && NayrusLoveUses() == 0) {
*should = false;
Sfx_PlaySfxCentered(NA_SE_SY_ERROR);
}
});
COND_ID_HOOK(OnActorInit, ACTOR_MAGIC_DARK, shouldRegister, [](void*) {
// Don't consume a usage if the timer is still active
if (gSaveContext.nayrusLoveTimer != 0) {
return;
}
if (NayrusLoveUses() > 0) {
NayrusLoveUses()--;
}
});
COND_VB_SHOULD(VB_OVERRIDE_AMMO_COUNT, shouldRegister, {
int16_t item = *va_arg(args, int16_t*);
int16_t* ammo = va_arg(args, int16_t*);
if (item == ITEM_NAYRUS_LOVE) {
*ammo = NayrusLoveUses();
*should = false;
}
});
COND_VB_SHOULD(VB_COLOR_AMMO_GREEN, shouldRegister, {
int16_t item = va_arg(args, int);
if (item == ITEM_NAYRUS_LOVE && NayrusLoveUses() >= RAND_GET_OPTION(RSK_NAYRUS_LOVE_USES).Get()) {
*should = true;
}
});
COND_ID_HOOK(OnOpenText, TEXT_GET_NAYRUS_LOVE, shouldRegister, BuildNayrusLoveMessage);
}
static RegisterShipInitFunc initFunc(RegisterConsumableNayrusLove, { "IS_RANDO" });
@@ -1890,6 +1890,9 @@ void RandomizerOnVanillaBehaviorHandler(GIVanillaBehavior id, bool* should, va_l
gPlayState->bombchuBowlingStatus < 1)) {
*should = false;
}
if (item == ITEM_NAYRUS_LOVE) {
return RAND_GET_OPTION(RSK_NAYRUS_LOVE_USES).Get() > 0;
}
break;
}
case VB_HAVE_OCARINA_NOTE_A4: {
+5 -1
View File
@@ -552,9 +552,13 @@ bool Logic::CanUse(RandomizerGet itemName) {
return true; // AmmoCanDrop || (HasBottle() && Get(LOGIC_BUY_MAGIC_POTION))
case RG_DINS_FIRE:
case RG_FARORES_WIND:
case RG_NAYRUS_LOVE:
case RG_LENS_OF_TRUTH:
return CanUse(RG_MAGIC_SINGLE);
case RG_NAYRUS_LOVE:
if (ctx->GetOption(RSK_NAYRUS_LOVE_USES).Get() > 0) {
return false;
}
return CanUse(RG_MAGIC_SINGLE);
case RG_FIRE_ARROWS:
case RG_ICE_ARROWS:
case RG_LIGHT_ARROWS:
@@ -437,6 +437,9 @@ ItemObtainability Randomizer::GetItemObtainabilityFromRandomizerGet(RandomizerGe
case RG_FARORES_WIND:
return INV_CONTENT(ITEM_FARORES_WIND) == ITEM_NONE ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE;
case RG_NAYRUS_LOVE:
if (GetRandoSettingValue(RSK_NAYRUS_LOVE_USES) > 0) {
return CAN_OBTAIN;
}
if (!GetRandoSettingValue(RSK_ROCS_FEATHER)) {
return INV_CONTENT(ITEM_NAYRUS_LOVE) == ITEM_NONE ? CAN_OBTAIN : CANT_OBTAIN_ALREADY_HAVE;
} else {
@@ -321,6 +321,7 @@ RANDO_ENUM_ITEM(RSK_STARTING_ZORA_MASK)
RANDO_ENUM_ITEM(RSK_STARTING_GERUDO_MASK)
RANDO_ENUM_ITEM(RSK_STARTING_MASK_OF_TRUTH)
RANDO_ENUM_ITEM(RSK_PROGRESSIVE_GORON_SWORD)
RANDO_ENUM_ITEM(RSK_NAYRUS_LOVE_USES)
RANDO_ENUM_ITEM(RSK_MAX)
RANDO_ENUM_END(RandomizerSettingKey)
@@ -533,6 +533,13 @@ void ItemTracker_LoadFromPreset(const nlohmann::json& trackerInfo) {
}
}
// Roc's Feather shares Nayru's Love's slot, so the slot being filled isn't enough
static bool HasNayrusLove() {
return IS_RANDO && OTRGlobals::Instance->gRandomizer->GetRandoSettingValue(RSK_ROCS_FEATHER)
? Flags_GetRandomizerInf(RAND_INF_OBTAINED_NAYRUS_LOVE)
: INV_CONTENT(ITEM_NAYRUS_LOVE) == ITEM_NAYRUS_LOVE;
}
ItemTrackerNumbers GetItemCurrentAndMax(ItemTrackerItem item) {
ItemTrackerNumbers result;
result.currentCapacity = 0;
@@ -599,6 +606,15 @@ ItemTrackerNumbers GetItemCurrentAndMax(ItemTrackerItem item) {
result.maxCapacity = 10;
result.currentAmmo = AMMO(ITEM_BEAN);
break;
case ITEM_NAYRUS_LOVE:
if (IS_RANDO) {
const uint8_t nlUses = RAND_GET_OPTION(RSK_NAYRUS_LOVE_USES).Get();
if (nlUses > 0 && HasNayrusLove()) {
result.currentCapacity = result.maxCapacity = nlUses;
result.currentAmmo = gSaveContext.ship.quest.data.randomizer.nayrusLoveUses;
}
}
break;
case ITEM_HEART_CONTAINER:
result.maxCapacity = result.currentCapacity = 8;
result.currentAmmo = gSaveContext.ship.stats.heartContainers;
@@ -770,8 +786,8 @@ void DrawItemCount(ItemTrackerItem item, bool hideMax) {
trackerNumberDisplayMode == ITEM_TRACKER_NUMBER_CURRENT_AMMO_ONLY ||
// These items have a static capacity, so display ammo instead
(item.kind == ITEM_KIND_QUEST && item.id == QUEST_SKULL_TOKEN) ||
(item.kind == ITEM_KIND_ITEM &&
(item.id == ITEM_BEAN || item.id == ITEM_HEART_CONTAINER || item.id == ITEM_HEART_PIECE));
(item.kind == ITEM_KIND_ITEM && (item.id == ITEM_BEAN || item.id == ITEM_NAYRUS_LOVE ||
item.id == ITEM_HEART_CONTAINER || item.id == ITEM_HEART_PIECE));
bool shouldDisplayMax = !(trackerNumberDisplayMode == ITEM_TRACKER_NUMBER_CURRENT_CAPACITY_ONLY ||
trackerNumberDisplayMode == ITEM_TRACKER_NUMBER_CURRENT_AMMO_ONLY);
@@ -50,6 +50,8 @@ static uint16_t rupeeCounts[] = {
200, // ITEM_RUPEE_GOLD
};
static uint8_t sStartingNayrusLoveGives = 0;
void StartingItemGive(GetItemEntry getItemEntry, RandomizerCheck randomizerCheck) {
if (randomizerCheck != RC_MAX) {
OTRGlobals::Instance->gRandoContext->GetItemLocation(randomizerCheck)->SetCheckStatus(RCSHOW_SAVED);
@@ -61,6 +63,9 @@ void StartingItemGive(GetItemEntry getItemEntry, RandomizerCheck randomizerCheck
if (getItemEntry.getItemId == GI_SWORD_BGS) {
gSaveContext.bgsFlag = true;
}
if (getItemEntry.itemId == ITEM_NAYRUS_LOVE) {
sStartingNayrusLoveGives++;
}
Item_Give(NULL, static_cast<uint8_t>(getItemEntry.itemId));
}
} else if (getItemEntry.modIndex == MOD_RANDOMIZER) {
@@ -451,6 +456,9 @@ extern "C" void Randomizer_InitSaveFile() {
// Reset Bombchu Bag Upgrade
gSaveContext.ship.quest.data.randomizer.bombchuUpgradeLevel = 0;
gSaveContext.ship.quest.data.randomizer.nayrusLoveUses = 0;
sStartingNayrusLoveGives = 0;
SetStartingItems();
// Debug saves start with bombchus but no bag level, which would cap them at 0
@@ -679,4 +687,12 @@ extern "C" void Randomizer_InitSaveFile() {
Item_Give(NULL, ITEM_GERUDO_CARD);
}
}
// The item hooks aren't registered yet, so count the starting gives here
if (Randomizer_GetSettingValue(RSK_NAYRUS_LOVE_USES) > 0) {
uint8_t uses = (Randomizer_GetSettingValue(RSK_STARTING_NAYRUS_LOVE) ? 1 : 0) + sStartingNayrusLoveGives;
gSaveContext.ship.quest.data.randomizer.nayrusLoveUses = uses < 99 ? uses : 99;
} else {
gSaveContext.ship.quest.data.randomizer.nayrusLoveUses = 0;
}
}
+17 -23
View File
@@ -1400,6 +1400,7 @@ void Settings::CreateOptions() {
OPT_BOOL(RSK_BUNNY_HOOD, CVAR_RANDOMIZER_SETTING("BunnyHood"));
OPT_BOOL(RSK_MASKS_AS_ADULT, CVAR_RANDOMIZER_SETTING("MasksAsAdult"));
OPT_BOOL(RSK_ROCS_FEATHER, CVAR_RANDOMIZER_SETTING("RocsFeather"));
OPT_U8(RSK_NAYRUS_LOVE_USES, {NumOpts(0, 5)}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("NayrusLoveUses"), WIDGET_CVAR_SLIDER_INT, 0);
OPT_U8(RSK_INFINITE_UPGRADES, {"Off", "Progressive", "Condensed Progressive"}, OptionCategory::Setting, CVAR_RANDOMIZER_SETTING("InfiniteUpgrades"));
OPT_BOOL(RSK_SKELETON_KEY, CVAR_RANDOMIZER_SETTING("SkeletonKey"));
OPT_BOOL(RSK_SLINGBOW_BREAK_BEEHIVES, CVAR_RANDOMIZER_SETTING("SlingBowBeehives"));
@@ -2109,29 +2110,21 @@ void Settings::CreateOptions() {
WidgetContainerType::SECTION);
mOptionGroups[RSG_MENU_COLUMN_SHOP_SHUFFLES] =
OptionGroup::SubGroup("", { &mOptionGroups[RSG_MENU_SECTION_SHOP_SHUFFLES] }, WidgetContainerType::COLUMN);
mOptionGroups[RSG_MENU_SECTION_ADDITIONAL_ITEMS] = OptionGroup::SubGroup("Additional Items",
{
&mOptions[RSK_SHUFFLE_CHILD_WALLET],
&mOptions[RSK_INCLUDE_TYCOON_WALLET],
&mOptions[RSK_SHUFFLE_FISHING_POLE],
&mOptions[RSK_SHUFFLE_DEKU_STICK_BAG],
&mOptions[RSK_SHUFFLE_DEKU_NUT_BAG],
&mOptions[RSK_SHUFFLE_OCARINA_BUTTONS],
&mOptions[RSK_SHUFFLE_SWIM],
&mOptions[RSK_SHUFFLE_GRAB],
&mOptions[RSK_SHUFFLE_CLIMB],
&mOptions[RSK_SHUFFLE_CRAWL],
&mOptions[RSK_SHUFFLE_SPEAK],
&mOptions[RSK_SHUFFLE_OPEN_CHEST],
&mOptions[RSK_SHUFFLE_BEAN_SOULS],
&mOptions[RSK_ROCS_FEATHER],
&mOptions[RSK_BOMBCHU_BAG],
&mOptions[RSK_ENABLE_BOMBCHU_DROPS],
&mOptions[RSK_PROGRESSIVE_GORON_SWORD],
&mOptions[RSK_INFINITE_UPGRADES],
&mOptions[RSK_SKELETON_KEY],
},
WidgetContainerType::SECTION);
mOptionGroups[RSG_MENU_SECTION_ADDITIONAL_ITEMS] =
OptionGroup::SubGroup("Additional Items",
{
&mOptions[RSK_SHUFFLE_CHILD_WALLET], &mOptions[RSK_INCLUDE_TYCOON_WALLET],
&mOptions[RSK_SHUFFLE_FISHING_POLE], &mOptions[RSK_SHUFFLE_DEKU_STICK_BAG],
&mOptions[RSK_SHUFFLE_DEKU_NUT_BAG], &mOptions[RSK_SHUFFLE_OCARINA_BUTTONS],
&mOptions[RSK_SHUFFLE_SWIM], &mOptions[RSK_SHUFFLE_GRAB],
&mOptions[RSK_SHUFFLE_CLIMB], &mOptions[RSK_SHUFFLE_CRAWL],
&mOptions[RSK_SHUFFLE_SPEAK], &mOptions[RSK_SHUFFLE_OPEN_CHEST],
&mOptions[RSK_SHUFFLE_BEAN_SOULS], &mOptions[RSK_ROCS_FEATHER],
&mOptions[RSK_NAYRUS_LOVE_USES], &mOptions[RSK_BOMBCHU_BAG],
&mOptions[RSK_ENABLE_BOMBCHU_DROPS], &mOptions[RSK_PROGRESSIVE_GORON_SWORD],
&mOptions[RSK_INFINITE_UPGRADES], &mOptions[RSK_SKELETON_KEY],
},
WidgetContainerType::SECTION);
mOptionGroups[RSG_MENU_COLUMN_ADDITIONAL_ITEMS] =
OptionGroup::SubGroup("", { &mOptionGroups[RSG_MENU_SECTION_ADDITIONAL_ITEMS] }, WidgetContainerType::COLUMN);
mOptionGroups[RSG_MENU_SIDEBAR_SHUFFLES] =
@@ -2321,6 +2314,7 @@ void Settings::CreateOptions() {
&mOptions[RSK_SHUFFLE_100_GS_REWARD],
&mOptions[RSK_SHUFFLE_BEAN_SOULS],
&mOptions[RSK_ROCS_FEATHER],
&mOptions[RSK_NAYRUS_LOVE_USES],
&mOptions[RSK_PROGRESSIVE_GORON_SWORD],
&mOptions[RSK_SHUFFLE_BOSS_SOULS],
&mOptions[RSK_SHUFFLE_DEKU_STICK_BAG],
+2
View File
@@ -303,6 +303,7 @@ void SaveManager::LoadRandomizer() {
SaveManager::Instance->LoadData("triforcePiecesCollected",
gSaveContext.ship.quest.data.randomizer.triforcePiecesCollected);
SaveManager::Instance->LoadData("bombchuUpgradeLevel", gSaveContext.ship.quest.data.randomizer.bombchuUpgradeLevel);
SaveManager::Instance->LoadData("nayrusLoveUses", gSaveContext.ship.quest.data.randomizer.nayrusLoveUses);
SaveManager::Instance->LoadData("silverShadowBlades", gSaveContext.ship.quest.data.randomizer.silverShadowBlades);
SaveManager::Instance->LoadData("silverShadowPit", gSaveContext.ship.quest.data.randomizer.silverShadowPit);
SaveManager::Instance->LoadData("silverShadowSpikes", gSaveContext.ship.quest.data.randomizer.silverShadowSpikes);
@@ -493,6 +494,7 @@ void SaveManager::SaveRandomizer(const SaveContext& saveContext, int sectionID,
SaveManager::Instance->SaveData("triforcePiecesCollected",
saveContext.ship.quest.data.randomizer.triforcePiecesCollected);
SaveManager::Instance->SaveData("bombchuUpgradeLevel", saveContext.ship.quest.data.randomizer.bombchuUpgradeLevel);
SaveManager::Instance->SaveData("nayrusLoveUses", saveContext.ship.quest.data.randomizer.nayrusLoveUses);
SaveManager::Instance->SaveData("silverShadowBlades", gSaveContext.ship.quest.data.randomizer.silverShadowBlades);
SaveManager::Instance->SaveData("silverShadowPit", gSaveContext.ship.quest.data.randomizer.silverShadowPit);
SaveManager::Instance->SaveData("silverShadowSpikes", gSaveContext.ship.quest.data.randomizer.silverShadowSpikes);
+3 -1
View File
@@ -4918,7 +4918,9 @@ void Interface_DrawAmmoCount(PlayState* play, s16 button, s16 alpha) {
i = ITEM_BOW;
}
ammo = AMMO(i);
if (GameInteractor_Should(VB_OVERRIDE_AMMO_COUNT, true, &i, &ammo)) {
ammo = AMMO(i);
}
gDPPipeSync(OVERLAY_DISP++);
@@ -31,8 +31,10 @@ extern const char* _gAmmoDigit0Tex[];
s8 ItemInSlotUsesAmmo(s16 slot) {
s16 item = gSaveContext.inventory.items[slot];
return item == ITEM_STICK || item == ITEM_NUT || item == ITEM_BOMB || item == ITEM_BOW || item == ITEM_SLINGSHOT ||
item == ITEM_BOMBCHU || item == ITEM_BEAN;
return GameInteractor_Should(VB_DRAW_AMMO_COUNT,
item == ITEM_STICK || item == ITEM_NUT || item == ITEM_BOMB || item == ITEM_BOW ||
item == ITEM_SLINGSHOT || item == ITEM_BOMBCHU || item == ITEM_BEAN,
&item);
}
void KaleidoScope_DrawAmmoCount(PauseContext* pauseCtx, GraphicsContext* gfxCtx, s16 item, int slot) {
@@ -45,7 +47,9 @@ void KaleidoScope_DrawAmmoCount(PauseContext* pauseCtx, GraphicsContext* gfxCtx,
OPEN_DISPS(gfxCtx);
ammo = AMMO(item);
if (GameInteractor_Should(VB_OVERRIDE_AMMO_COUNT, true, &item, &ammo)) {
ammo = AMMO(item);
}
gDPPipeSync(POLY_OPA_DISP++);