diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 415a8fe9aaf..d82acec1c20 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -13,6 +13,7 @@ import { Arena, ArenaBase } from "#app/field/arena"; import { GameData } from "#app/system/game-data"; import { addTextObject, getTextColor, TextStyle } from "#app/ui/text"; import { allMoves } from "#app/data/move"; +import { MusicPreference } from "#app/system/settings/settings"; import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import AbilityBar from "#app/ui/ability-bar"; import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, applyPostItemLostAbAttrs, BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr, PostItemLostAbAttr } from "#app/data/ability"; @@ -169,7 +170,7 @@ export default class BattleScene extends SceneBase { public uiTheme: UiTheme = UiTheme.DEFAULT; public windowType: integer = 0; public experimentalSprites: boolean = false; - public musicPreference: integer = 0; + public musicPreference: number = MusicPreference.MIXED; public moveAnimations: boolean = true; public expGainsSpeed: ExpGainsSpeed = ExpGainsSpeed.DEFAULT; public skipSeenDialogues: boolean = false; @@ -2998,22 +2999,16 @@ export default class BattleScene extends SceneBase { */ getActiveKeys(): string[] { const keys: string[] = []; - const playerParty = this.getPlayerParty(); - playerParty.forEach(p => { + let activePokemon: (PlayerPokemon | EnemyPokemon)[] = this.getPlayerParty(); + activePokemon = activePokemon.concat(this.getEnemyParty()); + activePokemon.forEach((p) => { keys.push(p.getSpriteKey(true)); - keys.push(p.getBattleSpriteKey(true, true)); - keys.push("cry/" + p.species.getCryKey(p.formIndex)); - if (p.fusionSpecies) { - keys.push("cry/" + p.fusionSpecies.getCryKey(p.fusionFormIndex)); + if (p instanceof PlayerPokemon) { + keys.push(p.getBattleSpriteKey(true, true)); } - }); - // enemyParty has to be operated on separately from playerParty because playerPokemon =/= enemyPokemon - const enemyParty = this.getEnemyParty(); - enemyParty.forEach(p => { - keys.push(p.getSpriteKey(true)); - keys.push("cry/" + p.species.getCryKey(p.formIndex)); + keys.push(p.species.getCryKey(p.formIndex)); if (p.fusionSpecies) { - keys.push("cry/" + p.fusionSpecies.getCryKey(p.fusionFormIndex)); + keys.push(p.fusionSpecies.getCryKey(p.fusionFormIndex)); } }); return keys; diff --git a/src/battle.ts b/src/battle.ts index 6086c2ceb4e..0356772bb07 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -6,11 +6,13 @@ import { GameMode } from "./game-mode"; import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/modifier"; import { PokeballType } from "./data/pokeball"; import { trainerConfigs } from "#app/data/trainer-config"; +import { SpeciesFormKey } from "#enums/species-form-key"; import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleSpec } from "#enums/battle-spec"; import { Moves } from "#enums/moves"; import { PlayerGender } from "#enums/player-gender"; +import { MusicPreference } from "#app/system/settings/settings"; import { Species } from "#enums/species"; import { TrainerType } from "#enums/trainer-type"; import i18next from "#app/plugins/i18n"; @@ -212,7 +214,6 @@ export default class Battle { } getBgmOverride(scene: BattleScene): string | null { - const battlers = this.enemyParty.slice(0, this.getBattlerCount()); if (this.isBattleMysteryEncounter() && this.mysteryEncounter?.encounterMode === MysteryEncounterMode.DEFAULT) { // Music is overridden for MEs during ME onInit() // Should not use any BGM overrides before swapping from DEFAULT mode @@ -221,7 +222,7 @@ export default class Battle { if (!this.started && this.trainer?.config.encounterBgm && this.trainer?.getEncounterMessages()?.length) { return `encounter_${this.trainer?.getEncounterBgm()}`; } - if (scene.musicPreference === 0) { + if (scene.musicPreference === MusicPreference.CONSISTENT) { return this.trainer?.getBattleBgm() ?? null; } else { return this.trainer?.getMixedBattleBgm() ?? null; @@ -229,147 +230,168 @@ export default class Battle { } else if (this.gameMode.isClassic && this.waveIndex > 195 && this.battleSpec !== BattleSpec.FINAL_BOSS) { return "end_summit"; } - for (const pokemon of battlers) { + const wildOpponents = scene.getEnemyParty(); + for (const pokemon of wildOpponents) { if (this.battleSpec === BattleSpec.FINAL_BOSS) { - if (pokemon.formIndex) { + if (pokemon.species.getFormSpriteKey(pokemon.formIndex) === SpeciesFormKey.ETERNAMAX) { return "battle_final"; } return "battle_final_encounter"; } if (pokemon.species.legendary || pokemon.species.subLegendary || pokemon.species.mythical) { - if (scene.musicPreference === 0) { - if (pokemon.species.speciesId === Species.REGIROCK || pokemon.species.speciesId === Species.REGICE || pokemon.species.speciesId === Species.REGISTEEL || pokemon.species.speciesId === Species.REGIGIGAS || pokemon.species.speciesId === Species.REGIELEKI || pokemon.species.speciesId === Species.REGIDRAGO) { - return "battle_legendary_regis_g5"; + if (scene.musicPreference === MusicPreference.CONSISTENT) { + switch (pokemon.species.speciesId) { + case Species.REGIROCK: + case Species.REGICE: + case Species.REGISTEEL: + case Species.REGIGIGAS: + case Species.REGIDRAGO: + case Species.REGIELEKI: + return "battle_legendary_regis_g5"; + case Species.KYUREM: + return "battle_legendary_kyurem"; + default: + if (pokemon.species.legendary) { + return "battle_legendary_res_zek"; + } + return "battle_legendary_unova"; } - if (pokemon.species.speciesId === Species.COBALION || pokemon.species.speciesId === Species.TERRAKION || pokemon.species.speciesId === Species.VIRIZION || pokemon.species.speciesId === Species.TORNADUS || pokemon.species.speciesId === Species.THUNDURUS || pokemon.species.speciesId === Species.LANDORUS || pokemon.species.speciesId === Species.KELDEO || pokemon.species.speciesId === Species.MELOETTA || pokemon.species.speciesId === Species.GENESECT) { - return "battle_legendary_unova"; - } - if (pokemon.species.speciesId === Species.KYUREM) { - return "battle_legendary_kyurem"; - } - if (pokemon.species.legendary) { - return "battle_legendary_res_zek"; - } - return "battle_legendary_unova"; - } else { - if (pokemon.species.speciesId === Species.ARTICUNO || pokemon.species.speciesId === Species.ZAPDOS || pokemon.species.speciesId === Species.MOLTRES || pokemon.species.speciesId === Species.MEWTWO || pokemon.species.speciesId === Species.MEW) { - return "battle_legendary_kanto"; - } - if (pokemon.species.speciesId === Species.RAIKOU) { - return "battle_legendary_raikou"; - } - if (pokemon.species.speciesId === Species.ENTEI) { - return "battle_legendary_entei"; - } - if (pokemon.species.speciesId === Species.SUICUNE) { - return "battle_legendary_suicune"; - } - if (pokemon.species.speciesId === Species.LUGIA) { - return "battle_legendary_lugia"; - } - if (pokemon.species.speciesId === Species.HO_OH) { - return "battle_legendary_ho_oh"; - } - if (pokemon.species.speciesId === Species.REGIROCK || pokemon.species.speciesId === Species.REGICE || pokemon.species.speciesId === Species.REGISTEEL || pokemon.species.speciesId === Species.REGIGIGAS || pokemon.species.speciesId === Species.REGIELEKI || pokemon.species.speciesId === Species.REGIDRAGO) { - return "battle_legendary_regis_g6"; - } - if (pokemon.species.speciesId === Species.GROUDON || pokemon.species.speciesId === Species.KYOGRE) { - return "battle_legendary_gro_kyo"; - } - if (pokemon.species.speciesId === Species.RAYQUAZA) { - return "battle_legendary_rayquaza"; - } - if (pokemon.species.speciesId === Species.DEOXYS) { - return "battle_legendary_deoxys"; - } - if (pokemon.species.speciesId === Species.UXIE || pokemon.species.speciesId === Species.MESPRIT || pokemon.species.speciesId === Species.AZELF) { - return "battle_legendary_lake_trio"; - } - if (pokemon.species.speciesId === Species.HEATRAN || pokemon.species.speciesId === Species.CRESSELIA || pokemon.species.speciesId === Species.DARKRAI || pokemon.species.speciesId === Species.SHAYMIN) { - return "battle_legendary_sinnoh"; - } - if (pokemon.species.speciesId === Species.DIALGA || pokemon.species.speciesId === Species.PALKIA) { - if (pokemon.getFormKey() === "") { + } else if (scene.musicPreference === MusicPreference.MIXED) { + switch (pokemon.species.speciesId) { + case Species.ARTICUNO: + case Species.ZAPDOS: + case Species.MOLTRES: + case Species.MEWTWO: + case Species.MEW: + return "battle_legendary_kanto"; + case Species.RAIKOU: + return "battle_legendary_raikou"; + case Species.ENTEI: + return "battle_legendary_entei"; + case Species.SUICUNE: + return "battle_legendary_suicune"; + case Species.LUGIA: + return "battle_legendary_lugia"; + case Species.HO_OH: + return "battle_legendary_ho_oh"; + case Species.REGIROCK: + case Species.REGICE: + case Species.REGISTEEL: + case Species.REGIGIGAS: + case Species.REGIDRAGO: + case Species.REGIELEKI: + return "battle_legendary_regis_g6"; + case Species.GROUDON: + case Species.KYOGRE: + return "battle_legendary_gro_kyo"; + case Species.RAYQUAZA: + return "battle_legendary_rayquaza"; + case Species.DEOXYS: + return "battle_legendary_deoxys"; + case Species.UXIE: + case Species.MESPRIT: + case Species.AZELF: + return "battle_legendary_lake_trio"; + case Species.HEATRAN: + case Species.CRESSELIA: + case Species.DARKRAI: + case Species.SHAYMIN: + return "battle_legendary_sinnoh"; + case Species.DIALGA: + case Species.PALKIA: + if (pokemon.species.getFormSpriteKey(pokemon.formIndex) === SpeciesFormKey.ORIGIN) { + return "battle_legendary_origin_forme"; + } return "battle_legendary_dia_pal"; - } - if (pokemon.getFormKey() === "origin") { - return "battle_legendary_origin_forme"; - } - } - if (pokemon.species.speciesId === Species.GIRATINA) { - return "battle_legendary_giratina"; - } - if (pokemon.species.speciesId === Species.ARCEUS) { - return "battle_legendary_arceus"; - } - if (pokemon.species.speciesId === Species.COBALION || pokemon.species.speciesId === Species.TERRAKION || pokemon.species.speciesId === Species.VIRIZION || pokemon.species.speciesId === Species.TORNADUS || pokemon.species.speciesId === Species.THUNDURUS || pokemon.species.speciesId === Species.LANDORUS || pokemon.species.speciesId === Species.KELDEO || pokemon.species.speciesId === Species.MELOETTA || pokemon.species.speciesId === Species.GENESECT) { - return "battle_legendary_unova"; - } - if (pokemon.species.speciesId === Species.KYUREM) { - return "battle_legendary_kyurem"; - } - if (pokemon.species.speciesId === Species.XERNEAS || pokemon.species.speciesId === Species.YVELTAL || pokemon.species.speciesId === Species.ZYGARDE) { - return "battle_legendary_xern_yvel"; - } - if (pokemon.species.speciesId === Species.TAPU_KOKO || pokemon.species.speciesId === Species.TAPU_LELE || pokemon.species.speciesId === Species.TAPU_BULU || pokemon.species.speciesId === Species.TAPU_FINI) { - return "battle_legendary_tapu"; - } - if ([ Species.COSMOG, Species.COSMOEM, Species.SOLGALEO, Species.LUNALA ].includes(pokemon.species.speciesId)) { - return "battle_legendary_sol_lun"; - } - if (pokemon.species.speciesId === Species.NECROZMA) { - if (pokemon.getFormKey() === "") { + case Species.GIRATINA: + return "battle_legendary_giratina"; + case Species.ARCEUS: + return "battle_legendary_arceus"; + case Species.COBALION: + case Species.TERRAKION: + case Species.VIRIZION: + case Species.KELDEO: + case Species.TORNADUS: + case Species.LANDORUS: + case Species.THUNDURUS: + case Species.MELOETTA: + case Species.GENESECT: + return "battle_legendary_unova"; + case Species.KYUREM: + return "battle_legendary_kyurem"; + case Species.XERNEAS: + case Species.YVELTAL: + case Species.ZYGARDE: + return "battle_legendary_xern_yvel"; + case Species.TAPU_KOKO: + case Species.TAPU_LELE: + case Species.TAPU_BULU: + case Species.TAPU_FINI: + return "battle_legendary_tapu"; + case Species.SOLGALEO: + case Species.LUNALA: return "battle_legendary_sol_lun"; - } - if (pokemon.getFormKey() === "dusk-mane" || pokemon.getFormKey() === "dawn-wings") { - return "battle_legendary_dusk_dawn"; - } - if (pokemon.getFormKey() === "ultra") { - return "battle_legendary_ultra_nec"; - } - } - if ([ Species.NIHILEGO, Species.BUZZWOLE, Species.PHEROMOSA, Species.XURKITREE, Species.CELESTEELA, Species.KARTANA, Species.GUZZLORD, Species.POIPOLE, Species.NAGANADEL, Species.STAKATAKA, Species.BLACEPHALON ].includes(pokemon.species.speciesId)) { - return "battle_legendary_ub"; - } - if (pokemon.species.speciesId === Species.ZACIAN || pokemon.species.speciesId === Species.ZAMAZENTA) { - return "battle_legendary_zac_zam"; - } - if (pokemon.species.speciesId === Species.GLASTRIER || pokemon.species.speciesId === Species.SPECTRIER) { - return "battle_legendary_glas_spec"; - } - if (pokemon.species.speciesId === Species.CALYREX) { - if (pokemon.getFormKey() === "") { + case Species.NECROZMA: + switch (pokemon.getFormKey()) { + case "dusk-mane": + case "dawn-wings": + return "battle_legendary_dusk_dawn"; + case "ultra": + return "battle_legendary_ultra_nec"; + default: + return "battle_legendary_sol_lun"; + } + case Species.NIHILEGO: + case Species.PHEROMOSA: + case Species.BUZZWOLE: + case Species.XURKITREE: + case Species.CELESTEELA: + case Species.KARTANA: + case Species.GUZZLORD: + case Species.POIPOLE: + case Species.NAGANADEL: + case Species.STAKATAKA: + case Species.BLACEPHALON: + return "battle_legendary_ub"; + case Species.ZACIAN: + case Species.ZAMAZENTA: + return "battle_legendary_zac_zam"; + case Species.GLASTRIER: + case Species.SPECTRIER: + return "battle_legendary_glas_spec"; + case Species.CALYREX: + if (pokemon.getFormKey() === "ice" || pokemon.getFormKey() === "shadow") { + return "battle_legendary_riders"; + } return "battle_legendary_calyrex"; - } - if (pokemon.getFormKey() === "ice" || pokemon.getFormKey() === "shadow") { - return "battle_legendary_riders"; - } + case Species.GALAR_ARTICUNO: + case Species.GALAR_ZAPDOS: + case Species.GALAR_MOLTRES: + return "battle_legendary_birds_galar"; + case Species.WO_CHIEN: + case Species.CHIEN_PAO: + case Species.TING_LU: + case Species.CHI_YU: + return "battle_legendary_ruinous"; + case Species.KORAIDON: + case Species.MIRAIDON: + return "battle_legendary_kor_mir"; + case Species.OKIDOGI: + case Species.MUNKIDORI: + case Species.FEZANDIPITI: + return "battle_legendary_loyal_three"; + case Species.OGERPON: + return "battle_legendary_ogerpon"; + case Species.TERAPAGOS: + return "battle_legendary_terapagos"; + case Species.PECHARUNT: + return "battle_legendary_pecharunt"; + default: + if (pokemon.species.legendary) { + return "battle_legendary_res_zek"; + } + return "battle_legendary_unova"; } - if (pokemon.species.speciesId === Species.GALAR_ARTICUNO || pokemon.species.speciesId === Species.GALAR_ZAPDOS || pokemon.species.speciesId === Species.GALAR_MOLTRES) { - return "battle_legendary_birds_galar"; - } - if (pokemon.species.speciesId === Species.WO_CHIEN || pokemon.species.speciesId === Species.CHIEN_PAO || pokemon.species.speciesId === Species.TING_LU || pokemon.species.speciesId === Species.CHI_YU) { - return "battle_legendary_ruinous"; - } - if (pokemon.species.speciesId === Species.KORAIDON || pokemon.species.speciesId === Species.MIRAIDON) { - return "battle_legendary_kor_mir"; - } - if (pokemon.species.speciesId === Species.OKIDOGI || pokemon.species.speciesId === Species.MUNKIDORI || pokemon.species.speciesId === Species.FEZANDIPITI) { - return "battle_legendary_loyal_three"; - } - if (pokemon.species.speciesId === Species.OGERPON) { - return "battle_legendary_ogerpon"; - } - if (pokemon.species.speciesId === Species.TERAPAGOS) { - return "battle_legendary_terapagos"; - } - if (pokemon.species.speciesId === Species.PECHARUNT) { - return "battle_legendary_pecharunt"; - } - if (pokemon.species.legendary) { - return "battle_legendary_res_zek"; - } - return "battle_legendary_unova"; } } } diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index fce46f0c98d..3dd526676a0 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -313,8 +313,8 @@ export class ConditionalProtectTag extends ArenaTag { * protection effect. * @param arena {@linkcode Arena} The arena containing the protection effect * @param moveId {@linkcode Moves} The move to check against this condition - * @returns `true` if the incoming move's priority is greater than 0. This includes - * moves with modified priorities from abilities (e.g. Prankster) + * @returns `true` if the incoming move's priority is greater than 0. + * This includes moves with modified priorities from abilities (e.g. Prankster) */ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const move = allMoves[moveId]; @@ -322,9 +322,11 @@ const QuickGuardConditionFunc: ProtectConditionFunc = (arena, moveId) => { const effectPhase = arena.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase) { - const attacker = effectPhase.getUserPokemon()!; + const attacker = effectPhase.getUserPokemon(); applyMoveAttrs(IncrementMovePriorityAttr, attacker, null, move, priority); - applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, false, move, priority); + if (attacker) { + applyAbAttrs(ChangeMovePriorityAbAttr, attacker, null, false, move, priority); + } } return priority.value > 0; }; diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 3051c2061a8..15bc745ec9d 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2496,7 +2496,10 @@ export class SubstituteTag extends BattlerTag { onHit(pokemon: Pokemon): void { const moveEffectPhase = pokemon.scene.getCurrentPhase(); if (moveEffectPhase instanceof MoveEffectPhase) { - const attacker = moveEffectPhase.getUserPokemon()!; + const attacker = moveEffectPhase.getUserPokemon(); + if (!attacker) { + return; + } const move = moveEffectPhase.move.getMove(); const firstHit = (attacker.turnData.hitCount === attacker.turnData.hitsLeft); diff --git a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index 1d65fd6c9a6..90f6ee942be 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -1,6 +1,7 @@ import { leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterRewards } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { TrainerSlot, } from "#app/data/trainer-config"; import { ModifierTier } from "#app/modifier/modifier-tier"; +import { MusicPreference } from "#app/system/settings/settings"; import { getPlayerModifierTypeOptions, ModifierPoolType, ModifierTypeOption, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import BattleScene from "#app/battle-scene"; @@ -105,7 +106,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = // Load bgm let bgmKey: string; - if (scene.musicPreference === 0) { + if (scene.musicPreference === MusicPreference.CONSISTENT) { bgmKey = "mystery_encounter_gen_5_gts"; scene.loadBgm(bgmKey, `${bgmKey}.mp3`); } else { diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 107fa5073af..0cdeef10d9e 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -460,7 +460,7 @@ export abstract class PokemonSpeciesForm { break; } } - return ret; + return `cry/${ret}`; } validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean { @@ -488,7 +488,7 @@ export abstract class PokemonSpeciesForm { return new Promise(resolve => { const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant); scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant)); - scene.load.audio(`cry/${this.getCryKey(formIndex)}`, `audio/cry/${this.getCryKey(formIndex)}.m4a`); + scene.load.audio(`${this.getCryKey(formIndex)}`, `audio/${this.getCryKey(formIndex)}.m4a`); scene.load.once(Phaser.Loader.Events.COMPLETE, () => { const originalWarn = console.warn; // Ignore warnings for missing frames, because there will be a lot @@ -546,7 +546,7 @@ export abstract class PokemonSpeciesForm { if (cry?.pendingRemove) { cry = null; } - cry = scene.playSound(`cry/${(cry ?? cryKey)}`, soundConfig); + cry = scene.playSound(cry ?? cryKey, soundConfig); if (ignorePlay) { cry.stop(); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5506c35e18a..e0ed96504b1 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -230,7 +230,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (this.variant === undefined) { - this.variant = this.shiny ? this.generateVariant() : 0; + this.variant = this.shiny ? this.generateShinyVariant() : 0; } this.customPokemonData = new CustomPokemonData(); @@ -1199,7 +1199,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @returns an array of {@linkcode Moves}, the length of which is determined * by how many learnable moves there are for the {@linkcode Pokemon}. */ - getLearnableLevelMoves(): Moves[] { + public getLearnableLevelMoves(): Moves[] { let levelMoves = this.getLevelMoves(1, true, false, true).map(lm => lm[1]); if (this.metBiome === -1 && !this.scene.gameMode.isFreshStartChallenge() && !this.scene.gameMode.isDaily) { levelMoves = this.getUnlockedEggMoves().concat(levelMoves); @@ -1213,13 +1213,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Gets the types of a pokemon - * @param includeTeraType boolean to include tera-formed type, default false - * @param forDefend boolean if the pokemon is defending from an attack - * @param ignoreOverride boolean if true, ignore ability changing effects + * @param includeTeraType - `true` to include tera-formed type; Default: `false` + * @param forDefend - `true` if the pokemon is defending from an attack; Default: `false` + * @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` * @returns array of {@linkcode Type} */ - getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride?: boolean): Type[] { - const types : Type[] = []; + public getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride: boolean = false): Type[] { + const types: Type[] = []; if (includeTeraType) { const teraType = this.getTeraType(); @@ -1284,14 +1284,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - // this.scene potentially can be undefined for a fainted pokemon in doubles - // use optional chaining to avoid runtime errors - - if (!types.length) { // become UNKNOWN if no types are present + // become UNKNOWN if no types are present + if (!types.length) { types.push(Type.UNKNOWN); } - if (types.length > 1 && types.includes(Type.UNKNOWN)) { // remove UNKNOWN if other types are present + // remove UNKNOWN if other types are present + if (types.length > 1 && types.includes(Type.UNKNOWN)) { const index = types.indexOf(Type.UNKNOWN); if (index !== -1) { types.splice(index, 1); @@ -1311,19 +1310,27 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return types; } - isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride?: boolean): boolean { - return !!this.getTypes(includeTeraType, forDefend, ignoreOverride).some(t => t === type); + /** + * Checks if the pokemon's typing includes the specified type + * @param type - {@linkcode Type} to check + * @param includeTeraType - `true` to include tera-formed type; Default: `true` + * @param forDefend - `true` if the pokemon is defending from an attack; Default: `false` + * @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` + * @returns `true` if the Pokemon's type matches + */ + public isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride: boolean = false): boolean { + return this.getTypes(includeTeraType, forDefend, ignoreOverride).some((t) => t === type); } /** * Gets the non-passive ability of the pokemon. This accounts for fusions and ability changing effects. - * This should rarely be called, most of the time {@link hasAbility} or {@link hasAbilityWithAttr} are better used as + * This should rarely be called, most of the time {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr} are better used as * those check both the passive and non-passive abilities and account for ability suppression. - * @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases - * @param {boolean} ignoreOverride If true, ignore ability changing effects - * @returns {Ability} The non-passive ability of the pokemon + * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases + * @param ignoreOverride - If `true`, ignore ability changing effects; Default: `false` + * @returns The non-passive {@linkcode Ability} of the pokemon */ - getAbility(ignoreOverride?: boolean): Ability { + public getAbility(ignoreOverride: boolean = false): Ability { if (!ignoreOverride && this.summonData?.ability) { return allAbilities[this.summonData.ability]; } @@ -1352,12 +1359,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Gets the passive ability of the pokemon. This should rarely be called, most of the time - * {@link hasAbility} or {@link hasAbilityWithAttr} are better used as those check both the passive and + * {@linkcode hasAbility} or {@linkcode hasAbilityWithAttr} are better used as those check both the passive and * non-passive abilities and account for ability suppression. - * @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases - * @returns {Ability} The passive ability of the pokemon + * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases + * @returns The passive {@linkcode Ability} of the pokemon */ - getPassiveAbility(): Ability { + public getPassiveAbility(): Ability { if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) { return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE]; } @@ -1379,12 +1386,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Gets a list of all instances of a given ability attribute among abilities this pokemon has. * Accounts for all the various effects which can affect whether an ability will be present or * in effect, and both passive and non-passive. - * @param attrType {@linkcode AbAttr} The ability attribute to check for. - * @param canApply {@linkcode Boolean} If false, it doesn't check whether the ability is currently active - * @param ignoreOverride {@linkcode Boolean} If true, it ignores ability changing effects - * @returns A list of all the ability attributes on this ability. + * @param attrType - {@linkcode AbAttr} The ability attribute to check for. + * @param canApply - If `false`, it doesn't check whether the ability is currently active; Default `true` + * @param ignoreOverride - If `true`, it ignores ability changing effects; Default `false` + * @returns An array of all the ability attributes on this ability. */ - getAbilityAttrs(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride?: boolean): T[] { + public getAbilityAttrs(attrType: { new(...args: any[]): T }, canApply: boolean = true, ignoreOverride: boolean = false): T[] { const abilityAttrs: T[] = []; if (!canApply || this.canApplyAbility()) { @@ -1403,12 +1410,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * - bought with starter candy * - set by override * - is a boss pokemon - * @returns whether or not a pokemon should have a passive + * @returns `true` if the Pokemon has a passive */ - hasPassive(): boolean { + public hasPassive(): boolean { // returns override if valid for current case - if ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && this.isPlayer()) || - (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && !this.isPlayer())) { + if ((Overrides.PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && this.isPlayer()) + || (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE !== Abilities.NONE && !this.isPlayer())) { return true; } @@ -1427,12 +1434,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Checks whether an ability of a pokemon can be currently applied. This should rarely be - * directly called, as {@link hasAbility} and {@link hasAbilityWithAttr} already call this. - * @see {@link hasAbility} {@link hasAbilityWithAttr} Intended ways to check abilities in most cases - * @param {boolean} passive If true, check if passive can be applied instead of non-passive - * @returns {Ability} The passive ability of the pokemon + * directly called, as {@linkcode hasAbility} and {@linkcode hasAbilityWithAttr} already call this. + * @see {@linkcode hasAbility} {@linkcode hasAbilityWithAttr} Intended ways to check abilities in most cases + * @param passive If true, check if passive can be applied instead of non-passive + * @returns `true` if the ability can be applied */ - canApplyAbility(passive: boolean = false): boolean { + public canApplyAbility(passive: boolean = false): boolean { if (passive && !this.hasPassive()) { return false; } @@ -1461,7 +1468,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return false; } } - return (!!this.hp || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); + return (this.hp > 0 || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); } /** @@ -1473,7 +1480,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} ignoreOverride If true, it ignores ability changing effects * @returns {boolean} Whether the ability is present and active */ - hasAbility(ability: Abilities, canApply: boolean = true, ignoreOverride?: boolean): boolean { + public hasAbility(ability: Abilities, canApply: boolean = true, ignoreOverride?: boolean): boolean { if (this.getAbility(ignoreOverride).id === ability && (!canApply || this.canApplyAbility())) { return true; } @@ -1493,7 +1500,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param {boolean} ignoreOverride If true, it ignores ability changing effects * @returns {boolean} Whether an ability with that attribute is present and active */ - hasAbilityWithAttr(attrType: Constructor, canApply: boolean = true, ignoreOverride?: boolean): boolean { + public hasAbilityWithAttr(attrType: Constructor, canApply: boolean = true, ignoreOverride?: boolean): boolean { if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).hasAttr(attrType)) { return true; } @@ -1508,7 +1515,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * and then multiplicative modifiers happening after (Heavy Metal and Light Metal) * @returns the kg of the Pokemon (minimum of 0.1) */ - getWeight(): number { + public getWeight(): number { const autotomizedTag = this.getTag(AutotomizedTag); let weightRemoved = 0; if (!Utils.isNullOrUndefined(autotomizedTag)) { @@ -1523,10 +1530,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Gets the tera-formed type of the pokemon, or UNKNOWN if not present - * @returns the {@linkcode Type} + * @returns The tera-formed type of the pokemon, or {@linkcode Type.UNKNOWN} if not present */ - getTeraType(): Type { + public getTeraType(): Type { // this.scene can be undefined for a fainted mon in doubles if (this.scene !== undefined) { const teraModifier = this.scene.findModifier(m => m instanceof TerastallizeModifier @@ -1540,23 +1546,23 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return Type.UNKNOWN; } - isTerastallized(): boolean { + public isTerastallized(): boolean { return this.getTeraType() !== Type.UNKNOWN; } - isGrounded(): boolean { + public isGrounded(): boolean { return !!this.getTag(GroundedTag) || (!this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.FLOATING) && !this.getTag(SemiInvulnerableTag)); } /** * Determines whether this Pokemon is prevented from running or switching due * to effects from moves and/or abilities. - * @param trappedAbMessages `string[]` If defined, ability trigger messages + * @param trappedAbMessages - If defined, ability trigger messages * (e.g. from Shadow Tag) are forwarded through this array. - * @param simulated `boolean` if `true`, applies abilities via simulated calls. - * @returns + * @param simulated - If `true`, applies abilities via simulated calls. + * @returns `true` if the pokemon is trapped */ - isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean { + public isTrapped(trappedAbMessages: string[] = [], simulated: boolean = true): boolean { if (this.isOfType(Type.GHOST)) { return false; } @@ -1564,7 +1570,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const trappedByAbility = new Utils.BooleanHolder(false); const opposingField = this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField(); - opposingField.forEach(opponent => + opposingField.forEach((opponent) => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, opponent, trappedByAbility, this, trappedAbMessages, simulated) ); @@ -1575,11 +1581,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Calculates the type of a move when used by this Pokemon after * type-changing move and ability attributes have applied. - * @param move {@linkcode Move} The move being used. - * @param simulated If `true`, prevents showing abilities applied in this calculation. - * @returns the {@linkcode Type} of the move after attributes are applied + * @param move - {@linkcode Move} The move being used. + * @param simulated - If `true`, prevents showing abilities applied in this calculation. + * @returns The {@linkcode Type} of the move after attributes are applied */ - getMoveType(move: Move, simulated: boolean = true): Type { + public getMoveType(move: Move, simulated: boolean = true): Type { const moveTypeHolder = new Utils.NumberHolder(move.type); applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder); @@ -1944,13 +1950,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Function that tries to set a Pokemon shiny based on seed. * For manual use only, usually to roll a Pokemon's shiny chance a second time. * - * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536 - * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) + * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / `65536` + * @param thresholdOverride number that is divided by `2^16` (`65536`) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} - * @returns true if the Pokemon has been set as a shiny, false otherwise + * @returns `true` if the Pokemon has been set as a shiny, `false` otherwise */ - trySetShinySeed(thresholdOverride?: integer, applyModifiersToOverride?: boolean): boolean { - const shinyThreshold = new Utils.IntegerHolder(BASE_SHINY_CHANCE); + public trySetShinySeed(thresholdOverride?: number, applyModifiersToOverride?: boolean): boolean { + const shinyThreshold = new Utils.NumberHolder(BASE_SHINY_CHANCE); if (thresholdOverride === undefined || applyModifiersToOverride) { if (thresholdOverride !== undefined && applyModifiersToOverride) { shinyThreshold.value = thresholdOverride; @@ -1975,13 +1981,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } /** - * Generates a variant - * Has a 10% of returning 2 (epic variant) - * And a 30% of returning 1 (rare variant) - * Returns 0 (basic shiny) if there is no variant or 60% of the time otherwise - * @returns the shiny variant + * Generates a shiny variant + * @returns `0-2`, with the following probabilities: + * - Has a 10% chance of returning `2` (epic variant) + * - Has a 30% chance of returning `1` (rare variant) + * - Has a 60% chance of returning `0` (basic shiny) */ - generateVariant(): Variant { + protected generateShinyVariant(): Variant { const formIndex: number = this.formIndex; let variantDataIndex: string | number = this.species.speciesId; if (this.species.forms.length > 0) { @@ -2007,8 +2013,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - generateFusionSpecies(forStarter?: boolean): void { - const hiddenAbilityChance = new Utils.IntegerHolder(BASE_HIDDEN_ABILITY_CHANCE); + public generateFusionSpecies(forStarter?: boolean): void { + const hiddenAbilityChance = new Utils.NumberHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); } @@ -2057,7 +2063,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.generateName(); } - clearFusionSpecies(): void { + public clearFusionSpecies(): void { this.fusionSpecies = null; this.fusionFormIndex = 0; this.fusionAbilityIndex = 0; @@ -2071,12 +2077,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.calculateStats(); } - generateAndPopulateMoveset(): void { + /** Generates a semi-random moveset for a Pokemon */ + public generateAndPopulateMoveset(): void { this.moveset = []; let movePool: [Moves, number][] = []; const allLevelMoves = this.getLevelMoves(1, true, true); if (!allLevelMoves) { - console.log(this.species.speciesId, "ERROR"); + console.warn("Error encountered trying to generate moveset for:", this.species.name); return; } @@ -2086,15 +2093,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { break; } let weight = levelMove[0]; - if (weight === 0) { // Evo Moves + // Evolution Moves + if (weight === 0) { weight = 50; } - if (weight === 1 && allMoves[levelMove[1]].power >= 80) { // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight + // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight + if (weight === 1 && allMoves[levelMove[1]].power >= 80) { weight = 40; } - if (allMoves[levelMove[1]].name.endsWith(" (N)")) { - weight /= 100; - } // Unimplemented level up moves are possible to generate, but 1% of their normal chance. if (!movePool.some(m => m[0] === levelMove[1])) { movePool.push([ levelMove[1], weight ]); } @@ -2127,7 +2133,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - if (this.level >= 60) { // No egg moves below level 60 + // No egg moves below level 60 + if (this.level >= 60) { for (let i = 0; i < 3; i++) { const moveId = speciesEggMoves[this.species.getRootSpeciesId()][i]; if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { @@ -2135,7 +2142,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3]; - if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) { // No rare egg moves before e4 + // No rare egg moves before e4 + if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) { movePool.push([ moveId, 30 ]); } if (this.fusionSpecies) { @@ -2146,14 +2154,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3]; - if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) {// No rare egg moves before e4 + // No rare egg moves before e4 + if (this.level >= 170 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)") && !this.isBoss()) { movePool.push([ moveId, 30 ]); } } } } - if (this.isBoss()) { // Bosses never get self ko moves + // Bosses never get self ko moves + if (this.isBoss()) { movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttr)); } movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttrOnHit)); @@ -2182,7 +2192,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const statRatio = worseCategory === MoveCategory.PHYSICAL ? atk / spAtk : spAtk / atk; movePool = movePool.map(m => [ m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1) ]); - let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight. + /** The higher this is the more the game weights towards higher level moves. At `0` all moves are equal weight. */ + let weightMultiplier = 0.9; if (this.hasTrainer()) { weightMultiplier += 0.7; } @@ -2191,7 +2202,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } const baseWeights: [Moves, number][] = movePool.map(m => [ m[0], Math.ceil(Math.pow(m[1], weightMultiplier) * 100) ]); - if (this.hasTrainer() || this.isBoss()) { // Trainers and bosses always force a stab move + // Trainers and bosses always force a stab move + if (this.hasTrainer() || this.isBoss()) { const stabMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS && this.isOfType(allMoves[m[0]].type)); if (stabMovePool.length) { @@ -2221,8 +2233,19 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { // Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier. // Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights double if STAB. // Status moves remain unchanged on weight, this encourages 1-2 - movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo?.moveId)).map(m => [ m[0], this.moveset.some(mo => mo?.getMove().category !== MoveCategory.STATUS && mo?.getMove().type === allMoves[m[0]].type) ? Math.ceil(Math.sqrt(m[1])) : allMoves[m[0]].category !== MoveCategory.STATUS ? Math.ceil(m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo?.getMove().power!) > 1).length) / 8, 0.5) * (this.isOfType(allMoves[m[0]].type) ? 2 : 1)) : m[1] ]); // TODO: is this bang correct? - } else { // Non-trainer pokemon just use normal weights + movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo?.moveId)).map((m) => { + let ret: number; + if (this.moveset.some(mo => mo?.getMove().category !== MoveCategory.STATUS && mo?.getMove().type === allMoves[m[0]].type)) { + ret = Math.ceil(Math.sqrt(m[1])); + } else if (allMoves[m[0]].category !== MoveCategory.STATUS) { + ret = Math.ceil(m[1] / Math.max(Math.pow(4, this.moveset.filter(mo => (mo?.getMove().power ?? 0) > 1).length) / 8, 0.5) * (this.isOfType(allMoves[m[0]].type) ? 2 : 1)); + } else { + ret = m[1]; + } + return [ m[0], ret ]; + }); + } else { + // Non-trainer pokemon just use normal weights movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo?.moveId)); } const totalWeight = movePool.reduce((v, m) => v + m[1], 0); @@ -2240,11 +2263,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } } - trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean { + public trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean { const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] : null; - return move?.isUsable(this, ignorePp)!; // TODO: is this bang correct? + return move?.isUsable(this, ignorePp) ?? false; } showInfo(): void { @@ -3243,7 +3266,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { try { SoundFade.fadeOut(scene, cry, Utils.fixedInt(Math.ceil(duration * 0.2))); fusionCry = this.getFusionSpeciesForm().cry(scene, Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0) }, soundConfig)); - SoundFade.fadeIn(scene, fusionCry, Utils.fixedInt(Math.ceil(duration * 0.2)), scene.masterVolume * scene.seVolume, 0); + SoundFade.fadeIn(scene, fusionCry, Utils.fixedInt(Math.ceil(duration * 0.2)), scene.masterVolume * scene.fieldVolume, 0); } catch (err) { console.error(err); } @@ -3258,11 +3281,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return this.fusionFaintCry(callback); } - const key = `cry/${this.species.getCryKey(this.formIndex)}`; - //eslint-disable-next-line @typescript-eslint/no-unused-vars - let i = 0; + const key = this.species.getCryKey(this.formIndex); let rate = 0.85; const cry = this.scene.playSound(key, { rate: rate }) as AnySound; + if (!cry || this.scene.fieldVolume === 0) { + return callback(); + } const sprite = this.getSprite(); const tintSprite = this.getTintSprite(); const delay = Math.max(this.scene.sound.get(key).totalDuration * 50, 25); @@ -3277,7 +3301,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { delay: Utils.fixedInt(delay), repeat: -1, callback: () => { - ++i; frameThreshold = sprite.anims.msPerFrame / rate; frameProgress += delay; while (frameProgress > frameThreshold) { @@ -3316,7 +3339,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } private fusionFaintCry(callback: Function): void { - const key = `cry/${this.species.getCryKey(this.formIndex)}`; + const key = this.species.getCryKey(this.formIndex); let i = 0; let rate = 0.85; const cry = this.scene.playSound(key, { rate: rate }) as AnySound; @@ -3324,12 +3347,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const tintSprite = this.getTintSprite(); let duration = cry.totalDuration * 1000; - const fusionCryKey = `cry/${this.fusionSpecies?.getCryKey(this.fusionFormIndex)}`; + const fusionCryKey = this.fusionSpecies!.getCryKey(this.fusionFormIndex); let fusionCry = this.scene.playSound(fusionCryKey, { rate: rate }) as AnySound; + if (!cry || !fusionCry || this.scene.fieldVolume === 0) { + return callback(); + } fusionCry.stop(); duration = Math.min(duration, fusionCry.totalDuration * 1000); fusionCry.destroy(); - const delay = Math.max(duration * 0.05, 25); let transitionIndex = 0; @@ -3367,10 +3392,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } frameProgress -= frameThreshold; } - if (i === transitionIndex) { + if (i === transitionIndex && fusionCryKey) { SoundFade.fadeOut(this.scene, cry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2))); fusionCry = this.scene.playSound(fusionCryKey, Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0), rate: rate })); - SoundFade.fadeIn(this.scene, fusionCry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)), this.scene.masterVolume * this.scene.seVolume, 0); + SoundFade.fadeIn(this.scene, fusionCry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)), this.scene.masterVolume * this.scene.fieldVolume, 0); } rate *= 0.99; if (cry && !cry.pendingRemove) { @@ -4576,7 +4601,7 @@ export class EnemyPokemon extends Pokemon { } if (this.shiny) { - this.variant = this.generateVariant(); + this.variant = this.generateShinyVariant(); if (Overrides.OPP_VARIANT_OVERRIDE !== null) { this.variant = Overrides.OPP_VARIANT_OVERRIDE; } diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 1aed61b34ba..c49b8d5aaa9 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -232,7 +232,7 @@ export class LoadingScene extends SceneBase { // Get current lang and load the types atlas for it. English will only load types while all other languages will load types and types_ const lang = i18next.resolvedLanguage; if (lang !== "en") { - if (Utils.verifyLang(lang)) { + if (Utils.hasAllLocalizedSprites(lang)) { this.loadAtlas(`statuses_${lang}`, ""); this.loadAtlas(`types_${lang}`, ""); } else { diff --git a/src/overrides.ts b/src/overrides.ts index 6760db79205..d7a8ee18f15 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -1,20 +1,20 @@ +import { type PokeballCounts } from "#app/battle-scene"; +import { Gender } from "#app/data/gender"; +import { Variant } from "#app/data/variant"; +import { type ModifierOverride } from "#app/modifier/modifier-type"; +import { Unlockables } from "#app/system/unlockables"; import { Abilities } from "#enums/abilities"; import { Biome } from "#enums/biome"; import { EggTier } from "#enums/egg-type"; import { Moves } from "#enums/moves"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PokeballType } from "#enums/pokeball"; import { Species } from "#enums/species"; import { StatusEffect } from "#enums/status-effect"; import { TimeOfDay } from "#enums/time-of-day"; import { VariantTier } from "#enums/variant-tier"; import { WeatherType } from "#enums/weather-type"; -import { type PokeballCounts } from "./battle-scene"; -import { Gender } from "./data/gender"; -import { Variant } from "./data/variant"; -import { type ModifierOverride } from "./modifier/modifier-type"; -import { Unlockables } from "./system/unlockables"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; /** * Overrides that are using when testing different in game situations diff --git a/src/phases/evolution-phase.ts b/src/phases/evolution-phase.ts index 7b079c3fbf9..dec65e2c945 100644 --- a/src/phases/evolution-phase.ts +++ b/src/phases/evolution-phase.ts @@ -1,12 +1,12 @@ import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import { Phase } from "#app/phase"; -import BattleScene from "#app/battle-scene"; +import BattleScene, { AnySound } from "#app/battle-scene"; import { SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import EvolutionSceneHandler from "#app/ui/evolution-scene-handler"; import * as Utils from "#app/utils"; import { Mode } from "#app/ui/ui"; import { cos, sin } from "#app/field/anims"; -import { PlayerPokemon } from "#app/field/pokemon"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import { getTypeRgb } from "#app/data/type"; import i18next from "i18next"; import { getPokemonNameWithAffix } from "#app/messages"; @@ -17,7 +17,11 @@ export class EvolutionPhase extends Phase { protected pokemon: PlayerPokemon; protected lastLevel: integer; + private preEvolvedPokemonName: string; + private evolution: SpeciesFormEvolution | null; + private evolutionBgm: AnySound; + private evolutionHandler: EvolutionSceneHandler; protected evolutionContainer: Phaser.GameObjects.Container; protected evolutionBaseBg: Phaser.GameObjects.Image; @@ -35,6 +39,8 @@ export class EvolutionPhase extends Phase { this.pokemon = pokemon; this.evolution = evolution; this.lastLevel = lastLevel; + this.evolutionBgm = this.scene.playSoundWithoutBgm("evolution"); + this.preEvolvedPokemonName = getPokemonNameWithAffix(this.pokemon); } validate(): boolean { @@ -117,10 +123,9 @@ export class EvolutionPhase extends Phase { } doEvolution(): void { - const evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler; - const preName = getPokemonNameWithAffix(this.pokemon); + this.evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler; - this.scene.ui.showText(i18next.t("menu:evolving", { pokemonName: preName }), null, () => { + this.scene.ui.showText(i18next.t("menu:evolving", { pokemonName: this.preEvolvedPokemonName }), null, () => { this.pokemon.cry(); this.pokemon.getPossibleEvolution(this.evolution).then(evolvedPokemon => { @@ -140,7 +145,6 @@ export class EvolutionPhase extends Phase { }); this.scene.time.delayedCall(1000, () => { - const evolutionBgm = this.scene.playSoundWithoutBgm("evolution"); this.scene.tweens.add({ targets: this.evolutionBgOverlay, alpha: 1, @@ -174,100 +178,13 @@ export class EvolutionPhase extends Phase { this.scene.time.delayedCall(1500, () => { this.pokemonEvoTintSprite.setScale(0.25); this.pokemonEvoTintSprite.setVisible(true); - evolutionHandler.canCancel = true; + this.evolutionHandler.canCancel = true; this.doCycle(1).then(success => { - if (!success) { - - this.pokemonSprite.setVisible(true); - this.pokemonTintSprite.setScale(1); - this.scene.tweens.add({ - targets: [ this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite ], - alpha: 0, - duration: 250, - onComplete: () => { - this.evolutionBg.setVisible(false); - } - }); - - SoundFade.fadeOut(this.scene, evolutionBgm, 100); - - this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); - - this.scene.ui.showText(i18next.t("menu:stoppedEvolving", { pokemonName: preName }), null, () => { - this.scene.ui.showText(i18next.t("menu:pauseEvolutionsQuestion", { pokemonName: preName }), null, () => { - const end = () => { - this.scene.ui.showText("", 0); - this.scene.playBgm(); - evolvedPokemon.destroy(); - this.end(); - }; - this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { - this.scene.ui.revertMode(); - this.pokemon.pauseEvolutions = true; - this.scene.ui.showText(i18next.t("menu:evolutionsPaused", { pokemonName: preName }), null, end, 3000); - }, () => { - this.scene.ui.revertMode(); - this.scene.time.delayedCall(3000, end); - }); - }); - }, null, true); - return; + if (success) { + this.handleSuccessEvolution(evolvedPokemon); + } else { + this.handleFailedEvolution(evolvedPokemon); } - - this.scene.playSound("se/sparkle"); - this.pokemonEvoSprite.setVisible(true); - this.doCircleInward(); - this.scene.time.delayedCall(900, () => { - evolutionHandler.canCancel = false; - - this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { - const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true); - for (const lm of levelMoves) { - this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1])); - } - this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); - - this.scene.playSound("se/shine"); - this.doSpray(); - this.scene.tweens.add({ - targets: this.evolutionOverlay, - alpha: 1, - duration: 250, - easing: "Sine.easeIn", - onComplete: () => { - this.evolutionBgOverlay.setAlpha(1); - this.evolutionBg.setVisible(false); - this.scene.tweens.add({ - targets: [ this.evolutionOverlay, this.pokemonEvoTintSprite ], - alpha: 0, - duration: 2000, - delay: 150, - easing: "Sine.easeIn", - onComplete: () => { - this.scene.tweens.add({ - targets: this.evolutionBgOverlay, - alpha: 0, - duration: 250, - onComplete: () => { - SoundFade.fadeOut(this.scene, evolutionBgm, 100); - this.scene.time.delayedCall(250, () => { - this.pokemon.cry(); - this.scene.time.delayedCall(1250, () => { - this.scene.playSoundWithoutBgm("evolution_fanfare"); - - evolvedPokemon.destroy(); - this.scene.ui.showText(i18next.t("menu:evolutionDone", { pokemonName: preName, evolvedPokemonName: this.pokemon.name }), null, () => this.end(), null, true, Utils.fixedInt(4000)); - this.scene.time.delayedCall(Utils.fixedInt(4250), () => this.scene.playBgm()); - }); - }); - } - }); - } - }); - } - }); - }); - }); }); }); }); @@ -280,6 +197,110 @@ export class EvolutionPhase extends Phase { }, 1000); } + /** + * Handles a failed/stopped evolution + * @param evolvedPokemon - The evolved Pokemon + */ + private handleFailedEvolution(evolvedPokemon: Pokemon): void { + this.pokemonSprite.setVisible(true); + this.pokemonTintSprite.setScale(1); + this.scene.tweens.add({ + targets: [ this.evolutionBg, this.pokemonTintSprite, this.pokemonEvoSprite, this.pokemonEvoTintSprite ], + alpha: 0, + duration: 250, + onComplete: () => { + this.evolutionBg.setVisible(false); + } + }); + + SoundFade.fadeOut(this.scene, this.evolutionBgm, 100); + + this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); + + this.scene.ui.showText(i18next.t("menu:stoppedEvolving", { pokemonName: this.preEvolvedPokemonName }), null, () => { + this.scene.ui.showText(i18next.t("menu:pauseEvolutionsQuestion", { pokemonName: this.preEvolvedPokemonName }), null, () => { + const end = () => { + this.scene.ui.showText("", 0); + this.scene.playBgm(); + evolvedPokemon.destroy(); + this.end(); + }; + this.scene.ui.setOverlayMode(Mode.CONFIRM, () => { + this.scene.ui.revertMode(); + this.pokemon.pauseEvolutions = true; + this.scene.ui.showText(i18next.t("menu:evolutionsPaused", { pokemonName: this.preEvolvedPokemonName }), null, end, 3000); + }, () => { + this.scene.ui.revertMode(); + this.scene.time.delayedCall(3000, end); + }); + }); + }, null, true); + } + + /** + * Handles a successful evolution + * @param evolvedPokemon - The evolved Pokemon + */ + private handleSuccessEvolution(evolvedPokemon: Pokemon): void { + this.scene.playSound("se/sparkle"); + this.pokemonEvoSprite.setVisible(true); + this.doCircleInward(); + + const onEvolutionComplete = () => { + SoundFade.fadeOut(this.scene, this.evolutionBgm, 100); + this.scene.time.delayedCall(250, () => { + this.pokemon.cry(); + this.scene.time.delayedCall(1250, () => { + this.scene.playSoundWithoutBgm("evolution_fanfare"); + + evolvedPokemon.destroy(); + this.scene.ui.showText(i18next.t("menu:evolutionDone", { pokemonName: this.preEvolvedPokemonName, evolvedPokemonName: this.pokemon.name }), null, () => this.end(), null, true, Utils.fixedInt(4000)); + this.scene.time.delayedCall(Utils.fixedInt(4250), () => this.scene.playBgm()); + }); + }); + }; + + this.scene.time.delayedCall(900, () => { + this.evolutionHandler.canCancel = false; + + this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { + const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true); + for (const lm of levelMoves) { + this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1])); + } + this.scene.unshiftPhase(new EndEvolutionPhase(this.scene)); + + this.scene.playSound("se/shine"); + this.doSpray(); + this.scene.tweens.add({ + targets: this.evolutionOverlay, + alpha: 1, + duration: 250, + easing: "Sine.easeIn", + onComplete: () => { + this.evolutionBgOverlay.setAlpha(1); + this.evolutionBg.setVisible(false); + this.scene.tweens.add({ + targets: [ this.evolutionOverlay, this.pokemonEvoTintSprite ], + alpha: 0, + duration: 2000, + delay: 150, + easing: "Sine.easeIn", + onComplete: () => { + this.scene.tweens.add({ + targets: this.evolutionBgOverlay, + alpha: 0, + duration: 250, + onComplete: onEvolutionComplete + }); + } + }); + } + }); + }); + }); + } + doSpiralUpward() { let f = 0; @@ -320,7 +341,6 @@ export class EvolutionPhase extends Phase { doCycle(l: number, lastCycle: integer = 15): Promise { return new Promise(resolve => { - const evolutionHandler = this.scene.ui.getHandler() as EvolutionSceneHandler; const isLastCycle = l === lastCycle; this.scene.tweens.add({ targets: this.pokemonTintSprite, @@ -336,7 +356,7 @@ export class EvolutionPhase extends Phase { duration: 500 / l, yoyo: !isLastCycle, onComplete: () => { - if (evolutionHandler.cancelled) { + if (this.evolutionHandler.cancelled) { return resolve(false); } if (l < lastCycle) { diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 8b4b462380c..4c813a881d3 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -52,11 +52,11 @@ import { HitHealModifier, PokemonMultiHitModifier, } from "#app/modifier/modifier"; +import { PokemonPhase } from "#app/phases/pokemon-phase"; import { BooleanHolder, executeIf, NumberHolder } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Moves } from "#enums/moves"; import i18next from "i18next"; -import { PokemonPhase } from "./pokemon-phase"; export class MoveEffectPhase extends PokemonPhase { public move: PokemonMove; @@ -517,7 +517,11 @@ export class MoveEffectPhase extends PokemonPhase { return true; } - const user = this.getUserPokemon()!; // TODO: is this bang correct? + const user = this.getUserPokemon(); + + if (!user) { + return false; + } // Hit check only calculated on first hit for multi-hit moves unless flag is set to check all hits. // However, if an ability with the MaxMultiHitAbAttr, namely Skill Link, is present, act as a normal @@ -566,9 +570,9 @@ export class MoveEffectPhase extends PokemonPhase { } /** @returns The {@linkcode Pokemon} using this phase's invoked move */ - public getUserPokemon(): Pokemon | undefined { + public getUserPokemon(): Pokemon | null { if (this.battlerIndex > BattlerIndex.ENEMY_2) { - return this.scene.getPokemonById(this.battlerIndex) ?? undefined; + return this.scene.getPokemonById(this.battlerIndex); } return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; } @@ -596,8 +600,7 @@ export class MoveEffectPhase extends PokemonPhase { /** * Prevents subsequent strikes of this phase's invoked move from occurring - * @param target {@linkcode Pokemon} if defined, only stop subsequent - * strikes against this Pokemon + * @param target - If defined, only stop subsequent strikes against this {@linkcode Pokemon} */ public stopMultiHit(target?: Pokemon): void { // If given a specific target, remove the target from subsequent strikes diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index e6fb884ffdf..d71edf603dd 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -163,6 +163,11 @@ export const SettingKeys = { Shop_Overlay_Opacity: "SHOP_OVERLAY_OPACITY" }; +export enum MusicPreference { + CONSISTENT, + MIXED +} + /** * All Settings not related to controls */ @@ -634,7 +639,7 @@ export const Setting: Array = [ label: i18next.t("settings:mixed") } ], - default: 0, + default: MusicPreference.MIXED, type: SettingType.AUDIO, requireReload: true }, diff --git a/src/test/abilities/unburden.test.ts b/src/test/abilities/unburden.test.ts index 33604efc534..7cd69f4a075 100644 --- a/src/test/abilities/unburden.test.ts +++ b/src/test/abilities/unburden.test.ts @@ -216,7 +216,7 @@ describe("Abilities - Unburden", () => { .moveset([ Moves.SPLASH ]); await game.classicMode.startBattle([ Species.TREECKO, Species.MEOWTH, Species.WEEZING ]); - const playerPokemon = game.scene.getParty(); + const playerPokemon = game.scene.getPlayerParty(); const treecko = playerPokemon[0]; const weezing = playerPokemon[2]; treecko.abilityIndex = 2; diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index a7d67911228..fe8d06c2c3b 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -156,6 +156,7 @@ export default class GameManager { this.scene.enableTutorials = false; this.scene.gameData.gender = PlayerGender.MALE; // set initial player gender this.scene.battleStyle = this.settings.battleStyle; + this.scene.fieldVolume = 0; } /** diff --git a/src/utils.ts b/src/utils.ts index b615dcf122b..8b779f32730 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -121,18 +121,6 @@ export function randSeedWeightedItem(items: T[]): T { : Phaser.Math.RND.weightedPick(items); } -export function randSeedEasedWeightedItem(items: T[], easingFunction: string = "Sine.easeIn"): T | null { - if (!items.length) { - return null; - } - if (items.length === 1) { - return items[0]; - } - const value = Phaser.Math.RND.realInRange(0, 1); - const easedValue = Phaser.Tweens.Builders.GetEaseFunction(easingFunction)(value); - return items[Math.floor(easedValue * items.length)]; -} - /** * Shuffle a list using the seeded rng. Utilises the Fisher-Yates algorithm. * @param {Array} items An array of items. @@ -380,18 +368,21 @@ export class NumberHolder { } } +/** @deprecated Use {@linkcode NumberHolder} */ export class IntegerHolder extends NumberHolder { constructor(value: integer) { super(value); } } +/** @deprecated Use {@linkcode NumberHolder}*/ export class FixedInt extends IntegerHolder { constructor(value: integer) { super(value); } } +/** @deprecated */ export function fixedInt(value: integer): integer { return new FixedInt(value) as unknown as integer; } @@ -474,14 +465,15 @@ export function hslToHex(h: number, s: number, l: number): string { return `#${f(0)}${f(8)}${f(4)}`; } -/*This function returns true if the current lang is available for some functions -If the lang is not in the function, it usually means that lang is going to use the default english version -This function is used in: -- summary-ui-handler.ts: If the lang is not available, it'll use types.json (english) -English itself counts as not available +/** + * This function returns `true` if all localized images used by the game have been added for the given language. + * + * If the lang is not in the function, it usually means that lang is going to use the default english version + * + * English itself counts as not available */ -export function verifyLang(lang?: string): boolean { - //IMPORTANT - ONLY ADD YOUR LANG HERE IF YOU'VE ALREADY ADDED ALL THE NECESSARY IMAGES +export function hasAllLocalizedSprites(lang?: string): boolean { + // IMPORTANT - ONLY ADD YOUR LANG HERE IF YOU'VE ALREADY ADDED ALL THE NECESSARY IMAGES if (!lang) { lang = i18next.resolvedLanguage; } @@ -503,7 +495,7 @@ export function verifyLang(lang?: string): boolean { } /** - * Prints the type and name of all game objects in a container for debuggin purposes + * Prints the type and name of all game objects in a container for debugging purposes * @param container container with game objects inside it */ export function printContainerList(container: Phaser.GameObjects.Container): void { @@ -578,17 +570,12 @@ export function capitalizeString(str: string, sep: string, lowerFirstChar: boole return null; } -/** - * Returns if an object is null or undefined - * @param object - */ export function isNullOrUndefined(object: any): object is undefined | null { return null === object || undefined === object; } /** * Capitalizes the first letter of a string - * @param str */ export function capitalizeFirstLetter(str: string) { return str.charAt(0).toUpperCase() + str.slice(1); @@ -614,7 +601,7 @@ export function toDmgValue(value: number, minValue: number = 1) { * @returns the localized sprite key */ export function getLocalizedSpriteKey(baseKey: string) { - return `${baseKey}${verifyLang(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`; + return `${baseKey}${hasAllLocalizedSprites(i18next.resolvedLanguage) ? `_${i18next.resolvedLanguage}` : ""}`; } /** @@ -622,7 +609,7 @@ export function getLocalizedSpriteKey(baseKey: string) { * @param num the number to check * @param min the minimum value (included) * @param max the maximum value (included) - * @returns true if number is **inclusive** between min and max + * @returns `true` if number is **inclusive** between min and max */ export function isBetween(num: number, min: number, max: number): boolean { return num >= min && num <= max;