Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expanded Pokémon Follower transformation functionality #5048

Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions include/config/overworld.h
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,10 @@
// 16x32, 32x32, 64x64 etc are fine
// Follower Pokémon
#define OW_FOLLOWERS_ENABLED FALSE // Enables follower Pokémon, HGSS style. Requires OW_POKEMON_OBJECT_EVENTS. Note that additional scripting may be required for them to be fully supported!
#define OW_FOLLOWERS_BOBBING TRUE // If true, follower pokemon will bob up and down during their idle & walking animations
#define OW_FOLLOWERS_POKEBALLS TRUE // Followers will emerge from the pokeball they are stored in, instead of a normal pokeball
#define OW_FOLLOWERS_BOBBING TRUE // If TRUE, follower Pokémon will bob up and down during their idle & walking animations
#define OW_FOLLOWERS_POKEBALLS TRUE // If TRUE, follower Pokémon will emerge from the Poké Ball they are stored in, instead of a normal Poké Ball
#define OW_FOLLOWERS_WEATHER_FORMS FALSE // If TRUE, follower Pokémon that transform in battle via weather will also transform in the overworld.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Forms still change in the party, even with this config disabled
image

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we may need a config to differentiate between them only changing appearance in the overworld (which is purely cosmetic, like OW_FOLLOWERS_COPY_WILD_PKMN) and one that actually changes the form (since this causes non-vanilla interactions when it enters a battle with starting entry hazards)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't even have to be starting hazards. Actually the first mon won't be affected by it but the transformed mons in the bag will which would be solved by:

This should only affect the Pokémon in the first slot of the party (the one currently following, rather). It's the fact they're out of the ball that makes them change forms. As a result, I think all other species should probably revert back to "normal" conditions if they're not currently outside of the ball.

#define OW_FOLLOWERS_COPY_WILD_PKMN FALSE // If TRUE, follower Pokémon that know Transform or have Illusion/Imposter will copy wild Pokémon at random.

// Out-of-battle Ability effects
#define OW_SYNCHRONIZE_NATURE GEN_LATEST // In Gen8+, if a Pokémon with Synchronize leads the party, wild Pokémon will always have their same Nature as opposed to the 50% chance in previous games. Gift Pokémon excluded.
Expand Down
4 changes: 4 additions & 0 deletions include/constants/form_change_types.h
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,8 @@
// param1: amount of days
#define FORM_CHANGE_DAYS_PASSED 23

// Form change that activates when overworld weather changes.
// param1: weather to check.
#define FORM_CHANGE_OVERWORLD_WEATHER 24

#endif // GUARD_CONSTANTS_FORM_CHANGE_TYPES_H
1 change: 1 addition & 0 deletions include/event_object_movement.h
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,7 @@ void ClearObjectEventMovement(struct ObjectEvent *objectEvent, struct Sprite *sp
void ObjectEventClearHeldMovement(struct ObjectEvent *);
void ObjectEventClearHeldMovementIfActive(struct ObjectEvent *);
struct Pokemon *GetFirstLiveMon(void);
u16 GetOverworldWeatherSpecies(u16 species);
void UpdateFollowingPokemon(void);
void RemoveFollowingPokemon(void);
struct ObjectEvent *GetFollowerObject(void);
Expand Down
176 changes: 84 additions & 92 deletions src/battle_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -10612,20 +10612,17 @@ bool32 DoesSpeciesUseHoldItemToChangeForm(u16 species, u16 heldItemId)
u32 i;
const struct FormChange *formChanges = GetSpeciesFormChanges(species);

if (formChanges != NULL)
for (i = 0; formChanges != NULL && formChanges[i].method != FORM_CHANGE_TERMINATOR; i++)
{
for (i = 0; formChanges[i].method != FORM_CHANGE_TERMINATOR; i++)
switch (formChanges[i].method)
{
switch (formChanges[i].method)
{
case FORM_CHANGE_BATTLE_MEGA_EVOLUTION_ITEM:
case FORM_CHANGE_BATTLE_PRIMAL_REVERSION:
case FORM_CHANGE_BATTLE_ULTRA_BURST:
case FORM_CHANGE_ITEM_HOLD:
if (formChanges[i].param1 == heldItemId)
return TRUE;
break;
}
case FORM_CHANGE_BATTLE_MEGA_EVOLUTION_ITEM:
case FORM_CHANGE_BATTLE_PRIMAL_REVERSION:
case FORM_CHANGE_BATTLE_ULTRA_BURST:
case FORM_CHANGE_ITEM_HOLD:
if (formChanges[i].param1 == heldItemId)
return TRUE;
break;
}
}
return FALSE;
Expand Down Expand Up @@ -10756,93 +10753,88 @@ u16 GetBattleFormChangeTargetSpecies(u32 battler, u16 method)
u16 species = gBattleMons[battler].species;
const struct FormChange *formChanges = GetSpeciesFormChanges(species);
struct Pokemon *mon = &GetBattlerParty(battler)[gBattlerPartyIndexes[battler]];
u16 heldItem;
u16 heldItem = gBattleMons[battler].item;

if (formChanges != NULL)
for (i = 0; formChanges != NULL && formChanges[i].method != FORM_CHANGE_TERMINATOR; i++)
{
heldItem = gBattleMons[battler].item;

for (i = 0; formChanges[i].method != FORM_CHANGE_TERMINATOR; i++)
if (method == formChanges[i].method && species != formChanges[i].targetSpecies)
{
if (method == formChanges[i].method && species != formChanges[i].targetSpecies)
switch (method)
{
switch (method)
{
case FORM_CHANGE_BATTLE_MEGA_EVOLUTION_ITEM:
case FORM_CHANGE_BATTLE_PRIMAL_REVERSION:
case FORM_CHANGE_BATTLE_ULTRA_BURST:
if (heldItem == formChanges[i].param1)
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_MEGA_EVOLUTION_MOVE:
if (gBattleMons[battler].moves[0] == formChanges[i].param1
|| gBattleMons[battler].moves[1] == formChanges[i].param1
|| gBattleMons[battler].moves[2] == formChanges[i].param1
|| gBattleMons[battler].moves[3] == formChanges[i].param1)
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_SWITCH:
if (formChanges[i].param1 == GetBattlerAbility(battler) || formChanges[i].param1 == ABILITY_NONE)
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_HP_PERCENT:
if (formChanges[i].param1 == GetBattlerAbility(battler))
{
// We multiply by 100 to make sure that integer division doesn't mess with the health check.
u32 hpCheck = gBattleMons[battler].hp * 100 * 100 / gBattleMons[battler].maxHP;
switch(formChanges[i].param2)
{
case HP_HIGHER_THAN:
if (hpCheck > formChanges[i].param3 * 100)
targetSpecies = formChanges[i].targetSpecies;
break;
case HP_LOWER_EQ_THAN:
if (hpCheck <= formChanges[i].param3 * 100)
targetSpecies = formChanges[i].targetSpecies;
break;
}
}
break;
case FORM_CHANGE_BATTLE_GIGANTAMAX:
if (GetMonData(mon, MON_DATA_GIGANTAMAX_FACTOR))
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_WEATHER:
// Check if there is a required ability and if the battler's ability does not match it
// or is suppressed. If so, revert to the no weather form.
if (formChanges[i].param2
&& GetBattlerAbility(battler) != formChanges[i].param2
&& formChanges[i].param1 == B_WEATHER_NONE)
{
targetSpecies = formChanges[i].targetSpecies;
}
// We need to revert the weather form if the field is under Air Lock, too.
else if (!WEATHER_HAS_EFFECT && formChanges[i].param1 == B_WEATHER_NONE)
{
targetSpecies = formChanges[i].targetSpecies;
}
// Otherwise, just check for a match between the weather and the form change table.
// Added a check for whether the weather is in effect to prevent end-of-turn soft locks with Cloud Nine / Air Lock
else if (((gBattleWeather & formChanges[i].param1) && WEATHER_HAS_EFFECT)
|| (gBattleWeather == B_WEATHER_NONE && formChanges[i].param1 == B_WEATHER_NONE))
case FORM_CHANGE_BATTLE_MEGA_EVOLUTION_ITEM:
case FORM_CHANGE_BATTLE_PRIMAL_REVERSION:
case FORM_CHANGE_BATTLE_ULTRA_BURST:
if (heldItem == formChanges[i].param1)
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_MEGA_EVOLUTION_MOVE:
if (gBattleMons[battler].moves[0] == formChanges[i].param1
|| gBattleMons[battler].moves[1] == formChanges[i].param1
|| gBattleMons[battler].moves[2] == formChanges[i].param1
|| gBattleMons[battler].moves[3] == formChanges[i].param1)
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_SWITCH:
if (formChanges[i].param1 == GetBattlerAbility(battler) || formChanges[i].param1 == ABILITY_NONE)
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_HP_PERCENT:
if (formChanges[i].param1 == GetBattlerAbility(battler))
{
// We multiply by 100 to make sure that integer division doesn't mess with the health check.
u32 hpCheck = gBattleMons[battler].hp * 100 * 100 / gBattleMons[battler].maxHP;
switch(formChanges[i].param2)
{
targetSpecies = formChanges[i].targetSpecies;
case HP_HIGHER_THAN:
if (hpCheck > formChanges[i].param3 * 100)
targetSpecies = formChanges[i].targetSpecies;
break;
case HP_LOWER_EQ_THAN:
if (hpCheck <= formChanges[i].param3 * 100)
targetSpecies = formChanges[i].targetSpecies;
break;
}
break;
case FORM_CHANGE_BATTLE_TURN_END:
case FORM_CHANGE_HIT_BY_MOVE:
if (formChanges[i].param1 == GetBattlerAbility(battler))
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_STATUS:
if (gBattleMons[battler].status1 & formChanges[i].param1)
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_TERASTALLIZATION:
if (GetBattlerTeraType(battler) == formChanges[i].param1)
targetSpecies = formChanges[i].targetSpecies;
break;
}
break;
case FORM_CHANGE_BATTLE_GIGANTAMAX:
if (GetMonData(mon, MON_DATA_GIGANTAMAX_FACTOR))
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_WEATHER:
// Check if there is a required ability and if the battler's ability does not match it
// or is suppressed. If so, revert to the no weather form.
if (formChanges[i].param2
&& GetBattlerAbility(battler) != formChanges[i].param2
&& formChanges[i].param1 == B_WEATHER_NONE)
{
targetSpecies = formChanges[i].targetSpecies;
}
// We need to revert the weather form if the field is under Air Lock, too.
else if (!WEATHER_HAS_EFFECT && formChanges[i].param1 == B_WEATHER_NONE)
{
targetSpecies = formChanges[i].targetSpecies;
}
// Otherwise, just check for a match between the weather and the form change table.
// Added a check for whether the weather is in effect to prevent end-of-turn soft locks with Cloud Nine / Air Lock
else if (((gBattleWeather & formChanges[i].param1) && WEATHER_HAS_EFFECT)
|| (gBattleWeather == B_WEATHER_NONE && formChanges[i].param1 == B_WEATHER_NONE))
{
targetSpecies = formChanges[i].targetSpecies;
}
break;
case FORM_CHANGE_BATTLE_TURN_END:
case FORM_CHANGE_HIT_BY_MOVE:
if (formChanges[i].param1 == GetBattlerAbility(battler))
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_STATUS:
if (gBattleMons[battler].status1 & formChanges[i].param1)
targetSpecies = formChanges[i].targetSpecies;
break;
case FORM_CHANGE_BATTLE_TERASTALLIZATION:
if (GetBattlerTeraType(battler) == formChanges[i].param1)
targetSpecies = formChanges[i].targetSpecies;
break;
}
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/data/pokemon/form_change_tables.h
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,13 @@ static const struct FormChange sGlalieFormChangeTable[] = {

#if P_FAMILY_CASTFORM
static const struct FormChange sCastformFormChangeTable[] = {
{FORM_CHANGE_OVERWORLD_WEATHER, SPECIES_CASTFORM_SUNNY, WEATHER_SUNNY_CLOUDS},
{FORM_CHANGE_OVERWORLD_WEATHER, SPECIES_CASTFORM_SUNNY, WEATHER_DROUGHT},
{FORM_CHANGE_OVERWORLD_WEATHER, SPECIES_CASTFORM_RAINY, WEATHER_RAIN},
{FORM_CHANGE_OVERWORLD_WEATHER, SPECIES_CASTFORM_RAINY, WEATHER_RAIN_THUNDERSTORM},
{FORM_CHANGE_OVERWORLD_WEATHER, SPECIES_CASTFORM_RAINY, WEATHER_DOWNPOUR},
{FORM_CHANGE_OVERWORLD_WEATHER, SPECIES_CASTFORM_SNOWY, WEATHER_SNOW},
{FORM_CHANGE_OVERWORLD_WEATHER, SPECIES_CASTFORM_NORMAL, WEATHER_NONE},
#if B_WEATHER_FORMS >= GEN_5
{FORM_CHANGE_BATTLE_WEATHER, SPECIES_CASTFORM_SUNNY, B_WEATHER_SUN, ABILITY_FORECAST},
{FORM_CHANGE_BATTLE_WEATHER, SPECIES_CASTFORM_RAINY, B_WEATHER_RAIN, ABILITY_FORECAST},
Expand Down Expand Up @@ -531,6 +538,8 @@ static const struct FormChange sBurmyFormChangeTable[] = {

#if P_FAMILY_CHERUBI
static const struct FormChange sCherrimFormChangeTable[] = {
{FORM_CHANGE_OVERWORLD_WEATHER, SPECIES_CHERRIM_SUNSHINE, WEATHER_DROUGHT},
{FORM_CHANGE_OVERWORLD_WEATHER, SPECIES_CHERRIM_OVERCAST, WEATHER_NONE},
#if B_WEATHER_FORMS >= GEN_5
{FORM_CHANGE_BATTLE_WEATHER, SPECIES_CHERRIM_SUNSHINE, B_WEATHER_SUN, ABILITY_FLOWER_GIFT},
{FORM_CHANGE_BATTLE_WEATHER, SPECIES_CHERRIM_OVERCAST, ~B_WEATHER_SUN, ABILITY_FLOWER_GIFT},
Expand Down
49 changes: 30 additions & 19 deletions src/event_object_movement.c
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
#include "metatile_behavior.h"
#include "overworld.h"
#include "palette.h"
#include "party_menu.h"
#include "pokemon.h"
#include "pokeball.h"
#include "random.h"
Expand Down Expand Up @@ -2018,21 +2019,24 @@ static void RefreshFollowerGraphics(struct ObjectEvent *objEvent)
}
}

static u16 GetOverworldCastformSpecies(void)
u16 GetOverworldWeatherSpecies(u16 species)
{
switch (GetCurrentWeather())
u32 i;
u32 weather = GetCurrentWeather();
const struct FormChange *formChanges = GetSpeciesFormChanges(species);

for (i = 0; formChanges != NULL && formChanges[i].method != FORM_CHANGE_TERMINATOR; i++)
{
case WEATHER_SUNNY_CLOUDS:
case WEATHER_DROUGHT:
return SPECIES_CASTFORM_SUNNY;
case WEATHER_RAIN:
case WEATHER_RAIN_THUNDERSTORM:
case WEATHER_DOWNPOUR:
return SPECIES_CASTFORM_RAINY;
case WEATHER_SNOW:
return SPECIES_CASTFORM_SNOWY;
// Unlike other form change checks, we don't do the "species != formChanges[i].targetSpecies" check
if (formChanges[i].method == FORM_CHANGE_OVERWORLD_WEATHER)
{
if (formChanges[i].param1 == weather)
return formChanges[i].targetSpecies;
else if (formChanges[i].param1 == WEATHER_NONE) // Set the default form for weather not defined in form change table
species = formChanges[i].targetSpecies;
}
}
return SPECIES_CASTFORM_NORMAL;
return species;
}

static bool8 GetMonInfo(struct Pokemon *mon, u16 *species, u8 *form, u8 *shiny)
Expand All @@ -2052,8 +2056,8 @@ static bool8 GetMonInfo(struct Pokemon *mon, u16 *species, u8 *form, u8 *shiny)
case SPECIES_UNOWN:
*form = GET_UNOWN_LETTER(mon->box.personality);
break;
case SPECIES_CASTFORM: // form is based on overworld weather
*species = GetOverworldCastformSpecies();
default:
*species = GetOverworldWeatherSpecies(*species);
break;
}
return TRUE;
Expand Down Expand Up @@ -5207,14 +5211,21 @@ static bool32 EndFollowerTransformEffect(struct ObjectEvent *objectEvent, struct
static bool32 TryStartFollowerTransformEffect(struct ObjectEvent *objectEvent, struct Sprite *sprite)
{
u32 multi;
if (GET_BASE_SPECIES_ID(OW_SPECIES(objectEvent)) == SPECIES_CASTFORM
&& OW_SPECIES(objectEvent) != (multi = GetOverworldCastformSpecies()))
struct Pokemon *mon;
u32 ability;
if (OW_FOLLOWERS_WEATHER_FORMS
&& DoesSpeciesHaveFormChangeMethod(OW_SPECIES(objectEvent), FORM_CHANGE_BATTLE_WEATHER)
&& OW_SPECIES(objectEvent) != (multi = GetOverworldWeatherSpecies(OW_SPECIES(objectEvent))))
{
sprite->data[7] = TRANSFORM_TYPE_WEATHER << 8;
PlaySE(SE_M_MINIMIZE);
return TRUE;
}
else if ((Random() & 0xFFFF) < 18 && GetLocalWildMon(FALSE)
&& (OW_SPECIES(objectEvent) == SPECIES_MEW || OW_SPECIES(objectEvent) == SPECIES_DITTO))

if (OW_FOLLOWERS_COPY_WILD_PKMN
&& (MonKnowsMove(mon = GetFirstLiveMon(), MOVE_TRANSFORM)
|| (ability = GetMonAbility(mon)) == ABILITY_IMPOSTER || ability == ABILITY_ILLUSION)
Comment on lines +5355 to +5356
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this sort of syntax exists outside of merrp's code, so I don't think we should use it even more

Copy link
Collaborator

@AlexOn1ine AlexOn1ine Dec 16, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pretty sure we have lots of those that look a bit scuffed.

This could look more readable with an early return since there is nothing but returns following this statement so something like:

&& (Random() & 0xFFFF) < 18 && GetLocalWildMon(FALSE))
{
sprite->data[7] = TRANSFORM_TYPE_RANDOM_WILD << 8;
PlaySE(SE_M_MINIMIZE);
Expand Down Expand Up @@ -5248,7 +5259,7 @@ static bool8 UpdateFollowerTransformEffect(struct ObjectEvent *objectEvent, stru
break;
case TRANSFORM_TYPE_WEATHER:
multi = objectEvent->graphicsId;
objectEvent->graphicsId = GetOverworldCastformSpecies();
objectEvent->graphicsId = GetOverworldWeatherSpecies(OW_SPECIES(objectEvent));
if (!objectEvent->graphicsId)
{
objectEvent->graphicsId = multi;
Expand Down
19 changes: 19 additions & 0 deletions src/field_weather.c
Original file line number Diff line number Diff line change
Expand Up @@ -198,11 +198,28 @@ void SetNextWeather(u8 weather)
gWeatherPtr->finishStep = 0;
}

static void UpdateWeatherForms(void)
{
s32 i;
for (i = 0; i < PARTY_SIZE; i++)
AlexOn1ine marked this conversation as resolved.
Show resolved Hide resolved
{
struct Pokemon *mon = &gPlayerParty[i];
u16 species = GetMonData(mon, MON_DATA_SPECIES);
u16 targetSpecies = GetOverworldWeatherSpecies(species);
if (species != targetSpecies)
{
SetMonData(mon, MON_DATA_SPECIES, &targetSpecies);
CalculateMonStats(mon);
}
}
}

void SetCurrentAndNextWeather(u8 weather)
{
PlayRainStoppingSoundEffect();
gWeatherPtr->currWeather = weather;
gWeatherPtr->nextWeather = weather;
UpdateWeatherForms();
}

void SetCurrentAndNextWeatherNoDelay(u8 weather)
Expand All @@ -212,6 +229,7 @@ void SetCurrentAndNextWeatherNoDelay(u8 weather)
gWeatherPtr->nextWeather = weather;
// Overrides the normal delay during screen fading.
gWeatherPtr->readyForInit = TRUE;
UpdateWeatherForms();
}

static void Task_WeatherInit(u8 taskId)
Expand Down Expand Up @@ -239,6 +257,7 @@ static void Task_WeatherMain(u8 taskId)
gWeatherPtr->palProcessingState = WEATHER_PAL_STATE_CHANGING_WEATHER;
gWeatherPtr->currWeather = gWeatherPtr->nextWeather;
gWeatherPtr->weatherChangeComplete = TRUE;
UpdateWeatherForms();
}
}
else
Expand Down
Loading
Loading