diff --git a/mm/2s2h/BenGui/BenMenu.cpp b/mm/2s2h/BenGui/BenMenu.cpp index ef420124f9..84b1a32d19 100644 --- a/mm/2s2h/BenGui/BenMenu.cpp +++ b/mm/2s2h/BenGui/BenMenu.cpp @@ -1024,6 +1024,10 @@ void BenMenu::AddEnhancements() { .CVar("gEnhancements.PlayerActions.ArrowCycle") .Options(CheckboxOptions().Tooltip( "While aiming the bow, use R to cycle between Normal, Fire, Ice and Light arrows.")); + AddWidget(path, "Bomb Arrows", WIDGET_CVAR_CHECKBOX) + .CVar("gEnhancements.Equipment.BombArrows") + .Options(CheckboxOptions().Tooltip( + "Allows equipping Bomb Arrows by equipping Bombs onto a bow button in the pause menu.")); AddWidget(path, "Remote Bombchu Control", WIDGET_CVAR_CHECKBOX) .CVar("gEnhancements.PlayerActions.RemoteBombchu") .Options(CheckboxOptions().Tooltip( diff --git a/mm/2s2h/Enhancements/Equipment/ArrowCycle.cpp b/mm/2s2h/Enhancements/Equipment/ArrowCycle.cpp index 1bae348ced..3c361abe01 100644 --- a/mm/2s2h/Enhancements/Equipment/ArrowCycle.cpp +++ b/mm/2s2h/Enhancements/Equipment/ArrowCycle.cpp @@ -13,8 +13,12 @@ s32 Player_UpperAction_7(Player* thisx, PlayState* play); s32 Player_UpperAction_8(Player* thisx, PlayState* play); } +extern bool IsBombArrowButton(s32 slot, bool isDpad); +extern void SetBombArrowButton(s32 slot, bool state, bool isDpad); + #define CVAR_NAME "gEnhancements.PlayerActions.ArrowCycle" #define CVAR CVarGetInteger(CVAR_NAME, 0) +#define BOMB_ARROW_CVAR_NAME "gEnhancements.Equipment.BombArrows" // Magic arrow costs based on z_player.c static const s16 sMagicArrowCosts[] = { 4, 4, 8 }; @@ -29,26 +33,30 @@ static s16 sButtonFlashTimer = 0; static s16 sButtonFlashCount = 0; static s8 sJustCycledFrames = 0; -// Arrow Type Definitions -static const PlayerItemAction ARROW_NORMAL = PLAYER_IA_BOW; -static const PlayerItemAction ARROW_FIRE = PLAYER_IA_BOW_FIRE; -static const PlayerItemAction ARROW_ICE = PLAYER_IA_BOW_ICE; -static const PlayerItemAction ARROW_LIGHT = PLAYER_IA_BOW_LIGHT; - -static const PlayerItemAction sArrowCycleOrder[] = { - ARROW_NORMAL, - ARROW_FIRE, - ARROW_ICE, - ARROW_LIGHT, +enum ArrowCycleType { + ARROW_CYCLE_NORMAL, + ARROW_CYCLE_FIRE, + ARROW_CYCLE_ICE, + ARROW_CYCLE_LIGHT, + ARROW_CYCLE_BOMB, + ARROW_CYCLE_MAX +}; + +static const PlayerItemAction sArrowCycleToPlayerIA[] = { + PLAYER_IA_BOW, // ARROW_CYCLE_NORMAL + PLAYER_IA_BOW_FIRE, // ARROW_CYCLE_FIRE + PLAYER_IA_BOW_ICE, // ARROW_CYCLE_ICE + PLAYER_IA_BOW_LIGHT, // ARROW_CYCLE_LIGHT + PLAYER_IA_BOW, // ARROW_CYCLE_BOMB }; // Utility Functions static bool IsHoldingBow(Player* player) { - return player->heldItemAction >= ARROW_NORMAL && player->heldItemAction <= ARROW_LIGHT; + return player->heldItemAction >= PLAYER_IA_BOW && player->heldItemAction <= PLAYER_IA_BOW_LIGHT; } static bool IsHoldingMagicBow(Player* player) { - return player->heldItemAction >= ARROW_FIRE && player->heldItemAction <= ARROW_LIGHT; + return player->heldItemAction >= PLAYER_IA_BOW_FIRE && player->heldItemAction <= PLAYER_IA_BOW_LIGHT; } static bool IsAimingBow(Player* player) { @@ -56,34 +64,65 @@ static bool IsAimingBow(Player* player) { (player->upperActionFunc == Player_UpperAction_7) /* Arrow pulled back on bow */); } -static bool HasArrowType(PlayerItemAction arrowType) { - switch (arrowType) { - case ARROW_NORMAL: +static bool IsBombArrowEnhancementEnabled() { + return CVarGetInteger(BOMB_ARROW_CVAR_NAME, 0); +} + +static bool HasBombArrows() { + return IsBombArrowEnhancementEnabled() && (INV_CONTENT(ITEM_BOMB) == ITEM_BOMB); +} + +static bool IsBombArrowButton(const Player* player) { + if (player->heldItemButton < 0) { + return false; + } + + if (IS_HELD_DPAD(player->heldItemButton)) { + s32 dpadSlot = HELD_ITEM_TO_DPAD(player->heldItemButton); + return IsBombArrowButton(dpadSlot, true); + } + + if (player->heldItemButton == EQUIP_SLOT_B) { + return false; + } + + return IsBombArrowButton(player->heldItemButton, false); +} + +static bool HasArrowType(ArrowCycleType cycleType) { + switch (cycleType) { + case ARROW_CYCLE_NORMAL: return true; - case ARROW_FIRE: + case ARROW_CYCLE_FIRE: return (INV_CONTENT(ITEM_ARROW_FIRE) == ITEM_ARROW_FIRE); - case ARROW_ICE: + case ARROW_CYCLE_ICE: return (INV_CONTENT(ITEM_ARROW_ICE) == ITEM_ARROW_ICE); - case ARROW_LIGHT: + case ARROW_CYCLE_LIGHT: return (INV_CONTENT(ITEM_ARROW_LIGHT) == ITEM_ARROW_LIGHT); + case ARROW_CYCLE_BOMB: + return HasBombArrows(); default: return false; } } -static s32 GetBowItemForArrow(PlayerItemAction arrowType) { - switch (arrowType) { - case ARROW_FIRE: +static s32 GetBowItemForCycleType(ArrowCycleType cycleType) { + switch (cycleType) { + case ARROW_CYCLE_FIRE: return ITEM_BOW_FIRE; - case ARROW_ICE: + case ARROW_CYCLE_ICE: return ITEM_BOW_ICE; - case ARROW_LIGHT: + case ARROW_CYCLE_LIGHT: return ITEM_BOW_LIGHT; default: return ITEM_BOW; } } +static s32 GetSlotForCycleType(ArrowCycleType cycleType) { + return cycleType == ARROW_CYCLE_BOMB ? SLOT_BOMB : SLOT_BOW; +} + static bool CanCycleArrows() { Player* player = GET_PLAYER(gPlayState); @@ -97,27 +136,37 @@ static bool CanCycleArrows() { return !gHorseIsMounted && player->rideActor == NULL && INV_CONTENT(SLOT_BOW) == ITEM_BOW && (INV_CONTENT(ITEM_ARROW_FIRE) == ITEM_ARROW_FIRE || INV_CONTENT(ITEM_ARROW_ICE) == ITEM_ARROW_ICE || - INV_CONTENT(ITEM_ARROW_LIGHT) == ITEM_ARROW_LIGHT); + INV_CONTENT(ITEM_ARROW_LIGHT) == ITEM_ARROW_LIGHT || HasBombArrows()); } // Arrow Cycling Logic -static s8 GetNextArrowType(s8 currentArrowType) { - int currentIndex = 0; - for (int i = 0; i < (int)ARRAY_COUNT(sArrowCycleOrder); i++) { - if (sArrowCycleOrder[i] == currentArrowType) { - currentIndex = i; - break; - } +static ArrowCycleType GetCurrentCycleType(Player* player) { + if (IsBombArrowButton(player)) { + return ARROW_CYCLE_BOMB; } + switch (player->heldItemAction) { + case PLAYER_IA_BOW_FIRE: + return ARROW_CYCLE_FIRE; + case PLAYER_IA_BOW_ICE: + return ARROW_CYCLE_ICE; + case PLAYER_IA_BOW_LIGHT: + return ARROW_CYCLE_LIGHT; + default: + return ARROW_CYCLE_NORMAL; + } +} - for (int offset = 1; offset <= (int)ARRAY_COUNT(sArrowCycleOrder); offset++) { - int nextIndex = (currentIndex + offset) % ARRAY_COUNT(sArrowCycleOrder); - if (HasArrowType(sArrowCycleOrder[nextIndex])) { - return sArrowCycleOrder[nextIndex]; +static ArrowCycleType GetNextArrowCycleType(Player* player) { + ArrowCycleType current = GetCurrentCycleType(player); + + for (int offset = 1; offset < ARROW_CYCLE_MAX; offset++) { + ArrowCycleType next = static_cast((current + offset) % ARROW_CYCLE_MAX); + if (HasArrowType(next)) { + return next; } } - return ARROW_NORMAL; + return ARROW_CYCLE_NORMAL; } // UI Update Functions @@ -188,19 +237,22 @@ static void UpdateFlashEffect(PlayState* play) { &play->interfaceCtx.shipInterface.dpad.dUpAlpha); } -static void UpdateEquippedBow(PlayState* play, s8 arrowType) { - s32 bowItem = GetBowItemForArrow(static_cast(arrowType)); +static void UpdateEquippedBow(PlayState* play, ArrowCycleType cycleType) { + s32 bowItem = GetBowItemForCycleType(cycleType); + s32 slot = GetSlotForCycleType(cycleType); // Update C-buttons for (s32 i = EQUIP_SLOT_C_LEFT; i <= EQUIP_SLOT_C_RIGHT; i++) { if ((BUTTON_ITEM_EQUIP(0, i) == ITEM_BOW) || (BUTTON_ITEM_EQUIP(0, i) >= ITEM_BOW_FIRE && BUTTON_ITEM_EQUIP(0, i) <= ITEM_BOW_LIGHT)) { BUTTON_ITEM_EQUIP(0, i) = bowItem; - C_SLOT_EQUIP(0, i) = SLOT_BOW; + C_SLOT_EQUIP(0, i) = slot; Interface_LoadItemIcon(play, i); gSaveContext.buttonStatus[i] = BTN_ENABLED; sButtonFlashTimer = BUTTON_FLASH_DURATION; sButtonFlashCount = 0; + + SetBombArrowButton(i, cycleType == ARROW_CYCLE_BOMB, false); } } @@ -209,11 +261,13 @@ static void UpdateEquippedBow(PlayState* play, s8 arrowType) { if ((DPAD_BUTTON_ITEM_EQUIP(0, i) == ITEM_BOW) || (DPAD_BUTTON_ITEM_EQUIP(0, i) >= ITEM_BOW_FIRE && DPAD_BUTTON_ITEM_EQUIP(0, i) <= ITEM_BOW_LIGHT)) { DPAD_BUTTON_ITEM_EQUIP(0, i) = bowItem; - DPAD_SLOT_EQUIP(0, i) = SLOT_BOW; + DPAD_SLOT_EQUIP(0, i) = slot; Interface_Dpad_LoadItemIcon(play, i); gSaveContext.shipSaveContext.dpad.status[i] = BTN_ENABLED; sButtonFlashTimer = BUTTON_FLASH_DURATION; sButtonFlashCount = 0; + + SetBombArrowButton(i, cycleType == ARROW_CYCLE_BOMB, true); } } @@ -222,7 +276,8 @@ static void UpdateEquippedBow(PlayState* play, s8 arrowType) { // Core Arrow Cycling Function static void CycleToNextArrow(PlayState* play, Player* player) { - s8 nextArrow = GetNextArrowType(player->heldItemAction); + ArrowCycleType nextType = GetNextArrowCycleType(player); + PlayerItemAction nextIA = sArrowCycleToPlayerIA[nextType]; if (player->heldActor != NULL && player->heldActor->id == ACTOR_EN_ARROW) { EnArrow* arrow = (EnArrow*)player->heldActor; @@ -234,8 +289,8 @@ static void CycleToNextArrow(PlayState* play, Player* player) { Actor_Kill(&arrow->actor); } - Player_InitItemAction(play, player, static_cast(nextArrow)); - UpdateEquippedBow(play, nextArrow); + Player_InitItemAction(play, player, nextIA); + UpdateEquippedBow(play, nextType); Audio_PlaySfx(NA_SE_PL_CHANGE_ARMS); } diff --git a/mm/2s2h/Enhancements/Equipment/BombArrows.cpp b/mm/2s2h/Enhancements/Equipment/BombArrows.cpp new file mode 100644 index 0000000000..2e547de427 --- /dev/null +++ b/mm/2s2h/Enhancements/Equipment/BombArrows.cpp @@ -0,0 +1,1134 @@ +#include +#include "2s2h/BenGui/HudEditor.h" +#include "2s2h/Enhancements/FrameInterpolation/FrameInterpolation.h" +#include "2s2h/GameInteractor/GameInteractor.h" +#include "2s2h/ObjectExtension/ObjectExtension.h" +#include "2s2h/ShipInit.hpp" + +extern "C" { +#include "archives/icon_item_static/icon_item_static_yar.h" +#include "assets/objects/object_gi_bomb_1/object_gi_bomb_1.h" +#include "overlays/actors/ovl_En_Arrow/z_en_arrow.h" +#include "overlays/actors/ovl_En_Bom/z_en_bom.h" +#include "overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_scope.h" +Gfx* ResourceMgr_LoadGfxByName(const char* path); +extern s16 sEquipState; +extern s16 sEquipAnimTimer; +extern const char* gAmmoDigitTextures[10]; +} + +#define CVAR_NAME "gEnhancements.Equipment.BombArrows" +#define CVAR CVarGetInteger(CVAR_NAME, 0) + +#define BOMB_ARROW_FLAG (1 << 0) +#define BOMB_ARROW_IS_SET(arrow) (((arrow)->actor.home.rot.x & BOMB_ARROW_FLAG) != 0) +#define BOMB_ARROW_SET(arrow) ((arrow)->actor.home.rot.x |= BOMB_ARROW_FLAG) + +struct BombArrowData { + std::vector clonedGfx; + s32 primIdx = -1; + s32 envIdx = -1; + f32 flashBrightness = 0.0f; + s16 timer = 70; + s16 flashSpeedScale = 7; +}; + +static ObjectExtension::Register BombArrowDataRegister; + +bool sBombSlotIsBombArrowMode = false; + +static bool sInventorySwapActive = false; +static int sBombPrevKaleidoCursorSlot = SLOT_NONE; + +struct PendingBombArrowEquip { + s32 targetBtn; + bool isDpad; + s32 newEquipSlot; + ItemId newItem; + bool isBowGroup; + bool isBombArrow; +}; + +static PendingBombArrowEquip sPendingEquip = {}; +static bool sHasPendingEquip = false; + +static Vtx sBombCycleExtraItemVtx[] = { + // Left item (unused in 2-item mode; kept for layout parity) + VTX(-48, 16, 0, 0 << 5, 0 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(-16, 16, 0, 32 << 5, 0 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(-48, -16, 0, 0 << 5, 32 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(-16, -16, 0, 32 << 5, 32 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + // Right item + VTX(16, 16, 0, 0 << 5, 0 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(48, 16, 0, 32 << 5, 0 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(16, -16, 0, 0 << 5, 32 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(48, -16, 0, 32 << 5, 32 << 5, 0xFF, 0xFF, 0xFF, 0xFF), +}; + +static Vtx sBombCycleAButtonVtx[] = { + VTX(-18, 12, 0, 0 << 5, 0 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(18, 12, 0, 24 << 5, 0 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(-18, -12, 0, 0 << 5, 16 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(18, -12, 0, 24 << 5, 16 << 5, 0xFF, 0xFF, 0xFF, 0xFF), +}; + +// 14x14 units (50% of 28-unit kaleido grid quad), matching HUD overlay ratio. +static Vtx sBombOverlayVtx[] = { + VTX(-7, 7, 0, 0 << 5, 0 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(7, 7, 0, 32 << 5, 0 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(-7, -7, 0, 0 << 5, 32 << 5, 0xFF, 0xFF, 0xFF, 0xFF), + VTX(7, -7, 0, 32 << 5, 32 << 5, 0xFF, 0xFF, 0xFF, 0xFF), +}; + +static const s16 sBCButtonXPositions[] = { + 167, // EQUIP_SLOT_B + 227, // EQUIP_SLOT_C_LEFT + 249, // EQUIP_SLOT_C_DOWN + 271, // EQUIP_SLOT_C_RIGHT +}; + +static const s16 sBCButtonYPositions[] = { + 17, // EQUIP_SLOT_B + 18, // EQUIP_SLOT_C_LEFT + 34, // EQUIP_SLOT_C_DOWN + 18, // EQUIP_SLOT_C_RIGHT +}; + +static const s16 sItemIconTextureDimensions[] = { + 30, // EQUIP_SLOT_B + 24, // EQUIP_SLOT_C_LEFT + 24, // EQUIP_SLOT_C_DOWN + 24, // EQUIP_SLOT_C_RIGHT +}; + +static const s16 sDpadItemIconLeft[] = { + 295, // EQUIP_SLOT_D_RIGHT + 263, // EQUIP_SLOT_D_LEFT + 279, // EQUIP_SLOT_D_DOWN + 279, // EQUIP_SLOT_D_UP +}; + +static const s16 sDpadItemIconTop[] = { + 63, // EQUIP_SLOT_D_RIGHT + 63, // EQUIP_SLOT_D_LEFT + 79, // EQUIP_SLOT_D_DOWN + 47, // EQUIP_SLOT_D_UP +}; + +static bool IsBowItem(ItemId item) { + return (item == ITEM_BOW) || ((item >= ITEM_BOW_FIRE) && (item <= ITEM_BOW_LIGHT)); +} + +static bool ApplyHudEditorAdjustments(HudEditorElementID element, s16* rectLeft, s16* rectTop, s16* rectWidth, + s16* rectHeight, s16* dsdx, s16* dtdy) { + HudEditor_SetActiveElement(element); + if (HudEditor_ShouldOverrideDraw()) { + if (HudEditor_IsActiveElementHidden()) { + return false; + } + HudEditor_ModifyDrawValues(rectLeft, rectTop, rectWidth, rectHeight, dsdx, dtdy); + } + return true; +} + +static bool ResolvePauseEquipTarget(const PauseContext* pauseCtx, s32* targetSlot, bool* isDpad) { + switch (pauseCtx->equipTargetCBtn) { + case PAUSE_EQUIP_C_LEFT: + *targetSlot = EQUIP_SLOT_C_LEFT; + *isDpad = false; + return true; + case PAUSE_EQUIP_C_DOWN: + *targetSlot = EQUIP_SLOT_C_DOWN; + *isDpad = false; + return true; + case PAUSE_EQUIP_C_RIGHT: + *targetSlot = EQUIP_SLOT_C_RIGHT; + *isDpad = false; + return true; + case PAUSE_EQUIP_D_RIGHT: + *targetSlot = EQUIP_SLOT_D_RIGHT; + *isDpad = true; + return true; + case PAUSE_EQUIP_D_LEFT: + *targetSlot = EQUIP_SLOT_D_LEFT; + *isDpad = true; + return true; + case PAUSE_EQUIP_D_DOWN: + *targetSlot = EQUIP_SLOT_D_DOWN; + *isDpad = true; + return true; + case PAUSE_EQUIP_D_UP: + *targetSlot = EQUIP_SLOT_D_UP; + *isDpad = true; + return true; + default: + return false; + } +} + +static bool CanCycleBombSlot() { + return INV_CONTENT(ITEM_BOW) == ITEM_BOW && INV_CONTENT(ITEM_BOMB) == ITEM_BOMB; +} + +bool IsBombArrowButton(s32 slot, bool isDpad) { + if (slot < 0 || slot >= 8) + return false; + s32 effectiveSlot = isDpad ? (slot + 4) : slot; + return (gSaveContext.save.shipSaveInfo.bombArrowsEquipped & (1 << effectiveSlot)) != 0; +} + +void SetBombArrowButton(s32 slot, bool state, bool isDpad) { + if (slot < 0 || slot >= 8) + return; + s32 effectiveSlot = isDpad ? (slot + 4) : slot; + if (state) { + gSaveContext.save.shipSaveInfo.bombArrowsEquipped |= (1 << effectiveSlot); + } else { + gSaveContext.save.shipSaveInfo.bombArrowsEquipped &= ~(1 << effectiveSlot); + } +} + +static void GetSlotItem(s32 slot, bool isDpad, ItemId* item, s32* equipSlot) { + if (isDpad) { + if (item) + *item = (ItemId)DPAD_BUTTON_ITEM_EQUIP(0, slot); + if (equipSlot) + *equipSlot = DPAD_SLOT_EQUIP(0, slot); + } else { + if (item) + *item = (ItemId)BUTTON_ITEM_EQUIP(0, slot); + if (equipSlot) + *equipSlot = C_SLOT_EQUIP(0, slot); + } +} + +static void SetSlotItem(s32 slot, bool isDpad, ItemId item, s32 equipSlot) { + if (isDpad) { + DPAD_BUTTON_ITEM_EQUIP(0, slot) = item; + DPAD_SLOT_EQUIP(0, slot) = equipSlot; + Interface_Dpad_LoadItemIcon(gPlayState, slot); + } else { + BUTTON_ITEM_EQUIP(0, slot) = item; + C_SLOT_EQUIP(0, slot) = equipSlot; + Interface_LoadItemIcon(gPlayState, slot); + } +} + +static bool FindSlotWithItem(s32 itemEquipSlot, s32* outSlot, bool* outIsDpad, s32 ignoreSlot = -1, + bool ignoreIsDpad = false) { + // C-buttons + for (int i = EQUIP_SLOT_C_LEFT; i <= EQUIP_SLOT_C_RIGHT; i++) { + if (!ignoreIsDpad && i == ignoreSlot) + continue; + + if (C_SLOT_EQUIP(0, i) == itemEquipSlot) { + *outSlot = i; + *outIsDpad = false; + return true; + } + } + + // D-pad + for (int i = EQUIP_SLOT_D_RIGHT; i <= EQUIP_SLOT_D_UP; i++) { + if (ignoreIsDpad && i == ignoreSlot) + continue; + + if (DPAD_SLOT_EQUIP(0, i) == itemEquipSlot) { + *outSlot = i; + *outIsDpad = true; + return true; + } + } + + return false; +} + +static bool FindEquippedBowType(s32* outSlot, bool* outIsDpad, s32 ignoreSlot = -1, bool ignoreIsDpad = false) { + for (int i = 0; i < 7; i++) { + bool isDpad = (i >= 3); + s32 slotIndex = isDpad ? (i - 3) : (i + 1); + + if (isDpad == ignoreIsDpad && slotIndex == ignoreSlot) + continue; + + s32 equippedSlot = isDpad ? DPAD_GET_CUR_FORM_BTN_SLOT(slotIndex) : GET_CUR_FORM_BTN_SLOT(slotIndex); + if (equippedSlot == SLOT_BOW || (equippedSlot == SLOT_BOMB && IsBombArrowButton(slotIndex, isDpad))) { + *outSlot = slotIndex; + *outIsDpad = isDpad; + return true; + } + } + return false; +} + +static void HandleSmartSwap(s32 targetBtn, bool targetIsDpad, s32 newEquipSlot, bool isBowType) { + s32 dupBtn; + bool dupIsDpad; + bool found = false; + + if (isBowType) { + found = FindEquippedBowType(&dupBtn, &dupIsDpad, targetBtn, targetIsDpad); + } else { + found = FindSlotWithItem(newEquipSlot, &dupBtn, &dupIsDpad, targetBtn, targetIsDpad); + } + + if (found) { + s32 dupSlot = dupIsDpad ? DPAD_GET_CUR_FORM_BTN_SLOT(dupBtn) : GET_CUR_FORM_BTN_SLOT(dupBtn); + bool isSameItemSlot = (dupSlot == newEquipSlot); + + if (isSameItemSlot) { + ItemId targetItem; + s32 targetEquipSlot; + GetSlotItem(targetBtn, targetIsDpad, &targetItem, &targetEquipSlot); + bool targetWasBombArrow = IsBombArrowButton(targetBtn, targetIsDpad); + + SetSlotItem(dupBtn, dupIsDpad, targetItem, targetEquipSlot); + SetBombArrowButton(dupBtn, targetWasBombArrow, dupIsDpad); + } else { + // Exclusivity clear within the same group (e.g., Bow vs Magic Arrow). + SetSlotItem(dupBtn, dupIsDpad, ITEM_NONE, SLOT_NONE); + SetBombArrowButton(dupBtn, false, dupIsDpad); + } + } +} + +static void InitiateEquipAnimation(PauseContext* pauseCtx, ItemId item, s32 slot) { + pauseCtx->equipTargetItem = item; + pauseCtx->equipTargetSlot = slot; + + pauseCtx->mainState = PAUSE_MAIN_STATE_EQUIP_ITEM; + u16 vtxIndex = pauseCtx->cursorSlot[pauseCtx->pageIndex] * 4; + pauseCtx->equipAnimX = pauseCtx->itemVtx[vtxIndex].v.ob[0] * 10; + pauseCtx->equipAnimY = pauseCtx->itemVtx[vtxIndex].v.ob[1] * 10; + pauseCtx->equipAnimAlpha = 255; + sEquipState = EQUIP_STATE_MOVE_TO_C_BTN; + sEquipAnimTimer = 10; +} + +static void HandleBombArrowEquip(u16 cursorItem, bool* should, PauseContext* pauseCtx) { + s32 targetBtn = -1; + bool isDpad = false; + + if (!ResolvePauseEquipTarget(pauseCtx, &targetBtn, &isDpad)) { + return; + } + + ItemId equippedItem; + GetSlotItem(targetBtn, isDpad, &equippedItem, nullptr); + bool equippedIsBombArrow = IsBombArrowButton(targetBtn, isDpad); + + if (equippedItem == cursorItem || (equippedItem == ITEM_BOW && cursorItem == ITEM_BOMB && equippedIsBombArrow)) { + if (cursorItem == ITEM_BOW && equippedIsBombArrow) { + *should = false; + sPendingEquip = { targetBtn, isDpad, SLOT_BOW, ITEM_BOW, true, false }; + sHasPendingEquip = true; + InitiateEquipAnimation(pauseCtx, ITEM_BOW, SLOT_BOW); + Audio_PlaySfx(NA_SE_SY_DECIDE); + return; + } else if (cursorItem != ITEM_BOMB && equippedIsBombArrow) { + *should = false; + return; + } + } + + // Magic arrow unequip logic + if (IsBowItem(equippedItem)) { + if (cursorItem == ITEM_ARROW_FIRE && equippedItem == ITEM_BOW_FIRE) + return; + if (cursorItem == ITEM_ARROW_ICE && equippedItem == ITEM_BOW_ICE) + return; + if (cursorItem == ITEM_ARROW_LIGHT && equippedItem == ITEM_BOW_LIGHT) + return; + if (cursorItem == ITEM_BOW && + (equippedItem == ITEM_BOW_FIRE || equippedItem == ITEM_BOW_ICE || equippedItem == ITEM_BOW_LIGHT)) + return; + } + + // Equipping Magic Arrow, Bomb Arrow, or regular Bow + bool isMagicArrow = (cursorItem >= ITEM_ARROW_FIRE && cursorItem <= ITEM_ARROW_LIGHT); + bool isBombArrow = (cursorItem == ITEM_BOMB && sBombSlotIsBombArrowMode); + bool isRegularBow = (cursorItem == ITEM_BOW); + + if (isMagicArrow || isBombArrow || isRegularBow) { + *should = false; + + ItemId finalAnimItem = ITEM_BOW; + s32 finalEquipSlot = SLOT_BOW; + + if (isMagicArrow) { + // Use vanilla magic arrow "item action" IDs for the animation sequence. + finalAnimItem = (ItemId)(0xB5 + (cursorItem - ITEM_ARROW_FIRE)); + } + + bool isBowGroup = true; + + // Defer swap/state changes until the equip animation reaches the button. + sPendingEquip = { targetBtn, isDpad, finalEquipSlot, finalAnimItem, isBowGroup, isBombArrow }; + sHasPendingEquip = true; + + InitiateEquipAnimation(pauseCtx, finalAnimItem, finalEquipSlot); + + if (isMagicArrow) { + // Magic arrows have a special 2-part animation. + pauseCtx->equipAnimAlpha = sEquipState = 0; // EQUIP_STATE_MAGIC_ARROW_GROW_ORB + sEquipAnimTimer = 6; + Audio_PlaySfx(NA_SE_SY_SET_FIRE_ARROW + (cursorItem - ITEM_ARROW_FIRE)); + } else { + Audio_PlaySfx(NA_SE_SY_DECIDE); + } + return; + } + + // Equipping bombs normally + if (cursorItem == ITEM_BOMB) { + *should = false; + + sPendingEquip = { targetBtn, isDpad, SLOT_BOMB, ITEM_BOMB, false, false }; + sHasPendingEquip = true; + InitiateEquipAnimation(pauseCtx, ITEM_BOMB, SLOT_BOMB); + Audio_PlaySfx(NA_SE_SY_DECIDE); + return; + } +} + +static void DrawBombArrowCycleExtras(PlayState* play, bool canCycle, ItemId alternateItem) { + PauseContext* pauseCtx = &play->pauseCtx; + u8 slot = SLOT_BOMB; + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL42_Opa(play->state.gfxCtx); + gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); + + if (canCycle && alternateItem != ITEM_NONE) { + Matrix_Push(); + + Vtx* itemTopLeft = &pauseCtx->itemVtx[slot * 4]; + Vtx* itemBottomRight = &itemTopLeft[3]; + + s16 halfX = (itemBottomRight->v.ob[0] - itemTopLeft->v.ob[0]) / 2; + s16 halfY = (itemBottomRight->v.ob[1] - itemTopLeft->v.ob[1]) / 2; + + Matrix_Translate(itemTopLeft->v.ob[0] + halfX, itemTopLeft->v.ob[1] + halfY, 0, MTXMODE_APPLY); + + // Rest position: 32.5% scale, 15 units below the main icon. + Matrix_Translate(0, -15.0f, 0, MTXMODE_APPLY); + Matrix_Scale(0.325f, 0.325f, 1.0f, MTXMODE_APPLY); + + MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, play->state.gfxCtx); + + // Show A-button prompt when hovered. + if (pauseCtx->cursorSlot[PAUSE_ITEM] == slot && pauseCtx->cursorSpecialPos == 0) { + Color_RGB8 aButtonColor = { 0, 100, 255 }; + + gSPVertex(POLY_OPA_DISP++, (uintptr_t)sBombCycleAButtonVtx, 4, 0); + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, aButtonColor.r, aButtonColor.g, aButtonColor.b, pauseCtx->alpha); + gDPLoadTextureBlock(POLY_OPA_DISP++, gABtnSymbolTex, G_IM_FMT_IA, G_IM_SIZ_8b, 24, 16, 0, + G_TX_NOMIRROR | G_TX_CLAMP, G_TX_NOMIRROR | G_TX_CLAMP, 4, 4, G_TX_NOLOD, G_TX_NOLOD); + gSP1Quadrangle(POLY_OPA_DISP++, 0, 2, 3, 1, 0); + } + + // Draw the right alternate item. + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, pauseCtx->alpha); + gSPVertex(POLY_OPA_DISP++, (uintptr_t)sBombCycleExtraItemVtx, 8, 0); + KaleidoScope_DrawTexQuadRGBA32(play->state.gfxCtx, gItemIcons[alternateItem], 32, 32, 4); + + // If the alternate is the bow (bomb arrows), overlay a bomb icon on the preview. + if (alternateItem == ITEM_BOW) { + Matrix_Push(); + Matrix_Translate(32.0f + 3.0f, 0.0f + 3.0f, 0, MTXMODE_APPLY); + Matrix_Scale(16.0f / 14.0f, 16.0f / 14.0f, 1.0f, MTXMODE_APPLY); + MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, play->state.gfxCtx); + gSPVertex(POLY_OPA_DISP++, (uintptr_t)sBombOverlayVtx, 4, 0); + KaleidoScope_DrawTexQuadRGBA32(play->state.gfxCtx, gItemIcons[ITEM_BOMB], 32, 32, 0); + Matrix_Pop(); + } + + Matrix_Pop(); + } + + CLOSE_DISPS(play->state.gfxCtx); +} + +static void DrawBombArrowSlotIcon(PlayState* play) { + PauseContext* pauseCtx = &play->pauseCtx; + u8 slot = SLOT_BOMB; + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL42_Opa(play->state.gfxCtx); + gDPSetCombineMode(POLY_OPA_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, pauseCtx->alpha); + + Matrix_Push(); + + Vtx* itemTopLeft = &pauseCtx->itemVtx[slot * 4]; + Vtx* itemBottomRight = &itemTopLeft[3]; + + s16 centerX = itemTopLeft->v.ob[0] + (itemBottomRight->v.ob[0] - itemTopLeft->v.ob[0]) / 2; + s16 centerY = itemTopLeft->v.ob[1] + (itemBottomRight->v.ob[1] - itemTopLeft->v.ob[1]) / 2; + Matrix_Translate(centerX + 2, centerY + 2, 1, MTXMODE_APPLY); + + // Scaling the Bomb Arrow icon when it is being hovered + if (pauseCtx->cursorSlot[PAUSE_ITEM] == slot && pauseCtx->pageIndex == PAUSE_ITEM && + pauseCtx->mainState == PAUSE_MAIN_STATE_IDLE && pauseCtx->cursorSpecialPos == 0) { + Matrix_Scale(1.1f, 1.1f, 1.0f, MTXMODE_APPLY); + } + + MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, play->state.gfxCtx); + + gSPVertex(POLY_OPA_DISP++, (uintptr_t)sBombOverlayVtx, 4, 0); + KaleidoScope_DrawTexQuadRGBA32(play->state.gfxCtx, gItemIcons[ITEM_BOMB], 32, 32, 0); + + Matrix_Pop(); + + CLOSE_DISPS(play->state.gfxCtx); +} + +static void HandleSlotCycling(PauseContext* pauseCtx) { + InterfaceContext* interfaceCtx = &gPlayState->interfaceCtx; + + if (pauseCtx->state != PAUSE_STATE_MAIN) { + sBombPrevKaleidoCursorSlot = SLOT_NONE; + return; + } + + u16 slot = pauseCtx->cursorSlot[PAUSE_ITEM]; + + if ((pauseCtx->debugEditor != DEBUG_EDITOR_NONE) || (pauseCtx->mainState != PAUSE_MAIN_STATE_IDLE) || + slot != SLOT_BOMB || !CanCycleBombSlot()) { + if (sBombPrevKaleidoCursorSlot == SLOT_BOMB) { + // Restore A-button action when leaving the bomb slot. + if (interfaceCtx->aButtonDoActionDelayed != DO_ACTION_INFO) { + Interface_SetAButtonDoAction(gPlayState, DO_ACTION_INFO); + } + } + + sBombPrevKaleidoCursorSlot = slot; + return; + } + + // Cursor is on SLOT_BOMB and cycling is available; toggle on A press. + if (CHECK_BTN_ALL(CONTROLLER1(&gPlayState->state)->press.button, BTN_A)) { + Audio_PlaySfx(NA_SE_SY_DECIDE); + sBombSlotIsBombArrowMode = !sBombSlotIsBombArrowMode; + } + + if (interfaceCtx->aButtonDoActionDelayed != DO_ACTION_DECIDE) { + Interface_SetAButtonDoAction(gPlayState, DO_ACTION_DECIDE); + } + if (gSaveContext.buttonStatus[EQUIP_SLOT_A] != BTN_ENABLED) { + gSaveContext.buttonStatus[EQUIP_SLOT_A] = BTN_ENABLED; + gSaveContext.hudVisibility = HUD_VISIBILITY_IDLE; + Interface_SetHudVisibility(HUD_VISIBILITY_ALL); + } + + sBombPrevKaleidoCursorSlot = slot; +} + +static void HandleEquipCleanup(PauseContext* pauseCtx) { + // Only run when the pause menu is idle. + if ((pauseCtx->state == PAUSE_STATE_MAIN) && (pauseCtx->mainState == PAUSE_MAIN_STATE_IDLE)) { + for (int i = EQUIP_SLOT_C_LEFT; i <= EQUIP_SLOT_C_RIGHT; i++) { + ItemId item = ITEM_NONE; + GetSlotItem(i, false, &item, nullptr); + if (!IsBowItem(item) && !(item == ITEM_BOMB && IsBombArrowButton(i, false))) + SetBombArrowButton(i, false, false); + } + for (int i = EQUIP_SLOT_D_RIGHT; i <= EQUIP_SLOT_D_UP; i++) { + ItemId item = ITEM_NONE; + GetSlotItem(i, true, &item, nullptr); + if (!IsBowItem(item) && !(item == ITEM_BOMB && IsBombArrowButton(i, true))) + SetBombArrowButton(i, false, true); + } + } +} + +static void DrawBombArrowAmmoCount(PlayState* play, s16 button, s16 alpha, bool isDpad) { + if (!IsBombArrowButton(button, isDpad)) { + return; + } + + s16 arrows = AMMO(ITEM_BOW); + s16 bombs = AMMO(ITEM_BOMB); + s16 maxArrows = CUR_CAPACITY(UPG_QUIVER); + s16 maxBombs = CUR_CAPACITY(UPG_BOMB_BAG); + + s16 effectiveAmmo = (arrows < bombs) ? arrows : bombs; + s16 effectiveMax = (maxArrows < maxBombs) ? maxArrows : maxBombs; + + s16 ammo = effectiveAmmo; + bool turnGreen = (effectiveAmmo == effectiveMax); + + OPEN_DISPS(play->state.gfxCtx); + + if (ammo == 0) { + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 100, 100, 100, alpha); + } else if (turnGreen) { + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 120, 255, 0, alpha); + } + + s16 tens = 0; + for (tens = 0; ammo >= 10; tens++) { + ammo -= 10; + } + + static s16 sAmmoDigitsXPositions[] = { 162, 228, 250, 272 }; + static s16 sAmmoDigitsYPositions[] = { 35, 35, 51, 35 }; + + static s16 sDpadItemAmmoX[] = { + 295, // EQUIP_SLOT_D_RIGHT + 263, // EQUIP_SLOT_D_LEFT + 279, // EQUIP_SLOT_D_DOWN + 279, // EQUIP_SLOT_D_UP + }; + + static s16 sDpadItemAmmoY[] = { + 63 + 11, // EQUIP_SLOT_D_RIGHT + 63 + 11, // EQUIP_SLOT_D_LEFT + 79 + 11, // EQUIP_SLOT_D_DOWN + 47 + 11, // EQUIP_SLOT_D_UP + }; + + s16 x = isDpad ? sDpadItemAmmoX[button] : sAmmoDigitsXPositions[button]; + s16 y = isDpad ? sDpadItemAmmoY[button] : sAmmoDigitsYPositions[button]; + HudEditorElementID elementId = isDpad ? HUD_EDITOR_ELEMENT_D_PAD : (HudEditorElementID)button; + + if (tens != 0) { + HudEditor_SetActiveElement(elementId); + OVERLAY_DISP = Gfx_DrawTexRectIA8(OVERLAY_DISP, (TexturePtr)gAmmoDigitTextures[tens], AMMO_DIGIT_TEX_WIDTH, + AMMO_DIGIT_TEX_HEIGHT, x, y, AMMO_DIGIT_TEX_WIDTH, AMMO_DIGIT_TEX_HEIGHT, + 1 << 10, 1 << 10); + } + + HudEditor_SetActiveElement(elementId); + OVERLAY_DISP = Gfx_DrawTexRectIA8(OVERLAY_DISP, (TexturePtr)gAmmoDigitTextures[ammo], AMMO_DIGIT_TEX_WIDTH, + AMMO_DIGIT_TEX_HEIGHT, x + 6, y, AMMO_DIGIT_TEX_WIDTH, AMMO_DIGIT_TEX_HEIGHT, + 1 << 10, 1 << 10); + + CLOSE_DISPS(play->state.gfxCtx); +} + +static void DrawBombArrowOverlayCButton(PlayState* play, EquipSlot slot, s16 alpha) { + s16 rectLeft = sBCButtonXPositions[slot]; + s16 rectTop = sBCButtonYPositions[slot]; + s16 baseSize = sItemIconTextureDimensions[slot]; + s16 overlaySize = 12; + s16 rectWidth = overlaySize; + s16 rectHeight = overlaySize; + s16 overlayLeft = rectLeft + (baseSize - overlaySize) / 2 + 2; + s16 overlayTop = rectTop + (baseSize - overlaySize) / 2 - 2; + s16 dsdx = ((ICON_ITEM_TEX_WIDTH << 10) / rectWidth) >> 1; + s16 dtdy = ((ICON_ITEM_TEX_HEIGHT << 10) / rectHeight) >> 1; + + if (!ApplyHudEditorAdjustments((HudEditorElementID)slot, &overlayLeft, &overlayTop, &rectWidth, &rectHeight, &dsdx, + &dtdy)) { + return; + } + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL39_Overlay(play->state.gfxCtx); + + gDPPipeSync(OVERLAY_DISP++); + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, alpha); + gDPSetCombineMode(OVERLAY_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); + + gDPLoadTextureBlock(OVERLAY_DISP++, gItemIcons[ITEM_BOMB], G_IM_FMT_RGBA, G_IM_SIZ_32b, ICON_ITEM_TEX_WIDTH, + ICON_ITEM_TEX_HEIGHT, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, + G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + + gSPTextureRectangle(OVERLAY_DISP++, overlayLeft << 2, overlayTop << 2, (overlayLeft + rectWidth) << 2, + (overlayTop + rectHeight) << 2, G_TX_RENDERTILE, 0, 0, dsdx << 1, dtdy << 1); + + CLOSE_DISPS(play->state.gfxCtx); +} + +static void DrawBombArrowOverlayDpad(PlayState* play, DpadEquipSlot slot, s16 alpha) { + s16 rectLeft = sDpadItemIconLeft[slot]; + s16 rectTop = sDpadItemIconTop[slot]; + s16 baseSize = 16; + s16 overlaySize = 8; + s16 rectWidth = overlaySize; + s16 rectHeight = overlaySize; + s16 overlayLeft = rectLeft + (baseSize - overlaySize) / 2 + 2; + s16 overlayTop = rectTop + (baseSize - overlaySize) / 2 - 2; + s16 dsdx = ((ICON_ITEM_TEX_WIDTH << 10) / rectWidth) >> 1; + s16 dtdy = ((ICON_ITEM_TEX_HEIGHT << 10) / rectHeight) >> 1; + + if (!ApplyHudEditorAdjustments(HUD_EDITOR_ELEMENT_D_PAD, &overlayLeft, &overlayTop, &rectWidth, &rectHeight, &dsdx, + &dtdy)) { + return; + } + + OPEN_DISPS(play->state.gfxCtx); + + Gfx_SetupDL39_Overlay(play->state.gfxCtx); + + gDPPipeSync(OVERLAY_DISP++); + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, alpha); + gDPSetCombineMode(OVERLAY_DISP++, G_CC_MODULATEIA_PRIM, G_CC_MODULATEIA_PRIM); + + gDPLoadTextureBlock(OVERLAY_DISP++, gItemIcons[ITEM_BOMB], G_IM_FMT_RGBA, G_IM_SIZ_32b, ICON_ITEM_TEX_WIDTH, + ICON_ITEM_TEX_HEIGHT, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, + G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + + gSPTextureRectangle(OVERLAY_DISP++, overlayLeft << 2, overlayTop << 2, (overlayLeft + rectWidth) << 2, + (overlayTop + rectHeight) << 2, G_TX_RENDERTILE, 0, 0, dsdx << 1, dtdy << 1); + + CLOSE_DISPS(play->state.gfxCtx); +} + +static void UpdateBombArrowFlash(PlayState* play, BombArrowData* data) { + if (data == nullptr) { + return; + } + + bool blink = (data->timer < 120 && (data->timer & (data->flashSpeedScale + 1))); + + if (blink) { + Math_ApproachF(&data->flashBrightness, 140.0f, 1.0f, 140.0f / data->flashSpeedScale); + } else { + Math_ApproachZeroF(&data->flashBrightness, 1.0f, 140.0f / data->flashSpeedScale); + } + + u8 colorIdx = (u8)data->flashBrightness; + + if (data->primIdx >= 0) { + data->clonedGfx[data->primIdx] = gsDPSetPrimColor(0, 0, colorIdx, 0, 40, 255); + } + if (data->envIdx >= 0) { + data->clonedGfx[data->envIdx] = gsDPSetEnvColor(colorIdx, 0, 40, 255); + } +} + +static void OnBombArrowInit(Actor* actor) { + if (gPlayState == nullptr) { + return; + } + + Player* player = GET_PLAYER(gPlayState); + + if (player == nullptr || actor->parent != &player->actor) { + return; + } + + if (player->heldItemButton < 0 || (AMMO(ITEM_BOMB) <= 0)) { + return; + } + + if (IS_HELD_DPAD(player->heldItemButton)) { + s32 dpadSlot = HELD_ITEM_TO_DPAD(player->heldItemButton); + if (!IsBombArrowButton(dpadSlot, true)) { + return; + } + } else { + if (player->heldItemButton == EQUIP_SLOT_B || !IsBombArrowButton(player->heldItemButton, false)) { + return; + } + } + + EnArrow* arrow = (EnArrow*)actor; + + if (!ARROW_IS_ARROW(arrow->actor.params)) { + return; + } + + BOMB_ARROW_SET(arrow); + Inventory_ChangeAmmo(ITEM_BOMB, -1); + + BombArrowData data; + + Gfx* originalDl = ResourceMgr_LoadGfxByName(gGiBombDL); + if (originalDl != nullptr) { + for (s32 i = 0; i < 256; i++) { + data.clonedGfx.push_back(originalDl[i]); + if ((originalDl[i].words.w0 >> 24) == G_ENDDL) { + break; + } + + u8 opcode = (u8)(originalDl[i].words.w0 >> 24); + u32 rgba = originalDl[i].words.w1; + u8 r = (rgba >> 24) & 0xFF; + u8 g = (rgba >> 16) & 0xFF; + + if (opcode == G_SETPRIMCOLOR && data.primIdx < 0) { + if (r == 0x00 && g == 0x3C) { + data.primIdx = i; + } + } else if (opcode == G_SETENVCOLOR && data.envIdx < 0) { + if (r == 0x00 && g == 0x14) { + data.envIdx = i; + } + } + } + } + + ObjectExtension::GetInstance().Set(actor, std::move(data)); +} + +static void OnBombArrowUpdate(Actor* actor) { + if (gPlayState == nullptr) { + return; + } + + EnArrow* arrow = (EnArrow*)actor; + + if (!BOMB_ARROW_IS_SET(arrow) || !ARROW_IS_ARROW(arrow->actor.params)) { + return; + } + + auto data = ObjectExtension::GetInstance().Get(actor); + if (data == nullptr) { + return; + } + + if (data->timer > 0) { + data->timer--; + } + + if ((data->timer == 40) || (data->timer == 20) || (data->timer == 3)) { + data->flashSpeedScale >>= 1; + } + + if (data->timer <= 0) { + EnBom* bomb = (EnBom*)Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_BOM, arrow->actor.world.pos.x, + arrow->actor.world.pos.y, arrow->actor.world.pos.z, 0, 0, 0, BOMB_TYPE_BODY); + if (bomb != NULL) { + bomb->timer = 0; + } + + if (arrow->actor.child != NULL) { + Actor_Kill(arrow->actor.child); + } + + Actor_Kill(&arrow->actor); + return; + } + + Actor_PlaySfx(actor, NA_SE_IT_BOMB_IGNIT - SFX_FLAG); + + Vec3f effectPos; + Matrix_Push(); + Matrix_SetTranslateRotateYXZ(actor->world.pos.x, actor->world.pos.y + (actor->shape.yOffset * actor->scale.y), + actor->world.pos.z, &actor->shape.rot); + Matrix_Scale(actor->scale.x, actor->scale.y, actor->scale.z, MTXMODE_APPLY); + Matrix_Translate(0.0f, 0.0f, 1000.0f, MTXMODE_APPLY); + Matrix_Scale(10.0f, 10.0f, 10.0f, MTXMODE_APPLY); + Vec3f effectLocal = { 0.0f, 31.0f, 0.0f }; + Matrix_MultVec3f(&effectLocal, &effectPos); + Matrix_Pop(); + + if ((gPlayState->gameplayFrames % 2) == 0) { + Vec3f velocity = { 0.0f, 0.0f, 0.0f }; + Vec3f accel = { 0.0f, 0.0f, 0.0f }; + Color_RGBA8 prim = { 255, 255, 150, 255 }; + Color_RGBA8 env = { 255, 0, 0, 0 }; + EffectSsGSpk_SpawnNoAccel(gPlayState, actor, &effectPos, &velocity, &accel, &prim, &env, 30, 5); + } + + { + Vec3f velocity = { 0.0f, 0.0f, 0.0f }; + Vec3f accel = { 0.0f, 0.2f, 0.0f }; + Color_RGBA8 color = { 255, 255, 255, 255 }; + func_800B0DE0(gPlayState, &effectPos, &velocity, &accel, &color, &color, 22, 3); + } + + bool hit = (arrow->collider.base.atFlags & AT_HIT) || (arrow->unk_262 != 0); + + // Only explode on valid hits once the arrow is detached from the player. + if (!hit || (actor->parent != NULL)) { + return; + } + + EnBom* bomb = (EnBom*)Actor_Spawn(&gPlayState->actorCtx, gPlayState, ACTOR_EN_BOM, arrow->actor.world.pos.x, + arrow->actor.world.pos.y, arrow->actor.world.pos.z, 0, 0, 0, BOMB_TYPE_BODY); + if (bomb != NULL) { + bomb->timer = 0; + } + + if (arrow->actor.child != NULL) { + Actor_Kill(arrow->actor.child); + } + + Actor_Kill(&arrow->actor); +} + +static void OnBombArrowDraw(Actor* actor) { + if (gPlayState == nullptr) { + return; + } + + EnArrow* arrow = (EnArrow*)actor; + if (!BOMB_ARROW_IS_SET(arrow) || !ARROW_IS_ARROW(arrow->actor.params)) { + return; + } + + auto data = ObjectExtension::GetInstance().Get(actor); + if (data == nullptr) { + return; + } + + UpdateBombArrowFlash(gPlayState, data); + + OPEN_DISPS(gPlayState->state.gfxCtx); + + Gfx_SetupDL25_Opa(gPlayState->state.gfxCtx); + + Matrix_Push(); + + Matrix_SetTranslateRotateYXZ(actor->world.pos.x, actor->world.pos.y + (actor->shape.yOffset * actor->scale.y), + actor->world.pos.z, &actor->shape.rot); + Matrix_Scale(actor->scale.x, actor->scale.y, actor->scale.z, MTXMODE_APPLY); + Matrix_Translate(0.0f, 0.0f, 1000.0f, MTXMODE_APPLY); + Matrix_Scale(10.0f, 10.0f, 10.0f, MTXMODE_APPLY); + + MATRIX_FINALIZE_AND_LOAD(POLY_OPA_DISP++, gPlayState->state.gfxCtx); + + gSPDisplayList(POLY_OPA_DISP++, data->clonedGfx.data()); + + Matrix_Pop(); + + CLOSE_DISPS(gPlayState->state.gfxCtx); +} + +static void OnKaleidoEquipItemToButton(bool* should, va_list args) { + va_arg(args, int); + u16 cursorItem = va_arg(args, int); + + if (*should) { + HandleBombArrowEquip(cursorItem, should, &gPlayState->pauseCtx); + } +} + +static void OnDrawItemEquippedOutline(bool* should, va_list args) { + ItemId* item = va_arg(args, ItemId*); + s32 btn = va_arg(args, s32); + s32 isDpad = va_arg(args, s32); + s32 pageIndex = va_arg(args, s32); + + if (pageIndex != PAUSE_ITEM) { + return; + } + + s32 slot = isDpad ? DPAD_GET_CUR_FORM_BTN_SLOT(btn) : GET_CUR_FORM_BTN_SLOT(btn); + // Don't show the border if the item is a mask (on the item page) + if (slot >= ITEM_NUM_SLOTS) { + return; + } + + bool isBombArrow = IsBombArrowButton(btn, isDpad); + bool shouldShow = false; + + if (isBombArrow) { + shouldShow = sBombSlotIsBombArrowMode; + if (shouldShow) { + // Redirect highlight to SLOT_BOMB by copying slot vtx and applying the vanilla expansion. + s32 bIndex = isDpad ? (27 + btn) : (24 + (btn - 1)); + Vtx* destVtx = &gPlayState->pauseCtx.itemVtx[bIndex * 4]; + Vtx* srcVtx = &gPlayState->pauseCtx.itemVtx[SLOT_BOMB * 4]; + + memcpy(destVtx, srcVtx, sizeof(Vtx) * 4); + + destVtx[0].v.ob[0] = destVtx[2].v.ob[0] = srcVtx[0].v.ob[0] + ITEM_GRID_SELECTED_QUAD_MARGIN; + destVtx[1].v.ob[0] = destVtx[3].v.ob[0] = destVtx[0].v.ob[0] + ITEM_GRID_SELECTED_QUAD_WIDTH; + destVtx[0].v.ob[1] = destVtx[1].v.ob[1] = srcVtx[0].v.ob[1] - ITEM_GRID_SELECTED_QUAD_MARGIN; + destVtx[2].v.ob[1] = destVtx[3].v.ob[1] = destVtx[0].v.ob[1] - ITEM_GRID_SELECTED_QUAD_WIDTH; + + for (int i = 0; i < 4; i++) { + destVtx[i].v.cn[3] = gPlayState->pauseCtx.alpha; + } + } + } else if (slot == SLOT_BOW) { + shouldShow = true; + } else if (slot == SLOT_BOMB) { + shouldShow = !sBombSlotIsBombArrowMode; + } else { + shouldShow = true; + } + + *should = shouldShow; +} + +static void OnKaleidoDrawEquipAnimIcon(bool* should, va_list args) { + ItemId* drawItem = va_arg(args, ItemId*); + if (*drawItem != ITEM_BOW) { + return; + } + + PauseContext* pauseCtx = &gPlayState->pauseCtx; + s32 targetBtn; + bool isDpad; + if (ResolvePauseEquipTarget(pauseCtx, &targetBtn, &isDpad) && IsBombArrowButton(targetBtn, isDpad)) { + // Draw composite bomb-arrow icon during the equip animation. + *should = false; + + OPEN_DISPS(gPlayState->state.gfxCtx); + + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, pauseCtx->equipAnimAlpha); + gSPVertex(OVERLAY_DISP++, (uintptr_t)&pauseCtx->cursorVtx[16], 4, 0); + gDPLoadTextureBlock(OVERLAY_DISP++, gItemIcons[ITEM_BOW], G_IM_FMT_RGBA, G_IM_SIZ_32b, ICON_ITEM_TEX_WIDTH, + ICON_ITEM_TEX_HEIGHT, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, + G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + gSP1Quadrangle(OVERLAY_DISP++, 0, 2, 3, 1, 0); + + f32 currentWidth = (f32)(pauseCtx->equipAnimScale / 10.0f); + f32 scaleFactor = currentWidth / 32.0f; + + f32 centerX = pauseCtx->cursorVtx[16].v.ob[0] + (currentWidth / 2.0f); + f32 centerY = pauseCtx->cursorVtx[16].v.ob[1] - (currentWidth / 2.0f); + + Matrix_Push(); + Matrix_Translate(centerX + (2.0f * scaleFactor), centerY + (2.0f * scaleFactor), 0.0f, MTXMODE_NEW); + Matrix_Scale(scaleFactor, scaleFactor, 1.0f, MTXMODE_APPLY); + + MATRIX_FINALIZE_AND_LOAD(OVERLAY_DISP++, gPlayState->state.gfxCtx); + + gSPVertex(OVERLAY_DISP++, (uintptr_t)sBombOverlayVtx, 4, 0); + gDPLoadTextureBlock(OVERLAY_DISP++, gItemIcons[ITEM_BOMB], G_IM_FMT_RGBA, G_IM_SIZ_32b, ICON_ITEM_TEX_WIDTH, + ICON_ITEM_TEX_HEIGHT, 0, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, + G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + gSP1Quadrangle(OVERLAY_DISP++, 0, 2, 3, 1, 0); + + Matrix_Pop(); + + gSPMatrix(OVERLAY_DISP++, &gIdentityMtx, G_MTX_NOPUSH | G_MTX_LOAD | G_MTX_MODELVIEW); + + CLOSE_DISPS(gPlayState->state.gfxCtx); + } +} + +static void OnDrawHudAmmoCount(bool* should, va_list args) { + s16 button = (s16)va_arg(args, int); + s16 alpha = (s16)va_arg(args, int); + bool isDpad = (bool)va_arg(args, int); + + if (IsBombArrowButton(button, isDpad)) { + *should = false; + DrawBombArrowAmmoCount(gPlayState, button, alpha, isDpad); + + // Draw the bomb overlay here so it shares the same render order as the C/D-button icons. + if (isDpad) { + DrawBombArrowOverlayDpad(gPlayState, (DpadEquipSlot)button, alpha); + } else { + DrawBombArrowOverlayCButton(gPlayState, (EquipSlot)button, alpha); + } + } +} + +static void OnKaleidoDisplayItemText(bool* should, va_list args) { + PauseContext* pauseCtx = &gPlayState->pauseCtx; + u16 slot = pauseCtx->cursorSlot[PAUSE_ITEM]; + + if (slot == SLOT_BOMB && CanCycleBombSlot()) { + *should = false; + } +} + +static void OnBeforeKaleidoDrawPage(PauseContext* pauseCtx, u16 pauseIndex) { + if (sBombSlotIsBombArrowMode && CanCycleBombSlot()) { + gSaveContext.save.saveInfo.inventory.items[SLOT_BOMB] = ITEM_BOW; + sInventorySwapActive = true; + } +} + +static void OnAfterKaleidoDrawPage(PauseContext* pauseCtx, u16 pauseIndex) { + if (sInventorySwapActive) { + gSaveContext.save.saveInfo.inventory.items[SLOT_BOMB] = ITEM_BOMB; + sInventorySwapActive = false; + + DrawBombArrowSlotIcon(gPlayState); + } + + bool canCycle = CanCycleBombSlot(); + ItemId alternateItem = sBombSlotIsBombArrowMode ? ITEM_BOMB : ITEM_BOW; + DrawBombArrowCycleExtras(gPlayState, canCycle, alternateItem); +} + +static void OnKaleidoDrawAmmoCount(bool* should, va_list args) { + PauseContext* pauseCtx = va_arg(args, PauseContext*); + GraphicsContext* gfxCtx = va_arg(args, GraphicsContext*); + s16 item = va_arg(args, int); + u16 ammoIndex = va_arg(args, int); + + if (ammoIndex == 1 && sBombSlotIsBombArrowMode) { + *should = false; + + s16 arrows = AMMO(ITEM_BOW); + s16 bombs = AMMO(ITEM_BOMB); + s16 maxArrows = CUR_CAPACITY(UPG_QUIVER); + s16 maxBombs = CUR_CAPACITY(UPG_BOMB_BAG); + + s16 effectiveAmmo = (arrows < bombs) ? arrows : bombs; + s16 effectiveMax = (maxArrows < maxBombs) ? maxArrows : maxBombs; + + s16 ammo = effectiveAmmo; + bool turnGreen = (effectiveAmmo == effectiveMax); + + s16 ammoUpperDigit = 0; + for (ammoUpperDigit = 0; ammo >= 10; ammoUpperDigit++) { + ammo -= 10; + } + + // Coordinates for SLOT_BOMB (ammoIndex=1) in vanilla kaleido item ammo rect. + s16 sAmmoRectLeft = 62; + s16 sAmmoRectHeight = 117; + + OPEN_DISPS(gfxCtx); + + gDPPipeSync(POLY_OPA_DISP++); + + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, pauseCtx->alpha); + if (effectiveAmmo == 0) { + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 130, 130, 130, pauseCtx->alpha); + } else if (turnGreen) { + gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 120, 255, 0, pauseCtx->alpha); + } + + if (ammoUpperDigit != 0) { + POLY_OPA_DISP = Gfx_DrawTexRectIA8(POLY_OPA_DISP, (TexturePtr)gAmmoDigitTextures[ammoUpperDigit], 8, 8, + sAmmoRectLeft, sAmmoRectHeight, 8, 8, 1 << 10, 1 << 10); + } + + POLY_OPA_DISP = Gfx_DrawTexRectIA8(POLY_OPA_DISP, (TexturePtr)gAmmoDigitTextures[ammo], 8, 8, sAmmoRectLeft + 6, + sAmmoRectHeight, 8, 8, 1 << 10, 1 << 10); + + CLOSE_DISPS(gfxCtx); + } +} + +static void OnKaleidoUpdate(PauseContext* pauseCtx) { + HandleEquipCleanup(pauseCtx); + HandleSlotCycling(pauseCtx); + + if (sHasPendingEquip) { + if (((sEquipState == EQUIP_STATE_MOVE_TO_C_BTN) && (sEquipAnimTimer == 1)) || + (pauseCtx->state != PAUSE_STATE_MAIN)) { + HandleSmartSwap(sPendingEquip.targetBtn, sPendingEquip.isDpad, sPendingEquip.newEquipSlot, + sPendingEquip.isBowGroup); + SetSlotItem(sPendingEquip.targetBtn, sPendingEquip.isDpad, sPendingEquip.newItem, + sPendingEquip.newEquipSlot); + SetBombArrowButton(sPendingEquip.targetBtn, sPendingEquip.isBombArrow, sPendingEquip.isDpad); + sHasPendingEquip = false; + } + } +} + +static void RegisterBombArrows() { + COND_HOOK(OnKaleidoUpdate, CVAR, OnKaleidoUpdate); + + COND_ID_HOOK(AfterKaleidoDrawPage, PAUSE_ITEM, CVAR, OnAfterKaleidoDrawPage); + COND_ID_HOOK(BeforeKaleidoDrawPage, PAUSE_ITEM, CVAR, OnBeforeKaleidoDrawPage); + COND_ID_HOOK(OnActorDraw, ACTOR_EN_ARROW, CVAR, OnBombArrowDraw); + COND_ID_HOOK(OnActorInit, ACTOR_EN_ARROW, CVAR, OnBombArrowInit); + COND_ID_HOOK(OnActorUpdate, ACTOR_EN_ARROW, CVAR, OnBombArrowUpdate); + + COND_VB_SHOULD(VB_DRAW_HUD_AMMO_COUNT, CVAR, OnDrawHudAmmoCount(should, args)); + COND_VB_SHOULD(VB_DRAW_ITEM_EQUIPPED_OUTLINE, CVAR, OnDrawItemEquippedOutline(should, args)); + COND_VB_SHOULD(VB_KALEIDO_DISPLAY_ITEM_TEXT, CVAR, OnKaleidoDisplayItemText(should, args)); + COND_VB_SHOULD(VB_KALEIDO_DRAW_AMMO_COUNT, CVAR, OnKaleidoDrawAmmoCount(should, args)); + COND_VB_SHOULD(VB_KALEIDO_DRAW_EQUIP_ANIM_ICON, CVAR, OnKaleidoDrawEquipAnimIcon(should, args)); + COND_VB_SHOULD(VB_KALEIDO_EQUIP_ITEM_TO_BUTTON, CVAR, OnKaleidoEquipItemToButton(should, args)); +} + +static RegisterShipInitFunc initFunc(RegisterBombArrows, { CVAR_NAME }); diff --git a/mm/2s2h/Enhancements/Equipment/ItemUnequip.cpp b/mm/2s2h/Enhancements/Equipment/ItemUnequip.cpp index 1038c8ef29..178cbd0286 100644 --- a/mm/2s2h/Enhancements/Equipment/ItemUnequip.cpp +++ b/mm/2s2h/Enhancements/Equipment/ItemUnequip.cpp @@ -13,6 +13,11 @@ extern "C" { #define CVAR_DPAD_NAME "gEnhancements.Dpad.DpadEquips" #define CVAR_DPAD CVarGetInteger(CVAR_DPAD_NAME, 0) + +extern bool IsBombArrowButton(s32 slot, bool isDpad); +extern void SetBombArrowButton(s32 slot, bool state, bool isDpad); +extern bool sBombSlotIsBombArrowMode; + #define CVAR_NAME "gEnhancements.Equipment.ItemUnequip" #define CVAR CVarGetInteger(CVAR_NAME, 0) @@ -87,22 +92,26 @@ void RegisterItemUnequip() { equippedSlot = DPAD_SLOT_EQUIP(0, targetSlot); } - // Check if we should unequip - if (equippedItem == cursorItem) { - if (isMask) { - // For masks, check the slot matches (cursorSlot is already offset by ITEM_NUM_SLOTS) - if (equippedSlot == cursorSlot) { + bool isBombArrow = IsBombArrowButton(targetSlot, isDpad); + + if (equippedItem == cursorItem || (equippedItem == ITEM_BOW && cursorItem == ITEM_BOMB && isBombArrow)) { + if (isBombArrow) { + if (cursorItem == ITEM_BOMB && sBombSlotIsBombArrowMode) { shouldUnequip = true; } + } else if (cursorItem != ITEM_BOMB || !sBombSlotIsBombArrowMode) { + shouldUnequip = true; } - // For bottles, we need to check the slot too (since there are multiple bottle items) - else if (cursorItem >= ITEM_BOTTLE && cursorItem <= ITEM_OBABA_DRINK) { - if (equippedSlot == cursorSlot) { - shouldUnequip = true; + + if (shouldUnequip) { + if (isMask && equippedSlot != cursorSlot) { + shouldUnequip = false; + } else if (cursorItem >= ITEM_BOTTLE && cursorItem <= ITEM_OBABA_DRINK && equippedSlot != cursorSlot) { + shouldUnequip = false; } - } else { - shouldUnequip = true; } + } else if (cursorItem == ITEM_BOMB && sBombSlotIsBombArrowMode && isBombArrow) { + shouldUnequip = true; } // Handle magic arrows (bow variants) else if (cursorItem == ITEM_ARROW_FIRE && equippedItem == ITEM_BOW_FIRE) { @@ -131,6 +140,8 @@ void RegisterItemUnequip() { (char*)gEmptyTexture; } + SetBombArrowButton(targetSlot, false, isDpad); + Audio_PlaySfx(NA_SE_SY_DECIDE); *should = false; } diff --git a/mm/2s2h/Enhancements/Masks/PersistentMasks.cpp b/mm/2s2h/Enhancements/Masks/PersistentMasks.cpp index 652dc6fe81..40639312e1 100644 --- a/mm/2s2h/Enhancements/Masks/PersistentMasks.cpp +++ b/mm/2s2h/Enhancements/Masks/PersistentMasks.cpp @@ -191,6 +191,9 @@ void RegisterPersistentMasks() { // underneath) COND_VB_SHOULD(VB_DRAW_ITEM_EQUIPPED_OUTLINE, CVAR, { ItemId* itemId = va_arg(args, ItemId*); + va_arg(args, s32); // slot + va_arg(args, s32); // isDpad + va_arg(args, s32); // pageIndex if (*itemId == ITEM_MASK_BUNNY && STATE_CVAR) { *should = false; } diff --git a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h index 9f8bd8bb16..a72e404743 100644 --- a/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h +++ b/mm/2s2h/GameInteractor/GameInteractor_VanillaBehavior.h @@ -449,6 +449,16 @@ typedef enum { // - `*DmHina` VB_DRAW_BOSS_REMAINS, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `s16` (button) + // - `s16` (alpha) + // - `bool` (isDpad) + VB_DRAW_HUD_AMMO_COUNT, + // #### `result` // ```c // true @@ -496,6 +506,9 @@ typedef enum { // ``` // #### `args` // - `*ItemId` + // - `s32` (slot) + // - `s32` (isDpad) + // - `s32` (pageIndex) VB_DRAW_ITEM_EQUIPPED_OUTLINE, // #### `result` @@ -1126,6 +1139,25 @@ typedef enum { // - `u16` (item under cursor) VB_KALEIDO_DISPLAY_ITEM_TEXT, + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `PauseContext*` + // - `GraphicsContext*` + // - `s16` (item) + // - `u16` (ammoIndex) + VB_KALEIDO_DRAW_AMMO_COUNT, + + // #### `result` + // ```c + // true + // ``` + // #### `args` + // - `ItemId*` (draw item, mutable) + VB_KALEIDO_DRAW_EQUIP_ANIM_ICON, + // #### `result` // ```c // true diff --git a/mm/include/z64save.h b/mm/include/z64save.h index d5e265e778..cd80e418df 100644 --- a/mm/include/z64save.h +++ b/mm/include/z64save.h @@ -402,6 +402,7 @@ typedef struct ShipSaveInfo { RespawnData respawn[RESPAWN_MODE_MAX]; char commitHash[8]; RandoSaveInfo rando; + u8 bombArrowsEquipped; } ShipSaveInfo; // #endregion diff --git a/mm/src/code/z_parameter.c b/mm/src/code/z_parameter.c index 911c319dd0..9d47460ee9 100644 --- a/mm/src/code/z_parameter.c +++ b/mm/src/code/z_parameter.c @@ -6148,6 +6148,11 @@ s16 sDpadItemAmmoY[] = { void Interface_Dpad_DrawAmmoCount(PlayState* play, s16 button, s16 alpha) { u8 i; u16 ammo; + + if (!GameInteractor_Should(VB_DRAW_HUD_AMMO_COUNT, true, button, alpha, true)) { + return; + } + OPEN_DISPS(play->state.gfxCtx); i = ((void)0, DPAD_GET_CUR_FORM_BTN_ITEM(button)); @@ -6259,6 +6264,11 @@ void Interface_DrawAmmoCount(PlayState* play, s16 button, s16 alpha) { static s16 sAmmoDigitsYPositions[] = { 35, 35, 51, 35 }; u8 i; u16 ammo; + + if (!GameInteractor_Should(VB_DRAW_HUD_AMMO_COUNT, true, button, alpha, false)) { + return; + } + OPEN_DISPS(play->state.gfxCtx); i = ((void)0, GET_CUR_FORM_BTN_ITEM(button)); @@ -6682,38 +6692,42 @@ void Interface_DrawPauseMenuEquippingIcons(PlayState* play) { pauseCtx->cursorVtx[18].v.ob[1] = pauseCtx->cursorVtx[19].v.ob[1] = pauseCtx->cursorVtx[16].v.ob[1] - (pauseCtx->equipAnimScale / 10); - if (pauseCtx->equipTargetItem < 0xB5) { - // Normal Equip (icon goes from the inventory slot to the C button when equipping it) - gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, pauseCtx->equipAnimAlpha); - gSPVertex(OVERLAY_DISP++, &pauseCtx->cursorVtx[16], 4, 0); - gDPLoadTextureBlock(OVERLAY_DISP++, gItemIcons[pauseCtx->equipTargetItem], G_IM_FMT_RGBA, G_IM_SIZ_32b, - ICON_ITEM_TEX_WIDTH, ICON_ITEM_TEX_HEIGHT, 0, G_TX_NOMIRROR | G_TX_WRAP, - G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); - } else { - // Magic Arrow Equip Effect - temp = pauseCtx->equipTargetItem - 0xB5; - gDPSetPrimColor(OVERLAY_DISP++, 0, 0, sMagicArrowEffectsR[temp], sMagicArrowEffectsG[temp], - sMagicArrowEffectsB[temp], pauseCtx->equipAnimAlpha); - - if ((pauseCtx->equipAnimAlpha > 0) && (pauseCtx->equipAnimAlpha < 255)) { - temp = (pauseCtx->equipAnimAlpha / 8) / 2; - pauseCtx->cursorVtx[16].v.ob[0] = pauseCtx->cursorVtx[18].v.ob[0] = - pauseCtx->cursorVtx[16].v.ob[0] - temp; - pauseCtx->cursorVtx[17].v.ob[0] = pauseCtx->cursorVtx[19].v.ob[0] = - pauseCtx->cursorVtx[16].v.ob[0] + temp * 2 + 32; - pauseCtx->cursorVtx[16].v.ob[1] = pauseCtx->cursorVtx[17].v.ob[1] = - pauseCtx->cursorVtx[16].v.ob[1] + temp; - pauseCtx->cursorVtx[18].v.ob[1] = pauseCtx->cursorVtx[19].v.ob[1] = - pauseCtx->cursorVtx[16].v.ob[1] - temp * 2 - 32; - } - - gSPVertex(OVERLAY_DISP++, &pauseCtx->cursorVtx[16], 4, 0); - gDPLoadTextureBlock(OVERLAY_DISP++, gMagicArrowEquipEffectTex, G_IM_FMT_IA, G_IM_SIZ_8b, 32, 32, 0, - G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, - G_TX_NOLOD, G_TX_NOLOD); - } + ItemId equipAnimDrawItem = pauseCtx->equipTargetItem; + if (GameInteractor_Should(VB_KALEIDO_DRAW_EQUIP_ANIM_ICON, true, &equipAnimDrawItem)) { + + if (pauseCtx->equipTargetItem < 0xB5) { + // Normal Equip (icon goes from the inventory slot to the C button when equipping it) + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, 255, 255, 255, pauseCtx->equipAnimAlpha); + gSPVertex(OVERLAY_DISP++, &pauseCtx->cursorVtx[16], 4, 0); + gDPLoadTextureBlock(OVERLAY_DISP++, gItemIcons[equipAnimDrawItem], G_IM_FMT_RGBA, G_IM_SIZ_32b, + ICON_ITEM_TEX_WIDTH, ICON_ITEM_TEX_HEIGHT, 0, G_TX_NOMIRROR | G_TX_WRAP, + G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, G_TX_NOLOD, G_TX_NOLOD); + } else { + // Magic Arrow Equip Effect + temp = pauseCtx->equipTargetItem - 0xB5; + gDPSetPrimColor(OVERLAY_DISP++, 0, 0, sMagicArrowEffectsR[temp], sMagicArrowEffectsG[temp], + sMagicArrowEffectsB[temp], pauseCtx->equipAnimAlpha); + + if ((pauseCtx->equipAnimAlpha > 0) && (pauseCtx->equipAnimAlpha < 255)) { + temp = (pauseCtx->equipAnimAlpha / 8) / 2; + pauseCtx->cursorVtx[16].v.ob[0] = pauseCtx->cursorVtx[18].v.ob[0] = + pauseCtx->cursorVtx[16].v.ob[0] - temp; + pauseCtx->cursorVtx[17].v.ob[0] = pauseCtx->cursorVtx[19].v.ob[0] = + pauseCtx->cursorVtx[16].v.ob[0] + temp * 2 + 32; + pauseCtx->cursorVtx[16].v.ob[1] = pauseCtx->cursorVtx[17].v.ob[1] = + pauseCtx->cursorVtx[16].v.ob[1] + temp; + pauseCtx->cursorVtx[18].v.ob[1] = pauseCtx->cursorVtx[19].v.ob[1] = + pauseCtx->cursorVtx[16].v.ob[1] - temp * 2 - 32; + } - gSP1Quadrangle(OVERLAY_DISP++, 0, 2, 3, 1, 0); + gSPVertex(OVERLAY_DISP++, &pauseCtx->cursorVtx[16], 4, 0); + gDPLoadTextureBlock(OVERLAY_DISP++, gMagicArrowEquipEffectTex, G_IM_FMT_IA, G_IM_SIZ_8b, 32, 32, 0, + G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMIRROR | G_TX_WRAP, G_TX_NOMASK, G_TX_NOMASK, + G_TX_NOLOD, G_TX_NOLOD); + } + + gSP1Quadrangle(OVERLAY_DISP++, 0, 2, 3, 1, 0); + } } CLOSE_DISPS(play->state.gfxCtx); diff --git a/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_item.c b/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_item.c index e997224bfa..0994442187 100644 --- a/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_item.c +++ b/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_item.c @@ -185,6 +185,10 @@ void KaleidoScope_DrawAmmoCount(PauseContext* pauseCtx, GraphicsContext* gfxCtx, s16 ammoUpperDigit; s16 ammo; + if (!GameInteractor_Should(VB_KALEIDO_DRAW_AMMO_COUNT, true, pauseCtx, gfxCtx, item, ammoIndex)) { + return; + } + OPEN_DISPS(gfxCtx); if (item == ITEM_PICTOGRAPH_BOX) { @@ -265,7 +269,9 @@ void KaleidoScope_DrawItemSelect(PlayState* play) { gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, pauseCtx->alpha); for (i = 0, j = ITEM_NUM_SLOTS * 4; i < 3; i++, j += 4) { if (GET_CUR_FORM_BTN_ITEM(i + 1) != ITEM_NONE) { - if (GET_CUR_FORM_BTN_SLOT(i + 1) < ITEM_NUM_SLOTS) { + ItemId item = GET_CUR_FORM_BTN_ITEM(i + 1); + if (GameInteractor_Should(VB_DRAW_ITEM_EQUIPPED_OUTLINE, (GET_CUR_FORM_BTN_SLOT(i + 1) < ITEM_NUM_SLOTS), + &item, (s32)(i + 1), 0, PAUSE_ITEM)) { gSPVertex(POLY_OPA_DISP++, &pauseCtx->itemVtx[j], 4, 0); POLY_OPA_DISP = Gfx_DrawTexQuadIA8(POLY_OPA_DISP, gEquippedItemOutlineTex, 32, 32, 0); } @@ -275,7 +281,10 @@ void KaleidoScope_DrawItemSelect(PlayState* play) { if (CVarGetInteger("gEnhancements.Dpad.DpadEquips", 0)) { for (i = EQUIP_SLOT_D_RIGHT; i <= EQUIP_SLOT_D_UP; i++, j += 4) { if (DPAD_GET_CUR_FORM_BTN_ITEM(i) != ITEM_NONE) { - if (DPAD_GET_CUR_FORM_BTN_SLOT(i) < ITEM_NUM_SLOTS) { + ItemId item = DPAD_GET_CUR_FORM_BTN_ITEM(i); + if (GameInteractor_Should(VB_DRAW_ITEM_EQUIPPED_OUTLINE, + (DPAD_GET_CUR_FORM_BTN_SLOT(i) < ITEM_NUM_SLOTS), &item, (s32)i, 1, + PAUSE_ITEM)) { gSPVertex(POLY_OPA_DISP++, &pauseCtx->itemVtx[j], 4, 0); POLY_OPA_DISP = Gfx_DrawTexQuadIA8(POLY_OPA_DISP, gEquippedItemOutlineTex, 32, 32, 0); } diff --git a/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_mask.c b/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_mask.c index 56e2856797..8e09dc5cb0 100644 --- a/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_mask.c +++ b/mm/src/overlays/kaleido_scope/ovl_kaleido_scope/z_kaleido_mask.c @@ -210,12 +210,11 @@ void KaleidoScope_DrawMaskSelect(PlayState* play) { gDPSetPrimColor(POLY_OPA_DISP++, 0, 0, 255, 255, 255, pauseCtx->alpha); for (i = 0, j = MASK_NUM_SLOTS * 4; i < 3; i++, j += 4) { if (GET_CUR_FORM_BTN_ITEM(i + 1) != ITEM_NONE) { - if (GET_CUR_FORM_BTN_SLOT(i + 1) >= ITEM_NUM_SLOTS) { - ItemId item = GET_CUR_FORM_BTN_ITEM(i + 1); - if (GameInteractor_Should(VB_DRAW_ITEM_EQUIPPED_OUTLINE, true, &item)) { - gSPVertex(POLY_OPA_DISP++, &pauseCtx->maskVtx[j], 4, 0); - POLY_OPA_DISP = Gfx_DrawTexQuadIA8(POLY_OPA_DISP, gEquippedItemOutlineTex, 32, 32, 0); - } + ItemId item = GET_CUR_FORM_BTN_ITEM(i + 1); + if (GameInteractor_Should(VB_DRAW_ITEM_EQUIPPED_OUTLINE, (GET_CUR_FORM_BTN_SLOT(i + 1) >= ITEM_NUM_SLOTS), + &item, (s32)(i + 1), 0, PAUSE_MASK)) { + gSPVertex(POLY_OPA_DISP++, &pauseCtx->maskVtx[j], 4, 0); + POLY_OPA_DISP = Gfx_DrawTexQuadIA8(POLY_OPA_DISP, gEquippedItemOutlineTex, 32, 32, 0); } } } @@ -223,12 +222,12 @@ void KaleidoScope_DrawMaskSelect(PlayState* play) { if (CVarGetInteger("gEnhancements.Dpad.DpadEquips", 0)) { for (i = EQUIP_SLOT_D_RIGHT; i <= EQUIP_SLOT_D_UP; i++, j += 4) { if (DPAD_GET_CUR_FORM_BTN_ITEM(i) != ITEM_NONE) { - if (DPAD_GET_CUR_FORM_BTN_SLOT(i) >= ITEM_NUM_SLOTS) { - ItemId item = DPAD_GET_CUR_FORM_BTN_ITEM(i); - if (GameInteractor_Should(VB_DRAW_ITEM_EQUIPPED_OUTLINE, true, &item)) { - gSPVertex(POLY_OPA_DISP++, &pauseCtx->maskVtx[j], 4, 0); - POLY_OPA_DISP = Gfx_DrawTexQuadIA8(POLY_OPA_DISP, gEquippedItemOutlineTex, 32, 32, 0); - } + ItemId item = DPAD_GET_CUR_FORM_BTN_ITEM(i); + if (GameInteractor_Should(VB_DRAW_ITEM_EQUIPPED_OUTLINE, + (DPAD_GET_CUR_FORM_BTN_SLOT(i) >= ITEM_NUM_SLOTS), &item, (s32)(i), 1, + PAUSE_MASK)) { + gSPVertex(POLY_OPA_DISP++, &pauseCtx->maskVtx[j], 4, 0); + POLY_OPA_DISP = Gfx_DrawTexQuadIA8(POLY_OPA_DISP, gEquippedItemOutlineTex, 32, 32, 0); } } }