diff --git a/src/@types/dex-data.ts b/src/@types/dex-data.ts index 167ba525af5..005e8034b18 100644 --- a/src/@types/dex-data.ts +++ b/src/@types/dex-data.ts @@ -1,4 +1,4 @@ -import type { RibbonData } from "#system/ribbon-data"; +import type { RibbonData } from "#system/ribbons/ribbon-data"; export interface DexData { [key: number]: DexEntry; diff --git a/src/constants.ts b/src/constants.ts index 6f9f4a6d2fb..b5837c7dd7f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -101,3 +101,9 @@ export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15; * Default: `10000` (0.01%) */ export const FAKE_TITLE_LOGO_CHANCE = 10000; + +/** + * The ceiling on friendshp amount that can be reached through the use of rare candies. + * Using rare candies will never increase friendship beyond this value. + */ +export const RARE_CANDY_FRIENDSHIP_CAP = 200; diff --git a/src/data/challenge.ts b/src/data/challenge.ts index 1157b47a515..1eb16b1c48a 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -20,7 +20,7 @@ import { Trainer } from "#field/trainer"; import type { ModifierTypeOption } from "#modifiers/modifier-type"; import { PokemonMove } from "#moves/pokemon-move"; import type { DexAttrProps, GameData } from "#system/game-data"; -import { RibbonData, type RibbonFlag } from "#system/ribbon-data"; +import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data"; import { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common"; import { deepCopy } from "#utils/data"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index fe85e92772c..98e86b12a4f 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -1,7 +1,7 @@ import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability"; import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs"; import type { AnySound, BattleScene } from "#app/battle-scene"; -import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; +import { PLAYER_PARTY_MAX_SIZE, RARE_CANDY_FRIENDSHIP_CAP } from "#app/constants"; import { timedEventManager } from "#app/global-event-manager"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -138,6 +138,8 @@ import { populateVariantColors, variantColorCache, variantData } from "#sprites/ import { achvs } from "#system/achv"; import type { StarterDataEntry, StarterMoveset } from "#system/game-data"; import type { PokemonData } from "#system/pokemon-data"; +import { RibbonData } from "#system/ribbons/ribbon-data"; +import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types"; import type { DamageCalculationResult, DamageResult } from "#types/damage-result"; import type { IllusionData } from "#types/illusion-data"; @@ -5821,45 +5823,59 @@ export class PlayerPokemon extends Pokemon { ); }); } - - addFriendship(friendship: number): void { - if (friendship > 0) { - const starterSpeciesId = this.species.getRootSpeciesId(); - const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0; - const starterData = [ - globalScene.gameData.starterData[starterSpeciesId], - fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null, - ].filter(d => !!d); - const amount = new NumberHolder(friendship); - globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); - const candyFriendshipMultiplier = globalScene.gameMode.isClassic - ? timedEventManager.getClassicFriendshipMultiplier() - : 1; - const fusionReduction = fusionStarterSpeciesId - ? timedEventManager.areFusionsBoosted() - ? 1.5 // Divide candy gain for fusions by 1.5 during events - : 2 // 2 for fusions outside events - : 1; // 1 for non-fused mons - const starterAmount = new NumberHolder(Math.floor((amount.value * candyFriendshipMultiplier) / fusionReduction)); - - // Add friendship to this PlayerPokemon - this.friendship = Math.min(this.friendship + amount.value, 255); - if (this.friendship === 255) { - globalScene.validateAchv(achvs.MAX_FRIENDSHIP); - } - // Add to candy progress for this mon's starter species and its fused species (if it has one) - starterData.forEach((sd: StarterDataEntry, i: number) => { - const speciesId = !i ? starterSpeciesId : (fusionStarterSpeciesId as SpeciesId); - sd.friendship = (sd.friendship || 0) + starterAmount.value; - if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[speciesId])) { - globalScene.gameData.addStarterCandy(getPokemonSpecies(speciesId), 1); - sd.friendship = 0; - } - }); - } else { - // Lose friendship upon fainting + /** + * Add friendship to this Pokemon + * + * @remarks + * This adds friendship to the pokemon's friendship stat (used for evolution, return, etc.) and candy progress. + * For fusions, candy progress for each species in the fusion is halved. + * + * @param friendship - The amount of friendship to add. Negative values will reduce friendship, though not below 0. + * @param capped - If true, don't allow the friendship gain to exceed 200. Used to cap friendship gains from rare candies. + */ + addFriendship(friendship: number, capped = false): void { + // Short-circuit friendship loss, which doesn't impact candy friendship + if (friendship <= 0) { this.friendship = Math.max(this.friendship + friendship, 0); + return; } + + const starterSpeciesId = this.species.getRootSpeciesId(); + const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0; + const starterGameData = globalScene.gameData.starterData; + const starterData: [StarterDataEntry, SpeciesId][] = [[starterGameData[starterSpeciesId], starterSpeciesId]]; + if (fusionStarterSpeciesId) { + starterData.push([starterGameData[fusionStarterSpeciesId], fusionStarterSpeciesId]); + } + const amount = new NumberHolder(friendship); + globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); + friendship = amount.value; + + const newFriendship = this.friendship + friendship; + // If capped is true, only adjust friendship if the new friendship is less than or equal to 200. + if (!capped || newFriendship <= RARE_CANDY_FRIENDSHIP_CAP) { + this.friendship = Math.min(newFriendship, 255); + if (newFriendship >= 255) { + globalScene.validateAchv(achvs.MAX_FRIENDSHIP); + awardRibbonsToSpeciesLine(this.species.speciesId, RibbonData.FRIENDSHIP); + } + } + + let candyFriendshipMultiplier = globalScene.gameMode.isClassic + ? timedEventManager.getClassicFriendshipMultiplier() + : 1; + if (fusionStarterSpeciesId) { + candyFriendshipMultiplier /= timedEventManager.areFusionsBoosted() ? 1.5 : 2; + } + const candyFriendshipAmount = Math.floor(friendship * candyFriendshipMultiplier); + // Add to candy progress for this mon's starter species and its fused species (if it has one) + starterData.forEach(([sd, id]: [StarterDataEntry, SpeciesId]) => { + sd.friendship = (sd.friendship || 0) + candyFriendshipAmount; + if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[id])) { + globalScene.gameData.addStarterCandy(getPokemonSpecies(id), 1); + sd.friendship = 0; + } + }); } getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 6907b6907ca..d6f21b35d02 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -2304,7 +2304,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier { playerPokemon.levelExp = 0; } - playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY); + playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY, true); globalScene.phaseManager.unshiftNew( "LevelUpPhase", diff --git a/src/phases/game-over-phase.ts b/src/phases/game-over-phase.ts index 49b61ad78f2..25dfffaa582 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -1,7 +1,7 @@ import { pokerogueApi } from "#api/pokerogue-api"; import { clientSessionId } from "#app/account"; import { globalScene } from "#app/global-scene"; -import { pokemonEvolutions, pokemonPrevolutions } from "#balance/pokemon-evolutions"; +import { pokemonEvolutions } from "#balance/pokemon-evolutions"; import { modifierTypes } from "#data/data-lists"; import { getCharVariantFromDialogue } from "#data/dialogue"; import type { PokemonSpecies } from "#data/pokemon-species"; @@ -19,11 +19,12 @@ import { ChallengeData } from "#system/challenge-data"; import type { SessionSaveData } from "#system/game-data"; import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { PokemonData } from "#system/pokemon-data"; -import { RibbonData, type RibbonFlag } from "#system/ribbon-data"; +import { RibbonData, type RibbonFlag } from "#system/ribbons/ribbon-data"; +import { awardRibbonsToSpeciesLine } from "#system/ribbons/ribbon-methods"; import { TrainerData } from "#system/trainer-data"; import { trainerConfigs } from "#trainers/trainer-config"; import { checkSpeciesValidForChallenge, isNuzlockeChallenge } from "#utils/challenge-utils"; -import { isLocal, isLocalServerConnected, isNullOrUndefined } from "#utils/common"; +import { isLocal, isLocalServerConnected } from "#utils/common"; import { getPokemonSpecies } from "#utils/pokemon-utils"; import i18next from "i18next"; @@ -134,25 +135,15 @@ export class GameOverPhase extends BattlePhase { // Award ribbons to all Pokémon in the player's party that are considered valid // for the current game mode and challenges. for (const pokemon of globalScene.getPlayerParty()) { + const species = pokemon.species; if ( checkSpeciesValidForChallenge( - pokemon.species, - globalScene.gameData.getSpeciesDexAttrProps(pokemon.species, pokemon.getDexAttr()), + species, + globalScene.gameData.getSpeciesDexAttrProps(species, pokemon.getDexAttr()), false, ) ) { - const speciesId = pokemon.species.speciesId; - const dexData = globalScene.gameData.dexData; - dexData[speciesId].ribbons.award(ribbonFlags as RibbonFlag); - - // Mark all pre-evolutions of the Pokémon with the same ribbon flags. - for ( - let prevoId = pokemonPrevolutions[speciesId]; - !isNullOrUndefined(prevoId); - prevoId = pokemonPrevolutions[prevoId] - ) { - dexData[speciesId].ribbons.award(ribbonFlags as RibbonFlag); - } + awardRibbonsToSpeciesLine(species.speciesId, ribbonFlags as RibbonFlag); } } } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 0bc92dfa57e..c92f2a8c2e9 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -48,7 +48,7 @@ import { EggData } from "#system/egg-data"; import { GameStats } from "#system/game-stats"; import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { PokemonData } from "#system/pokemon-data"; -import { RibbonData } from "#system/ribbon-data"; +import { RibbonData } from "#system/ribbons/ribbon-data"; import { resetSettings, SettingKeys, setSetting } from "#system/settings"; import { SettingGamepad, setSettingGamepad, settingGamepadDefaults } from "#system/settings-gamepad"; import type { SettingKeyboard } from "#system/settings-keyboard"; diff --git a/src/system/ribbon-data.ts b/src/system/ribbons/ribbon-data.ts similarity index 97% rename from src/system/ribbon-data.ts rename to src/system/ribbons/ribbon-data.ts index 46bf48c9a8e..20d5470db00 100644 --- a/src/system/ribbon-data.ts +++ b/src/system/ribbons/ribbon-data.ts @@ -55,11 +55,11 @@ export class RibbonData { public static readonly MONO_GEN = 0x40000 as RibbonFlag; /** Flag for winning classic */ - public static readonly CLASSIC = 0x80000; + public static readonly CLASSIC = 0x80000 as RibbonFlag; /** Flag for winning the nuzzlocke challenge */ - public static readonly NUZLOCKE = 0x80000; + public static readonly NUZLOCKE = 0x80000 as RibbonFlag; /** Flag for reaching max friendship */ - public static readonly FRIENDSHIP = 0x100000; + public static readonly FRIENDSHIP = 0x100000 as RibbonFlag; //#endregion Ribbons constructor(value: number) { diff --git a/src/system/ribbons/ribbon-methods.ts b/src/system/ribbons/ribbon-methods.ts new file mode 100644 index 00000000000..aa88f8e6c62 --- /dev/null +++ b/src/system/ribbons/ribbon-methods.ts @@ -0,0 +1,20 @@ +import { globalScene } from "#app/global-scene"; +import { pokemonPrevolutions } from "#balance/pokemon-evolutions"; +import type { SpeciesId } from "#enums/species-id"; +import type { RibbonFlag } from "#system/ribbons/ribbon-data"; +import { isNullOrUndefined } from "#utils/common"; + +/** + * Award one or more ribbons to a species and its pre-evolutions + * + * @param id - The ID of the species to award ribbons to + * @param ribbons The ribbon(s) to award (use bitwise OR to combine multiple) + */ +export function awardRibbonsToSpeciesLine(id: SpeciesId, ribbons: RibbonFlag): void { + const dexData = globalScene.gameData.dexData; + dexData[id].ribbons.award(ribbons); + // Mark all pre-evolutions of the Pokémon with the same ribbon flags. + for (let prevoId = pokemonPrevolutions[id]; !isNullOrUndefined(prevoId); prevoId = pokemonPrevolutions[prevoId]) { + dexData[id].ribbons.award(ribbons); + } +}