pokeemmo/src/fishing.c
2025-10-04 09:05:28 -07:00

650 lines
19 KiB
C

#include "global.h"
#include "main.h"
#include "event_object_movement.h"
#include "fieldmap.h"
#include "field_effect_helpers.h"
#include "field_player_avatar.h"
#include "menu.h"
#include "metatile_behavior.h"
#include "random.h"
#include "script.h"
#include "strings.h"
#include "task.h"
#include "text.h"
#include "tv.h"
#include "wild_encounter.h"
#include "config/fishing.h"
static void Task_Fishing(u8);
static bool32 Fishing_Init(struct Task *);
static bool32 Fishing_GetRodOut(struct Task *);
static bool32 Fishing_WaitBeforeDots(struct Task *);
static bool32 Fishing_InitDots(struct Task *);
static bool32 Fishing_ShowDots(struct Task *);
static bool32 Fishing_CheckForBite(struct Task *);
static bool32 Fishing_GotBite(struct Task *);
static bool32 Fishing_ChangeMinigame(struct Task *);
static bool32 Fishing_WaitForA(struct Task *);
static bool32 Fishing_APressNoMinigame(struct Task *);
static bool32 Fishing_CheckMoreDots(struct Task *);
static bool32 Fishing_MonOnHook(struct Task *);
static bool32 Fishing_StartEncounter(struct Task *);
static bool32 Fishing_NotEvenNibble(struct Task *);
static bool32 Fishing_GotAway(struct Task *);
static bool32 Fishing_NoMon(struct Task *);
static bool32 Fishing_PutRodAway(struct Task *);
static bool32 Fishing_EndNoMon(struct Task *);
static void AlignFishingAnimationFrames(void);
static bool32 DoesFishingMinigameAllowCancel(void);
static bool32 Fishing_DoesFirstMonInPartyHaveSuctionCupsOrStickyHold(void);
static bool32 Fishing_RollForBite(u32, bool32);
static u32 CalculateFishingBiteOdds(u32, bool32);
static u32 CalculateFishingFollowerBoost(void);
static u32 CalculateFishingProximityBoost(void);
static u32 CalculateFishingTimeOfDayBoost(void);
#define FISHING_PROXIMITY_BOOST 20 //Active if config I_FISHING_PROXIMITY is TRUE
#define FISHING_TIME_OF_DAY_BOOST 20 //Active if config I_FISHING_TIME_OF_DAY_BOOST is TRUE
#define FISHING_GEN3_STICKY_CHANCE 85 //Active if config I_FISHING_STICKY_BOOST is set to GEN_3 or lower
#if I_FISHING_BITE_ODDS >= GEN_4
#define FISHING_OLD_ROD_ODDS 25
#define FISHING_GOOD_ROD_ODDS 50
#define FISHING_SUPER_ROD_ODDS 75
#elif I_FISHING_BITE_ODDS >= GEN_3
#define FISHING_OLD_ROD_ODDS 50
#define FISHING_GOOD_ROD_ODDS 50
#define FISHING_SUPER_ROD_ODDS 50
#else
#define FISHING_OLD_ROD_ODDS 100
#define FISHING_GOOD_ROD_ODDS 33
#define FISHING_SUPER_ROD_ODDS 50
#endif
struct FriendshipHookChanceBoost
{
u8 threshold;
u8 bonus;
};
//Needs to be defined in descending order and end with the 0 friendship boost
//Active if config I_FISHING_FOLLOWER_BOOST is TRUE
static const struct FriendshipHookChanceBoost sFriendshipHookChanceBoostArray[] =
{
{.threshold = 250, .bonus = 50},
{.threshold = 200, .bonus = 40},
{.threshold = 150, .bonus = 30},
{.threshold = 100, .bonus = 20},
{.threshold = 0, .bonus = 0},
};
#define FISHING_CHAIN_SHINY_STREAK_MAX 20
enum
{
FISHING_INIT,
FISHING_GET_ROD_OUT,
FISHING_WAIT_BEFORE_DOTS,
FISHING_INIT_DOTS,
FISHING_SHOW_DOTS,
FISHING_CHECK_FOR_BITE,
FISHING_GOT_BITE,
FISHING_CHANGE_MINIGAME,
FISHING_WAIT_FOR_A,
FISHING_A_PRESS_NO_MINIGAME,
FISHING_CHECK_MORE_DOTS,
FISHING_MON_ON_HOOK,
FISHING_START_ENCOUNTER,
FISHING_NOT_EVEN_NIBBLE,
FISHING_GOT_AWAY,
FISHING_NO_MON,
FISHING_PUT_ROD_AWAY,
FISHING_END_NO_MON,
};
static bool32 (*const sFishingStateFuncs[])(struct Task *) =
{
[FISHING_INIT] = Fishing_Init,
[FISHING_GET_ROD_OUT] = Fishing_GetRodOut,
[FISHING_WAIT_BEFORE_DOTS] = Fishing_WaitBeforeDots,
[FISHING_INIT_DOTS] = Fishing_InitDots,
[FISHING_SHOW_DOTS] = Fishing_ShowDots,
[FISHING_CHECK_FOR_BITE] = Fishing_CheckForBite,
[FISHING_GOT_BITE] = Fishing_GotBite,
[FISHING_CHANGE_MINIGAME] = Fishing_ChangeMinigame,
[FISHING_WAIT_FOR_A] = Fishing_WaitForA,
[FISHING_A_PRESS_NO_MINIGAME] = Fishing_APressNoMinigame,
[FISHING_CHECK_MORE_DOTS] = Fishing_CheckMoreDots,
[FISHING_MON_ON_HOOK] = Fishing_MonOnHook,
[FISHING_START_ENCOUNTER] = Fishing_StartEncounter,
[FISHING_NOT_EVEN_NIBBLE] = Fishing_NotEvenNibble,
[FISHING_GOT_AWAY] = Fishing_GotAway,
[FISHING_NO_MON] = Fishing_NoMon,
[FISHING_PUT_ROD_AWAY] = Fishing_PutRodAway,
[FISHING_END_NO_MON] = Fishing_EndNoMon,
};
#define tStep data[0]
#define tFrameCounter data[1]
#define tNumDots data[2]
#define tDotsRequired data[3]
#define tRoundsPlayed data[12]
#define tMinRoundsRequired data[13]
#define tPlayerGfxId data[14]
#define tFishingRod data[15]
void StartFishing(u8 rod)
{
u8 taskId = CreateTask(Task_Fishing, 0xFF);
gTasks[taskId].tFishingRod = rod;
Task_Fishing(taskId);
}
static void Task_Fishing(u8 taskId)
{
while (sFishingStateFuncs[gTasks[taskId].tStep](&gTasks[taskId]))
;
}
static bool32 Fishing_Init(struct Task *task)
{
LockPlayerFieldControls();
gPlayerAvatar.preventStep = TRUE;
task->tStep = FISHING_GET_ROD_OUT;
return FALSE;
}
static bool32 Fishing_GetRodOut(struct Task *task)
{
struct ObjectEvent *playerObjEvent;
const s16 minRounds1[] = {
[OLD_ROD] = 1,
[GOOD_ROD] = 1,
[SUPER_ROD] = 1
};
const s16 minRounds2[] = {
[OLD_ROD] = 1,
[GOOD_ROD] = 3,
[SUPER_ROD] = 6
};
task->tRoundsPlayed = 0;
task->tMinRoundsRequired = minRounds1[task->tFishingRod] + (Random() % minRounds2[task->tFishingRod]);
task->tPlayerGfxId = gObjectEvents[gPlayerAvatar.objectEventId].graphicsId;
playerObjEvent = &gObjectEvents[gPlayerAvatar.objectEventId];
ObjectEventClearHeldMovementIfActive(playerObjEvent);
playerObjEvent->enableAnim = TRUE;
SetPlayerAvatarFishing(playerObjEvent->facingDirection);
task->tStep = FISHING_WAIT_BEFORE_DOTS;
return FALSE;
}
static bool32 Fishing_WaitBeforeDots(struct Task *task)
{
AlignFishingAnimationFrames();
// Wait one second
task->tFrameCounter++;
if (task->tFrameCounter >= 60)
task->tStep = FISHING_INIT_DOTS;
return FALSE;
}
static bool32 Fishing_InitDots(struct Task *task)
{
u32 randVal;
LoadMessageBoxAndFrameGfx(0, TRUE);
task->tStep = FISHING_SHOW_DOTS;
task->tFrameCounter = 0;
task->tNumDots = 0;
randVal = Random();
randVal %= 10;
task->tDotsRequired = randVal + 1;
if (task->tRoundsPlayed == 0)
task->tDotsRequired = randVal + 4;
if (task->tDotsRequired >= 10)
task->tDotsRequired = 10;
return TRUE;
}
static bool32 Fishing_ShowDots(struct Task *task)
{
const u8 dot[] = _("·");
AlignFishingAnimationFrames();
task->tFrameCounter++;
if (JOY_NEW(A_BUTTON))
{
if (!DoesFishingMinigameAllowCancel())
return FALSE;
task->tStep = FISHING_NOT_EVEN_NIBBLE;
if (task->tRoundsPlayed != 0)
task->tStep = FISHING_GOT_AWAY;
return TRUE;
}
else
{
if (task->tFrameCounter >= 20)
{
task->tFrameCounter = 0;
if (task->tNumDots >= task->tDotsRequired)
{
task->tStep = FISHING_CHECK_FOR_BITE;
if (task->tRoundsPlayed != 0)
task->tStep = FISHING_GOT_BITE;
task->tRoundsPlayed++;
}
else
{
AddTextPrinterParameterized(0, FONT_NORMAL, dot, task->tNumDots * 8, 1, 0, NULL);
task->tNumDots++;
}
}
return FALSE;
}
}
static bool32 Fishing_CheckForBite(struct Task *task)
{
bool32 bite, firstMonHasSuctionOrSticky;
AlignFishingAnimationFrames();
task->tStep = FISHING_GOT_BITE;
bite = FALSE;
if (!DoesCurrentMapHaveFishingMons())
{
task->tStep = FISHING_NOT_EVEN_NIBBLE;
return TRUE;
}
firstMonHasSuctionOrSticky = Fishing_DoesFirstMonInPartyHaveSuctionCupsOrStickyHold();
if(firstMonHasSuctionOrSticky && I_FISHING_STICKY_BOOST < GEN_4)
bite = RandomPercentage(RNG_FISHING_GEN3_STICKY, FISHING_GEN3_STICKY_CHANCE);
if (!bite)
bite = Fishing_RollForBite(task->tFishingRod, firstMonHasSuctionOrSticky);
if (!bite)
task->tStep = FISHING_NOT_EVEN_NIBBLE;
if (bite)
StartSpriteAnim(&gSprites[gPlayerAvatar.spriteId], GetFishingBiteDirectionAnimNum(GetPlayerFacingDirection()));
return TRUE;
}
static bool32 Fishing_GotBite(struct Task *task)
{
AlignFishingAnimationFrames();
AddTextPrinterParameterized(0, FONT_NORMAL, gText_OhABite, 0, 17, 0, NULL);
task->tStep = FISHING_CHANGE_MINIGAME;
task->tFrameCounter = 0;
return FALSE;
}
static bool32 Fishing_ChangeMinigame(struct Task *task)
{
switch (I_FISHING_MINIGAME)
{
case GEN_1:
case GEN_2:
task->tStep = FISHING_A_PRESS_NO_MINIGAME;
break;
case GEN_3:
default:
task->tStep = FISHING_WAIT_FOR_A;
break;
}
return TRUE;
}
// We have a bite. Now, wait for the player to press A, or the timer to expire.
static bool32 Fishing_WaitForA(struct Task *task)
{
const s16 reelTimeouts[3] = {
[OLD_ROD] = 36,
[GOOD_ROD] = 33,
[SUPER_ROD] = 30
};
AlignFishingAnimationFrames();
task->tFrameCounter++;
if (task->tFrameCounter >= reelTimeouts[task->tFishingRod])
task->tStep = FISHING_GOT_AWAY;
else if (JOY_NEW(A_BUTTON))
task->tStep = FISHING_CHECK_MORE_DOTS;
return FALSE;
}
static bool32 Fishing_APressNoMinigame(struct Task *task)
{
AlignFishingAnimationFrames();
if (JOY_NEW(A_BUTTON))
task->tStep = FISHING_MON_ON_HOOK;
return FALSE;
}
// Determine if we're going to play the dot game again
static bool32 Fishing_CheckMoreDots(struct Task *task)
{
const s16 moreDotsChance[][2] =
{
[OLD_ROD] = {0, 0},
[GOOD_ROD] = {40, 10},
[SUPER_ROD] = {70, 30}
};
AlignFishingAnimationFrames();
task->tStep = FISHING_MON_ON_HOOK;
if (task->tRoundsPlayed < task->tMinRoundsRequired)
{
task->tStep = FISHING_INIT_DOTS;
}
else if (task->tRoundsPlayed < 2)
{
// probability of having to play another round
s16 probability = Random() % 100;
if (moreDotsChance[task->tFishingRod][task->tRoundsPlayed] > probability)
task->tStep = FISHING_INIT_DOTS;
}
return FALSE;
}
static bool32 Fishing_MonOnHook(struct Task *task)
{
AlignFishingAnimationFrames();
FillWindowPixelBuffer(0, PIXEL_FILL(1));
AddTextPrinterParameterized2(0, FONT_NORMAL, gText_PokemonOnHook, 1, 0, TEXT_COLOR_DARK_GRAY, TEXT_COLOR_WHITE, TEXT_COLOR_LIGHT_GRAY);
task->tStep = FISHING_START_ENCOUNTER;
task->tFrameCounter = 0;
return FALSE;
}
static bool32 Fishing_StartEncounter(struct Task *task)
{
if (task->tFrameCounter == 0)
AlignFishingAnimationFrames();
RunTextPrinters();
if (task->tFrameCounter == 0)
{
if (!IsTextPrinterActive(0))
{
struct ObjectEvent *playerObjEvent = &gObjectEvents[gPlayerAvatar.objectEventId];
ObjectEventSetGraphicsId(playerObjEvent, task->tPlayerGfxId);
ObjectEventTurn(playerObjEvent, playerObjEvent->movementDirection);
if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_SURFING)
SetSurfBlob_PlayerOffset(gObjectEvents[gPlayerAvatar.objectEventId].fieldEffectSpriteId, FALSE, 0);
gSprites[gPlayerAvatar.spriteId].x2 = 0;
gSprites[gPlayerAvatar.spriteId].y2 = 0;
ClearDialogWindowAndFrame(0, TRUE);
task->tFrameCounter++;
return FALSE;
}
}
if (task->tFrameCounter != 0)
{
gPlayerAvatar.preventStep = FALSE;
UnlockPlayerFieldControls();
FishingWildEncounter(task->tFishingRod);
RecordFishingAttemptForTV(TRUE);
DestroyTask(FindTaskIdByFunc(Task_Fishing));
}
return FALSE;
}
static bool32 Fishing_NotEvenNibble(struct Task *task)
{
gChainFishingDexNavStreak = 0;
AlignFishingAnimationFrames();
StartSpriteAnim(&gSprites[gPlayerAvatar.spriteId], GetFishingNoCatchDirectionAnimNum(GetPlayerFacingDirection()));
FillWindowPixelBuffer(0, PIXEL_FILL(1));
AddTextPrinterParameterized2(0, FONT_NORMAL, gText_NotEvenANibble, 1, 0, TEXT_COLOR_DARK_GRAY, TEXT_COLOR_WHITE, TEXT_COLOR_LIGHT_GRAY);
task->tStep = FISHING_NO_MON;
return TRUE;
}
static bool32 Fishing_GotAway(struct Task *task)
{
gChainFishingDexNavStreak = 0;
AlignFishingAnimationFrames();
StartSpriteAnim(&gSprites[gPlayerAvatar.spriteId], GetFishingNoCatchDirectionAnimNum(GetPlayerFacingDirection()));
FillWindowPixelBuffer(0, PIXEL_FILL(1));
AddTextPrinterParameterized2(0, FONT_NORMAL, gText_ItGotAway, 1, 0, TEXT_COLOR_DARK_GRAY, TEXT_COLOR_WHITE, TEXT_COLOR_LIGHT_GRAY);
task->tStep = FISHING_NO_MON;
return TRUE;
}
static bool32 Fishing_NoMon(struct Task *task)
{
AlignFishingAnimationFrames();
task->tStep = FISHING_PUT_ROD_AWAY;
return FALSE;
}
static bool32 Fishing_PutRodAway(struct Task *task)
{
AlignFishingAnimationFrames();
if (gSprites[gPlayerAvatar.spriteId].animEnded)
{
struct ObjectEvent *playerObjEvent = &gObjectEvents[gPlayerAvatar.objectEventId];
ObjectEventSetGraphicsId(playerObjEvent, task->tPlayerGfxId);
ObjectEventTurn(playerObjEvent, playerObjEvent->movementDirection);
if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_SURFING)
SetSurfBlob_PlayerOffset(gObjectEvents[gPlayerAvatar.objectEventId].fieldEffectSpriteId, FALSE, 0);
gSprites[gPlayerAvatar.spriteId].x2 = 0;
gSprites[gPlayerAvatar.spriteId].y2 = 0;
task->tStep = FISHING_END_NO_MON;
}
return FALSE;
}
static bool32 Fishing_EndNoMon(struct Task *task)
{
RunTextPrinters();
if (!IsTextPrinterActive(0))
{
gPlayerAvatar.preventStep = FALSE;
UnlockPlayerFieldControls();
UnfreezeObjectEvents();
ClearDialogWindowAndFrame(0, TRUE);
RecordFishingAttemptForTV(FALSE);
DestroyTask(FindTaskIdByFunc(Task_Fishing));
}
return FALSE;
}
static bool32 DoesFishingMinigameAllowCancel(void)
{
switch(I_FISHING_MINIGAME)
{
case GEN_1:
case GEN_2:
return FALSE;
case GEN_3:
default:
return TRUE;
}
}
static bool32 Fishing_DoesFirstMonInPartyHaveSuctionCupsOrStickyHold(void)
{
enum Ability ability;
if (GetMonData(&gPlayerParty[0], MON_DATA_SANITY_IS_EGG))
return FALSE;
ability = GetMonAbility(&gPlayerParty[0]);
return (ability == ABILITY_SUCTION_CUPS || ability == ABILITY_STICKY_HOLD);
}
static bool32 Fishing_RollForBite(u32 rod, bool32 isStickyHold)
{
return ((RandomUniform(RNG_FISHING_BITE, 1, 100)) <= CalculateFishingBiteOdds(rod, isStickyHold));
}
static u32 CalculateFishingBiteOdds(u32 rod, bool32 isStickyHold)
{
u32 odds;
if (rod == OLD_ROD)
odds = FISHING_OLD_ROD_ODDS;
if (rod == GOOD_ROD)
odds = FISHING_GOOD_ROD_ODDS;
if (rod == SUPER_ROD)
odds = FISHING_SUPER_ROD_ODDS;
odds += CalculateFishingFollowerBoost();
odds += CalculateFishingProximityBoost();
odds += CalculateFishingTimeOfDayBoost();
if (isStickyHold && I_FISHING_STICKY_BOOST >= GEN_4)
odds *= 2;
odds = min(100, odds);
DebugPrintf("Fishing odds: %d", odds);
return odds;
}
static u32 CalculateFishingFollowerBoost()
{
u32 friendship;
struct Pokemon *mon = GetFirstLiveMon();
if (!I_FISHING_FOLLOWER_BOOST || !mon)
return 0;
friendship = GetMonData(mon, MON_DATA_FRIENDSHIP);
for (u32 i = 0;; i++)
{
if (friendship >= sFriendshipHookChanceBoostArray[i].threshold)
return sFriendshipHookChanceBoostArray[i].bonus;
}
}
static u32 CalculateFishingProximityBoost()
{
s16 bobber_x, bobber_y, tile_x, tile_y;
u32 direction, facingDirection, numQualifyingTile = 0;
struct ObjectEvent *objectEvent;
if (!I_FISHING_PROXIMITY)
return 0;
objectEvent = &gObjectEvents[gPlayerAvatar.objectEventId];
bobber_x = objectEvent->currentCoords.x;
bobber_y = objectEvent->currentCoords.y;
facingDirection = GetPlayerFacingDirection();
MoveCoords(facingDirection, &bobber_x, &bobber_y);
numQualifyingTile = 0;
for (direction = DIR_SOUTH; direction < CARDINAL_DIRECTION_COUNT; direction++)
{
tile_x = bobber_x;
tile_y = bobber_y;
MoveCoords(direction, &tile_x, &tile_y);
if (tile_x == objectEvent->currentCoords.x && tile_y == objectEvent->currentCoords.y)
continue;
if (!MetatileBehavior_IsSurfableFishableWater(MapGridGetMetatileBehaviorAt(tile_x, tile_y)))
numQualifyingTile++;
else if (MapGridGetCollisionAt(tile_x, tile_y))
numQualifyingTile++;
else if (GetMapBorderIdAt(tile_x, tile_y) == -1)
numQualifyingTile++;
}
return (numQualifyingTile * FISHING_PROXIMITY_BOOST);
}
static u32 CalculateFishingTimeOfDayBoost()
{
if (!I_FISHING_TIME_OF_DAY_BOOST)
return 0;
enum TimeOfDay timeOfDay = GetTimeOfDay();
if (timeOfDay == TIME_MORNING || timeOfDay == TIME_EVENING)
return FISHING_TIME_OF_DAY_BOOST;
return 0;
}
#undef tStep
#undef tFrameCounter
#undef tNumDots
#undef tDotsRequired
#undef tRoundsPlayed
#undef tMinRoundsRequired
#undef tPlayerGfxId
#undef tFishingRod
static void AlignFishingAnimationFrames(void)
{
struct Sprite *playerSprite = &gSprites[gPlayerAvatar.spriteId];
u8 animCmdIndex;
u8 animType;
AnimateSprite(playerSprite);
playerSprite->x2 = 0;
playerSprite->y2 = 0;
animCmdIndex = playerSprite->animCmdIndex;
if (playerSprite->anims[playerSprite->animNum][animCmdIndex].type == -1)
{
animCmdIndex--;
}
else
{
playerSprite->animDelayCounter++;
if (playerSprite->anims[playerSprite->animNum][animCmdIndex].type == -1)
animCmdIndex--;
}
animType = playerSprite->anims[playerSprite->animNum][animCmdIndex].type;
if (animType == 1 || animType == 2 || animType == 3)
{
playerSprite->x2 = 8;
if (GetPlayerFacingDirection() == 3)
playerSprite->x2 = -8;
}
if (animType == 5)
playerSprite->y2 = -8;
if (animType == 10 || animType == 11)
playerSprite->y2 = 8;
if (gPlayerAvatar.flags & PLAYER_AVATAR_FLAG_SURFING)
SetSurfBlob_PlayerOffset(gObjectEvents[gPlayerAvatar.objectEventId].fieldEffectSpriteId, TRUE, playerSprite->y2);
}
void UpdateChainFishingStreak()
{
if (!I_FISHING_CHAIN)
return;
if (gChainFishingDexNavStreak == MAX_u8)
return;
gChainFishingDexNavStreak++;
}
u32 CalculateChainFishingShinyRolls(void)
{
if (!I_FISHING_CHAIN || !gIsFishingEncounter)
return 0;
u32 a = 2 * min(gChainFishingDexNavStreak, FISHING_CHAIN_SHINY_STREAK_MAX);
DebugPrintf("Total Shiny Rolls %d", a);
return a;
}
bool32 ShouldUseFishingEnvironmentInBattle()
{
return (I_FISHING_ENVIRONMENT >= GEN_4 && gIsFishingEncounter);
}