Add tracking for friendship ribbon

This commit is contained in:
Sirz Benjie 2025-08-10 13:26:31 -05:00
parent c1085dcc34
commit 92d8c5289b
No known key found for this signature in database
GPG Key ID: 4A524B4D196C759E
9 changed files with 95 additions and 62 deletions

View File

@ -1,4 +1,4 @@
import type { RibbonData } from "#system/ribbon-data"; import type { RibbonData } from "#system/ribbons/ribbon-data";
export interface DexData { export interface DexData {
[key: number]: DexEntry; [key: number]: DexEntry;

View File

@ -101,3 +101,9 @@ export const ANTI_VARIANCE_WEIGHT_MODIFIER = 15;
* Default: `10000` (0.01%) * Default: `10000` (0.01%)
*/ */
export const FAKE_TITLE_LOGO_CHANCE = 10000; 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;

View File

@ -20,7 +20,7 @@ import { Trainer } from "#field/trainer";
import type { ModifierTypeOption } from "#modifiers/modifier-type"; import type { ModifierTypeOption } from "#modifiers/modifier-type";
import { PokemonMove } from "#moves/pokemon-move"; import { PokemonMove } from "#moves/pokemon-move";
import type { DexAttrProps, GameData } from "#system/game-data"; 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 { type BooleanHolder, isBetween, type NumberHolder, randSeedItem } from "#utils/common";
import { deepCopy } from "#utils/data"; import { deepCopy } from "#utils/data";
import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils"; import { getPokemonSpecies, getPokemonSpeciesForm } from "#utils/pokemon-utils";

View File

@ -1,7 +1,7 @@
import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability"; import type { Ability, PreAttackModifyDamageAbAttrParams } from "#abilities/ability";
import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs"; import { applyAbAttrs, applyOnGainAbAttrs, applyOnLoseAbAttrs } from "#abilities/apply-ab-attrs";
import type { AnySound, BattleScene } from "#app/battle-scene"; 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 { timedEventManager } from "#app/global-event-manager";
import { globalScene } from "#app/global-scene"; import { globalScene } from "#app/global-scene";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
@ -138,6 +138,8 @@ import { populateVariantColors, variantColorCache, variantData } from "#sprites/
import { achvs } from "#system/achv"; import { achvs } from "#system/achv";
import type { StarterDataEntry, StarterMoveset } from "#system/game-data"; import type { StarterDataEntry, StarterMoveset } from "#system/game-data";
import type { PokemonData } from "#system/pokemon-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 { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types/ability-types";
import type { DamageCalculationResult, DamageResult } from "#types/damage-result"; import type { DamageCalculationResult, DamageResult } from "#types/damage-result";
import type { IllusionData } from "#types/illusion-data"; import type { IllusionData } from "#types/illusion-data";
@ -5821,45 +5823,59 @@ export class PlayerPokemon extends Pokemon {
); );
}); });
} }
/**
addFriendship(friendship: number): void { * Add friendship to this Pokemon
if (friendship > 0) { *
const starterSpeciesId = this.species.getRootSpeciesId(); * @remarks
const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0; * This adds friendship to the pokemon's friendship stat (used for evolution, return, etc.) and candy progress.
const starterData = [ * For fusions, candy progress for each species in the fusion is halved.
globalScene.gameData.starterData[starterSpeciesId], *
fusionStarterSpeciesId ? globalScene.gameData.starterData[fusionStarterSpeciesId] : null, * @param friendship - The amount of friendship to add. Negative values will reduce friendship, though not below 0.
].filter(d => !!d); * @param capped - If true, don't allow the friendship gain to exceed 200. Used to cap friendship gains from rare candies.
const amount = new NumberHolder(friendship); */
globalScene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); addFriendship(friendship: number, capped = false): void {
const candyFriendshipMultiplier = globalScene.gameMode.isClassic // Short-circuit friendship loss, which doesn't impact candy friendship
? timedEventManager.getClassicFriendshipMultiplier() if (friendship <= 0) {
: 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
this.friendship = Math.max(this.friendship + 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<Pokemon> { getPossibleEvolution(evolution: SpeciesFormEvolution | null): Promise<Pokemon> {

View File

@ -2304,7 +2304,7 @@ export class PokemonLevelIncrementModifier extends ConsumablePokemonModifier {
playerPokemon.levelExp = 0; playerPokemon.levelExp = 0;
} }
playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY); playerPokemon.addFriendship(FRIENDSHIP_GAIN_FROM_RARE_CANDY, true);
globalScene.phaseManager.unshiftNew( globalScene.phaseManager.unshiftNew(
"LevelUpPhase", "LevelUpPhase",

View File

@ -1,7 +1,7 @@
import { pokerogueApi } from "#api/pokerogue-api"; import { pokerogueApi } from "#api/pokerogue-api";
import { clientSessionId } from "#app/account"; import { clientSessionId } from "#app/account";
import { globalScene } from "#app/global-scene"; 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 { modifierTypes } from "#data/data-lists";
import { getCharVariantFromDialogue } from "#data/dialogue"; import { getCharVariantFromDialogue } from "#data/dialogue";
import type { PokemonSpecies } from "#data/pokemon-species"; 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 type { SessionSaveData } from "#system/game-data";
import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { ModifierData as PersistentModifierData } from "#system/modifier-data";
import { PokemonData } from "#system/pokemon-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 { TrainerData } from "#system/trainer-data";
import { trainerConfigs } from "#trainers/trainer-config"; import { trainerConfigs } from "#trainers/trainer-config";
import { checkSpeciesValidForChallenge, isNuzlockeChallenge } from "#utils/challenge-utils"; 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 { getPokemonSpecies } from "#utils/pokemon-utils";
import i18next from "i18next"; 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 // Award ribbons to all Pokémon in the player's party that are considered valid
// for the current game mode and challenges. // for the current game mode and challenges.
for (const pokemon of globalScene.getPlayerParty()) { for (const pokemon of globalScene.getPlayerParty()) {
const species = pokemon.species;
if ( if (
checkSpeciesValidForChallenge( checkSpeciesValidForChallenge(
pokemon.species, species,
globalScene.gameData.getSpeciesDexAttrProps(pokemon.species, pokemon.getDexAttr()), globalScene.gameData.getSpeciesDexAttrProps(species, pokemon.getDexAttr()),
false, false,
) )
) { ) {
const speciesId = pokemon.species.speciesId; awardRibbonsToSpeciesLine(species.speciesId, ribbonFlags as RibbonFlag);
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);
}
} }
} }
} }

View File

@ -48,7 +48,7 @@ import { EggData } from "#system/egg-data";
import { GameStats } from "#system/game-stats"; import { GameStats } from "#system/game-stats";
import { ModifierData as PersistentModifierData } from "#system/modifier-data"; import { ModifierData as PersistentModifierData } from "#system/modifier-data";
import { PokemonData } from "#system/pokemon-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 { resetSettings, SettingKeys, setSetting } from "#system/settings";
import { SettingGamepad, setSettingGamepad, settingGamepadDefaults } from "#system/settings-gamepad"; import { SettingGamepad, setSettingGamepad, settingGamepadDefaults } from "#system/settings-gamepad";
import type { SettingKeyboard } from "#system/settings-keyboard"; import type { SettingKeyboard } from "#system/settings-keyboard";

View File

@ -55,11 +55,11 @@ export class RibbonData {
public static readonly MONO_GEN = 0x40000 as RibbonFlag; public static readonly MONO_GEN = 0x40000 as RibbonFlag;
/** Flag for winning classic */ /** Flag for winning classic */
public static readonly CLASSIC = 0x80000; public static readonly CLASSIC = 0x80000 as RibbonFlag;
/** Flag for winning the nuzzlocke challenge */ /** Flag for winning the nuzzlocke challenge */
public static readonly NUZLOCKE = 0x80000; public static readonly NUZLOCKE = 0x80000 as RibbonFlag;
/** Flag for reaching max friendship */ /** Flag for reaching max friendship */
public static readonly FRIENDSHIP = 0x100000; public static readonly FRIENDSHIP = 0x100000 as RibbonFlag;
//#endregion Ribbons //#endregion Ribbons
constructor(value: number) { constructor(value: number) {

View File

@ -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);
}
}