import Phaser from "phaser"; import BattleScene, { AnySound } from "../battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info"; import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr, OneHitKOAccuracyAttr } from "../data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species"; import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type"; import { getLevelTotalExp } from "../data/exp"; import { Stat } from "../data/pokemon-stat"; import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, CritBoosterModifier, TerastallizeModifier } from "../modifier/modifier"; import { PokeballType } from "../data/pokeball"; import { Gender } from "../data/gender"; import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase, MoveEndPhase } from "../phases"; import { BattleStat } from "../data/battle-stat"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag } from "../data/battler-tags"; import { WeatherType } from "../data/weather"; import { TempBattleStat } from "../data/temp-battle-stat"; import { ArenaTagSide, WeakenMoveScreenTag } from "../data/arena-tag"; import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr, IgnoreOpponentEvasionAbAttr } from "../data/ability"; import PokemonData from "../system/pokemon-data"; import { BattlerIndex } from "../battle"; import { Mode } from "../ui/ui"; import PartyUiHandler, { PartyOption, PartyUiMode } from "../ui/party-ui-handler"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import { LevelMoves } from "../data/pokemon-level-moves"; import { DamageAchv, achvs } from "../system/achv"; import { DexAttr, StarterDataEntry, StarterMoveset } from "../system/game-data"; import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities"; import { Nature, getNatureStatMultiplier } from "../data/nature"; import { SpeciesFormChange, SpeciesFormChangeActiveTrigger, SpeciesFormChangeMoveLearnedTrigger, SpeciesFormChangePostMoveTrigger, SpeciesFormChangeStatusEffectTrigger } from "../data/pokemon-forms"; import { TerrainType } from "../data/terrain"; import { TrainerSlot } from "../data/trainer-config"; import Overrides from "#app/overrides"; import i18next from "i18next"; import { speciesEggMoves } from "../data/egg-moves"; import { ModifierTier } from "../modifier/modifier-tier"; import { applyChallenges, ChallengeType } from "#app/data/challenge.js"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; import { BattleSpec } from "#enums/battle-spec"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { getPokemonNameWithAffix } from "#app/messages.js"; export enum FieldPosition { CENTER, LEFT, RIGHT } export default abstract class Pokemon extends Phaser.GameObjects.Container { public id: integer; public name: string; public nickname: string; public species: PokemonSpecies; public formIndex: integer; public abilityIndex: integer; public passive: boolean; public shiny: boolean; public variant: Variant; public pokeball: PokeballType; protected battleInfo: BattleInfo; public level: integer; public exp: integer; public levelExp: integer; public gender: Gender; public hp: integer; public stats: integer[]; public ivs: integer[]; public nature: Nature; public natureOverride: Nature | -1; public moveset: PokemonMove[]; public status: Status; public friendship: integer; public metLevel: integer; public metBiome: Biome | -1; public luck: integer; public pauseEvolutions: boolean; public pokerus: boolean; public fusionSpecies: PokemonSpecies; public fusionFormIndex: integer; public fusionAbilityIndex: integer; public fusionShiny: boolean; public fusionVariant: Variant; public fusionGender: Gender; public fusionLuck: integer; private summonDataPrimer: PokemonSummonData; public summonData: PokemonSummonData; public battleData: PokemonBattleData; public battleSummonData: PokemonBattleSummonData; public turnData: PokemonTurnData; public fieldPosition: FieldPosition; public maskEnabled: boolean; public maskSprite: Phaser.GameObjects.Sprite; private shinySparkle: Phaser.GameObjects.Sprite; constructor(scene: BattleScene, x: number, y: number, species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData) { super(scene, x, y); if (!species.isObtainable() && this.isPlayer()) { throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`; } const hiddenAbilityChance = new Utils.IntegerHolder(256); if (!this.hasTrainer()) { this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); } const hasHiddenAbility = !Utils.randSeedInt(hiddenAbilityChance.value); const randAbilityIndex = Utils.randSeedInt(2); this.species = species; this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL; this.level = level; // Determine the ability index if (abilityIndex !== undefined) { this.abilityIndex = abilityIndex; // Use the provided ability index if it is defined } else { // If abilityIndex is not provided, determine it based on species and hidden ability if (species.abilityHidden && hasHiddenAbility) { // If the species has a hidden ability and the hidden ability is present this.abilityIndex = species.ability2 ? 2 : 1; // Use ability index 2 if species has a second ability, otherwise use 1 } else { // If there is no hidden ability or species does not have a hidden ability this.abilityIndex = species.ability2 ? randAbilityIndex : 0; // Use random ability index if species has a second ability, otherwise use 0 } } if (formIndex !== undefined) { this.formIndex = formIndex; } if (gender !== undefined) { this.gender = gender; } if (shiny !== undefined) { this.shiny = shiny; } if (variant !== undefined) { this.variant = variant; } this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate); this.levelExp = dataSource?.levelExp || 0; if (dataSource) { this.id = dataSource.id; this.hp = dataSource.hp; this.stats = dataSource.stats; this.ivs = dataSource.ivs; this.passive = !!dataSource.passive; if (this.variant === undefined) { this.variant = 0; } this.nature = dataSource.nature || 0 as Nature; this.nickname = dataSource.nickname; this.natureOverride = dataSource.natureOverride !== undefined ? dataSource.natureOverride : -1; this.moveset = dataSource.moveset; this.status = dataSource.status; this.friendship = dataSource.friendship !== undefined ? dataSource.friendship : this.species.baseFriendship; this.metLevel = dataSource.metLevel || 5; this.luck = dataSource.luck; this.metBiome = dataSource.metBiome; this.pauseEvolutions = dataSource.pauseEvolutions; this.pokerus = !!dataSource.pokerus; this.fusionSpecies = dataSource.fusionSpecies instanceof PokemonSpecies ? dataSource.fusionSpecies : getPokemonSpecies(dataSource.fusionSpecies); this.fusionFormIndex = dataSource.fusionFormIndex; this.fusionAbilityIndex = dataSource.fusionAbilityIndex; this.fusionShiny = dataSource.fusionShiny; this.fusionVariant = dataSource.fusionVariant || 0; this.fusionGender = dataSource.fusionGender; this.fusionLuck = dataSource.fusionLuck; } else { this.id = Utils.randSeedInt(4294967296); this.ivs = ivs || Utils.getIvsFromId(this.id); if (this.gender === undefined) { this.generateGender(); } if (this.formIndex === undefined) { this.formIndex = this.scene.getSpeciesFormIndex(species, this.gender, this.nature, this.isPlayer()); } if (this.shiny === undefined) { this.trySetShiny(); } if (this.variant === undefined) { this.variant = this.shiny ? this.generateVariant() : 0; } if (nature !== undefined) { this.setNature(nature); } else { this.generateNature(); } this.natureOverride = -1; this.friendship = species.baseFriendship; this.metLevel = level; this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1; this.pokerus = false; if (level > 1) { const fused = new Utils.BooleanHolder(scene.gameMode.isSplicedOnly); if (!fused.value && !this.isPlayer() && !this.hasTrainer()) { this.scene.applyModifier(EnemyFusionChanceModifier, false, fused); } if (fused.value) { this.calculateStats(); this.generateFusionSpecies(); } } this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0); this.fusionLuck = this.luck; } this.generateName(); if (!species.isObtainable()) { this.shiny = false; } this.calculateStats(); } getNameToRender() { try { if (this.nickname) { return decodeURIComponent(escape(atob(this.nickname))); } return this.name; } catch (err) { console.error(`Failed to decode nickname for ${this.name}`, err); return this.name; } } init(): void { this.fieldPosition = FieldPosition.CENTER; this.initBattleInfo(); this.scene.fieldUI.addAt(this.battleInfo, 0); const getSprite = (hasShadow?: boolean) => { const ret = this.scene.addPokemonSprite(this, 0, 0, `pkmn__${this.isPlayer() ? "back__" : ""}sub`, undefined, true); ret.setOrigin(0.5, 1); ret.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: !!hasShadow, teraColor: getTypeRgb(this.getTeraType()) }); return ret; }; this.setScale(this.getSpriteScale()); const sprite = getSprite(true); const tintSprite = getSprite(); tintSprite.setVisible(false); this.addAt(sprite, 0); this.addAt(tintSprite, 1); if (this.isShiny() && !this.shinySparkle) { this.initShinySparkle(); } } abstract initBattleInfo(): void; isOnField(): boolean { if (!this.scene) { return false; } return this.scene.field.getIndex(this) > -1; } isFainted(checkStatus?: boolean): boolean { return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT); } /** * Check if this pokemon is both not fainted and allowed to be in battle. * This is frequently a better alternative to {@link isFainted} * @returns {boolean} True if pokemon is allowed in battle */ isAllowedInBattle(): boolean { const challengeAllowed = new Utils.BooleanHolder(true); applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed); return !this.isFainted() && challengeAllowed.value; } isActive(onField?: boolean): boolean { if (!this.scene) { return false; } return this.isAllowedInBattle() && !!this.scene && (!onField || this.isOnField()); } getDexAttr(): bigint { let ret = 0n; ret |= this.gender !== Gender.FEMALE ? DexAttr.MALE : DexAttr.FEMALE; ret |= !this.shiny ? DexAttr.NON_SHINY : DexAttr.SHINY; ret |= this.variant >= 2 ? DexAttr.VARIANT_3 : this.variant === 1 ? DexAttr.VARIANT_2 : DexAttr.DEFAULT_VARIANT; ret |= this.scene.gameData.getFormAttr(this.formIndex); return ret; } /** * Sets the Pokemon's name. Only called when loading a Pokemon so this function needs to be called when * initializing hardcoded Pokemon or else it will not display the form index name properly. * @returns n/a */ generateName(): void { if (!this.fusionSpecies) { this.name = this.species.getName(this.formIndex); return; } this.name = getFusedSpeciesName(this.species.getName(this.formIndex), this.fusionSpecies.getName(this.fusionFormIndex)); if (this.battleInfo) { this.updateInfo(true); } } abstract isPlayer(): boolean; abstract hasTrainer(): boolean; abstract getFieldIndex(): integer; abstract getBattlerIndex(): BattlerIndex; loadAssets(ignoreOverride: boolean = true): Promise { return new Promise(resolve => { const moveIds = this.getMoveset().map(m => m.getMove().id); Promise.allSettled(moveIds.map(m => initMoveAnim(this.scene, m))) .then(() => { loadMoveAnimAssets(this.scene, moveIds); this.getSpeciesForm().loadAssets(this.scene, this.getGender() === Gender.FEMALE, this.formIndex, this.shiny, this.variant); if (this.isPlayer() || this.getFusionSpeciesForm()) { this.scene.loadPokemonAtlas(this.getBattleSpriteKey(true, ignoreOverride), this.getBattleSpriteAtlasPath(true, ignoreOverride)); } if (this.getFusionSpeciesForm()) { this.getFusionSpeciesForm().loadAssets(this.scene, this.getFusionGender() === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant); this.scene.loadPokemonAtlas(this.getFusionBattleSpriteKey(true, ignoreOverride), this.getFusionBattleSpriteAtlasPath(true, ignoreOverride)); } this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => { if (this.isPlayer()) { const originalWarn = console.warn; // Ignore warnings for missing frames, because there will be a lot console.warn = () => {}; const battleFrameNames = this.scene.anims.generateFrameNames(this.getBattleSpriteKey(), { zeroPad: 4, suffix: ".png", start: 1, end: 400 }); console.warn = originalWarn; if (!(this.scene.anims.exists(this.getBattleSpriteKey()))) { this.scene.anims.create({ key: this.getBattleSpriteKey(), frames: battleFrameNames, frameRate: 12, repeat: -1 }); } } this.playAnim(); const updateFusionPaletteAndResolve = () => { this.updateFusionPalette(); if (this.summonData?.speciesForm) { this.updateFusionPalette(true); } resolve(); }; if (this.shiny) { const populateVariantColors = (key: string, back: boolean = false): Promise => { return new Promise(resolve => { const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); let config = variantData; const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride)); battleSpritePath.split("/").map(p => config ? config = config[p] : null); const variantSet: VariantSet = config as VariantSet; if (variantSet && variantSet[this.variant] === 1) { if (variantColorCache.hasOwnProperty(key)) { return resolve(); } this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`). then(res => { // Prevent the JSON from processing if it failed to load if (!res.ok) { console.error(`Could not load ${res.url}!`); return; } return res.json(); }).then(c => { variantColorCache[key] = c; resolve(); }); } else { resolve(); } }); }; if (this.isPlayer()) { Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve()); } else { populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve()); } } else { updateFusionPaletteAndResolve(); } }); if (!this.scene.load.isLoading()) { this.scene.load.start(); } }); }); } getFormKey(): string { if (!this.species.forms.length || this.species.forms.length <= this.formIndex) { return ""; } return this.species.forms[this.formIndex].formKey; } getFusionFormKey(): string { if (!this.fusionSpecies) { return null; } if (!this.fusionSpecies.forms.length || this.fusionSpecies.forms.length <= this.fusionFormIndex) { return ""; } return this.fusionSpecies.forms[this.fusionFormIndex].formKey; } getSpriteAtlasPath(ignoreOverride?: boolean): string { const spriteId = this.getSpriteId(ignoreOverride).replace(/\_{2}/g, "/"); return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; } getBattleSpriteAtlasPath(back?: boolean, ignoreOverride?: boolean): string { const spriteId = this.getBattleSpriteId(back, ignoreOverride).replace(/\_{2}/g, "/"); return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; } getSpriteId(ignoreOverride?: boolean): string { return this.getSpeciesForm(ignoreOverride).getSpriteId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant); } getBattleSpriteId(back?: boolean, ignoreOverride?: boolean): string { if (back === undefined) { back = this.isPlayer(); } return this.getSpeciesForm(ignoreOverride).getSpriteId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant, back); } getSpriteKey(ignoreOverride?: boolean): string { return this.getSpeciesForm(ignoreOverride).getSpriteKey(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant); } getBattleSpriteKey(back?: boolean, ignoreOverride?: boolean): string { return `pkmn__${this.getBattleSpriteId(back, ignoreOverride)}`; } getFusionSpriteId(ignoreOverride?: boolean): string { return this.getFusionSpeciesForm(ignoreOverride).getSpriteId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant); } getFusionBattleSpriteId(back?: boolean, ignoreOverride?: boolean): string { if (back === undefined) { back = this.isPlayer(); } return this.getFusionSpeciesForm(ignoreOverride).getSpriteId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant, back); } getFusionBattleSpriteKey(back?: boolean, ignoreOverride?: boolean): string { return `pkmn__${this.getFusionBattleSpriteId(back, ignoreOverride)}`; } getFusionBattleSpriteAtlasPath(back?: boolean, ignoreOverride?: boolean): string { return this.getFusionBattleSpriteId(back, ignoreOverride).replace(/\_{2}/g, "/"); } getIconAtlasKey(ignoreOverride?: boolean): string { return this.getSpeciesForm(ignoreOverride).getIconAtlasKey(this.formIndex, this.shiny, this.variant); } getFusionIconAtlasKey(ignoreOverride?: boolean): string { return this.getFusionSpeciesForm(ignoreOverride).getIconAtlasKey(this.fusionFormIndex, this.fusionShiny, this.fusionVariant); } getIconId(ignoreOverride?: boolean): string { return this.getSpeciesForm(ignoreOverride).getIconId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant); } getFusionIconId(ignoreOverride?: boolean): string { return this.getFusionSpeciesForm(ignoreOverride).getIconId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant); } getSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm { if (!ignoreOverride && this.summonData?.speciesForm) { return this.summonData.speciesForm; } if (!this.species.forms?.length) { return this.species; } return this.species.forms[this.formIndex]; } getFusionSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm { if (!ignoreOverride && this.summonData?.speciesForm) { return this.summonData.fusionSpeciesForm; } if (!this.fusionSpecies?.forms?.length || this.fusionFormIndex >= this.fusionSpecies?.forms.length) { return this.fusionSpecies; } return this.fusionSpecies?.forms[this.fusionFormIndex]; } getSprite(): Phaser.GameObjects.Sprite { return this.getAt(0) as Phaser.GameObjects.Sprite; } getTintSprite(): Phaser.GameObjects.Sprite { return !this.maskEnabled ? this.getAt(1) as Phaser.GameObjects.Sprite : this.maskSprite; } getSpriteScale(): number { const formKey = this.getFormKey(); if (formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 || formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1) { return 1.5; } return 1; } getHeldItems(): PokemonHeldItemModifier[] { if (!this.scene) { return []; } return this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, this.isPlayer()) as PokemonHeldItemModifier[]; } updateScale(): void { this.setScale(this.getSpriteScale()); } updateSpritePipelineData(): void { [ this.getSprite(), this.getTintSprite() ].map(s => s.pipelineData["teraColor"] = getTypeRgb(this.getTeraType())); this.updateInfo(true); } initShinySparkle(): void { const keySuffix = this.variant ? `_${this.variant + 1}` : ""; const key = `shiny${keySuffix}`; const shinySparkle = this.scene.addFieldSprite(0, 0, key); shinySparkle.setVisible(false); shinySparkle.setOrigin(0.5, 1); const frameNames = this.scene.anims.generateFrameNames(key, { suffix: ".png", end: 34 }); if (!(this.scene.anims.exists(`sparkle${keySuffix}`))) { this.scene.anims.create({ key: `sparkle${keySuffix}`, frames: frameNames, frameRate: 32, showOnStart: true, hideOnComplete: true, }); } this.add(shinySparkle); this.shinySparkle = shinySparkle; } /** * Attempts to animate a given {@linkcode Phaser.GameObjects.Sprite} * @see {@linkcode Phaser.GameObjects.Sprite.play} * @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate * @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint * @param animConfig {@linkcode String} to pass to {@linkcode Phaser.GameObjects.Sprite.play} * @returns true if the sprite was able to be animated */ tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, key: string): boolean { // Catch errors when trying to play an animation that doesn't exist try { sprite.play(key); tintSprite.play(key); } catch (error: unknown) { console.error(`Couldn't play animation for '${key}'!\nIs the image for this Pokemon missing?\n`, error); return false; } return true; } playAnim(): void { this.tryPlaySprite(this.getSprite(), this.getTintSprite(), this.getBattleSpriteKey()); } getFieldPositionOffset(): [ number, number ] { switch (this.fieldPosition) { case FieldPosition.CENTER: return [ 0, 0 ]; case FieldPosition.LEFT: return [ -32, -8 ]; case FieldPosition.RIGHT: return [ 32, 0 ]; } } setFieldPosition(fieldPosition: FieldPosition, duration?: integer): Promise { return new Promise(resolve => { if (fieldPosition === this.fieldPosition) { resolve(); return; } const initialOffset = this.getFieldPositionOffset(); this.fieldPosition = fieldPosition; this.battleInfo.setMini(fieldPosition !== FieldPosition.CENTER); this.battleInfo.setOffset(fieldPosition === FieldPosition.RIGHT); const newOffset = this.getFieldPositionOffset(); const relX = newOffset[0] - initialOffset[0]; const relY = newOffset[1] - initialOffset[1]; if (duration) { this.scene.tweens.add({ targets: this, x: (_target, _key, value: number) => value + relX, y: (_target, _key, value: number) => value + relY, duration: duration, ease: "Sine.easeOut", onComplete: () => resolve() }); } else { this.x += relX; this.y += relY; } }); } getStat(stat: Stat): integer { return this.stats[stat]; } getBattleStat(stat: Stat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer { if (stat === Stat.HP) { return this.getStat(Stat.HP); } const battleStat = (stat - 1) as BattleStat; const statLevel = new Utils.IntegerHolder(this.summonData.battleStats[battleStat]); if (opponent) { if (isCritical) { switch (stat) { case Stat.ATK: case Stat.SPATK: statLevel.value = Math.max(statLevel.value, 0); break; case Stat.DEF: case Stat.SPDEF: statLevel.value = Math.min(statLevel.value, 0); break; } } applyAbAttrs(IgnoreOpponentStatChangesAbAttr, opponent, null, statLevel); if (move) { applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel); } } if (this.isPlayer()) { this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), battleStat as integer as TempBattleStat, statLevel); } const statValue = new Utils.NumberHolder(this.getStat(stat)); this.scene.applyModifiers(StatBoosterModifier, this.isPlayer(), this, stat, statValue); const fieldApplied = new Utils.BooleanHolder(false); for (const pokemon of this.scene.getField(true)) { applyFieldBattleStatMultiplierAbAttrs(FieldMultiplyBattleStatAbAttr, pokemon, stat, statValue, this, fieldApplied); if (fieldApplied.value) { break; } } applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, battleStat, statValue); let ret = statValue.value * (Math.max(2, 2 + statLevel.value) / Math.max(2, 2 - statLevel.value)); switch (stat) { case Stat.ATK: if (this.getTag(BattlerTagType.SLOW_START)) { ret >>= 1; } break; case Stat.DEF: if (this.isOfType(Type.ICE) && this.scene.arena.weather?.weatherType === WeatherType.SNOW) { ret *= 1.5; } break; case Stat.SPATK: break; case Stat.SPDEF: if (this.isOfType(Type.ROCK) && this.scene.arena.weather?.weatherType === WeatherType.SANDSTORM) { ret *= 1.5; } break; case Stat.SPD: // Check both the player and enemy to see if Tailwind should be multiplying the speed of the Pokemon if ((this.isPlayer() && this.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER)) || (!this.isPlayer() && this.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.ENEMY))) { ret *= 2; } if (this.getTag(BattlerTagType.SLOW_START)) { ret >>= 1; } if (this.status && this.status.effect === StatusEffect.PARALYSIS) { ret >>= 1; } break; } const highestStatBoost = this.findTag(t => t instanceof HighestStatBoostTag && (t as HighestStatBoostTag).stat === stat) as HighestStatBoostTag; if (highestStatBoost) { ret *= highestStatBoost.multiplier; } return Math.floor(ret); } calculateStats(): void { if (!this.stats) { this.stats = [ 0, 0, 0, 0, 0, 0 ]; } const baseStats = this.getSpeciesForm().baseStats.slice(0); if (this.fusionSpecies) { const fusionBaseStats = this.getFusionSpeciesForm().baseStats; for (let s = 0; s < this.stats.length; s++) { baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2); } } else if (this.scene.gameMode.isSplicedOnly) { for (let s = 0; s < this.stats.length; s++) { baseStats[s] = Math.ceil(baseStats[s] / 2); } } this.scene.applyModifiers(PokemonBaseStatModifier, this.isPlayer(), this, baseStats); const stats = Utils.getEnumValues(Stat); for (const s of stats) { const isHp = s === Stat.HP; const baseStat = baseStats[s]; let value = Math.floor(((2 * baseStat + this.ivs[s]) * this.level) * 0.01); if (isHp) { value = value + this.level + 10; if (this.hasAbility(Abilities.WONDER_GUARD, false, true)) { value = 1; } if (this.hp > value || this.hp === undefined) { this.hp = value; } else if (this.hp) { const lastMaxHp = this.getMaxHp(); if (lastMaxHp && value > lastMaxHp) { this.hp += value - lastMaxHp; } } } else { value += 5; const natureStatMultiplier = new Utils.NumberHolder(getNatureStatMultiplier(this.getNature(), s)); this.scene.applyModifier(PokemonNatureWeightModifier, this.isPlayer(), this, natureStatMultiplier); if (natureStatMultiplier.value !== 1) { value = Math.max(Math[natureStatMultiplier.value > 1 ? "ceil" : "floor"](value * natureStatMultiplier.value), 1); } } this.stats[s] = value; } } getNature(): Nature { return this.natureOverride !== -1 ? this.natureOverride : this.nature; } setNature(nature: Nature): void { this.nature = nature; this.calculateStats(); } generateNature(naturePool?: Nature[]): void { if (naturePool === undefined) { naturePool = Utils.getEnumValues(Nature); } const nature = naturePool[Utils.randSeedInt(naturePool.length)]; this.setNature(nature); } isFullHp(): boolean { return this.hp >= this.getMaxHp(); } getMaxHp(): integer { return this.getStat(Stat.HP); } getInverseHp(): integer { return this.getMaxHp() - this.hp; } getHpRatio(precise: boolean = false): number { return precise ? this.hp / this.getMaxHp() : Math.round((this.hp / this.getMaxHp()) * 100) / 100; } generateGender(): void { if (this.species.malePercent === null) { this.gender = Gender.GENDERLESS; } else { const genderChance = (this.id % 256) * 0.390625; if (genderChance < this.species.malePercent) { this.gender = Gender.MALE; } else { this.gender = Gender.FEMALE; } } } getGender(ignoreOverride?: boolean): Gender { if (!ignoreOverride && this.summonData?.gender !== undefined) { return this.summonData.gender; } return this.gender; } getFusionGender(ignoreOverride?: boolean): Gender { if (!ignoreOverride && this.summonData?.fusionGender !== undefined) { return this.summonData.fusionGender; } return this.fusionGender; } isShiny(): boolean { return this.shiny || (this.isFusion() && this.fusionShiny); } getVariant(): Variant { return !this.isFusion() ? this.variant : Math.max(this.variant, this.fusionVariant) as Variant; } getLuck(): integer { return this.luck + (this.isFusion() ? this.fusionLuck : 0); } isFusion(): boolean { return !!this.fusionSpecies; } abstract isBoss(): boolean; getMoveset(ignoreOverride?: boolean): PokemonMove[] { const ret = !ignoreOverride && this.summonData?.moveset ? this.summonData.moveset : this.moveset; // Overrides moveset based on arrays specified in overrides.ts const overrideArray: Array = this.isPlayer() ? Overrides.MOVESET_OVERRIDE : Overrides.OPP_MOVESET_OVERRIDE; if (overrideArray.length > 0) { overrideArray.forEach((move: Moves, index: number) => { const ppUsed = this.moveset[index]?.ppUsed || 0; this.moveset[index] = new PokemonMove(move, Math.min(ppUsed, allMoves[move].pp)); }); } return ret; } /** * All moves that could be relearned by this pokemon at this point. Used for memory mushrooms. * @returns {Moves[]} The valid moves */ getLearnableLevelMoves(): Moves[] { return this.getLevelMoves(1, true, false, true).map(lm => lm[1]).filter(lm => !this.moveset.filter(m => m.moveId === lm).length).filter((move: Moves, i: integer, array: Moves[]) => array.indexOf(move) === i); } /** * 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 * @returns array of {@linkcode Type} */ getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride?: boolean): Type[] { const types = []; if (includeTeraType) { const teraType = this.getTeraType(); if (teraType !== Type.UNKNOWN) { types.push(teraType); } } if (!types.length || !includeTeraType) { if (!ignoreOverride && this.summonData?.types) { this.summonData.types.forEach(t => types.push(t)); } else { const speciesForm = this.getSpeciesForm(ignoreOverride); types.push(speciesForm.type1); const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride); if (fusionSpeciesForm) { if (fusionSpeciesForm.type2 !== null && fusionSpeciesForm.type2 !== speciesForm.type1) { types.push(fusionSpeciesForm.type2); } else if (fusionSpeciesForm.type1 !== speciesForm.type1) { types.push(fusionSpeciesForm.type1); } } if (types.length === 1 && speciesForm.type2 !== null) { types.push(speciesForm.type2); } } } // this.scene potentially can be undefined for a fainted pokemon in doubles // use optional chaining to avoid runtime errors if (forDefend && (this.getTag(GroundedTag) || this.scene?.arena.getTag(ArenaTagType.GRAVITY))) { const flyingIndex = types.indexOf(Type.FLYING); if (flyingIndex > -1) { types.splice(flyingIndex, 1); } } if (!types.length) { // become UNKNOWN if no types are present types.push(Type.UNKNOWN); } if (types.length > 1 && types.includes(Type.UNKNOWN)) { // remove UNKNOWN if other types are present const index = types.indexOf(Type.UNKNOWN); if (index !== -1) { types.splice(index, 1); } } return types; } isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride?: boolean): 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 * 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 */ getAbility(ignoreOverride?: boolean): Ability { if (!ignoreOverride && this.summonData?.ability) { return allAbilities[this.summonData.ability]; } if (Overrides.ABILITY_OVERRIDE && this.isPlayer()) { return allAbilities[Overrides.ABILITY_OVERRIDE]; } if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; } if (this.isFusion()) { return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; } let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex); if (abilityId === Abilities.NONE) { abilityId = this.species.ability1; } return allAbilities[abilityId]; } /** * 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 * 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 */ getPassiveAbility(): Ability { if (Overrides.PASSIVE_ABILITY_OVERRIDE && this.isPlayer()) { return allAbilities[Overrides.PASSIVE_ABILITY_OVERRIDE]; } if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; } let starterSpeciesId = this.species.speciesId; while (pokemonPrevolutions.hasOwnProperty(starterSpeciesId)) { starterSpeciesId = pokemonPrevolutions[starterSpeciesId]; } return allAbilities[starterPassiveAbilities[starterSpeciesId]]; } /** * 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 {AbAttr[]} A list of all the ability attributes on this ability. */ getAbilityAttrs(attrType: { new(...args: any[]): AbAttr }, canApply: boolean = true, ignoreOverride?: boolean): AbAttr[] { const abilityAttrs: AbAttr[] = []; if (!canApply || this.canApplyAbility()) { abilityAttrs.push(...this.getAbility(ignoreOverride).getAttrs(attrType)); } if (!canApply || this.canApplyAbility(true)) { abilityAttrs.push(...this.getPassiveAbility().getAttrs(attrType)); } return abilityAttrs; } /** * Checks if a pokemon has a passive either from: * - bought with starter candy * - set by override * - is a boss pokemon * @returns whether or not a pokemon should have a passive */ 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())) { return true; } return this.passive || this.isBoss(); } /** * 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 */ canApplyAbility(passive: boolean = false): boolean { if (passive && !this.hasPassive()) { return false; } const ability = (!passive ? this.getAbility() : this.getPassiveAbility()); if (this.isFusion() && ability.hasAttr(NoFusionAbilityAbAttr)) { return false; } if (this.scene?.arena.ignoreAbilities && ability.isIgnorable) { return false; } if (this.summonData?.abilitySuppressed && !ability.hasAttr(UnsuppressableAbilityAbAttr)) { return false; } if (this.isOnField() && !ability.hasAttr(SuppressFieldAbilitiesAbAttr)) { const suppressed = new Utils.BooleanHolder(false); this.scene.getField(true).filter(p => p !== this).map(p => { if (p.getAbility().hasAttr(SuppressFieldAbilitiesAbAttr) && p.canApplyAbility()) { p.getAbility().getAttrs(SuppressFieldAbilitiesAbAttr).map(a => a.apply(this, false, suppressed, [ability])); } if (p.getPassiveAbility().hasAttr(SuppressFieldAbilitiesAbAttr) && p.canApplyAbility(true)) { p.getPassiveAbility().getAttrs(SuppressFieldAbilitiesAbAttr).map(a => a.apply(this, true, suppressed, [ability])); } }); if (suppressed.value) { return false; } } return (this.hp || ability.isBypassFaint) && !ability.conditions.find(condition => !condition(this)); } /** * Checks whether a pokemon has the specified ability and it's in effect. Accounts for all the various * effects which can affect whether an ability will be present or in effect, and both passive and * non-passive. This is the primary way to check whether a pokemon has a particular ability. * @param {Abilities} ability The ability to check for * @param {boolean} canApply If false, it doesn't check whether the abiltiy is currently active * @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 { if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).id === ability) { return true; } if (this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().id === ability) { return true; } return false; } /** * Checks whether a pokemon has an ability with the specified attribute and it's in effect. * Accounts for all the various effects which can affect whether an ability will be present or * in effect, and both passive and non-passive. This is one of the two primary ways to check * whether a pokemon has a particular ability. * @param {AbAttr} attrType The ability attribute to check for * @param {boolean} canApply If false, it doesn't check whether the ability is currently active * @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 { if ((!canApply || this.canApplyAbility()) && this.getAbility(ignoreOverride).hasAttr(attrType)) { return true; } if (this.hasPassive() && (!canApply || this.canApplyAbility(true)) && this.getPassiveAbility().hasAttr(attrType)) { return true; } return false; } getWeight(): number { const weight = new Utils.NumberHolder(this.species.weight); // This will trigger the ability overlay so only call this function when necessary applyAbAttrs(WeightMultiplierAbAttr, this, null, weight); return weight.value; } /** * Gets the tera-formed type of the pokemon, or UNKNOWN if not present * @returns the {@linkcode Type} */ 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 && m.pokemonId === this.id && !!m.getBattlesLeft(), this.isPlayer()) as TerastallizeModifier; // return teraType if (teraModifier) { return teraModifier.teraType; } } // if scene is undefined, or if teraModifier is considered false, then return unknown type return Type.UNKNOWN; } isTerastallized(): boolean { return this.getTeraType() !== Type.UNKNOWN; } isGrounded(): boolean { return !!this.getTag(GroundedTag) || (!this.isOfType(Type.FLYING, true, true) && !this.hasAbility(Abilities.LEVITATE) && !this.getTag(BattlerTagType.MAGNET_RISEN) && !this.getTag(SemiInvulnerableTag)); } /** * Calculates the effectiveness of a move against the Pokémon. * * @param source - The Pokémon using the move. * @param move - The move being used. * @returns The type damage multiplier or undefined if it's a status move */ getMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier | undefined { if (move.getMove().category === MoveCategory.STATUS) { return undefined; } return this.getAttackMoveEffectiveness(source, move, !this.battleData?.abilityRevealed); } /** * Calculates the effectiveness of an attack move against the Pokémon. * * @param source - The attacking Pokémon. * @param pokemonMove - The move being used by the attacking Pokémon. * @param ignoreAbility - Whether to check for abilities that might affect type effectiveness or immunity. * @returns The type damage multiplier, indicating the effectiveness of the move */ getAttackMoveEffectiveness(source: Pokemon, pokemonMove: PokemonMove, ignoreAbility: boolean = false): TypeDamageMultiplier { const move = pokemonMove.getMove(); const typeless = move.hasAttr(TypelessAttr); const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move, source)); const cancelled = new Utils.BooleanHolder(false); applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier); if (!typeless && !ignoreAbility) { applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier, true); } if (!cancelled.value && !ignoreAbility) { applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, typeMultiplier, true); } return (!cancelled.value ? Number(typeMultiplier.value) : 0) as TypeDamageMultiplier; } /** * Calculates the type effectiveness multiplier for an attack type * @param moveOrType The move being used, or a type if the move is unknown * @param source the Pokemon using the move * @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks) * @param simulated tag to only apply the strong winds effect message when the move is used * @returns a multiplier for the type effectiveness */ getAttackTypeEffectiveness(moveOrType: Move | Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier { const move = (moveOrType instanceof Move) ? moveOrType : undefined; const moveType = (moveOrType instanceof Move) ? move.type : moveOrType; if (moveType === Type.STELLAR) { return this.isTerastallized() ? 2 : 1; } const types = this.getTypes(true, true); let multiplier = types.map(defType => { if (source) { const ignoreImmunity = new Utils.BooleanHolder(false); if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) { applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, moveType, defType); } if (ignoreImmunity.value) { return 1; } } return getTypeDamageMultiplier(moveType, defType); }).reduce((acc, cur) => acc * cur, 1) as TypeDamageMultiplier; // Handle strong winds lowering effectiveness of types super effective against pure flying if (!ignoreStrongWinds && this.scene.arena.weather?.weatherType === WeatherType.STRONG_WINDS && !this.scene.arena.weather.isEffectSuppressed(this.scene) && this.isOfType(Type.FLYING) && getTypeDamageMultiplier(moveType, Type.FLYING) === 2) { multiplier /= 2; if (!simulated) { this.scene.queueMessage(i18next.t("weather:strongWindsEffectMessage")); } } const immuneTags = this.findTags(tag => tag instanceof TypeImmuneTag && tag.immuneType === moveType); for (const tag of immuneTags) { if (move && !move.getAttrs(HitsTagAttr).some(attr => attr.tagType === tag.tagType)) { multiplier = 0; break; } } return multiplier; } getMatchupScore(pokemon: Pokemon): number { const types = this.getTypes(true); const enemyTypes = pokemon.getTypes(true, true); const outspeed = (this.isActive(true) ? this.getBattleStat(Stat.SPD, pokemon) : this.getStat(Stat.SPD)) <= pokemon.getBattleStat(Stat.SPD, this); let atkScore = pokemon.getAttackTypeEffectiveness(types[0], this) * (outspeed ? 1.25 : 1); let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], pokemon), 0.25); if (types.length > 1) { atkScore *= pokemon.getAttackTypeEffectiveness(types[1], this); } if (enemyTypes.length > 1) { defScore *= (1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], pokemon), 0.25)); } let hpDiffRatio = this.getHpRatio() + (1 - pokemon.getHpRatio()); if (outspeed) { hpDiffRatio = Math.min(hpDiffRatio * 1.5, 1); } return (atkScore + defScore) * hpDiffRatio; } getEvolution(): SpeciesFormEvolution { if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) { const evolutions = pokemonEvolutions[this.species.speciesId]; for (const e of evolutions) { if (!e.item && this.level >= e.level && (!e.preFormKey || this.getFormKey() === e.preFormKey)) { if (e.condition === null || (e.condition as SpeciesEvolutionCondition).predicate(this)) { return e; } } } } if (this.isFusion() && pokemonEvolutions.hasOwnProperty(this.fusionSpecies.speciesId)) { const fusionEvolutions = pokemonEvolutions[this.fusionSpecies.speciesId].map(e => new FusionSpeciesFormEvolution(this.species.speciesId, e)); for (const fe of fusionEvolutions) { if (!fe.item && this.level >= fe.level && (!fe.preFormKey || this.getFusionFormKey() === fe.preFormKey)) { if (fe.condition === null || (fe.condition as SpeciesEvolutionCondition).predicate(this)) { return fe; } } } } return null; } /** * Gets all level up moves in a given range for a particular pokemon. * @param {integer} startingLevel Don't include moves below this level * @param {boolean} includeEvolutionMoves Whether to include evolution moves * @param {boolean} simulateEvolutionChain Whether to include moves from prior evolutions * @param {boolean} includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves * @returns {LevelMoves} A list of moves and the levels they can be learned at */ getLevelMoves(startingLevel?: integer, includeEvolutionMoves: boolean = false, simulateEvolutionChain: boolean = false, includeRelearnerMoves: boolean = false): LevelMoves { const ret: LevelMoves = []; let levelMoves: LevelMoves = []; if (!startingLevel) { startingLevel = this.level; } if (simulateEvolutionChain) { const evolutionChain = this.species.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer()); for (let e = 0; e < evolutionChain.length; e++) { // TODO: Might need to pass specific form index in simulated evolution chain const speciesLevelMoves = getPokemonSpeciesForm(evolutionChain[e][0] as Species, this.formIndex).getLevelMoves(); if (includeRelearnerMoves) { levelMoves.push(...speciesLevelMoves); } else { levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === evolutionChain.length - 1 || lm[0] <= evolutionChain[e + 1][1])))); } } } else { levelMoves = this.getSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0); } if (this.fusionSpecies) { if (simulateEvolutionChain) { const fusionEvolutionChain = this.fusionSpecies.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer()); for (let e = 0; e < fusionEvolutionChain.length; e++) { // TODO: Might need to pass specific form index in simulated evolution chain const speciesLevelMoves = getPokemonSpeciesForm(fusionEvolutionChain[e][0] as Species, this.fusionFormIndex).getLevelMoves(); if (includeRelearnerMoves) { levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || lm[0] !== 0)); } else { levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === fusionEvolutionChain.length - 1 || lm[0] <= fusionEvolutionChain[e + 1][1])))); } } } else { levelMoves.push(...this.getFusionSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0)); } } levelMoves.sort((lma: [integer, integer], lmb: [integer, integer]) => lma[0] > lmb[0] ? 1 : lma[0] < lmb[0] ? -1 : 0); const uniqueMoves: Moves[] = []; levelMoves = levelMoves.filter(lm => { if (uniqueMoves.find(m => m === lm[1])) { return false; } uniqueMoves.push(lm[1]); return true; }); if (levelMoves) { for (const lm of levelMoves) { const level = lm[0]; if (!includeRelearnerMoves && ((level > 0 && level < startingLevel) || (!includeEvolutionMoves && level === 0) || level < 0)) { continue; } else if (level > this.level) { break; } ret.push(lm); } } return ret; } setMove(moveIndex: integer, moveId: Moves): void { const move = moveId ? new PokemonMove(moveId) : null; this.moveset[moveIndex] = move; if (this.summonData?.moveset) { this.summonData.moveset[moveIndex] = move; } } /** * Function that tries to set a Pokemon shiny based on the trainer's trainer ID and secret ID * Endless Pokemon in the end biome are unable to be set to shiny * * The exact mechanic is that it calculates E as the XOR of the player's trainer ID and secret ID * F is calculated as the XOR of the first 16 bits of the Pokemon's ID with the last 16 bits * The XOR of E and F are then compared to the thresholdOverride (default case 32) to see whether or not to generate a shiny * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance * @returns true if the Pokemon has been set as a shiny, false otherwise */ trySetShiny(thresholdOverride?: integer): boolean { // Shiny Pokemon should not spawn in the end biome in endless if (this.scene.gameMode.isEndless && this.scene.arena.biomeType === Biome.END) { return false; } const rand1 = Utils.binToDec(Utils.decToBin(this.id).substring(0, 16)); const rand2 = Utils.binToDec(Utils.decToBin(this.id).substring(16, 32)); const E = this.scene.gameData.trainerId ^ this.scene.gameData.secretId; const F = rand1 ^ rand2; const shinyThreshold = new Utils.IntegerHolder(32); if (thresholdOverride === undefined) { if (this.scene.eventManager.isEventActive()) { shinyThreshold.value *= this.scene.eventManager.getShinyMultiplier(); } if (!this.hasTrainer()) { this.scene.applyModifiers(ShinyRateBoosterModifier, true, shinyThreshold); } } else { shinyThreshold.value = thresholdOverride; } this.shiny = (E ^ F) < shinyThreshold.value; if ((E ^ F) < 32) { console.log("REAL SHINY!!"); } if (this.shiny) { this.initShinySparkle(); } return this.shiny; } /** * 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 */ generateVariant(): Variant { const formIndex: number = this.formIndex; let variantDataIndex: string | number = this.species.speciesId; if (this.species.forms.length > 0) { const formKey = this.species.forms[formIndex]?.formKey; if (formKey) { variantDataIndex = `${variantDataIndex}-${formKey}`; } } // Checks if there is no variant data for both the index or index with form if (!this.shiny || (!variantData.hasOwnProperty(variantDataIndex) && !variantData.hasOwnProperty(this.species.speciesId))) { return 0; } const rand = Utils.randSeedInt(10); if (rand >= 4) { return 0; // 6/10 } else if (rand >= 1) { return 1; // 3/10 } else { return 2; // 1/10 } } generateFusionSpecies(forStarter?: boolean): void { const hiddenAbilityChance = new Utils.IntegerHolder(256); if (!this.hasTrainer()) { this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); } const hasHiddenAbility = !Utils.randSeedInt(hiddenAbilityChance.value); const randAbilityIndex = Utils.randSeedInt(2); const filter = !forStarter ? this.species.getCompatibleFusionSpeciesFilter() : species => { return pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && !species.pseudoLegendary && !species.legendary && !species.mythical && !species.isTrainerForbidden() && species.speciesId !== this.species.speciesId; }; this.fusionSpecies = this.scene.randomSpecies(this.scene.currentBattle?.waveIndex || 0, this.level, false, filter, true); this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? this.fusionSpecies.ability2 ? 2 : 1 : this.fusionSpecies.ability2 ? randAbilityIndex : 0); this.fusionShiny = this.shiny; this.fusionVariant = this.variant; if (this.fusionSpecies.malePercent === null) { this.fusionGender = Gender.GENDERLESS; } else { const genderChance = (this.id % 256) * 0.390625; if (genderChance < this.fusionSpecies.malePercent) { this.fusionGender = Gender.MALE; } else { this.fusionGender = Gender.FEMALE; } } this.fusionFormIndex = this.scene.getSpeciesFormIndex(this.fusionSpecies, this.fusionGender, this.getNature(), true); this.fusionLuck = this.luck; this.generateName(); } clearFusionSpecies(): void { this.fusionSpecies = undefined; this.fusionFormIndex = 0; this.fusionAbilityIndex = 0; this.fusionShiny = false; this.fusionVariant = 0; this.fusionGender = 0; this.fusionLuck = 0; this.generateName(); this.calculateStats(); } generateAndPopulateMoveset(): void { this.moveset = []; let movePool: [Moves, number][] = []; const allLevelMoves = this.getLevelMoves(1, true, true); if (!allLevelMoves) { console.log(this.species.speciesId, "ERROR"); return; } for (let m = 0; m < allLevelMoves.length; m++) { const levelMove = allLevelMoves[m]; if (this.level < levelMove[0]) { break; } let weight = levelMove[0]; if (weight === 0) { // Evo Moves 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 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]); } } if (this.hasTrainer()) { const tms = Object.keys(tmSpecies); for (const tm of tms) { const moveId = parseInt(tm) as Moves; let compatible = false; for (const p of tmSpecies[tm]) { if (Array.isArray(p)) { if (p[0] === this.species.speciesId || (this.fusionSpecies && p[0] === this.fusionSpecies.speciesId) && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) { compatible = true; break; } } else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) { compatible = true; break; } } if (compatible && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { if (tmPoolTiers[moveId] === ModifierTier.COMMON && this.level >= 15) { movePool.push([moveId, 4]); } else if (tmPoolTiers[moveId] === ModifierTier.GREAT && this.level >= 30) { movePool.push([moveId, 8]); } else if (tmPoolTiers[moveId] === ModifierTier.ULTRA && this.level >= 50) { movePool.push([moveId, 14]); } } } if (this.level >= 60) { // No egg moves below 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)")) { movePool.push([moveId, 40]); } } 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 movePool.push([moveId, 30]); } if (this.fusionSpecies) { for (let i = 0; i < 3; i++) { const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][i]; if (!movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(" (N)")) { movePool.push([moveId, 40]); } } 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 movePool.push([moveId, 30]); } } } } if (this.isBoss()) { // Bosses never get self ko moves movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttr)); } movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(SacrificialAttrOnHit)); if (this.hasTrainer()) { // Trainers never get OHKO moves movePool = movePool.filter(m => !allMoves[m[0]].hasAttr(OneHitKOAttr)); // Half the weight of self KO moves movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttr) ? 0.5 : 1)]); movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(SacrificialAttrOnHit) ? 0.5 : 1)]); // Trainers get a weight bump to stat buffing moves movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].getAttrs(StatChangeAttr).some(a => a.levels > 1 && a.selfTarget) ? 1.25 : 1)]); // Trainers get a weight decrease to multiturn moves movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].hasAttr(ChargeAttr) || !!allMoves[m[0]].hasAttr(RechargeAttr) ? 0.7 : 1)]); } // Weight towards higher power moves, by reducing the power of moves below the highest power. // Caps max power at 90 to avoid something like hyper beam ruining the stats. // This is a pretty soft weighting factor, although it is scaled with the weight multiplier. const maxPower = Math.min(movePool.reduce((v, m) => Math.max(allMoves[m[0]].power, v), 40), 90); movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power/maxPower, 1), 0.5))]); // Weight damaging moves against the lower stat const worseCategory: MoveCategory = this.stats[Stat.ATK] > this.stats[Stat.SPATK] ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL; const statRatio = worseCategory === MoveCategory.PHYSICAL ? this.stats[Stat.ATK]/this.stats[Stat.SPATK] : this.stats[Stat.SPATK]/this.stats[Stat.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. if (this.hasTrainer()) { weightMultiplier += 0.7; } if (this.isBoss()) { weightMultiplier += 0.4; } 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 const stabMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS && this.isOfType(allMoves[m[0]].type)); if (stabMovePool.length) { const totalWeight = stabMovePool.reduce((v, m) => v + m[1], 0); let rand = Utils.randSeedInt(totalWeight); let index = 0; while (rand > stabMovePool[index][1]) { rand -= stabMovePool[index++][1]; } this.moveset.push(new PokemonMove(stabMovePool[index][0], 0, 0)); } } else { // Normal wild pokemon just force a random damaging move const attackMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS); if (attackMovePool.length) { const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0); let rand = Utils.randSeedInt(totalWeight); let index = 0; while (rand > attackMovePool[index][1]) { rand -= attackMovePool[index++][1]; } this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0)); } } while (baseWeights.length > this.moveset.length && this.moveset.length < 4) { if (this.hasTrainer()) { // 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]]); } 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); let rand = Utils.randSeedInt(totalWeight); let index = 0; while (rand > movePool[index][1]) { rand -= movePool[index++][1]; } this.moveset.push(new PokemonMove(movePool[index][0], 0, 0)); } this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger); } trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean { const move = this.getMoveset().length > moveIndex ? this.getMoveset()[moveIndex] : null; return move?.isUsable(this, ignorePp); } showInfo(): void { if (!this.battleInfo.visible) { const otherBattleInfo = this.scene.fieldUI.getAll().slice(0, 4).filter(ui => ui instanceof BattleInfo && ((ui as BattleInfo) instanceof PlayerBattleInfo) === this.isPlayer()).find(() => true); if (!otherBattleInfo || !this.getFieldIndex()) { this.scene.fieldUI.sendToBack(this.battleInfo); this.scene.sendTextToBack(); // Push the top right text objects behind everything else } else { this.scene.fieldUI.moveAbove(this.battleInfo, otherBattleInfo); } this.battleInfo.setX(this.battleInfo.x + (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198)); this.battleInfo.setVisible(true); if (this.isPlayer()) { this.battleInfo.expMaskRect.x += 150; } this.scene.tweens.add({ targets: [ this.battleInfo, this.battleInfo.expMaskRect ], x: this.isPlayer() ? "-=150" : `+=${!this.isBoss() ? 150 : 246}`, duration: 1000, ease: "Cubic.easeOut" }); } } hideInfo(): Promise { return new Promise(resolve => { if (this.battleInfo.visible) { this.scene.tweens.add({ targets: [ this.battleInfo, this.battleInfo.expMaskRect ], x: this.isPlayer() ? "+=150" : `-=${!this.isBoss() ? 150 : 246}`, duration: 500, ease: "Cubic.easeIn", onComplete: () => { if (this.isPlayer()) { this.battleInfo.expMaskRect.x -= 150; } this.battleInfo.setVisible(false); this.battleInfo.setX(this.battleInfo.x - (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198)); resolve(); } }); } else { resolve(); } }); } updateInfo(instant?: boolean): Promise { return this.battleInfo.updateInfo(this, instant); } /** * Show or hide the type effectiveness multiplier window * Passing undefined will hide the window */ updateEffectiveness(effectiveness?: string) { this.battleInfo.updateEffectiveness(effectiveness); } toggleStats(visible: boolean): void { this.battleInfo.toggleStats(visible); } toggleFlyout(visible: boolean): void { this.battleInfo.toggleFlyout(visible); } addExp(exp: integer) { const maxExpLevel = this.scene.getMaxExpLevel(); const initialExp = this.exp; this.exp += exp; while (this.level < maxExpLevel && this.exp >= getLevelTotalExp(this.level + 1, this.species.growthRate)) { this.level++; } if (this.level >= maxExpLevel) { console.log(initialExp, this.exp, getLevelTotalExp(this.level, this.species.growthRate)); this.exp = Math.max(getLevelTotalExp(this.level, this.species.growthRate), initialExp); } this.levelExp = this.exp - getLevelTotalExp(this.level, this.species.growthRate); } getOpponent(targetIndex: integer): Pokemon { const ret = this.getOpponents()[targetIndex]; if (ret.summonData) { return ret; } return null; } getOpponents(): Pokemon[] { return ((this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField()) as Pokemon[]).filter(p => p.isActive()); } getOpponentDescriptor(): string { const opponents = this.getOpponents(); if (opponents.length === 1) { return opponents[0].name; } return this.isPlayer() ? i18next.t("arenaTag:opposingTeam") : i18next.t("arenaTag:yourTeam"); } getAlly(): Pokemon { return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1]; } /** * Calculates the accuracy multiplier of the user against a target. * * This method considers various factors such as the user's accuracy level, the target's evasion level, * abilities, and modifiers to compute the final accuracy multiplier. * * @param target {@linkcode Pokemon} - The target Pokémon against which the move is used. * @param sourceMove {@linkcode Move} - The move being used by the user. * @returns The calculated accuracy multiplier. */ getAccuracyMultiplier(target: Pokemon, sourceMove: Move): number { const isOhko = sourceMove.hasAttr(OneHitKOAccuracyAttr); if (isOhko) { return 1; } const userAccuracyLevel = new Utils.IntegerHolder(this.summonData.battleStats[BattleStat.ACC]); const targetEvasionLevel = new Utils.IntegerHolder(target.summonData.battleStats[BattleStat.EVA]); applyAbAttrs(IgnoreOpponentStatChangesAbAttr, target, null, userAccuracyLevel); applyAbAttrs(IgnoreOpponentStatChangesAbAttr, this, null, targetEvasionLevel); applyAbAttrs(IgnoreOpponentEvasionAbAttr, this, null, targetEvasionLevel); applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, target, sourceMove, targetEvasionLevel); this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), TempBattleStat.ACC, userAccuracyLevel); const accuracyMultiplier = new Utils.NumberHolder(1); if (userAccuracyLevel.value !== targetEvasionLevel.value) { accuracyMultiplier.value = userAccuracyLevel.value > targetEvasionLevel.value ? (3 + Math.min(userAccuracyLevel.value - targetEvasionLevel.value, 6)) / 3 : 3 / (3 + Math.min(targetEvasionLevel.value - userAccuracyLevel.value, 6)); } applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, BattleStat.ACC, accuracyMultiplier, sourceMove); const evasionMultiplier = new Utils.NumberHolder(1); applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, target, BattleStat.EVA, evasionMultiplier); accuracyMultiplier.value /= evasionMultiplier.value; return accuracyMultiplier.value; } apply(source: Pokemon, move: Move): HitResult { let result: HitResult; const damage = new Utils.NumberHolder(0); const defendingSidePlayField = this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField(); const variableCategory = new Utils.IntegerHolder(move.category); applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, variableCategory); const moveCategory = variableCategory.value as MoveCategory; applyMoveAttrs(VariableMoveTypeAttr, source, this, move); const types = this.getTypes(true, true); const cancelled = new Utils.BooleanHolder(false); const typeless = move.hasAttr(TypelessAttr); const typeMultiplier = new Utils.NumberHolder(!typeless && (moveCategory !== MoveCategory.STATUS || move.getAttrs(StatusMoveTypeImmunityAttr).find(attr => types.includes(attr.immuneType))) ? this.getAttackTypeEffectiveness(move, source, false, false) : 1); applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier); if (typeless) { typeMultiplier.value = 1; } if (types.find(t => move.isTypeImmune(source, this, t))) { typeMultiplier.value = 0; } // Apply arena tags for conditional protection if (!move.checkFlag(MoveFlags.IGNORE_PROTECT, source, this) && !move.isAllyTarget()) { const defendingSide = this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; this.scene.arena.applyTagsForSide(ArenaTagType.QUICK_GUARD, defendingSide, cancelled, this, move.priority); this.scene.arena.applyTagsForSide(ArenaTagType.WIDE_GUARD, defendingSide, cancelled, this, move.moveTarget); this.scene.arena.applyTagsForSide(ArenaTagType.MAT_BLOCK, defendingSide, cancelled, this, move.category); this.scene.arena.applyTagsForSide(ArenaTagType.CRAFTY_SHIELD, defendingSide, cancelled, this, move.category, move.moveTarget); } switch (moveCategory) { case MoveCategory.PHYSICAL: case MoveCategory.SPECIAL: const isPhysical = moveCategory === MoveCategory.PHYSICAL; const power = move.calculateBattlePower(source, this); const sourceTeraType = source.getTeraType(); if (!typeless) { applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier); applyMoveAttrs(NeutralDamageAgainstFlyingTypeMultiplierAttr, source, this, move, typeMultiplier); } if (!cancelled.value) { applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, typeMultiplier); defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, move, cancelled, typeMultiplier)); } if (cancelled.value) { source.stopMultiHit(this); result = HitResult.NO_EFFECT; } else { const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag; if (typeBoost?.oneUse) { source.removeTag(typeBoost.tagType); } const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(move.type, source.isGrounded())); applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier); const glaiveRushModifier = new Utils.IntegerHolder(1); if (this.getTag(BattlerTagType.RECEIVE_DOUBLE_DAMAGE)) { glaiveRushModifier.value = 2; } let isCritical: boolean; const critOnly = new Utils.BooleanHolder(false); const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT); applyMoveAttrs(CritOnlyAttr, source, this, move, critOnly); applyAbAttrs(ConditionalCritAbAttr, source, null, critOnly, this, move); if (critOnly.value || critAlways) { isCritical = true; } else { const critLevel = new Utils.IntegerHolder(0); applyMoveAttrs(HighCritAttr, source, this, move, critLevel); this.scene.applyModifiers(CritBoosterModifier, source.isPlayer(), source, critLevel); this.scene.applyModifiers(TempBattleStatBoosterModifier, source.isPlayer(), TempBattleStat.CRIT, critLevel); const bonusCrit = new Utils.BooleanHolder(false); if (applyAbAttrs(BonusCritAbAttr, source, null, bonusCrit)) { if (bonusCrit.value) { critLevel.value += 1; } } if (source.getTag(BattlerTagType.CRIT_BOOST)) { critLevel.value += 2; } console.log(`crit stage: +${critLevel.value}`); const critChance = [24, 8, 2, 1][Math.max(0, Math.min(critLevel.value, 3))]; isCritical = !source.getTag(BattlerTagType.NO_CRIT) && (critChance === 1 || !this.scene.randBattleSeedInt(critChance)); if (Overrides.NEVER_CRIT_OVERRIDE) { isCritical = false; } } if (isCritical) { const blockCrit = new Utils.BooleanHolder(false); applyAbAttrs(BlockCritAbAttr, this, null, blockCrit); if (blockCrit.value) { isCritical = false; } } const sourceAtk = new Utils.IntegerHolder(source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this, null, isCritical)); const targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical)); const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1); applyAbAttrs(MultCritAbAttr, source, null, criticalMultiplier); const screenMultiplier = new Utils.NumberHolder(1); if (!isCritical) { this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, move.category, this.scene.currentBattle.double, screenMultiplier); } const isTypeImmune = (typeMultiplier.value * arenaAttackTypeMultiplier.value) === 0; const sourceTypes = source.getTypes(); const matchesSourceType = sourceTypes[0] === move.type || (sourceTypes.length > 1 && sourceTypes[1] === move.type); const stabMultiplier = new Utils.NumberHolder(1); if (sourceTeraType === Type.UNKNOWN && matchesSourceType) { stabMultiplier.value += 0.5; } else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === move.type) { stabMultiplier.value += 0.5; } applyAbAttrs(StabBoostAbAttr, source, null, stabMultiplier); if (sourceTeraType !== Type.UNKNOWN && matchesSourceType) { stabMultiplier.value = Math.min(stabMultiplier.value + 0.5, 2.25); } const targetCount = getMoveTargets(source, move.id).targets.length; const targetMultiplier = targetCount > 1 ? 0.75 : 1; // 25% damage debuff on multi-target hits (even if it's immune) applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk); applyMoveAttrs(VariableDefAttr, source, this, move, targetDef); const effectPhase = this.scene.getCurrentPhase(); let numTargets = 1; if (effectPhase instanceof MoveEffectPhase) { numTargets = effectPhase.getTargets().length; } const twoStrikeMultiplier = new Utils.NumberHolder(1); applyPreAttackAbAttrs(AddSecondStrikeAbAttr, source, this, move, numTargets, new Utils.IntegerHolder(0), twoStrikeMultiplier); if (!isTypeImmune) { const levelMultiplier = (2 * source.level / 5 + 2); const randomMultiplier = ((this.scene.randBattleSeedInt(16) + 85) / 100); damage.value = Math.ceil((((levelMultiplier * power * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * twoStrikeMultiplier.value * targetMultiplier * criticalMultiplier.value * glaiveRushModifier.value * randomMultiplier); if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) { if (!move.hasAttr(BypassBurnDamageReductionAttr)) { const burnDamageReductionCancelled = new Utils.BooleanHolder(false); applyAbAttrs(BypassBurnDamageReductionAbAttr, source, burnDamageReductionCancelled); if (!burnDamageReductionCancelled.value) { damage.value = Math.floor(damage.value / 2); } } } applyPreAttackAbAttrs(DamageBoostAbAttr, source, this, move, damage); /** * For each {@link HitsTagAttr} the move has, doubles the damage of the move if: * The target has a {@link BattlerTagType} that this move interacts with * AND * The move doubles damage when used against that tag */ move.getAttrs(HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => { if (this.getTag(hta.tagType)) { damage.value *= 2; } }); } if (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && move.type === Type.DRAGON) { damage.value = Math.floor(damage.value / 2); } const fixedDamage = new Utils.IntegerHolder(0); applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage); if (!isTypeImmune && fixedDamage.value) { damage.value = fixedDamage.value; isCritical = false; result = HitResult.EFFECTIVE; } if (!result) { if (!typeMultiplier.value) { result = move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT; } else { const isOneHitKo = new Utils.BooleanHolder(false); applyMoveAttrs(OneHitKOAttr, source, this, move, isOneHitKo); if (isOneHitKo.value) { result = HitResult.ONE_HIT_KO; isCritical = false; damage.value = this.hp; } else if (typeMultiplier.value >= 2) { result = HitResult.SUPER_EFFECTIVE; } else if (typeMultiplier.value >= 1) { result = HitResult.EFFECTIVE; } else { result = HitResult.NOT_VERY_EFFECTIVE; } } } const isOneHitKo = result === HitResult.ONE_HIT_KO; if (!fixedDamage.value && !isOneHitKo) { if (!source.isPlayer()) { this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage); } if (!this.isPlayer()) { this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage); } applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, damage); } // This attribute may modify damage arbitrarily, so be careful about changing its order of application. applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage); console.log("damage", damage.value, move.name, power, sourceAtk, targetDef); // In case of fatal damage, this tag would have gotten cleared before we could lapse it. const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); if (damage.value) { if (this.isFullHp()) { applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, damage); } else if (!this.isPlayer() && damage.value >= this.hp) { this.scene.applyModifiers(EnemyEndureChanceModifier, false, this); } /** * We explicitly require to ignore the faint phase here, as we want to show the messages * about the critical hit and the super effective/not very effective messages before the faint phase. */ damage.value = this.damageAndUpdate(damage.value, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true); this.turnData.damageTaken += damage.value; if (isCritical) { this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit")); } if (source.isPlayer()) { this.scene.validateAchvs(DamageAchv, damage); if (damage.value > this.scene.gameData.gameStats.highestDamage) { this.scene.gameData.gameStats.highestDamage = damage.value; } } source.turnData.damageDealt += damage.value; source.turnData.currDamageDealt = damage.value; this.battleData.hitCount++; const attackResult = { move: move.id, result: result as DamageResult, damage: damage.value, critical: isCritical, sourceId: source.id, attackingPosition: source.getBattlerIndex() }; this.turnData.attacksReceived.unshift(attackResult); if (source.isPlayer() && !this.isPlayer()) { this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, damage); } } // want to include is.Fainted() in case multi hit move ends early, still want to render message if (source.turnData.hitsLeft === 1 || this.isFainted()) { switch (result) { case HitResult.SUPER_EFFECTIVE: this.scene.queueMessage(i18next.t("battle:hitResultSuperEffective")); break; case HitResult.NOT_VERY_EFFECTIVE: this.scene.queueMessage(i18next.t("battle:hitResultNotVeryEffective")); break; case HitResult.NO_EFFECT: this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); break; case HitResult.IMMUNE: this.scene.queueMessage(`${this.name} is unaffected!`); break; case HitResult.ONE_HIT_KO: this.scene.queueMessage(i18next.t("battle:hitResultOneHitKO")); break; } } if (this.isFainted()) { // set splice index here, so future scene queues happen before FaintedPhase this.scene.setPhaseQueueSplice(); this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), isOneHitKo)); this.resetSummonData(); } if (damage) { const attacker = this.scene.getPokemonById(source.id); destinyTag?.lapse(attacker, BattlerTagLapseType.CUSTOM); } } break; case MoveCategory.STATUS: if (!typeless) { applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier); } if (!cancelled.value) { applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, typeMultiplier); defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, move, cancelled, typeMultiplier)); } if (!typeMultiplier.value) { this.scene.queueMessage(i18next.t("battle:hitResultNoEffect", { pokemonName: getPokemonNameWithAffix(this) })); } result = cancelled.value || !typeMultiplier.value ? HitResult.NO_EFFECT : HitResult.STATUS; break; } return result; } /** * Called by damageAndUpdate() * @param damage integer * @param ignoreSegments boolean, not currently used * @param preventEndure used to update damage if endure or sturdy * @param ignoreFaintPhase flag on wheter to add FaintPhase if pokemon after applying damage faints * @returns integer representing damage */ damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { if (this.isFainted()) { return 0; } const surviveDamage = new Utils.BooleanHolder(false); if (!preventEndure && this.hp - damage <= 0) { if (this.hp >= 1 && this.getTag(BattlerTagType.ENDURING)) { surviveDamage.value = this.lapseTag(BattlerTagType.ENDURING); } else if (this.hp > 1 && this.getTag(BattlerTagType.STURDY)) { surviveDamage.value = this.lapseTag(BattlerTagType.STURDY); } if (!surviveDamage.value) { this.scene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage); } if (surviveDamage.value) { damage = this.hp - 1; } } damage = Math.min(damage, this.hp); this.hp = this.hp - damage; if (this.isFainted() && !ignoreFaintPhase) { /** * When adding the FaintPhase, want to toggle future unshiftPhase() and queueMessage() calls * to appear before the FaintPhase (as FaintPhase will potentially end the encounter and add Phases such as * GameOverPhase, VictoryPhase, etc.. that will interfere with anything else that happens during this MoveEffectPhase) * * Once the MoveEffectPhase is over (and calls it's .end() function, shiftPhase() will reset the PhaseQueueSplice via clearPhaseQueueSplice() ) */ this.scene.setPhaseQueueSplice(); this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure)); this.resetSummonData(); } return damage; } /** * Called by apply(), given the damage, adds a new DamagePhase and actually updates HP values, etc. * @param damage integer - passed to damage() * @param result an enum if it's super effective, not very, etc. * @param critical boolean if move is a critical hit * @param ignoreSegments boolean, passed to damage() and not used currently * @param preventEndure boolean, ignore endure properties of pokemon, passed to damage() * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() * @returns integer of damage done */ damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { const damagePhase = new DamagePhase(this.scene, this.getBattlerIndex(), damage, result as DamageResult, critical); this.scene.unshiftPhase(damagePhase); damage = this.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); // Damage amount may have changed, but needed to be queued before calling damage function damagePhase.updateAmount(damage); return damage; } heal(amount: integer): integer { const healAmount = Math.min(amount, this.getMaxHp() - this.hp); this.hp += healAmount; return healAmount; } isBossImmune(): boolean { return this.isBoss(); } isMax(): boolean { const maxForms = [SpeciesFormKey.GIGANTAMAX, SpeciesFormKey.GIGANTAMAX_RAPID, SpeciesFormKey.GIGANTAMAX_SINGLE, SpeciesFormKey.ETERNAMAX] as string[]; return maxForms.includes(this.getFormKey()) || maxForms.includes(this.getFusionFormKey()); } addTag(tagType: BattlerTagType, turnCount: integer = 0, sourceMove?: Moves, sourceId?: integer): boolean { const existingTag = this.getTag(tagType); if (existingTag) { existingTag.onOverlap(this); return false; } const newTag = getBattlerTag(tagType, turnCount, sourceMove, sourceId); const cancelled = new Utils.BooleanHolder(false); applyPreApplyBattlerTagAbAttrs(PreApplyBattlerTagAbAttr, this, newTag, cancelled); if (!cancelled.value && newTag.canAdd(this)) { this.summonData.tags.push(newTag); newTag.onAdd(this); return true; } return false; } /** @overload */ getTag(tagType: BattlerTagType): BattlerTag; /** @overload */ getTag(tagType: Constructor): T; getTag(tagType: BattlerTagType | Constructor): BattlerTag { if (!this.summonData) { return null; } return tagType instanceof Function ? this.summonData.tags.find(t => t instanceof tagType) : this.summonData.tags.find(t => t.tagType === tagType); } findTag(tagFilter: ((tag: BattlerTag) => boolean)) { if (!this.summonData) { return null; } return this.summonData.tags.find(t => tagFilter(t)); } findTags(tagFilter: ((tag: BattlerTag) => boolean)): BattlerTag[] { if (!this.summonData) { return []; } return this.summonData.tags.filter(t => tagFilter(t)); } lapseTag(tagType: BattlerTagType): boolean { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (tag && !(tag.lapse(this, BattlerTagLapseType.CUSTOM))) { tag.onRemove(this); tags.splice(tags.indexOf(tag), 1); } return !!tag; } lapseTags(lapseType: BattlerTagLapseType): void { const tags = this.summonData.tags; tags.filter(t => lapseType === BattlerTagLapseType.FAINT || ((t.lapseTypes.some(lType => lType === lapseType)) && !(t.lapse(this, lapseType)))).forEach(t => { t.onRemove(this); tags.splice(tags.indexOf(t), 1); }); } removeTag(tagType: BattlerTagType): boolean { const tags = this.summonData.tags; const tag = tags.find(t => t.tagType === tagType); if (tag) { tag.onRemove(this); tags.splice(tags.indexOf(tag), 1); } return !!tag; } findAndRemoveTags(tagFilter: ((tag: BattlerTag) => boolean)): boolean { if (!this.summonData) { return false; } const tags = this.summonData.tags; const tagsToRemove = tags.filter(t => tagFilter(t)); for (const tag of tagsToRemove) { tag.turnCount = 0; tag.onRemove(this); tags.splice(tags.indexOf(tag), 1); } return true; } removeTagsBySourceId(sourceId: integer): void { this.findAndRemoveTags(t => t.isSourceLinked() && t.sourceId === sourceId); } transferTagsBySourceId(sourceId: integer, newSourceId: integer): void { if (!this.summonData) { return; } const tags = this.summonData.tags; tags.filter(t => t.sourceId === sourceId).forEach(t => t.sourceId = newSourceId); } /** * Transferring stat changes and Tags * @param source {@linkcode Pokemon} the pokemon whose stats/Tags are to be passed on from, ie: the Pokemon using Baton Pass */ transferSummon(source: Pokemon): void { const battleStats = Utils.getEnumValues(BattleStat); for (const stat of battleStats) { this.summonData.battleStats[stat] = source.summonData.battleStats[stat]; } for (const tag of source.summonData.tags) { // bypass yawn, and infatuation as those can not be passed via Baton Pass if (tag.sourceMove === Moves.YAWN || tag.tagType === BattlerTagType.INFATUATED) { continue; } this.summonData.tags.push(tag); } if (this instanceof PlayerPokemon && source.summonData.battleStats.find(bs => bs === 6)) { this.scene.validateAchv(achvs.TRANSFER_MAX_BATTLE_STAT); } this.updateInfo(); } getMoveHistory(): TurnMove[] { return this.battleSummonData.moveHistory; } pushMoveHistory(turnMove: TurnMove) { turnMove.turn = this.scene.currentBattle?.turn; this.getMoveHistory().push(turnMove); } getLastXMoves(turnCount?: integer): TurnMove[] { const moveHistory = this.getMoveHistory(); return moveHistory.slice(turnCount >= 0 ? Math.max(moveHistory.length - (turnCount || 1), 0) : 0, moveHistory.length).reverse(); } getMoveQueue(): QueuedMove[] { return this.summonData.moveQueue; } /** * If this Pokemon is using a multi-hit move, cancels all subsequent strikes * @param {Pokemon} target If specified, this only cancels subsequent strikes against the given target */ stopMultiHit(target?: Pokemon): void { const effectPhase = this.scene.getCurrentPhase(); if (effectPhase instanceof MoveEffectPhase && effectPhase.getUserPokemon() === this) { effectPhase.stopMultiHit(target); } } changeForm(formChange: SpeciesFormChange): Promise { return new Promise(resolve => { this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0); this.generateName(); const abilityCount = this.getSpeciesForm().getAbilityCount(); if (this.abilityIndex >= abilityCount) {// Shouldn't happen this.abilityIndex = abilityCount - 1; } this.scene.gameData.setPokemonSeen(this, false); this.setScale(this.getSpriteScale()); this.loadAssets().then(() => { this.calculateStats(); this.scene.updateModifiers(this.isPlayer(), true); Promise.all([ this.updateInfo(), this.scene.updateFieldScale() ]).then(() => resolve()); }); }); } cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound { const scene = sceneOverride || this.scene; const cry = this.getSpeciesForm().cry(scene, soundConfig); let duration = cry.totalDuration * 1000; if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) { let fusionCry = this.getFusionSpeciesForm().cry(scene, soundConfig, true); duration = Math.min(duration, fusionCry.totalDuration * 1000); fusionCry.destroy(); scene.time.delayedCall(Utils.fixedInt(Math.ceil(duration * 0.4)), () => { 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); } catch (err) { console.error(err); } }); } return cry; } faintCry(callback: Function): void { if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) { return this.fusionFaintCry(callback); } const key = this.getSpeciesForm().getCryKey(this.formIndex); //eslint-disable-next-line @typescript-eslint/no-unused-vars let i = 0; let rate = 0.85; const cry = this.scene.playSound(key, { rate: rate }) as AnySound; const sprite = this.getSprite(); const tintSprite = this.getTintSprite(); const delay = Math.max(this.scene.sound.get(key).totalDuration * 50, 25); let frameProgress = 0; let frameThreshold: number; sprite.anims.pause(); tintSprite.anims.pause(); let faintCryTimer = this.scene.time.addEvent({ delay: Utils.fixedInt(delay), repeat: -1, callback: () => { ++i; frameThreshold = sprite.anims.msPerFrame / rate; frameProgress += delay; while (frameProgress > frameThreshold) { if (sprite.anims.duration) { sprite.anims.nextFrame(); tintSprite.anims.nextFrame(); } frameProgress -= frameThreshold; } if (cry && !cry.pendingRemove) { rate *= 0.99; cry.setRate(rate); } else { faintCryTimer.destroy(); faintCryTimer = null; if (callback) { callback(); } } } }); // Failsafe this.scene.time.delayedCall(Utils.fixedInt(3000), () => { if (!faintCryTimer || !this.scene) { return; } if (cry?.isPlaying) { cry.stop(); } faintCryTimer.destroy(); if (callback) { callback(); } }); } private fusionFaintCry(callback: Function): void { const key = this.getSpeciesForm().getCryKey(this.formIndex); let i = 0; let rate = 0.85; const cry = this.scene.playSound(key, { rate: rate }) as AnySound; const sprite = this.getSprite(); const tintSprite = this.getTintSprite(); let duration = cry.totalDuration * 1000; let fusionCry = this.scene.playSound(this.getFusionSpeciesForm().getCryKey(this.fusionFormIndex), { rate: rate }) as AnySound; fusionCry.stop(); duration = Math.min(duration, fusionCry.totalDuration * 1000); fusionCry.destroy(); const delay = Math.max(duration * 0.05, 25); let transitionIndex = 0; let durationProgress = 0; const transitionThreshold = Math.ceil(duration * 0.4); while (durationProgress < transitionThreshold) { ++i; durationProgress += delay * rate; rate *= 0.99; } transitionIndex = i; i = 0; rate = 0.85; let frameProgress = 0; let frameThreshold: number; sprite.anims.pause(); tintSprite.anims.pause(); let faintCryTimer = this.scene.time.addEvent({ delay: Utils.fixedInt(delay), repeat: -1, callback: () => { ++i; frameThreshold = sprite.anims.msPerFrame / rate; frameProgress += delay; while (frameProgress > frameThreshold) { if (sprite.anims.duration) { sprite.anims.nextFrame(); tintSprite.anims.nextFrame(); } frameProgress -= frameThreshold; } if (i === transitionIndex) { SoundFade.fadeOut(this.scene, cry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2))); fusionCry = this.scene.playSound(this.getFusionSpeciesForm().getCryKey(this.fusionFormIndex), 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); } rate *= 0.99; if (cry && !cry.pendingRemove) { cry.setRate(rate); } if (fusionCry && !fusionCry.pendingRemove) { fusionCry.setRate(rate); } if ((!cry || cry.pendingRemove) && (!fusionCry || fusionCry.pendingRemove)) { faintCryTimer.destroy(); faintCryTimer = null; if (callback) { callback(); } } } }); // Failsafe this.scene.time.delayedCall(Utils.fixedInt(3000), () => { if (!faintCryTimer || !this.scene) { return; } if (cry?.isPlaying) { cry.stop(); } if (fusionCry?.isPlaying) { fusionCry.stop(); } faintCryTimer.destroy(); if (callback) { callback(); } }); } isOppositeGender(pokemon: Pokemon): boolean { return this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE); } canSetStatus(effect: StatusEffect, quiet: boolean = false, overrideStatus: boolean = false, sourcePokemon: Pokemon = null): boolean { if (effect !== StatusEffect.FAINT) { if (overrideStatus ? this.status?.effect === effect : this.status) { return false; } if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.MISTY) { return false; } } const types = this.getTypes(true, true); switch (effect) { case StatusEffect.POISON: case StatusEffect.TOXIC: // Check if the Pokemon is immune to Poison/Toxic or if the source pokemon is canceling the immunity const poisonImmunity = types.map(defType => { // Check if the Pokemon is not immune to Poison/Toxic if (defType !== Type.POISON && defType !== Type.STEEL) { return false; } // Check if the source Pokemon has an ability that cancels the Poison/Toxic immunity const cancelImmunity = new Utils.BooleanHolder(false); if (sourcePokemon) { applyAbAttrs(IgnoreTypeStatusEffectImmunityAbAttr, sourcePokemon, cancelImmunity, effect, defType); if (cancelImmunity.value) { return false; } } return true; }); if (this.isOfType(Type.POISON) || this.isOfType(Type.STEEL)) { if (poisonImmunity.includes(true)) { return false; } } break; case StatusEffect.PARALYSIS: if (this.isOfType(Type.ELECTRIC)) { return false; } break; case StatusEffect.SLEEP: if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.ELECTRIC) { return false; } break; case StatusEffect.FREEZE: if (this.isOfType(Type.ICE) || [WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(this.scene?.arena.weather?.weatherType)) { return false; } break; case StatusEffect.BURN: if (this.isOfType(Type.FIRE)) { return false; } break; } const cancelled = new Utils.BooleanHolder(false); applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled, quiet); if (cancelled.value) { return false; } return true; } trySetStatus(effect: StatusEffect, asPhase: boolean = false, sourcePokemon: Pokemon = null, cureTurn: integer = 0, sourceText: string = null): boolean { if (!this.canSetStatus(effect, asPhase, false, sourcePokemon)) { return false; } /** * If this Pokemon falls asleep or freezes in the middle of a multi-hit attack, * cancel the attack's subsequent hits. */ if (effect === StatusEffect.SLEEP || effect === StatusEffect.FREEZE) { this.stopMultiHit(); } if (asPhase) { this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText, sourcePokemon)); return true; } let statusCureTurn: Utils.IntegerHolder; if (effect === StatusEffect.SLEEP) { statusCureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4)); applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, effect, statusCureTurn); this.setFrameRate(4); // If the user is invulnerable, lets remove their invulnerability when they fall asleep const invulnerableTags = [ BattlerTagType.UNDERGROUND, BattlerTagType.UNDERWATER, BattlerTagType.HIDDEN, BattlerTagType.FLYING ]; const tag = invulnerableTags.find((t) => this.getTag(t)); if (tag) { this.removeTag(tag); this.getMoveQueue().pop(); } } this.status = new Status(effect, 0, statusCureTurn?.value); if (effect !== StatusEffect.FAINT) { this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true); } return true; } /** * Resets the status of a pokemon. * @param revive Whether revive should be cured; defaults to true. * @param confusion Whether resetStatus should include confusion or not; defaults to false. * @param reloadAssets Whether to reload the assets or not; defaults to false. */ resetStatus(revive: boolean = true, confusion: boolean = false, reloadAssets: boolean = false): void { const lastStatus = this.status?.effect; if (!revive && lastStatus === StatusEffect.FAINT) { return; } this.status = undefined; if (lastStatus === StatusEffect.SLEEP) { this.setFrameRate(12); if (this.getTag(BattlerTagType.NIGHTMARE)) { this.lapseTag(BattlerTagType.NIGHTMARE); } } if (confusion) { if (this.getTag(BattlerTagType.CONFUSED)) { this.lapseTag(BattlerTagType.CONFUSED); } } if (reloadAssets) { this.loadAssets(false).then(() => this.playAnim()); } } primeSummonData(summonDataPrimer: PokemonSummonData): void { this.summonDataPrimer = summonDataPrimer; } resetSummonData(): void { if (this.summonData?.speciesForm) { this.summonData.speciesForm = null; this.updateFusionPalette(); } this.summonData = new PokemonSummonData(); if (!this.battleData) { this.resetBattleData(); } this.resetBattleSummonData(); if (this.summonDataPrimer) { for (const k of Object.keys(this.summonData)) { if (this.summonDataPrimer[k]) { this.summonData[k] = this.summonDataPrimer[k]; } } this.summonDataPrimer = null; } this.updateInfo(); } resetBattleData(): void { this.battleData = new PokemonBattleData(); } resetBattleSummonData(): void { this.battleSummonData = new PokemonBattleSummonData(); if (this.getTag(BattlerTagType.SEEDED)) { this.lapseTag(BattlerTagType.SEEDED); } if (this.scene) { this.scene.triggerPokemonFormChange(this, SpeciesFormChangePostMoveTrigger, true); } } resetTurnData(): void { this.turnData = new PokemonTurnData(); } getExpValue(): integer { // Logic to factor in victor level has been removed for balancing purposes, so the player doesn't have to focus on EXP maxxing return ((this.getSpeciesForm().getBaseExp() * this.level) / 5 + 1); } setFrameRate(frameRate: integer) { this.scene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate; this.getSprite().play(this.getBattleSpriteKey()); this.getTintSprite().play(this.getBattleSpriteKey()); } tint(color: number, alpha?: number, duration?: integer, ease?: string) { const tintSprite = this.getTintSprite(); tintSprite.setTintFill(color); tintSprite.setVisible(true); if (duration) { tintSprite.setAlpha(0); this.scene.tweens.add({ targets: tintSprite, alpha: alpha || 1, duration: duration, ease: ease || "Linear" }); } else { tintSprite.setAlpha(alpha); } } untint(duration: integer, ease?: string) { const tintSprite = this.getTintSprite(); if (duration) { this.scene.tweens.add({ targets: tintSprite, alpha: 0, duration: duration, ease: ease || "Linear", onComplete: () => { tintSprite.setVisible(false); tintSprite.setAlpha(1); } }); } else { tintSprite.setVisible(false); tintSprite.setAlpha(1); } } enableMask() { if (!this.maskEnabled) { this.maskSprite = this.getTintSprite(); this.maskSprite.setVisible(true); this.maskSprite.setPosition(this.x * this.parentContainer.scale + this.parentContainer.x, this.y * this.parentContainer.scale + this.parentContainer.y); this.maskSprite.setScale(this.getSpriteScale() * this.parentContainer.scale); this.maskEnabled = true; } } disableMask() { if (this.maskEnabled) { this.maskSprite.setVisible(false); this.maskSprite.setPosition(0, 0); this.maskSprite.setScale(this.getSpriteScale()); this.maskSprite = null; this.maskEnabled = false; } } sparkle(): void { if (this.shinySparkle) { this.shinySparkle.play(`sparkle${this.variant ? `_${this.variant + 1}` : ""}`); this.scene.playSound("sparkle"); } } updateFusionPalette(ignoreOveride?: boolean): void { if (!this.getFusionSpeciesForm(ignoreOveride)) { [ this.getSprite(), this.getTintSprite() ].map(s => { s.pipelineData[`spriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = []; s.pipelineData[`fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = []; }); return; } const speciesForm = this.getSpeciesForm(ignoreOveride); const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOveride); const spriteKey = speciesForm.getSpriteKey(this.getGender(ignoreOveride) === Gender.FEMALE, speciesForm.formIndex, this.shiny, this.variant); const backSpriteKey = speciesForm.getSpriteKey(this.getGender(ignoreOveride) === Gender.FEMALE, speciesForm.formIndex, this.shiny, this.variant).replace("pkmn__", "pkmn__back__"); const fusionSpriteKey = fusionSpeciesForm.getSpriteKey(this.getFusionGender(ignoreOveride) === Gender.FEMALE, fusionSpeciesForm.formIndex, this.fusionShiny, this.fusionVariant); const fusionBackSpriteKey = fusionSpeciesForm.getSpriteKey(this.getFusionGender(ignoreOveride) === Gender.FEMALE, fusionSpeciesForm.formIndex, this.fusionShiny, this.fusionVariant).replace("pkmn__", "pkmn__back__"); const sourceTexture = this.scene.textures.get(spriteKey); const sourceBackTexture = this.scene.textures.get(backSpriteKey); const fusionTexture = this.scene.textures.get(fusionSpriteKey); const fusionBackTexture = this.scene.textures.get(fusionBackSpriteKey); const [ sourceFrame, sourceBackFrame, fusionFrame, fusionBackFrame ] = [ sourceTexture, sourceBackTexture, fusionTexture, fusionBackTexture ].map(texture => texture.frames[texture.firstFrame]); const [ sourceImage, sourceBackImage, fusionImage, fusionBackImage ] = [ sourceTexture, sourceBackTexture, fusionTexture, fusionBackTexture ].map(i => i.getSourceImage() as HTMLImageElement); const canvas = document.createElement("canvas"); const backCanvas = document.createElement("canvas"); const fusionCanvas = document.createElement("canvas"); const fusionBackCanvas = document.createElement("canvas"); const spriteColors: integer[][] = []; const pixelData: Uint8ClampedArray[] = []; [ canvas, backCanvas, fusionCanvas, fusionBackCanvas ].forEach((canv: HTMLCanvasElement, c: integer) => { const context = canv.getContext("2d"); const frame = [ sourceFrame, sourceBackFrame, fusionFrame, fusionBackFrame ][c]; canv.width = frame.width; canv.height = frame.height; context.drawImage([ sourceImage, sourceBackImage, fusionImage, fusionBackImage ][c], frame.cutX, frame.cutY, frame.width, frame.height, 0, 0, frame.width, frame.height); const imageData = context.getImageData(frame.cutX, frame.cutY, frame.width, frame.height); pixelData.push(imageData.data); }); for (let f = 0; f < 2; f++) { const variantColors = variantColorCache[!f ? spriteKey : backSpriteKey]; const variantColorSet = new Map(); if (this.shiny && variantColors && variantColors[this.variant]) { Object.keys(variantColors[this.variant]).forEach(k => { variantColorSet.set(Utils.rgbaToInt(Array.from(Object.values(Utils.rgbHexToRgba(k)))), Array.from(Object.values(Utils.rgbHexToRgba(variantColors[this.variant][k])))); }); } for (let i = 0; i < pixelData[f].length; i += 4) { if (pixelData[f][i + 3]) { const pixel = pixelData[f].slice(i, i + 4); let [ r, g, b, a ] = pixel; if (variantColors) { const color = Utils.rgbaToInt([r, g, b, a]); if (variantColorSet.has(color)) { const mappedPixel = variantColorSet.get(color); [ r, g, b, a ] = mappedPixel; } } if (!spriteColors.find(c => c[0] === r && c[1] === g && c[2] === b)) { spriteColors.push([ r, g, b, a ]); } } } } const fusionSpriteColors = JSON.parse(JSON.stringify(spriteColors)); const pixelColors = []; for (let f = 0; f < 2; f++) { for (let i = 0; i < pixelData[f].length; i += 4) { const total = pixelData[f].slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0); if (!total) { continue; } pixelColors.push(argbFromRgba({ r: pixelData[f][i], g: pixelData[f][i + 1], b: pixelData[f][i + 2], a: pixelData[f][i + 3] })); } } const fusionPixelColors = []; for (let f = 0; f < 2; f++) { const variantColors = variantColorCache[!f ? fusionSpriteKey : fusionBackSpriteKey]; const variantColorSet = new Map(); if (this.fusionShiny && variantColors && variantColors[this.fusionVariant]) { Object.keys(variantColors[this.fusionVariant]).forEach(k => { variantColorSet.set(Utils.rgbaToInt(Array.from(Object.values(Utils.rgbHexToRgba(k)))), Array.from(Object.values(Utils.rgbHexToRgba(variantColors[this.fusionVariant][k])))); }); } for (let i = 0; i < pixelData[2 + f].length; i += 4) { const total = pixelData[2 + f].slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0); if (!total) { continue; } let [ r, g, b, a ] = [ pixelData[2 + f][i], pixelData[2 + f][i + 1], pixelData[2 + f][i + 2], pixelData[2 + f][i + 3] ]; if (variantColors) { const color = Utils.rgbaToInt([r, g, b, a]); if (variantColorSet.has(color)) { const mappedPixel = variantColorSet.get(color); [ r, g, b, a ] = mappedPixel; } } fusionPixelColors.push(argbFromRgba({ r, g, b, a })); } } let paletteColors: Map; let fusionPaletteColors: Map; const originalRandom = Math.random; Math.random = () => Phaser.Math.RND.realInRange(0, 1); this.scene.executeWithSeedOffset(() => { paletteColors = QuantizerCelebi.quantize(pixelColors, 4); fusionPaletteColors = QuantizerCelebi.quantize(fusionPixelColors, 4); }, 0, "This result should not vary"); Math.random = originalRandom; const [ palette, fusionPalette ] = [ paletteColors, fusionPaletteColors ] .map(paletteColors => { let keys = Array.from(paletteColors.keys()).sort((a: integer, b: integer) => paletteColors.get(a) < paletteColors.get(b) ? 1 : -1); let rgbaColors: Map; let hsvColors: Map; const mappedColors = new Map(); do { mappedColors.clear(); rgbaColors = keys.reduce((map: Map, k: number) => { map.set(k, Object.values(rgbaFromArgb(k))); return map; }, new Map()); hsvColors = Array.from(rgbaColors.keys()).reduce((map: Map, k: number) => { const rgb = rgbaColors.get(k).slice(0, 3); map.set(k, Utils.rgbToHsv(rgb[0], rgb[1], rgb[2])); return map; }, new Map()); for (let c = keys.length - 1; c >= 0; c--) { const hsv = hsvColors.get(keys[c]); for (let c2 = 0; c2 < c; c2++) { const hsv2 = hsvColors.get(keys[c2]); const diff = Math.abs(hsv[0] - hsv2[0]); if (diff < 30 || diff >= 330) { if (mappedColors.has(keys[c])) { mappedColors.get(keys[c]).push(keys[c2]); } else { mappedColors.set(keys[c], [ keys[c2] ]); } break; } } } mappedColors.forEach((values: integer[], key: integer) => { const keyColor = rgbaColors.get(key); const valueColors = values.map(v => rgbaColors.get(v)); const color = keyColor.slice(0); let count = paletteColors.get(key); for (const value of values) { const valueCount = paletteColors.get(value); if (!valueCount) { continue; } count += valueCount; } for (let c = 0; c < 3; c++) { color[c] *= (paletteColors.get(key) / count); values.forEach((value: integer, i: integer) => { if (paletteColors.has(value)) { const valueCount = paletteColors.get(value); color[c] += valueColors[i][c] * (valueCount / count); } }); color[c] = Math.round(color[c]); } paletteColors.delete(key); for (const value of values) { paletteColors.delete(value); if (mappedColors.has(value)) { mappedColors.delete(value); } } paletteColors.set(argbFromRgba({ r: color[0], g: color[1], b: color[2], a: color[3] }), count); }); keys = Array.from(paletteColors.keys()).sort((a: integer, b: integer) => paletteColors.get(a) < paletteColors.get(b) ? 1 : -1); } while (mappedColors.size); return keys.map(c => Object.values(rgbaFromArgb(c))); } ); const paletteDeltas: number[][] = []; spriteColors.forEach((sc: integer[], i: integer) => { paletteDeltas.push([]); for (let p = 0; p < palette.length; p++) { paletteDeltas[i].push(Utils.deltaRgb(sc, palette[p])); } }); const easeFunc = Phaser.Tweens.Builders.GetEaseFunction("Cubic.easeIn"); for (let sc = 0; sc < spriteColors.length; sc++) { const delta = Math.min(...paletteDeltas[sc]); const paletteIndex = Math.min(paletteDeltas[sc].findIndex(pd => pd === delta), fusionPalette.length - 1); if (delta < 255) { const ratio = easeFunc(delta / 255); const color = [ 0, 0, 0, fusionSpriteColors[sc][3] ]; for (let c = 0; c < 3; c++) { color[c] = Math.round((fusionSpriteColors[sc][c] * ratio) + (fusionPalette[paletteIndex][c] * (1 - ratio))); } fusionSpriteColors[sc] = color; } } [ this.getSprite(), this.getTintSprite() ].map(s => { s.pipelineData[`spriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = spriteColors; s.pipelineData[`fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = fusionSpriteColors; }); canvas.remove(); fusionCanvas.remove(); } randSeedInt(range: integer, min: integer = 0): integer { return this.scene.currentBattle ? this.scene.randBattleSeedInt(range, min) : Utils.randSeedInt(range, min); } randSeedIntRange(min: integer, max: integer): integer { return this.randSeedInt((max - min) + 1, min); } destroy(): void { this.battleInfo?.destroy(); super.destroy(); } getBattleInfo(): BattleInfo { return this.battleInfo; } /** * Checks whether or not the Pokemon's root form has the same ability * @param abilityIndex the given ability index we are checking * @returns true if the abilities are the same */ hasSameAbilityInRootForm(abilityIndex: number): boolean { const currentAbilityIndex = this.abilityIndex; const rootForm = getPokemonSpecies(this.species.getRootSpeciesId()); return rootForm.getAbility(abilityIndex) === rootForm.getAbility(currentAbilityIndex); } } export default interface Pokemon { scene: BattleScene } export class PlayerPokemon extends Pokemon { public compatibleTms: Moves[]; constructor(scene: BattleScene, species: PokemonSpecies, level: integer, abilityIndex: integer, formIndex: integer, gender: Gender, shiny: boolean, variant: Variant, ivs: integer[], nature: Nature, dataSource: Pokemon | PokemonData) { super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource); if (Overrides.STATUS_OVERRIDE) { this.status = new Status(Overrides.STATUS_OVERRIDE); } if (Overrides.SHINY_OVERRIDE) { this.shiny = true; this.initShinySparkle(); if (Overrides.VARIANT_OVERRIDE) { this.variant = Overrides.VARIANT_OVERRIDE; } } if (!dataSource) { this.generateAndPopulateMoveset(); } this.generateCompatibleTms(); } initBattleInfo(): void { this.battleInfo = new PlayerBattleInfo(this.scene); this.battleInfo.initInfo(this); } isPlayer(): boolean { return true; } hasTrainer(): boolean { return true; } isBoss(): boolean { return false; } getFieldIndex(): integer { return this.scene.getPlayerField().indexOf(this); } getBattlerIndex(): BattlerIndex { return this.getFieldIndex(); } generateCompatibleTms(): void { this.compatibleTms = []; const tms = Object.keys(tmSpecies); for (const tm of tms) { const moveId = parseInt(tm) as Moves; let compatible = false; for (const p of tmSpecies[tm]) { if (Array.isArray(p)) { if (p[0] === this.species.speciesId || (this.fusionSpecies && p[0] === this.fusionSpecies.speciesId) && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) { compatible = true; break; } } else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) { compatible = true; break; } } if (reverseCompatibleTms.indexOf(moveId) > -1) { compatible = !compatible; } if (compatible) { this.compatibleTms.push(moveId); } } } tryPopulateMoveset(moveset: StarterMoveset): boolean { if (!this.getSpeciesForm().validateStarterMoveset(moveset, this.scene.gameData.starterData[this.species.getRootSpeciesId()].eggMoves)) { return false; } this.moveset = moveset.map(m => new PokemonMove(m)); return true; } switchOut(batonPass: boolean, removeFromField: boolean = false): Promise { return new Promise(resolve => { this.resetTurnData(); if (!batonPass) { this.resetSummonData(); } this.hideInfo(); this.setVisible(false); this.scene.ui.setMode(Mode.PARTY, PartyUiMode.FAINT_SWITCH, this.getFieldIndex(), (slotIndex: integer, option: PartyOption) => { if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) { this.scene.prependToPhase(new SwitchSummonPhase(this.scene, this.getFieldIndex(), slotIndex, false, batonPass), MoveEndPhase); } if (removeFromField) { this.setVisible(false); this.scene.field.remove(this); this.scene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); } this.scene.ui.setMode(Mode.MESSAGE).then(() => resolve()); }, PartyUiHandler.FilterNonFainted); }); } addFriendship(friendship: integer): void { const starterSpeciesId = this.species.getRootSpeciesId(); const fusionStarterSpeciesId = this.isFusion() ? this.fusionSpecies.getRootSpeciesId() : 0; const starterData = [ this.scene.gameData.starterData[starterSpeciesId], fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null ].filter(d => d); const amount = new Utils.IntegerHolder(friendship); const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? 2 : 1) / (fusionStarterSpeciesId ? 2 : 1))); if (amount.value > 0) { this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount); this.friendship = Math.min(this.friendship + amount.value, 255); if (this.friendship === 255) { this.scene.validateAchv(achvs.MAX_FRIENDSHIP); } starterData.forEach((sd: StarterDataEntry, i: integer) => { const speciesId = !i ? starterSpeciesId : fusionStarterSpeciesId as Species; sd.friendship = (sd.friendship || 0) + starterAmount.value; if (sd.friendship >= getStarterValueFriendshipCap(speciesStarters[speciesId])) { this.scene.gameData.addStarterCandy(getPokemonSpecies(speciesId), 1); sd.friendship = 0; } }); } else { this.friendship = Math.max(this.friendship + amount.value, 0); for (const sd of starterData) { sd.friendship = Math.max((sd.friendship || 0) + starterAmount.value, 0); } } } /** * Handles Revival Blessing when used by player. * @returns Promise to revive a pokemon. * @see {@linkcode RevivalBlessingAttr} */ revivalBlessing(): Promise { return new Promise(resolve => { this.scene.ui.setMode(Mode.PARTY, PartyUiMode.REVIVAL_BLESSING, this.getFieldIndex(), (slotIndex:integer, option: PartyOption) => { if (slotIndex >= 0 && slotIndex<6) { const pokemon = this.scene.getParty()[slotIndex]; if (!pokemon || !pokemon.isFainted()) { resolve(); } pokemon.resetTurnData(); pokemon.resetStatus(); pokemon.heal(Math.min(Math.max(Math.ceil(Math.floor(0.5 * pokemon.getMaxHp())), 1), pokemon.getMaxHp())); this.scene.queueMessage(`${pokemon.name} was revived!`,0,true); if (this.scene.currentBattle.double && this.scene.getParty().length > 1) { const allyPokemon = this.getAlly(); if (slotIndex<=1) { // Revived ally pokemon this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), slotIndex, false, false, true)); this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); } else if (allyPokemon.isFainted()) { // Revived party pokemon, and ally pokemon is fainted this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, allyPokemon.getFieldIndex(), slotIndex, false, false, true)); this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true)); } } } this.scene.ui.setMode(Mode.MESSAGE).then(() => resolve()); }, PartyUiHandler.FilterFainted); }); } getPossibleEvolution(evolution: SpeciesFormEvolution): Promise { return new Promise(resolve => { const evolutionSpecies = getPokemonSpecies(evolution.speciesId); const isFusion = evolution instanceof FusionSpeciesFormEvolution; let ret: PlayerPokemon; if (isFusion) { const originalFusionSpecies = this.fusionSpecies; const originalFusionFormIndex = this.fusionFormIndex; this.fusionSpecies = evolutionSpecies; this.fusionFormIndex = evolution.evoFormKey !== null ? Math.max(evolutionSpecies.forms.findIndex(f => f.formKey === evolution.evoFormKey), 0) : this.fusionFormIndex; ret = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this); this.fusionSpecies = originalFusionSpecies; this.fusionFormIndex = originalFusionFormIndex; } else { const formIndex = evolution.evoFormKey !== null && !isFusion ? Math.max(evolutionSpecies.forms.findIndex(f => f.formKey === evolution.evoFormKey), 0) : this.formIndex; ret = this.scene.addPlayerPokemon(!isFusion ? evolutionSpecies : this.species, this.level, this.abilityIndex, formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this); } ret.loadAssets().then(() => resolve(ret)); }); } evolve(evolution: SpeciesFormEvolution, preEvolution: PokemonSpeciesForm): Promise { return new Promise(resolve => { this.pauseEvolutions = false; // Handles Nincada evolving into Ninjask + Shedinja this.handleSpecialEvolutions(evolution); const isFusion = evolution instanceof FusionSpeciesFormEvolution; if (!isFusion) { this.species = getPokemonSpecies(evolution.speciesId); } else { this.fusionSpecies = getPokemonSpecies(evolution.speciesId); } if (evolution.preFormKey !== null) { const formIndex = Math.max((!isFusion ? this.species : this.fusionSpecies).forms.findIndex(f => f.formKey === evolution.evoFormKey), 0); if (!isFusion) { this.formIndex = formIndex; } else { this.fusionFormIndex = formIndex; } } this.generateName(); if (!isFusion) { const abilityCount = this.getSpeciesForm().getAbilityCount(); const preEvoAbilityCount = preEvolution.getAbilityCount(); if ([0, 1, 2].includes(this.abilityIndex)) { // Handles cases where a Pokemon with 3 abilities evolves into a Pokemon with 2 abilities (ie: Eevee -> any Eeveelution) if (this.abilityIndex === 2 && preEvoAbilityCount === 3 && abilityCount === 2) { this.abilityIndex = 1; } } else { // Prevent pokemon with an illegal ability value from breaking things console.warn("this.abilityIndex is somehow an illegal value, please report this"); console.warn(this.abilityIndex); this.abilityIndex = 0; } } else { // Do the same as above, but for fusions const abilityCount = this.getFusionSpeciesForm().getAbilityCount(); const preEvoAbilityCount = preEvolution.getAbilityCount(); if ([0, 1, 2].includes(this.fusionAbilityIndex)) { if (this.fusionAbilityIndex === 2 && preEvoAbilityCount === 3 && abilityCount === 2) { this.fusionAbilityIndex = 1; } } else { console.warn("this.fusionAbilityIndex is somehow an illegal value, please report this"); console.warn(this.fusionAbilityIndex); this.fusionAbilityIndex = 0; } } this.compatibleTms.splice(0, this.compatibleTms.length); this.generateCompatibleTms(); const updateAndResolve = () => { this.loadAssets().then(() => { this.calculateStats(); this.updateInfo(true).then(() => resolve()); }); }; if (!this.scene.gameMode.isDaily || this.metBiome > -1) { this.scene.gameData.updateSpeciesDexIvs(this.species.speciesId, this.ivs); this.scene.gameData.setPokemonSeen(this, false); this.scene.gameData.setPokemonCaught(this, false).then(() => updateAndResolve()); } else { updateAndResolve(); } }); } private handleSpecialEvolutions(evolution: SpeciesFormEvolution) { const isFusion = evolution instanceof FusionSpeciesFormEvolution; const evoSpecies = (!isFusion ? this.species : this.fusionSpecies); if (evoSpecies.speciesId === Species.NINCADA && evolution.speciesId === Species.NINJASK) { const newEvolution = pokemonEvolutions[evoSpecies.speciesId][1]; if (newEvolution.condition.predicate(this)) { const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, undefined, this.shiny, this.variant, this.ivs, this.nature); newPokemon.natureOverride = this.natureOverride; newPokemon.passive = this.passive; newPokemon.moveset = this.moveset.slice(); newPokemon.moveset = this.copyMoveset(); newPokemon.luck = this.luck; newPokemon.fusionSpecies = this.fusionSpecies; newPokemon.fusionFormIndex = this.fusionFormIndex; newPokemon.fusionAbilityIndex = this.fusionAbilityIndex; newPokemon.fusionShiny = this.fusionShiny; newPokemon.fusionVariant = this.fusionVariant; newPokemon.fusionGender = this.fusionGender; newPokemon.fusionLuck = this.fusionLuck; this.scene.getParty().push(newPokemon); newPokemon.evolve((!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution)), evoSpecies); const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === this.id, true) as PokemonHeldItemModifier[]; modifiers.forEach(m => { const clonedModifier = m.clone() as PokemonHeldItemModifier; clonedModifier.pokemonId = newPokemon.id; this.scene.addModifier(clonedModifier, true); }); this.scene.updateModifiers(true); } } } getPossibleForm(formChange: SpeciesFormChange): Promise { return new Promise(resolve => { const formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0); const ret = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this); ret.loadAssets().then(() => resolve(ret)); }); } changeForm(formChange: SpeciesFormChange): Promise { return new Promise(resolve => { this.formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0); this.generateName(); const abilityCount = this.getSpeciesForm().getAbilityCount(); if (this.abilityIndex >= abilityCount) { // Shouldn't happen this.abilityIndex = abilityCount - 1; } this.compatibleTms.splice(0, this.compatibleTms.length); this.generateCompatibleTms(); const updateAndResolve = () => { this.loadAssets().then(() => { this.calculateStats(); this.scene.updateModifiers(true, true); this.updateInfo(true).then(() => resolve()); }); }; if (!this.scene.gameMode.isDaily || this.metBiome > -1) { this.scene.gameData.setPokemonSeen(this, false); this.scene.gameData.setPokemonCaught(this, false).then(() => updateAndResolve()); } else { updateAndResolve(); } }); } clearFusionSpecies(): void { super.clearFusionSpecies(); this.generateCompatibleTms(); } /** * Returns a Promise to fuse two PlayerPokemon together * @param pokemon The PlayerPokemon to fuse to this one */ fuse(pokemon: PlayerPokemon): Promise { return new Promise(resolve => { this.fusionSpecies = pokemon.species; this.fusionFormIndex = pokemon.formIndex; this.fusionAbilityIndex = pokemon.abilityIndex; this.fusionShiny = pokemon.shiny; this.fusionVariant = pokemon.variant; this.fusionGender = pokemon.gender; this.fusionLuck = pokemon.luck; this.scene.validateAchv(achvs.SPLICE); this.scene.gameData.gameStats.pokemonFused++; // Store the average HP% that each Pokemon has const newHpPercent = ((pokemon.hp / pokemon.stats[Stat.HP]) + (this.hp / this.stats[Stat.HP])) / 2; this.generateName(); this.calculateStats(); // Set this Pokemon's HP to the average % of both fusion components this.hp = Math.round(this.stats[Stat.HP] * newHpPercent); if (!this.isFainted()) { // If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum this.hp = Math.min(this.hp, this.stats[Stat.HP]); this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two } else if (!pokemon.isFainted()) { // If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero this.hp = Math.max(this.hp, 1); this.status = pokemon.status; // Inherit the other Pokemon's status } this.generateCompatibleTms(); this.updateInfo(true); const fusedPartyMemberIndex = this.scene.getParty().indexOf(pokemon); let partyMemberIndex = this.scene.getParty().indexOf(this); if (partyMemberIndex > fusedPartyMemberIndex) { partyMemberIndex--; } const fusedPartyMemberHeldModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === pokemon.id, true) as PokemonHeldItemModifier[]; const transferModifiers: Promise[] = []; for (const modifier of fusedPartyMemberHeldModifiers) { transferModifiers.push(this.scene.tryTransferHeldItemModifier(modifier, this, false, modifier.getStackCount(), true, true)); } Promise.allSettled(transferModifiers).then(() => { this.scene.updateModifiers(true, true).then(() => { this.scene.removePartyMemberModifiers(fusedPartyMemberIndex); this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0]; const newPartyMemberIndex = this.scene.getParty().indexOf(this); pokemon.getMoveset(true).map(m => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m.getMove().id))); pokemon.destroy(); this.updateFusionPalette(); resolve(); }); }); }); } unfuse(): Promise { return new Promise(resolve => { this.clearFusionSpecies(); this.updateInfo(true).then(() => resolve()); this.updateFusionPalette(); }); } /** Returns a deep copy of this Pokemon's moveset array */ copyMoveset(): PokemonMove[] { const newMoveset = []; this.moveset.forEach(move => newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.virtual))); return newMoveset; } } export class EnemyPokemon extends Pokemon { public trainerSlot: TrainerSlot; public aiType: AiType; public bossSegments: integer; public bossSegmentIndex: integer; /** To indicate of the instance was populated with a dataSource -> e.g. loaded & populated from session data */ public readonly isPopulatedFromDataSource: boolean; constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, dataSource: PokemonData) { super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex, dataSource?.gender, dataSource ? dataSource.shiny : false, dataSource ? dataSource.variant : undefined, null, dataSource ? dataSource.nature : undefined, dataSource); this.trainerSlot = trainerSlot; this.isPopulatedFromDataSource = !!dataSource; // if a dataSource is provided, then it was populated from dataSource if (boss) { this.setBoss(boss, dataSource?.bossSegments); } if (Overrides.OPP_STATUS_OVERRIDE) { this.status = new Status(Overrides.OPP_STATUS_OVERRIDE); } if (!dataSource) { this.generateAndPopulateMoveset(); this.trySetShiny(); if (Overrides.OPP_SHINY_OVERRIDE) { this.shiny = true; this.initShinySparkle(); } if (this.shiny) { this.variant = this.generateVariant(); if (Overrides.OPP_VARIANT_OVERRIDE) { this.variant = Overrides.OPP_VARIANT_OVERRIDE; } } this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0); let prevolution: Species; let speciesId = species.speciesId; while ((prevolution = pokemonPrevolutions[speciesId])) { const evolution = pokemonEvolutions[prevolution].find(pe => pe.speciesId === speciesId && (!pe.evoFormKey || pe.evoFormKey === this.getFormKey())); if (evolution.condition?.enforceFunc) { evolution.condition.enforceFunc(this); } speciesId = prevolution; } } this.aiType = boss || this.hasTrainer() ? AiType.SMART : AiType.SMART_RANDOM; } initBattleInfo(): void { if (!this.battleInfo) { this.battleInfo = new EnemyBattleInfo(this.scene); this.battleInfo.updateBossSegments(this); this.battleInfo.initInfo(this); } else { this.battleInfo.updateBossSegments(this); } } /** * Sets the pokemons boss status. If true initializes the boss segments either from the arguments * or through the the Scene.getEncounterBossSegments function * * @param boss if the pokemon is a boss * @param bossSegments amount of boss segments (health-bar segments) */ setBoss(boss: boolean = true, bossSegments: integer = 0): void { if (boss) { this.bossSegments = bossSegments || this.scene.getEncounterBossSegments(this.scene.currentBattle.waveIndex, this.level, this.species, true); this.bossSegmentIndex = this.bossSegments - 1; } else { this.bossSegments = 0; this.bossSegmentIndex = 0; } } generateAndPopulateMoveset(formIndex?: integer): void { switch (true) { case (this.species.speciesId === Species.SMEARGLE): this.moveset = [ new PokemonMove(Moves.SKETCH), new PokemonMove(Moves.SKETCH), new PokemonMove(Moves.SKETCH), new PokemonMove(Moves.SKETCH) ]; break; case (this.species.speciesId === Species.ETERNATUS): this.moveset = (formIndex !== undefined ? formIndex : this.formIndex) ? [ new PokemonMove(Moves.DYNAMAX_CANNON), new PokemonMove(Moves.CROSS_POISON), new PokemonMove(Moves.FLAMETHROWER), new PokemonMove(Moves.RECOVER, 0, -4) ] : [ new PokemonMove(Moves.ETERNABEAM), new PokemonMove(Moves.SLUDGE_BOMB), new PokemonMove(Moves.DRAGON_DANCE), new PokemonMove(Moves.COSMIC_POWER) ]; break; default: super.generateAndPopulateMoveset(); break; } } getNextMove(): QueuedMove { const queuedMove = this.getMoveQueue().length ? this.getMoveset().find(m => m.moveId === this.getMoveQueue()[0].move) : null; if (queuedMove) { if (queuedMove.isUsable(this, this.getMoveQueue()[0].ignorePP)) { return { move: queuedMove.moveId, targets: this.getMoveQueue()[0].targets, ignorePP: this.getMoveQueue()[0].ignorePP }; } else { this.getMoveQueue().shift(); return this.getNextMove(); } } const movePool = this.getMoveset().filter(m => m.isUsable(this)); if (movePool.length) { if (movePool.length === 1) { return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) }; } const encoreTag = this.getTag(EncoreTag) as EncoreTag; if (encoreTag) { const encoreMove = movePool.find(m => m.moveId === encoreTag.moveId); if (encoreMove) { return { move: encoreMove.moveId, targets: this.getNextTargets(encoreMove.moveId) }; } } switch (this.aiType) { case AiType.RANDOM: const moveId = movePool[this.scene.randBattleSeedInt(movePool.length)].moveId; return { move: moveId, targets: this.getNextTargets(moveId) }; case AiType.SMART_RANDOM: case AiType.SMART: const moveScores = movePool.map(() => 0); const moveTargets = Object.fromEntries(movePool.map(m => [ m.moveId, this.getNextTargets(m.moveId) ])); for (const m in movePool) { const pokemonMove = movePool[m]; const move = pokemonMove.getMove(); let moveScore = moveScores[m]; const targetScores: integer[] = []; for (const mt of moveTargets[move.id]) { // Prevent a target score from being calculated when the target is whoever attacks the user if (mt === BattlerIndex.ATTACKER) { break; } const target = this.scene.getField()[mt]; let targetScore = move.getUserBenefitScore(this, target, move) + move.getTargetBenefitScore(this, target, move) * (mt < BattlerIndex.ENEMY === this.isPlayer() ? 1 : -1); if (Number.isNaN(targetScore)) { console.error(`Move ${move.name} returned score of NaN`); targetScore = 0; } if ((move.name.endsWith(" (N)") || !move.applyConditions(this, target, move)) && ![Moves.SUCKER_PUNCH, Moves.UPPER_HAND, Moves.THUNDERCLAP].includes(move.id)) { targetScore = -20; } else if (move instanceof AttackMove) { const effectiveness = target.getAttackMoveEffectiveness(this, pokemonMove); if (target.isPlayer() !== this.isPlayer()) { targetScore *= effectiveness; if (this.isOfType(move.type)) { targetScore *= 1.5; } } else if (effectiveness) { targetScore /= effectiveness; if (this.isOfType(move.type)) { targetScore /= 1.5; } } if (!targetScore) { targetScore = -20; } } targetScores.push(targetScore); } moveScore += Math.max(...targetScores); // could make smarter by checking opponent def/spdef moveScores[m] = moveScore; } console.log(moveScores); const sortedMovePool = movePool.slice(0); sortedMovePool.sort((a, b) => { const scoreA = moveScores[movePool.indexOf(a)]; const scoreB = moveScores[movePool.indexOf(b)]; return scoreA < scoreB ? 1 : scoreA > scoreB ? -1 : 0; }); let r = 0; if (this.aiType === AiType.SMART_RANDOM) { while (r < sortedMovePool.length - 1 && this.scene.randBattleSeedInt(8) >= 5) { r++; } } else if (this.aiType === AiType.SMART) { while (r < sortedMovePool.length - 1 && (moveScores[movePool.indexOf(sortedMovePool[r + 1])] / moveScores[movePool.indexOf(sortedMovePool[r])]) >= 0 && this.scene.randBattleSeedInt(100) < Math.round((moveScores[movePool.indexOf(sortedMovePool[r + 1])] / moveScores[movePool.indexOf(sortedMovePool[r])]) * 50)) { r++; } } console.log(movePool.map(m => m.getName()), moveScores, r, sortedMovePool.map(m => m.getName())); return { move: sortedMovePool[r].moveId, targets: moveTargets[sortedMovePool[r].moveId] }; } } return { move: Moves.STRUGGLE, targets: this.getNextTargets(Moves.STRUGGLE) }; } getNextTargets(moveId: Moves): BattlerIndex[] { const moveTargets = getMoveTargets(this, moveId); const targets = this.scene.getField(true).filter(p => moveTargets.targets.indexOf(p.getBattlerIndex()) > -1); if (moveTargets.multiple) { return targets.map(p => p.getBattlerIndex()); } const move = allMoves[moveId]; const benefitScores = targets .map(p => [ p.getBattlerIndex(), move.getTargetBenefitScore(this, p, move) * (p.isPlayer() === this.isPlayer() ? 1 : -1) ]); const sortedBenefitScores = benefitScores.slice(0); sortedBenefitScores.sort((a, b) => { const scoreA = a[1]; const scoreB = b[1]; return scoreA < scoreB ? 1 : scoreA > scoreB ? -1 : 0; }); if (!sortedBenefitScores.length) { // Set target to BattlerIndex.ATTACKER when using a counter move // This is the same as when the player does so if (move.hasAttr(CounterDamageAttr)) { return [BattlerIndex.ATTACKER]; } return []; } let targetWeights = sortedBenefitScores.map(s => s[1]); const lowestWeight = targetWeights[targetWeights.length - 1]; if (lowestWeight < 1) { for (let w = 0; w < targetWeights.length; w++) { targetWeights[w] += Math.abs(lowestWeight - 1); } } const benefitCutoffIndex = targetWeights.findIndex(s => s < targetWeights[0] / 2); if (benefitCutoffIndex > -1) { targetWeights = targetWeights.slice(0, benefitCutoffIndex); } const thresholds: integer[] = []; let totalWeight: integer; targetWeights.reduce((total: integer, w: integer) => { total += w; thresholds.push(total); totalWeight = total; return total; }, 0); const randValue = this.scene.randBattleSeedInt(totalWeight); let targetIndex: integer; thresholds.every((t, i) => { if (randValue >= t) { return true; } targetIndex = i; return false; }); return [ sortedBenefitScores[targetIndex][0] ]; } isPlayer() { return false; } hasTrainer(): boolean { return !!this.trainerSlot; } isBoss(): boolean { return !!this.bossSegments; } getBossSegmentIndex(): integer { const segments = (this as EnemyPokemon).bossSegments; const segmentSize = this.getMaxHp() / segments; for (let s = segments - 1; s > 0; s--) { const hpThreshold = Math.round(segmentSize * s); if (this.hp > hpThreshold) { return s; } } return 0; } damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer { if (this.isFainted()) { return 0; } let clearedBossSegmentIndex = this.isBoss() ? this.bossSegmentIndex + 1 : 0; if (this.isBoss() && !ignoreSegments) { const segmentSize = this.getMaxHp() / this.bossSegments; for (let s = this.bossSegmentIndex; s > 0; s--) { const hpThreshold = segmentSize * s; const roundedHpThreshold = Math.round(hpThreshold); if (this.hp >= roundedHpThreshold) { if (this.hp - damage <= roundedHpThreshold) { const hpRemainder = this.hp - roundedHpThreshold; let segmentsBypassed = 0; while (segmentsBypassed < this.bossSegmentIndex && this.canBypassBossSegments(segmentsBypassed + 1) && (damage - hpRemainder) >= Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))) { segmentsBypassed++; //console.log('damage', damage, 'segment', segmentsBypassed + 1, 'segment size', segmentSize, 'damage needed', Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))); } damage = hpRemainder + Math.round(segmentSize * segmentsBypassed); clearedBossSegmentIndex = s - segmentsBypassed; } break; } } } switch (this.scene.currentBattle.battleSpec) { case BattleSpec.FINAL_BOSS: if (!this.formIndex && this.bossSegmentIndex < 1) { damage = Math.min(damage, this.hp - 1); } } const ret = super.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase); if (this.isBoss()) { if (ignoreSegments) { const segmentSize = this.getMaxHp() / this.bossSegments; clearedBossSegmentIndex = Math.ceil(this.hp / segmentSize); } if (clearedBossSegmentIndex <= this.bossSegmentIndex) { this.handleBossSegmentCleared(clearedBossSegmentIndex); } this.battleInfo.updateBossSegments(this); } return ret; } canBypassBossSegments(segmentCount: integer = 1): boolean { if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) { if (!this.formIndex && (this.bossSegmentIndex - segmentCount) < 1) { return false; } } return true; } handleBossSegmentCleared(segmentIndex: integer): void { while (segmentIndex - 1 < this.bossSegmentIndex) { let boostedStat = BattleStat.RAND; const battleStats = Utils.getEnumValues(BattleStat).slice(0, -3); const statWeights = new Array().fill(battleStats.length).filter((bs: BattleStat) => this.summonData.battleStats[bs] < 6).map((bs: BattleStat) => this.getStat(bs + 1)); const statThresholds: integer[] = []; let totalWeight = 0; for (const bs of battleStats) { totalWeight += statWeights[bs]; statThresholds.push(totalWeight); } const randInt = Utils.randSeedInt(totalWeight); for (const bs of battleStats) { if (randInt < statThresholds[bs]) { boostedStat = bs; break; } } let statLevels = 1; switch (segmentIndex) { case 1: if (this.bossSegments >= 3) { statLevels++; } break; case 2: if (this.bossSegments >= 5) { statLevels++; } break; } this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels, true, true)); this.bossSegmentIndex--; } } heal(amount: integer): integer { if (this.isBoss()) { const amountRatio = amount / this.getMaxHp(); const segmentBypassCount = Math.floor(amountRatio / (1 / this.bossSegments)); const segmentSize = this.getMaxHp() / this.bossSegments; for (let s = 1; s < this.bossSegments; s++) { const hpThreshold = segmentSize * s; if (this.hp <= Math.round(hpThreshold)) { const healAmount = Math.min(amount, this.getMaxHp() - this.hp, Math.round(hpThreshold + (segmentSize * segmentBypassCount) - this.hp)); this.hp += healAmount; return healAmount; } else if (s >= this.bossSegmentIndex) { return super.heal(amount); } } } return super.heal(amount); } getFieldIndex(): integer { return this.scene.getEnemyField().indexOf(this); } getBattlerIndex(): BattlerIndex { return BattlerIndex.ENEMY + this.getFieldIndex(); } addToParty(pokeballType: PokeballType) { const party = this.scene.getParty(); let ret: PlayerPokemon = null; if (party.length < 6) { this.pokeball = pokeballType; this.metLevel = this.level; this.metBiome = this.scene.arena.biomeType; const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this); party.push(newPokemon); ret = newPokemon; this.scene.triggerPokemonFormChange(newPokemon, SpeciesFormChangeActiveTrigger, true); } return ret; } } export interface TurnMove { move: Moves; targets?: BattlerIndex[]; result: MoveResult; virtual?: boolean; turn?: integer; } export interface QueuedMove { move: Moves; targets: BattlerIndex[]; ignorePP?: boolean; } export interface AttackMoveResult { move: Moves; result: DamageResult; damage: integer; critical: boolean; sourceId: integer; attackingPosition: BattlerIndex; } export class PokemonSummonData { public battleStats: integer[] = [ 0, 0, 0, 0, 0, 0, 0 ]; public moveQueue: QueuedMove[] = []; public disabledMove: Moves = Moves.NONE; public disabledTurns: integer = 0; public tags: BattlerTag[] = []; public abilitySuppressed: boolean = false; public abilitiesApplied: Abilities[] = []; public speciesForm: PokemonSpeciesForm; public fusionSpeciesForm: PokemonSpeciesForm; public ability: Abilities = Abilities.NONE; public gender: Gender; public fusionGender: Gender; public stats: integer[]; public moveset: PokemonMove[]; // If not initialized this value will not be populated from save data. public types: Type[] = null; } export class PokemonBattleData { public hitCount: integer = 0; public endured: boolean = false; public berriesEaten: BerryType[] = []; public abilitiesApplied: Abilities[] = []; public abilityRevealed: boolean = false; } export class PokemonBattleSummonData { /** The number of turns the pokemon has passed since entering the battle */ public turnCount: integer = 1; /** The list of moves the pokemon has used since entering the battle */ public moveHistory: TurnMove[] = []; } export class PokemonTurnData { public flinched: boolean; public acted: boolean; public hitCount: integer; public hitsLeft: integer; public damageDealt: integer = 0; public currDamageDealt: integer = 0; public damageTaken: integer = 0; public attacksReceived: AttackMoveResult[] = []; } export enum AiType { RANDOM, SMART_RANDOM, SMART } export enum MoveResult { PENDING, SUCCESS, FAIL, MISS, OTHER } export enum HitResult { EFFECTIVE = 1, SUPER_EFFECTIVE, NOT_VERY_EFFECTIVE, ONE_HIT_KO, NO_EFFECT, STATUS, HEAL, FAIL, MISS, OTHER, IMMUNE } export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO | HitResult.OTHER; /** * Wrapper class for the {@linkcode Move} class for Pokemon to interact with. * These are the moves assigned to a {@linkcode Pokemon} object. * It links to {@linkcode Move} class via the move ID. * Compared to {@linkcode Move}, this class also tracks if a move has received. * PP Ups, amount of PP used, and things like that. * @see {@linkcode isUsable} - checks if move is disabled, out of PP, or not implemented. * @see {@linkcode getMove} - returns {@linkcode Move} object by looking it up via ID. * @see {@linkcode usePp} - removes a point of PP from the move. * @see {@linkcode getMovePp} - returns amount of PP a move currently has. * @see {@linkcode getPpRatio} - returns the current PP amount / max PP amount. * @see {@linkcode getName} - returns name of {@linkcode Move}. **/ export class PokemonMove { public moveId: Moves; public ppUsed: integer; public ppUp: integer; public virtual: boolean; constructor(moveId: Moves, ppUsed?: integer, ppUp?: integer, virtual?: boolean) { this.moveId = moveId; this.ppUsed = ppUsed || 0; this.ppUp = ppUp || 0; this.virtual = !!virtual; } isUsable(pokemon: Pokemon, ignorePp?: boolean): boolean { if (this.moveId && pokemon.summonData?.disabledMove === this.moveId) { return false; } return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1) && !this.getMove().name.endsWith(" (N)"); } getMove(): Move { return allMoves[this.moveId]; } /** * Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp} * @param {number} count Amount of PP to use */ usePp(count: number = 1) { this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp()); } getMovePp(): integer { return this.getMove().pp + this.ppUp * Math.max(Math.floor(this.getMove().pp / 5), 1); } getPpRatio(): number { return 1 - (this.ppUsed / this.getMovePp()); } getName(): string { return this.getMove().name; } /** * Copies an existing move or creates a valid PokemonMove object from json representing one * @param {PokemonMove | any} source The data for the move to copy * @return {PokemonMove} A valid pokemonmove object */ static loadMove(source: PokemonMove | any): PokemonMove { return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.virtual); } }