[Enhancement] Arrow Cycle (#6105)
This commit is contained in:
283
soh/soh/Enhancements/ArrowCycle.cpp
Normal file
283
soh/soh/Enhancements/ArrowCycle.cpp
Normal file
@@ -0,0 +1,283 @@
|
||||
#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
|
||||
#include "soh/ShipInit.hpp"
|
||||
|
||||
extern "C" {
|
||||
#include "macros.h"
|
||||
#include "variables.h"
|
||||
#include "functions.h"
|
||||
#include "overlays/actors/ovl_En_Arrow/z_en_arrow.h"
|
||||
|
||||
s32 func_808351D4(Player* thisx, PlayState* play); // Arrow nocked
|
||||
s32 func_808353D8(Player* thisx, PlayState* play); // Aiming in first person
|
||||
void Player_InitItemAction(PlayState* play, Player* thisx, PlayerItemAction itemAction);
|
||||
|
||||
extern PlayState* gPlayState;
|
||||
}
|
||||
|
||||
#define CVAR_ARROW_CYCLE_NAME CVAR_ENHANCEMENT("BowArrowCycle")
|
||||
#define CVAR_ARROW_CYCLE_DEFAULT 0
|
||||
#define CVAR_ARROW_CYCLE_VALUE CVarGetInteger(CVAR_ARROW_CYCLE_NAME, CVAR_ARROW_CYCLE_DEFAULT)
|
||||
|
||||
static const s16 sMagicArrowCosts[] = { 4, 4, 8 };
|
||||
|
||||
#define MINIGAME_STATUS_ACTIVE 1
|
||||
|
||||
static const s16 BUTTON_FLASH_DURATION = 3;
|
||||
static const s16 BUTTON_FLASH_COUNT = 3;
|
||||
static const s16 BUTTON_HIGHLIGHT_ALPHA = 128;
|
||||
|
||||
static s16 sButtonFlashTimer = 0;
|
||||
static s16 sButtonFlashCount = 0;
|
||||
|
||||
static const PlayerItemAction sArrowCycleOrder[] = {
|
||||
PLAYER_IA_BOW,
|
||||
PLAYER_IA_BOW_FIRE,
|
||||
PLAYER_IA_BOW_ICE,
|
||||
PLAYER_IA_BOW_LIGHT,
|
||||
};
|
||||
|
||||
static bool IsHoldingBow(Player* player) {
|
||||
return player->heldItemAction >= PLAYER_IA_BOW && player->heldItemAction <= PLAYER_IA_BOW_LIGHT;
|
||||
}
|
||||
|
||||
static bool IsHoldingMagicBow(Player* player) {
|
||||
return player->heldItemAction >= PLAYER_IA_BOW_FIRE && player->heldItemAction <= PLAYER_IA_BOW_LIGHT;
|
||||
}
|
||||
|
||||
static bool IsAimingBow(Player* player) {
|
||||
return IsHoldingBow(player) && ((player->unk_6AD == 2) || (player->upperActionFunc == func_808351D4));
|
||||
}
|
||||
|
||||
static bool HasArrowType(PlayerItemAction itemAction) {
|
||||
switch (itemAction) {
|
||||
case PLAYER_IA_BOW:
|
||||
return true;
|
||||
case PLAYER_IA_BOW_FIRE:
|
||||
return (INV_CONTENT(ITEM_ARROW_FIRE) == ITEM_ARROW_FIRE);
|
||||
case PLAYER_IA_BOW_ICE:
|
||||
return (INV_CONTENT(ITEM_ARROW_ICE) == ITEM_ARROW_ICE);
|
||||
case PLAYER_IA_BOW_LIGHT:
|
||||
return (INV_CONTENT(ITEM_ARROW_LIGHT) == ITEM_ARROW_LIGHT);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static s32 GetBowItemForArrow(PlayerItemAction itemAction) {
|
||||
switch (itemAction) {
|
||||
case PLAYER_IA_BOW_FIRE:
|
||||
return ITEM_BOW_ARROW_FIRE;
|
||||
case PLAYER_IA_BOW_ICE:
|
||||
return ITEM_BOW_ARROW_ICE;
|
||||
case PLAYER_IA_BOW_LIGHT:
|
||||
return ITEM_BOW_ARROW_LIGHT;
|
||||
default:
|
||||
return ITEM_BOW;
|
||||
}
|
||||
}
|
||||
|
||||
static bool CanCycleArrows() {
|
||||
Player* player = GET_PLAYER(gPlayState);
|
||||
|
||||
// don't allow cycling during minigames
|
||||
if (gSaveContext.minigameState == MINIGAME_STATUS_ACTIVE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return !(player->stateFlags1 & PLAYER_STATE1_ON_HORSE) && 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);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
for (int offset = 1; offset <= (int)ARRAY_COUNT(sArrowCycleOrder); offset++) {
|
||||
int nextIndex = (currentIndex + offset) % ARRAY_COUNT(sArrowCycleOrder);
|
||||
if (HasArrowType(sArrowCycleOrder[nextIndex])) {
|
||||
return sArrowCycleOrder[nextIndex];
|
||||
}
|
||||
}
|
||||
|
||||
return PLAYER_IA_BOW;
|
||||
}
|
||||
|
||||
static void UpdateButtonAlpha(s16 flashAlpha, bool isButtonBow, u16* buttonAlpha) {
|
||||
if (isButtonBow) {
|
||||
*buttonAlpha = flashAlpha;
|
||||
if (sButtonFlashTimer == 0) {
|
||||
*buttonAlpha = 255;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static void UpdateFlashEffect(PlayState* play) {
|
||||
if (sButtonFlashTimer <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
sButtonFlashTimer--;
|
||||
s16 flashAlpha = (sButtonFlashTimer % 3) ? BUTTON_HIGHLIGHT_ALPHA : 255;
|
||||
|
||||
if (sButtonFlashTimer == 0 && sButtonFlashCount < BUTTON_FLASH_COUNT - 1) {
|
||||
sButtonFlashTimer = BUTTON_FLASH_DURATION;
|
||||
sButtonFlashCount++;
|
||||
}
|
||||
UpdateButtonAlpha(flashAlpha,
|
||||
(gSaveContext.equips.buttonItems[1] == ITEM_BOW) ||
|
||||
(gSaveContext.equips.buttonItems[1] >= ITEM_BOW_ARROW_FIRE &&
|
||||
gSaveContext.equips.buttonItems[1] <= ITEM_BOW_ARROW_LIGHT),
|
||||
&play->interfaceCtx.cLeftAlpha);
|
||||
|
||||
UpdateButtonAlpha(flashAlpha,
|
||||
(gSaveContext.equips.buttonItems[2] == ITEM_BOW) ||
|
||||
(gSaveContext.equips.buttonItems[2] >= ITEM_BOW_ARROW_FIRE &&
|
||||
gSaveContext.equips.buttonItems[2] <= ITEM_BOW_ARROW_LIGHT),
|
||||
&play->interfaceCtx.cDownAlpha);
|
||||
|
||||
UpdateButtonAlpha(flashAlpha,
|
||||
(gSaveContext.equips.buttonItems[3] == ITEM_BOW) ||
|
||||
(gSaveContext.equips.buttonItems[3] >= ITEM_BOW_ARROW_FIRE &&
|
||||
gSaveContext.equips.buttonItems[3] <= ITEM_BOW_ARROW_LIGHT),
|
||||
&play->interfaceCtx.cRightAlpha);
|
||||
|
||||
if (CVarGetInteger(CVAR_ENHANCEMENT("DpadEquips"), 0)) {
|
||||
UpdateButtonAlpha(flashAlpha,
|
||||
(gSaveContext.equips.buttonItems[4] == ITEM_BOW) ||
|
||||
(gSaveContext.equips.buttonItems[4] >= ITEM_BOW_ARROW_FIRE &&
|
||||
gSaveContext.equips.buttonItems[4] <= ITEM_BOW_ARROW_LIGHT),
|
||||
&play->interfaceCtx.dpadRightAlpha);
|
||||
|
||||
UpdateButtonAlpha(flashAlpha,
|
||||
(gSaveContext.equips.buttonItems[5] == ITEM_BOW) ||
|
||||
(gSaveContext.equips.buttonItems[5] >= ITEM_BOW_ARROW_FIRE &&
|
||||
gSaveContext.equips.buttonItems[5] <= ITEM_BOW_ARROW_LIGHT),
|
||||
&play->interfaceCtx.dpadLeftAlpha);
|
||||
|
||||
UpdateButtonAlpha(flashAlpha,
|
||||
(gSaveContext.equips.buttonItems[6] == ITEM_BOW) ||
|
||||
(gSaveContext.equips.buttonItems[6] >= ITEM_BOW_ARROW_FIRE &&
|
||||
gSaveContext.equips.buttonItems[6] <= ITEM_BOW_ARROW_LIGHT),
|
||||
&play->interfaceCtx.dpadDownAlpha);
|
||||
|
||||
UpdateButtonAlpha(flashAlpha,
|
||||
(gSaveContext.equips.buttonItems[7] == ITEM_BOW) ||
|
||||
(gSaveContext.equips.buttonItems[7] >= ITEM_BOW_ARROW_FIRE &&
|
||||
gSaveContext.equips.buttonItems[7] <= ITEM_BOW_ARROW_LIGHT),
|
||||
&play->interfaceCtx.dpadUpAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
static void UpdateEquippedBow(PlayState* play, s8 arrowType) {
|
||||
s32 bowItem = GetBowItemForArrow((PlayerItemAction)arrowType);
|
||||
bool dpadEnabled = CVarGetInteger(CVAR_ENHANCEMENT("DpadEquips"), 0);
|
||||
s32 maxButton = dpadEnabled ? 7 : 3;
|
||||
|
||||
for (s32 i = 1; i <= maxButton; i++) {
|
||||
if ((gSaveContext.equips.buttonItems[i] == ITEM_BOW) ||
|
||||
(gSaveContext.equips.buttonItems[i] >= ITEM_BOW_ARROW_FIRE &&
|
||||
gSaveContext.equips.buttonItems[i] <= ITEM_BOW_ARROW_LIGHT)) {
|
||||
gSaveContext.equips.buttonItems[i] = bowItem;
|
||||
gSaveContext.equips.cButtonSlots[i - 1] = SLOT_BOW;
|
||||
|
||||
if (i <= 3) {
|
||||
Interface_LoadItemIcon1(play, i);
|
||||
}
|
||||
|
||||
gSaveContext.buttonStatus[i] = BTN_ENABLED;
|
||||
sButtonFlashTimer = BUTTON_FLASH_DURATION;
|
||||
sButtonFlashCount = 0;
|
||||
}
|
||||
}
|
||||
|
||||
UpdateFlashEffect(play);
|
||||
}
|
||||
|
||||
static void CycleToNextArrow(PlayState* play, Player* player) {
|
||||
s8 nextArrow = GetNextArrowType(player->heldItemAction);
|
||||
|
||||
if (player->heldActor != NULL && player->heldActor->id == ACTOR_EN_ARROW) {
|
||||
EnArrow* arrow = (EnArrow*)player->heldActor;
|
||||
|
||||
if (arrow->actor.child != NULL) {
|
||||
Actor_Kill(arrow->actor.child);
|
||||
}
|
||||
|
||||
Actor_Kill(&arrow->actor);
|
||||
}
|
||||
|
||||
Player_InitItemAction(play, player, (PlayerItemAction)nextArrow);
|
||||
UpdateEquippedBow(play, nextArrow);
|
||||
Audio_PlaySoundGeneral(NA_SE_PL_CHANGE_ARMS, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
|
||||
&gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
|
||||
}
|
||||
|
||||
void ArrowCycleMain() {
|
||||
if (gPlayState == nullptr || !CanCycleArrows()) {
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateFlashEffect(gPlayState);
|
||||
|
||||
Player* player = GET_PLAYER(gPlayState);
|
||||
Input* input = &gPlayState->state.input[0];
|
||||
|
||||
if (IsAimingBow(player) && CHECK_BTN_ANY(input->press.button, BTN_R)) {
|
||||
if (IsHoldingMagicBow(player) && gSaveContext.magicState != MAGIC_STATE_IDLE && player->heldActor == NULL) {
|
||||
Audio_PlaySoundGeneral(NA_SE_SY_ERROR, &gSfxDefaultPos, 4, &gSfxDefaultFreqAndVolScale,
|
||||
&gSfxDefaultFreqAndVolScale, &gSfxDefaultReverb);
|
||||
return;
|
||||
}
|
||||
|
||||
// reset magic state to IDLE before cycling to prevent error sound
|
||||
gSaveContext.magicState = MAGIC_STATE_IDLE;
|
||||
|
||||
CycleToNextArrow(gPlayState, player);
|
||||
}
|
||||
}
|
||||
|
||||
void RegisterArrowCycle() {
|
||||
COND_ID_HOOK(OnActorUpdate, ACTOR_PLAYER, CVAR_ARROW_CYCLE_VALUE, [](void* actor) { ArrowCycleMain(); });
|
||||
|
||||
// suppress shield input when R is held while aiming to allow arrow cycling
|
||||
COND_VB_SHOULD(VB_EXECUTE_PLAYER_ACTION_FUNC, CVAR_ARROW_CYCLE_VALUE, {
|
||||
Player* player = (Player*)va_arg(args, void*);
|
||||
Input* input = (Input*)va_arg(args, void*);
|
||||
if (IsAimingBow(player) && CHECK_BTN_ANY(input->cur.button, BTN_R)) {
|
||||
*should = false;
|
||||
}
|
||||
});
|
||||
|
||||
// don't consume magic on draw, but check if we have enough to fire
|
||||
COND_VB_SHOULD(VB_PLAYER_ARROW_MAGIC_CONSUMPTION, CVAR_ARROW_CYCLE_VALUE, {
|
||||
Player* player = va_arg(args, Player*);
|
||||
int32_t magicArrowType = va_arg(args, int32_t);
|
||||
int32_t* arrowType = va_arg(args, int32_t*);
|
||||
|
||||
if (gSaveContext.magic < sMagicArrowCosts[magicArrowType]) {
|
||||
*arrowType = ARROW_NORMAL;
|
||||
}
|
||||
|
||||
*should = false;
|
||||
});
|
||||
|
||||
COND_VB_SHOULD(VB_EN_ARROW_MAGIC_CONSUMPTION, CVAR_ARROW_CYCLE_VALUE, {
|
||||
EnArrow* arrow = va_arg(args, EnArrow*);
|
||||
|
||||
if (arrow->actor.params < ARROW_FIRE || arrow->actor.params > ARROW_LIGHT) {
|
||||
return;
|
||||
}
|
||||
|
||||
int32_t magicArrowType = arrow->actor.params - ARROW_FIRE;
|
||||
Magic_RequestChange(gPlayState, sMagicArrowCosts[magicArrowType], MAGIC_CONSUME_NOW);
|
||||
});
|
||||
}
|
||||
|
||||
static RegisterShipInitFunc initFunc(RegisterArrowCycle, { CVAR_ARROW_CYCLE_NAME });
|
||||
@@ -521,6 +521,14 @@ typedef enum {
|
||||
// - None
|
||||
VB_END_GERUDO_MEMBERSHIP_TALK,
|
||||
|
||||
// #### `result`
|
||||
// ```c
|
||||
// true
|
||||
// ```
|
||||
// #### `args`
|
||||
// - `*EnArrow`
|
||||
VB_EN_ARROW_MAGIC_CONSUMPTION,
|
||||
|
||||
// #### `result`
|
||||
// ```c
|
||||
// !(this->stateFlags3 & PLAYER_STATE3_PAUSE_ACTION_FUNC)
|
||||
@@ -1788,6 +1796,16 @@ typedef enum {
|
||||
// - `*DemoIm`
|
||||
VB_PLAY_ZELDAS_LULLABY_CS,
|
||||
|
||||
// #### `result`
|
||||
// ```c
|
||||
// true
|
||||
// ```
|
||||
// #### `args`
|
||||
// - `*Player`
|
||||
// - `int32_t` (magicArrowType)
|
||||
// - `*int32_t` (arrowType)
|
||||
VB_PLAYER_ARROW_MAGIC_CONSUMPTION,
|
||||
|
||||
// #### `result`
|
||||
// ```c
|
||||
// item == ITEM_SAW
|
||||
|
||||
@@ -894,6 +894,12 @@ void SohMenu::AddMenuEnhancements() {
|
||||
.CVar(CVAR_ENHANCEMENT("BowReticle"))
|
||||
.Options(CheckboxOptions().Tooltip("Aiming with a Bow or Slingshot will display a reticle as with the Hookshot "
|
||||
"when the projectile is ready to fire."));
|
||||
AddWidget(path, "Arrow Cycle", WIDGET_CVAR_CHECKBOX)
|
||||
.CVar(CVAR_ENHANCEMENT("BowArrowCycle"))
|
||||
.Options(CheckboxOptions().Tooltip(
|
||||
"Allows cycling between different arrow types (Normal, Fire, Ice, Light) while aiming the bow. "
|
||||
"Press the R button to cycle to the next available arrow type. "
|
||||
"Only works when aiming and only cycles to arrow types you own with sufficient magic."));
|
||||
|
||||
path.column = SECTION_COLUMN_3;
|
||||
AddWidget(path, "Hookshot", WIDGET_SEPARATOR_TEXT);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include "objects/gameplay_keep/gameplay_keep.h"
|
||||
#include "objects/object_gi_nuts/object_gi_nuts.h"
|
||||
|
||||
#include "soh/Enhancements/game-interactor/GameInteractor_Hooks.h"
|
||||
|
||||
#define FLAGS (ACTOR_FLAG_UPDATE_CULLING_DISABLED | ACTOR_FLAG_DRAW_CULLING_DISABLED)
|
||||
|
||||
void EnArrow_Init(Actor* thisx, PlayState* play);
|
||||
@@ -227,6 +229,7 @@ void EnArrow_Shoot(EnArrow* this, PlayState* play) {
|
||||
case ARROW_FIRE:
|
||||
case ARROW_ICE:
|
||||
case ARROW_LIGHT:
|
||||
GameInteractor_Should(VB_EN_ARROW_MAGIC_CONSUMPTION, true, this);
|
||||
Player_PlaySfx(&player->actor, NA_SE_IT_MAGIC_ARROW_SHOT);
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -2692,9 +2692,13 @@ s32 func_8083442C(Player* this, PlayState* play) {
|
||||
magicArrowType = arrowType - ARROW_FIRE;
|
||||
|
||||
if (this->unk_860 >= 0) {
|
||||
if ((magicArrowType >= 0) && (magicArrowType <= 2) &&
|
||||
!Magic_RequestChange(play, sMagicArrowCosts[magicArrowType], MAGIC_CONSUME_NOW)) {
|
||||
arrowType = ARROW_NORMAL;
|
||||
if ((magicArrowType >= 0) && (magicArrowType <= 2)) {
|
||||
if (GameInteractor_Should(VB_PLAYER_ARROW_MAGIC_CONSUMPTION, true, this, magicArrowType,
|
||||
&arrowType)) {
|
||||
if (!Magic_RequestChange(play, sMagicArrowCosts[magicArrowType], MAGIC_CONSUME_NOW)) {
|
||||
arrowType = ARROW_NORMAL;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this->heldActor = Actor_SpawnAsChild(
|
||||
|
||||
Reference in New Issue
Block a user