pokerogue/src/field/pokemon.ts

3995 lines
153 KiB
TypeScript
Raw Normal View History

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, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr } 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 { AttackTypeBoosterModifier, DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonMultiHitModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, 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, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase } from "../phases";
import { BattleStat } from "../data/battle-stat";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, TypeImmuneTag, getBattlerTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather";
import { TempBattleStat } from "../data/temp-battle-stat";
import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag";
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr } 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 * as Overrides from "../overrides";
import i18next from "i18next";
import { speciesEggMoves } from "../data/egg-moves";
import { ModifierTier } from "../modifier/modifier-tier";
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 15:07:23 +10:00
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";
export enum FieldPosition {
CENTER,
LEFT,
RIGHT
}
2023-03-28 14:54:52 -04:00
export default abstract class Pokemon extends Phaser.GameObjects.Container {
public id: integer;
public name: string;
public species: PokemonSpecies;
public formIndex: integer;
public abilityIndex: integer;
2024-05-24 01:45:04 +02:00
public passive: boolean;
2023-03-28 14:54:52 -04:00
public shiny: boolean;
2024-04-18 22:52:26 -04:00
public variant: Variant;
2023-03-28 14:54:52 -04:00
public pokeball: PokeballType;
protected battleInfo: BattleInfo;
public level: integer;
public exp: integer;
public levelExp: integer;
2023-04-01 20:06:44 -04:00
public gender: Gender;
2023-03-28 14:54:52 -04:00
public hp: integer;
public stats: integer[];
public ivs: integer[];
2024-01-05 22:24:05 -05:00
public nature: Nature;
public natureOverride: Nature | -1;
2023-03-28 14:54:52 -04:00
public moveset: PokemonMove[];
2023-04-11 19:08:03 -04:00
public status: Status;
public friendship: integer;
2024-01-05 11:29:34 -05:00
public metLevel: integer;
public metBiome: Biome | -1;
public luck: integer;
public pauseEvolutions: boolean;
public pokerus: boolean;
2023-03-28 14:54:52 -04:00
2023-11-04 00:32:12 -04:00
public fusionSpecies: PokemonSpecies;
public fusionFormIndex: integer;
public fusionAbilityIndex: integer;
public fusionShiny: boolean;
2024-04-18 22:52:26 -04:00
public fusionVariant: Variant;
2023-11-04 00:32:12 -04:00
public fusionGender: Gender;
2024-04-26 18:27:00 -04:00
public fusionLuck: integer;
2023-11-04 00:32:12 -04:00
private summonDataPrimer: PokemonSummonData;
2023-04-03 23:38:31 -04:00
public summonData: PokemonSummonData;
2023-11-28 21:35:52 -05:00
public battleData: PokemonBattleData;
2023-04-03 23:38:31 -04:00
public battleSummonData: PokemonBattleSummonData;
public turnData: PokemonTurnData;
public fieldPosition: FieldPosition;
2023-04-10 23:15:06 -04:00
public maskEnabled: boolean;
public maskSprite: Phaser.GameObjects.Sprite;
2023-03-28 14:54:52 -04:00
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) {
2023-03-28 14:54:52 -04:00
super(scene, x, y);
2023-04-26 16:07:29 -04:00
if (!species.isObtainable() && this.isPlayer()) {
2023-12-07 17:43:56 -05:00
throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`;
}
2023-04-26 16:07:29 -04:00
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);
2023-03-28 14:54:52 -04:00
this.species = species;
this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL;
this.level = level;
this.abilityIndex = abilityIndex !== undefined
? abilityIndex
: (species.abilityHidden && hasHiddenAbility ? species.ability2 ? 2 : 1 : species.ability2 ? randAbilityIndex : 0);
if (formIndex !== undefined) {
this.formIndex = formIndex;
}
if (gender !== undefined) {
this.gender = gender;
}
if (shiny !== undefined) {
this.shiny = shiny;
}
if (variant !== undefined) {
2024-04-18 22:52:26 -04:00
this.variant = variant;
}
2023-03-28 14:54:52 -04:00
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;
}
2024-01-05 22:24:05 -05:00
this.nature = dataSource.nature || 0 as Nature;
this.natureOverride = dataSource.natureOverride !== undefined ? dataSource.natureOverride : -1;
2023-03-28 14:54:52 -04:00
this.moveset = dataSource.moveset;
2023-04-03 23:38:31 -04:00
this.status = dataSource.status;
this.friendship = dataSource.friendship !== undefined ? dataSource.friendship : this.species.baseFriendship;
2024-01-05 11:29:34 -05:00
this.metLevel = dataSource.metLevel || 5;
this.luck = dataSource.luck;
2024-01-05 11:29:34 -05:00
this.metBiome = dataSource.metBiome;
this.pauseEvolutions = dataSource.pauseEvolutions;
this.pokerus = !!dataSource.pokerus;
2023-11-04 00:32:12 -04:00
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;
2023-11-04 00:32:12 -04:00
this.fusionGender = dataSource.fusionGender;
2024-04-26 18:27:00 -04:00
this.fusionLuck = dataSource.fusionLuck;
2023-03-28 14:54:52 -04:00
} else {
this.id = Utils.randSeedInt(4294967296);
2024-03-29 00:03:54 -04:00
this.ivs = ivs || Utils.getIvsFromId(this.id);
2024-05-24 01:45:04 +02:00
if (this.gender === undefined) {
this.generateGender();
}
2023-03-28 14:54:52 -04:00
if (this.formIndex === undefined) {
2024-04-02 23:12:30 -04:00
this.formIndex = this.scene.getSpeciesFormIndex(species, this.gender, this.nature, this.isPlayer());
}
if (this.shiny === undefined) {
this.trySetShiny();
}
2023-03-28 14:54:52 -04:00
if (this.variant === undefined) {
2024-04-19 01:25:19 -04:00
this.variant = this.shiny ? this.generateVariant() : 0;
}
2024-04-18 22:52:26 -04:00
if (nature !== undefined) {
2024-04-02 23:12:30 -04:00
this.setNature(nature);
} else {
2024-04-02 23:12:30 -04:00
this.generateNature();
}
2024-04-02 23:12:30 -04:00
this.natureOverride = -1;
this.friendship = species.baseFriendship;
2024-01-05 11:29:34 -05:00
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();
}
2023-12-31 11:15:57 -05:00
}
2024-04-26 19:36:27 -04:00
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
this.fusionLuck = this.luck;
2023-03-28 14:54:52 -04:00
}
this.generateName();
if (!species.isObtainable()) {
2023-04-26 16:07:29 -04:00
this.shiny = false;
}
2023-04-26 16:07:29 -04:00
2023-03-28 14:54:52 -04:00
this.calculateStats();
2024-01-07 23:17:24 -05:00
}
2023-03-28 14:54:52 -04:00
2024-01-07 23:17:24 -05:00
init(): void {
this.fieldPosition = FieldPosition.CENTER;
2024-01-07 23:17:24 -05:00
this.initBattleInfo();
this.scene.fieldUI.addAt(this.battleInfo, 0);
2023-03-28 14:54:52 -04:00
2023-06-04 21:47:43 -04:00
const getSprite = (hasShadow?: boolean) => {
const ret = this.scene.addPokemonSprite(this, 0, 0, `pkmn__${this.isPlayer() ? "back__" : ""}sub`, undefined, true);
2023-03-28 14:54:52 -04:00
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()) });
2023-03-28 14:54:52 -04:00
return ret;
};
this.setScale(this.getSpriteScale());
2024-05-24 01:45:04 +02:00
2023-06-04 21:47:43 -04:00
const sprite = getSprite(true);
2023-03-28 14:54:52 -04:00
const tintSprite = getSprite();
tintSprite.setVisible(false);
this.addAt(sprite, 0);
this.addAt(tintSprite, 1);
2023-03-28 14:54:52 -04:00
if (this.isShiny() && !this.shinySparkle) {
this.initShinySparkle();
}
2023-03-28 14:54:52 -04:00
}
2024-01-07 23:17:24 -05:00
abstract initBattleInfo(): void;
2023-06-06 10:14:53 -04:00
isOnField(): boolean {
if (!this.scene) {
2023-10-31 14:09:33 -04:00
return false;
}
2023-06-06 10:14:53 -04:00
return this.scene.field.getIndex(this) > -1;
}
isFainted(checkStatus?: boolean): boolean {
return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT);
}
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 15:07:23 +10:00
/**
* 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) {
2023-10-31 14:09:33 -04:00
return false;
}
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 15:07:23 +10:00
return this.isAllowedInBattle() && !!this.scene && (!onField || this.isOnField());
}
getDexAttr(): bigint {
2023-11-12 23:47:04 -05:00
let ret = 0n;
ret |= this.gender !== Gender.FEMALE ? DexAttr.MALE : DexAttr.FEMALE;
ret |= !this.shiny ? DexAttr.NON_SHINY : DexAttr.SHINY;
2024-04-18 22:52:26 -04:00
ret |= this.variant >= 2 ? DexAttr.VARIANT_3 : this.variant === 1 ? DexAttr.VARIANT_2 : DexAttr.DEFAULT_VARIANT;
2023-11-12 23:47:04 -05:00
ret |= this.scene.gameData.getFormAttr(this.formIndex);
return ret;
}
generateName(): void {
if (!this.fusionSpecies) {
2023-12-07 17:43:56 -05:00
this.name = this.species.getName(this.formIndex);
return;
}
2023-12-07 17:43:56 -05:00
this.name = getFusedSpeciesName(this.species.getName(this.formIndex), this.fusionSpecies.getName(this.fusionFormIndex));
if (this.battleInfo) {
this.updateInfo(true);
}
}
2023-03-28 14:54:52 -04:00
abstract isPlayer(): boolean;
abstract hasTrainer(): boolean;
abstract getFieldIndex(): integer;
abstract getBattlerIndex(): BattlerIndex;
loadAssets(ignoreOverride: boolean = true): Promise<void> {
2023-03-29 00:31:25 -04:00
return new Promise(resolve => {
const moveIds = this.getMoveset().map(m => m.getMove().id);
2024-04-23 22:00:23 -04:00
Promise.allSettled(moveIds.map(m => initMoveAnim(this.scene, m)))
2023-04-03 20:47:41 -04:00
.then(() => {
2023-04-14 18:21:33 -04:00
loadMoveAnimAssets(this.scene, moveIds);
2024-04-18 22:52:26 -04:00
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()) {
2024-04-18 22:52:26 -04:00
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));
}
2023-04-03 20:47:41 -04:00
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
if (this.isPlayer()) {
2023-04-12 19:09:15 -04:00
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 });
2023-04-12 19:09:15 -04:00
console.warn = originalWarn;
2024-05-24 22:57:28 -04:00
if (!(this.scene.anims.exists(this.getBattleSpriteKey()))) {
this.scene.anims.create({
key: this.getBattleSpriteKey(),
frames: battleFrameNames,
frameRate: 12,
repeat: -1
});
}
}
2023-04-04 21:10:11 -04:00
this.playAnim();
2024-04-18 22:52:26 -04:00
const updateFusionPaletteAndResolve = () => {
this.updateFusionPalette();
if (this.summonData?.speciesForm) {
2024-04-18 22:52:26 -04:00
this.updateFusionPalette(true);
}
2024-04-18 22:52:26 -04:00
resolve();
};
2024-04-19 11:11:19 -04:00
if (this.shiny) {
2024-04-18 22:52:26 -04:00
const populateVariantColors = (key: string, back: boolean = false): Promise<void> => {
return new Promise(resolve => {
const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, "");
2024-04-18 22:52:26 -04:00
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;
2024-04-18 22:52:26 -04:00
if (variantSet && variantSet[this.variant] === 1) {
if (variantColorCache.hasOwnProperty(key)) {
2024-04-18 22:52:26 -04:00
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 {
2024-04-18 22:52:26 -04:00
resolve();
}
2024-04-18 22:52:26 -04:00
});
};
if (this.isPlayer()) {
Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve());
} else {
populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve());
}
} else {
2024-04-18 22:52:26 -04:00
updateFusionPaletteAndResolve();
}
2023-04-03 20:47:41 -04:00
});
if (!this.scene.load.isLoading()) {
2023-04-03 20:47:41 -04:00
this.scene.load.start();
}
2023-03-29 00:31:25 -04:00
});
});
}
2023-12-07 17:43:56 -05:00
getFormKey(): string {
if (!this.species.forms.length || this.species.forms.length <= this.formIndex) {
return "";
}
2023-12-07 17:43:56 -05:00
return this.species.forms[this.formIndex].formKey;
}
getFusionFormKey(): string {
if (!this.fusionSpecies) {
2023-12-07 17:43:56 -05:00
return null;
}
if (!this.fusionSpecies.forms.length || this.fusionSpecies.forms.length <= this.fusionFormIndex) {
return "";
}
2023-12-07 17:43:56 -05:00
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}`;
2023-03-28 14:54:52 -04:00
}
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 {
2024-04-18 22:52:26 -04:00
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();
}
2024-04-19 00:58:59 -04:00
return this.getSpeciesForm(ignoreOverride).getSpriteId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant, back);
2023-03-28 14:54:52 -04:00
}
getSpriteKey(ignoreOverride?: boolean): string {
2024-04-18 22:52:26 -04:00
return this.getSpeciesForm(ignoreOverride).getSpriteKey(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
2023-03-28 14:54:52 -04:00
}
getBattleSpriteKey(back?: boolean, ignoreOverride?: boolean): string {
return `pkmn__${this.getBattleSpriteId(back, ignoreOverride)}`;
}
getFusionSpriteId(ignoreOverride?: boolean): string {
2024-04-18 22:52:26 -04:00
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();
}
2024-04-19 00:58:59 -04:00
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 {
2024-04-18 22:52:26 -04:00
return this.getSpeciesForm(ignoreOverride).getIconAtlasKey(this.formIndex, this.shiny, this.variant);
}
2024-04-10 10:57:06 -04:00
getFusionIconAtlasKey(ignoreOverride?: boolean): string {
2024-04-18 22:52:26 -04:00
return this.getFusionSpeciesForm(ignoreOverride).getIconAtlasKey(this.fusionFormIndex, this.fusionShiny, this.fusionVariant);
2024-04-10 10:57:06 -04:00
}
getIconId(ignoreOverride?: boolean): string {
2024-04-19 11:11:19 -04:00
return this.getSpeciesForm(ignoreOverride).getIconId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
2023-03-28 14:54:52 -04:00
}
2024-04-10 10:57:06 -04:00
getFusionIconId(ignoreOverride?: boolean): string {
2024-05-12 04:15:01 +10:00
return this.getFusionSpeciesForm(ignoreOverride).getIconId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant);
2024-04-10 10:57:06 -04:00
}
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) {
2023-11-04 00:32:12 -04:00
return this.fusionSpecies;
}
return this.fusionSpecies?.forms[this.fusionFormIndex];
2023-11-04 00:32:12 -04:00
}
2023-03-29 00:31:25 -04:00
getSprite(): Phaser.GameObjects.Sprite {
2023-03-28 14:54:52 -04:00
return this.getAt(0) as Phaser.GameObjects.Sprite;
}
2023-03-29 00:31:25 -04:00
getTintSprite(): Phaser.GameObjects.Sprite {
2023-04-10 23:15:06 -04:00
return !this.maskEnabled
? this.getAt(1) as Phaser.GameObjects.Sprite
: this.maskSprite;
2023-03-28 14:54:52 -04:00
}
getSpriteScale(): number {
2024-02-14 23:25:12 -05:00
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 as PokemonHeldItemModifier).pokemonId === this.id, this.isPlayer()) as PokemonHeldItemModifier[];
}
2024-02-27 22:50:27 -05:00
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}` : "";
2024-04-18 22:52:26 -04:00
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 });
2024-05-24 22:57:28 -04:00
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;
}
2024-05-24 01:45:04 +02:00
return true;
}
2024-05-24 01:45:04 +02:00
playAnim(): void {
this.tryPlaySprite(this.getSprite(), this.getTintSprite(), this.getBattleSpriteKey());
2023-04-04 21:10:11 -04:00
}
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<void> {
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];
}
2024-04-05 00:13:03 -04:00
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) {
2024-04-05 00:13:03 -04:00
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;
2024-04-05 00:13:03 -04:00
}
}
2023-11-05 23:27:40 -05:00
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, opponent, null, statLevel);
if (move) {
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel);
}
}
if (this.isPlayer()) {
2023-04-20 19:44:56 -04:00
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), battleStat as integer as TempBattleStat, statLevel);
}
const statValue = new Utils.NumberHolder(this.getStat(stat));
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;
}
}
2023-05-02 15:56:41 -04:00
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, battleStat, statValue);
let ret = statValue.value * (Math.max(2, 2 + statLevel.value) / Math.max(2, 2 - statLevel.value));
2023-12-22 22:46:05 -05:00
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
2024-05-24 01:45:04 +02:00
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;
2023-12-22 22:46:05 -05:00
}
const highestStatBoost = this.findTag(t => t instanceof HighestStatBoostTag && (t as HighestStatBoostTag).stat === stat) as HighestStatBoostTag;
if (highestStatBoost) {
ret *= highestStatBoost.multiplier;
}
return Math.floor(ret);
2023-04-10 23:15:06 -04:00
}
2023-04-10 07:59:00 -04:00
calculateStats(): void {
if (!this.stats) {
2023-03-28 14:54:52 -04:00
this.stats = [ 0, 0, 0, 0, 0, 0 ];
}
const baseStats = this.getSpeciesForm().baseStats.slice(0);
2023-11-04 00:32:12 -04:00
if (this.fusionSpecies) {
const fusionBaseStats = this.getFusionSpeciesForm().baseStats;
for (let s = 0; s < this.stats.length; s++) {
2023-11-04 00:32:12 -04:00
baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2);
}
2024-03-14 16:26:57 -04:00
} else if (this.scene.gameMode.isSplicedOnly) {
for (let s = 0; s < this.stats.length; s++) {
baseStats[s] = Math.ceil(baseStats[s] / 2);
}
2023-11-04 00:32:12 -04:00
}
2023-04-20 19:44:56 -04:00
this.scene.applyModifiers(PokemonBaseStatModifier, this.isPlayer(), this, baseStats);
2023-03-28 14:54:52 -04:00
const stats = Utils.getEnumValues(Stat);
for (const s of stats) {
2023-03-28 14:54:52 -04:00
const isHp = s === Stat.HP;
const baseStat = baseStats[s];
2023-11-12 23:47:04 -05:00
let value = Math.floor(((2 * baseStat + this.ivs[s]) * this.level) * 0.01);
2023-03-28 14:54:52 -04:00
if (isHp) {
2024-02-29 15:25:15 -05:00
value = value + this.level + 10;
if (this.hasAbility(Abilities.WONDER_GUARD, false, true)) {
2023-05-29 13:25:36 -04:00
value = 1;
}
if (this.hp > value || this.hp === undefined) {
2023-03-28 14:54:52 -04:00
this.hp = value;
} else if (this.hp) {
2023-03-28 14:54:52 -04:00
const lastMaxHp = this.getMaxHp();
if (lastMaxHp && value > lastMaxHp) {
2023-03-28 14:54:52 -04:00
this.hp += value - lastMaxHp;
}
2023-03-28 14:54:52 -04:00
}
2024-01-05 22:24:05 -05:00
} 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);
}
2024-01-05 22:24:05 -05:00
}
2023-03-28 14:54:52 -04:00
this.stats[s] = value;
}
}
getNature(): Nature {
return this.natureOverride !== -1 ? this.natureOverride : this.nature;
}
2024-01-05 22:24:05 -05:00
setNature(nature: Nature): void {
this.nature = nature;
this.calculateStats();
}
2024-01-09 23:34:43 -05:00
generateNature(naturePool?: Nature[]): void {
if (naturePool === undefined) {
2024-01-09 23:34:43 -05:00
naturePool = Utils.getEnumValues(Nature);
}
2024-01-09 23:34:43 -05:00
const nature = naturePool[Utils.randSeedInt(naturePool.length)];
this.setNature(nature);
}
2023-04-10 07:59:00 -04:00
getMaxHp(): integer {
return this.getStat(Stat.HP);
2023-03-28 14:54:52 -04:00
}
2023-04-11 11:04:39 -04:00
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;
2023-03-29 12:23:52 -04:00
}
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;
}
2023-11-05 23:48:04 -05:00
isShiny(): boolean {
2024-04-18 22:52:26 -04:00
return this.shiny || (this.isFusion() && this.fusionShiny);
}
getVariant(): Variant {
return !this.isFusion() ? this.variant : Math.max(this.variant, this.fusionVariant) as Variant;
2023-11-05 23:48:04 -05:00
}
2024-04-26 18:27:00 -04:00
getLuck(): integer {
2024-04-26 19:36:27 -04:00
return this.luck + (this.isFusion() ? this.fusionLuck : 0);
2024-04-26 18:27:00 -04:00
}
isFusion(): boolean {
return !!this.fusionSpecies;
}
2024-01-07 23:17:24 -05:00
abstract isBoss(): boolean;
getMoveset(ignoreOverride?: boolean): PokemonMove[] {
2023-12-11 21:46:49 -05:00
const ret = !ignoreOverride && this.summonData?.moveset
? this.summonData.moveset
: this.moveset;
// Overrides moveset based on arrays specified in overrides.ts
const overrideArray: Array<Moves> = 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));
});
}
2023-12-11 21:46:49 -05:00
return ret;
}
getLearnableLevelMoves(): Moves[] {
return this.getLevelMoves(1, 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}
*/
2024-03-28 10:28:05 -04:00
getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride?: boolean): Type[] {
2023-04-21 19:30:04 -04:00
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);
2024-05-24 01:45:04 +02:00
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);
}
}
2023-04-21 19:30:04 -04:00
}
2023-04-16 00:29:55 -04:00
// 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))) {
2023-04-21 19:30:04 -04:00
const flyingIndex = types.indexOf(Type.FLYING);
if (flyingIndex > -1) {
2023-04-21 19:30:04 -04:00
types.splice(flyingIndex, 1);
}
2023-04-16 00:29:55 -04:00
}
if (!types.length) { // become UNKNOWN if no types are present
types.push(Type.UNKNOWN);
}
2023-04-16 00:29:55 -04:00
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);
}
}
2023-04-21 19:30:04 -04:00
return types;
2023-04-16 00:29:55 -04:00
}
Add Challenges (#1459) * Initial challenge framework * Add type localisation * Change how challenges are tracked Also fixes the difficulty total text * MVP Renames challenge types, temporarily hides difficulty, and implements challenge saving. * Attempt to fix one legal pokemon in a double battle * Make monotype ignore type changing effects * Make isOfType correctly detect normal types * Try to fix double battles again * Make challenge function more like classic * Add helper function for fainted or not allowed * Add framework for fresh start challenge and improve comments * Try to fix evolution issues * Make form changing items only usable from rewards screen * Update localisation * Additional localisation change * Add achievements for completing challenges * Fix initialisation bug with challenge achievements * Add support for gamemode specific fixed battles Also make monogen challenges face the e4 of their generation * Add better support for mobile in challenges * Localise illegal evolution/form change message * Update achievement names * Make alternate forms count for monogen * Update monotype achievement icons * Add more comments * Improve comments * Fix mid battle form changes * Reorder mode list * Remove currently unused localisation entry * Add type overrides for monotype challenges Meloetta always counts for psychic and castform always counts for normal * Change how form changes are handled Now attempts a switch at the start of each turn instead of immediately * Add start button to challenge select screen * Make starter select back out to challenge screen if using challenges * Fix daily runs * Update tests to new game mode logic
2024-06-08 15:07:23 +10:00
isOfType(type: Type, includeTeraType: boolean = true, forDefend: boolean = false, ignoreOverride?: boolean): boolean {
return !!this.getTypes(includeTeraType, forDefend, ignoreOverride).some(t => t === type);
2023-04-22 22:14:53 -04:00
}
/**
* 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];
2023-04-26 23:33:13 -04:00
}
/**
* 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]];
2024-05-24 01:45:04 +02:00
}
/**
* 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;
}
/**
2024-05-24 01:45:04 +02:00
* 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;
}
/**
2024-05-24 01:45:04 +02:00
* 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<AbAttr>, 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;
}
2023-12-10 22:29:13 -05:00
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));
}
/**
* 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.type, 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 ? typeMultiplier.value : 0) as TypeDamageMultiplier;
}
getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false): TypeDamageMultiplier {
if (moveType === Type.STELLAR) {
return this.isTerastallized() ? 2 : 1;
}
2024-03-28 10:28:05 -04:00
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;
2024-04-06 23:03:20 -04:00
// 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) && multiplier >= 2 && this.isOfType(Type.FLYING) && getTypeDamageMultiplier(moveType, Type.FLYING) === 2) {
multiplier /= 2;
}
if (!!this.summonData?.tags.find((tag) => tag instanceof TypeImmuneTag && tag.immuneType === moveType)) {
multiplier = 0;
}
return multiplier;
}
2023-10-07 16:08:33 -04:00
getMatchupScore(pokemon: Pokemon): number {
const types = this.getTypes(true);
2024-03-28 10:28:05 -04:00
const enemyTypes = pokemon.getTypes(true, true);
2024-02-17 10:51:11 -05:00
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));
}
2024-02-17 10:51:11 -05:00
let hpDiffRatio = this.getHpRatio() + (1 - pokemon.getHpRatio());
if (outspeed) {
2024-02-17 10:51:11 -05:00
hpDiffRatio = Math.min(hpDiffRatio * 1.5, 1);
}
2024-02-17 10:51:11 -05:00
return (atkScore + defScore) * hpDiffRatio;
2023-10-07 16:08:33 -04:00
}
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;
}
}
}
}
2023-04-10 07:59:00 -04:00
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;
}
}
2023-04-10 07:59:00 -04:00
}
}
return null;
}
getLevelMoves(startingLevel?: integer, includeEvolutionMoves: boolean = false, simulateEvolutionChain: 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();
levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && !lm[0]) || ((!e || lm[0] > 1) && (e === evolutionChain.length - 1 || lm[0] <= evolutionChain[e + 1][1]))));
}
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;
});
} else {
levelMoves = this.getSpeciesForm(true).getLevelMoves();
}
if (this.fusionSpecies) {
2023-12-14 08:55:08 -05:00
const evolutionLevelMoves = levelMoves.slice(0, Math.max(levelMoves.findIndex(lm => !!lm[0]), 0));
const fusionLevelMoves = this.getFusionSpeciesForm(true).getLevelMoves();
const fusionEvolutionLevelMoves = fusionLevelMoves.slice(0, Math.max(fusionLevelMoves.findIndex(flm => !!flm[0]), 0));
const newLevelMoves: LevelMoves = [];
while (levelMoves.length && levelMoves[0][0] < startingLevel) {
levelMoves.shift();
}
while (fusionLevelMoves.length && fusionLevelMoves[0][0] < startingLevel) {
fusionLevelMoves.shift();
}
2023-12-14 08:55:08 -05:00
if (includeEvolutionMoves) {
for (const elm of evolutionLevelMoves.reverse()) {
2023-12-14 08:55:08 -05:00
levelMoves.unshift(elm);
}
for (const felm of fusionEvolutionLevelMoves.reverse()) {
fusionLevelMoves.unshift(felm);
}
2023-12-14 08:55:08 -05:00
}
2023-12-09 10:03:36 -05:00
for (let l = includeEvolutionMoves ? 0 : startingLevel; l <= this.level; l++) {
if (l === 1 && startingLevel > 1) {
2023-12-09 10:03:36 -05:00
l = startingLevel;
}
while (levelMoves.length && levelMoves[0][0] === l) {
const levelMove = levelMoves.shift();
if (!newLevelMoves.find(lm => lm[1] === levelMove[1])) {
newLevelMoves.push(levelMove);
}
}
while (fusionLevelMoves.length && fusionLevelMoves[0][0] === l) {
const fusionLevelMove = fusionLevelMoves.shift();
if (!newLevelMoves.find(lm => lm[1] === fusionLevelMove[1])) {
newLevelMoves.push(fusionLevelMove);
}
}
}
levelMoves = newLevelMoves;
}
if (levelMoves) {
for (const lm of levelMoves) {
2023-04-10 13:54:06 -04:00
const level = lm[0];
if ((!includeEvolutionMoves || level) && level < startingLevel) {
2023-04-10 13:54:06 -04:00
continue;
} else if (level > this.level) {
2023-04-10 13:54:06 -04:00
break;
}
ret.push(lm);
2023-04-10 13:54:06 -04:00
}
}
return ret;
2023-04-10 07:59:00 -04:00
}
2024-05-24 01:45:04 +02:00
setMove(moveIndex: integer, moveId: Moves): void {
const move = moveId ? new PokemonMove(moveId) : null;
2023-05-06 17:31:45 -04:00
this.moveset[moveIndex] = move;
if (this.summonData?.moveset) {
this.summonData.moveset[moveIndex] = move;
}
}
2023-04-10 07:59:00 -04:00
/**
* 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) {
2024-06-14 12:42:37 -04:00
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)
2024-05-30 17:25:53 -04:00
* 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
*/
2024-04-18 22:52:26 -04:00
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))) {
2024-04-18 22:52:26 -04:00
return 0;
}
2024-04-18 22:52:26 -04:00
const rand = Utils.randSeedInt(10);
2024-05-30 17:25:53 -04:00
if (rand >= 4) {
return 0; // 6/10
} else if (rand >= 1) {
2024-05-30 17:25:53 -04:00
return 1; // 3/10
} else {
return 2; // 1/10
}
2024-04-18 22:52:26 -04:00
}
2023-11-08 18:36:30 -05:00
generateFusionSpecies(forStarter?: boolean): void {
const hiddenAbilityChance = new Utils.IntegerHolder(256);
if (!this.hasTrainer()) {
2023-11-08 18:36:30 -05:00
this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
}
2023-11-08 18:36:30 -05:00
const hasHiddenAbility = !Utils.randSeedInt(hiddenAbilityChance.value);
const randAbilityIndex = Utils.randSeedInt(2);
const filter = !forStarter ? this.species.getCompatibleFusionSpeciesFilter()
: species => {
return pokemonEvolutions.hasOwnProperty(species.speciesId)
2023-11-08 18:36:30 -05:00
&& !pokemonPrevolutions.hasOwnProperty(species.speciesId)
&& !species.pseudoLegendary
&& !species.legendary
&& !species.mythical
&& !species.isTrainerForbidden()
&& species.speciesId !== this.species.speciesId;
};
2024-05-24 01:45:04 +02:00
2023-11-08 18:36:30 -05:00
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;
2024-04-18 22:52:26 -04:00
this.fusionVariant = this.variant;
2024-05-24 01:45:04 +02:00
if (this.fusionSpecies.malePercent === null) {
2023-11-08 18:36:30 -05:00
this.fusionGender = Gender.GENDERLESS;
} else {
2023-11-08 18:36:30 -05:00
const genderChance = (this.id % 256) * 0.390625;
if (genderChance < this.fusionSpecies.malePercent) {
2023-11-08 18:36:30 -05:00
this.fusionGender = Gender.MALE;
} else {
2023-11-08 18:36:30 -05:00
this.fusionGender = Gender.FEMALE;
}
2023-11-08 18:36:30 -05:00
}
this.fusionFormIndex = this.scene.getSpeciesFormIndex(this.fusionSpecies, this.fusionGender, this.getNature(), true);
2024-04-26 18:27:00 -04:00
this.fusionLuck = this.luck;
this.generateName();
2023-11-08 18:36:30 -05:00
}
clearFusionSpecies(): void {
this.fusionSpecies = undefined;
this.fusionFormIndex = 0;
this.fusionAbilityIndex = 0;
this.fusionShiny = false;
this.fusionVariant = 0;
this.fusionGender = 0;
2024-04-26 18:27:00 -04:00
this.fusionLuck = 0;
this.generateName();
this.calculateStats();
}
2023-04-10 07:59:00 -04:00
generateAndPopulateMoveset(): void {
2023-03-28 14:54:52 -04:00
this.moveset = [];
let movePool: [Moves, number][] = [];
const allLevelMoves = this.getLevelMoves(1, true, true);
2023-03-28 14:54:52 -04:00
if (!allLevelMoves) {
console.log(this.species.speciesId, "ERROR");
2023-03-28 14:54:52 -04:00
return;
}
2023-04-29 01:40:24 -04:00
2023-03-28 14:54:52 -04:00
for (let m = 0; m < allLevelMoves.length; m++) {
const levelMove = allLevelMoves[m];
if (this.level < levelMove[0]) {
2023-03-28 14:54:52 -04:00
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]);
}
2023-03-28 14:54:52 -04:00
}
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))]);
2023-03-28 14:54:52 -04:00
// 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));
}
2023-03-28 14:54:52 -04:00
}
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));
2023-03-28 14:54:52 -04:00
}
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger);
2023-03-28 14:54:52 -04:00
}
trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean {
const move = this.getMoveset().length > moveIndex
? this.getMoveset()[moveIndex]
2023-03-28 14:54:52 -04:00
: null;
2023-10-25 09:41:37 -04:00
return move?.isUsable(this, ignorePp);
2023-03-28 14:54:52 -04:00
}
2024-01-07 23:17:24 -05:00
showInfo(): void {
2023-03-28 14:54:52 -04:00
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);
}
2024-01-07 23:17:24 -05:00
this.battleInfo.setX(this.battleInfo.x + (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
2023-03-28 14:54:52 -04:00
this.battleInfo.setVisible(true);
if (this.isPlayer()) {
2024-04-04 18:54:50 -04:00
this.battleInfo.expMaskRect.x += 150;
}
2023-03-28 14:54:52 -04:00
this.scene.tweens.add({
2024-04-04 18:54:50 -04:00
targets: [ this.battleInfo, this.battleInfo.expMaskRect ],
x: this.isPlayer() ? "-=150" : `+=${!this.isBoss() ? 150 : 246}`,
2023-03-28 14:54:52 -04:00
duration: 1000,
ease: "Cubic.easeOut"
2023-03-28 14:54:52 -04:00
});
}
}
hideInfo(): Promise<void> {
return new Promise(resolve => {
if (this.battleInfo.visible) {
this.scene.tweens.add({
2024-04-04 18:54:50 -04:00
targets: [ this.battleInfo, this.battleInfo.expMaskRect ],
x: this.isPlayer() ? "+=150" : `-=${!this.isBoss() ? 150 : 246}`,
duration: 500,
ease: "Cubic.easeIn",
onComplete: () => {
if (this.isPlayer()) {
2024-04-04 18:54:50 -04:00
this.battleInfo.expMaskRect.x -= 150;
}
this.battleInfo.setVisible(false);
2024-01-07 23:17:24 -05:00
this.battleInfo.setX(this.battleInfo.x - (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
resolve();
}
});
} else {
resolve();
}
});
2023-03-28 14:54:52 -04:00
}
2023-04-10 07:59:00 -04:00
updateInfo(instant?: boolean): Promise<void> {
return this.battleInfo.updateInfo(this, instant);
2023-03-28 14:54:52 -04:00
}
/**
* Show or hide the type effectiveness multiplier window
* Passing undefined will hide the window
*/
updateEffectiveness(effectiveness?: string) {
this.battleInfo.updateEffectiveness(effectiveness);
}
2024-04-30 23:02:16 -04:00
toggleStats(visible: boolean): void {
this.battleInfo.toggleStats(visible);
}
toggleFlyout(visible: boolean): void {
this.battleInfo.toggleFlyout(visible);
}
2024-04-30 23:02:16 -04:00
2023-03-28 14:54:52 -04:00
addExp(exp: integer) {
const maxExpLevel = this.scene.getMaxExpLevel();
const initialExp = this.exp;
2023-03-28 14:54:52 -04:00
this.exp += exp;
while (this.level < maxExpLevel && this.exp >= getLevelTotalExp(this.level + 1, this.species.growthRate)) {
2023-03-28 14:54:52 -04:00
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);
2023-03-28 14:54:52 -04:00
}
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() ? "the opposing team" : "your team";
}
getAlly(): Pokemon {
return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1];
}
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();
2024-05-24 01:45:04 +02:00
const variableCategory = new Utils.IntegerHolder(move.category);
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, variableCategory);
const moveCategory = variableCategory.value as MoveCategory;
2024-04-07 22:27:07 -07:00
const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1);
applyMoveAttrs(VariableMoveTypeAttr, source, this, move);
applyPreAttackAbAttrs(MoveTypeChangeAttr, source, this, move, typeChangeMovePowerMultiplier);
const types = this.getTypes(true, true);
2024-04-07 22:27:07 -07:00
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.type, source)
: 1);
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
if (typeless) {
typeMultiplier.value = 1;
}
if (types.find(t => move.isTypeImmune(source, this, t))) {
2024-03-30 17:23:58 -04:00
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);
}
2023-04-15 01:32:16 -04:00
switch (moveCategory) {
case MoveCategory.PHYSICAL:
case MoveCategory.SPECIAL:
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
const power = new Utils.NumberHolder(move.power);
const sourceTeraType = source.getTeraType();
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === move.type && power.value < 60 && move.priority <= 0 && !move.hasAttr(MultiHitAttr) && !this.scene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
power.value = 60;
}
applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, this, move, power);
if (source.getAlly()?.hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) {
applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, source, this, move, power);
}
const fieldAuras = new Set(
this.scene.getField(true)
.map((p) => p.getAbilityAttrs(FieldMoveTypePowerBoostAbAttr) as FieldMoveTypePowerBoostAbAttr[])
.flat(),
);
for (const aura of fieldAuras) {
// The only relevant values are `move` and the `power` holder
aura.applyPreAttack(null, null, null, move, [power]);
}
power.value *= typeChangeMovePowerMultiplier.value;
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) {
result = HitResult.NO_EFFECT;
} else {
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag;
if (typeBoost) {
power.value *= typeBoost.boostValue;
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);
if (this.scene.arena.getTerrainType() === TerrainType.GRASSY && this.isGrounded() && move.type === Type.GROUND && move.moveTarget === MoveTarget.ALL_NEAR_OTHERS) {
power.value /= 2;
}
applyMoveAttrs(VariablePowerAttr, source, this, move, power);
this.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power);
if (!typeless) {
this.scene.arena.applyTags(WeakenMoveTypeTag, move.type, power);
this.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, move.type, power);
}
if (source.getTag(HelpingHandTag)) {
power.value *= 1.5;
}
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(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;
}
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));
[Testing] Flexible Testing Wrapper for Phaser-Based Battle-Scenes (#1908) * refactor executed code while importing and initializing all of these in loading-scene * reset to main * fix server url * added rule no-trailing-spaces * made progress * test somme data from a session save is working * trying to launch a battle * added fetch wrapper to load data locally * trying to mockAllSettled * pushPhase & shiftPhase * check integrity of exported session * set toke + loggedInUser in tests * progress on starting new battle * tring to test phase but it's async * mocking fetch * working mock fetch * need to handle pile of data * attempt to use real phaser classes * reorder overrides * refactored to use some real classes from phaser * removed useless things * started to work on some container mock * finished the mockContainer time to add some logic * some more mock containers * removed addMethods since there is the mock classes now * commented issues * attempt to create mockTextureManager * fix tests * mockSprite & mockText * yes but not really * yes but not really * fix tutorial callback * reached mode title * added achievement tests * fix test achievements with current state of mock * correct sequence loading for BattleScene with mockLoader ! * deep dive into next step * working wait until starter selection screen * added newGame method into wrapper * expect to save_slot * trying to manage pokemon sprite for getAll without success yet * added test for egg output * fixed egg test for June * fix tests + locate next issue to fix * we are in battle baby * added new game in one-line * export is working but export only what's in the fetch * fix start game as guest * refactored how we start a battle + cleanup * overrided mewtwo but issue with currentBattle * refactor: rename InitAchievements to initAchievements * added missing mock method * override level and pokemon forms working as intended * bringToTop Obj * remove launch battle in achivement test * fix getIndex when same pokemon * can run all tests * first attack, faint, and shop modifiers, MockClock * on method for container * added doAttack one-liner * one-line export data * removed throw error * feat: Make `scenes` property of `GameWrapper` class public The `scenes` property of the `GameWrapper` class was changed from private to public. This change allows external access to the `scenes` map, which is used to store Phaser scenes. This modification was made to enable easier manipulation and interaction with the scenes in the game. * correction * removed CanvasRenderer * added a param to remove console.log and added a param to preven scene create call * fix encounter wave 30 when it's a trainer * test double-battle * test fight without KO * test double fight no ko * fix crashing texture + added Text wrapper to log fight * fix tests on boss - trainer - rival * chore: Refactor BattleScene initialization and add new phases Refactor the BattleScene initialization code to remove unnecessary delay and improve performance. Also, add new phases for the title and unavailable states to enhance the game experience. * rework of Game tests * skipFn is working * added onNextPrompt and restore Og Start * better newGame * added skipFN in remove * not yet working test but updated interceptors * do attack work but not on PostSummonPhase phase when there is mention of silcoon and wurmple * error located, it's just a double fight, i was not there yet * single OHKO & double no OHKO * added expirationFn into next prompt * all tests are passing * working test on non damaging move from opponent * cleaned a bit * removed phaser initialisation on every tests * renamed test file * added load system data * added some ability support * added onKill & onSummon abilities test * removed useless test + cleanup * removed useless test + cleanup * fixed tests after merge main * added itemHeld endTurn trigger test (toxic orb) * added runFrom..To * added mustRun to assert currentPhase * added no-miss move to test things * cleaner restore mock * fix test * fix moxie test + game speed * improve test speed * added onOurself and onOpponent mvoe test * added onDamage test for tackle * removed timeout in intervals to run tests faster * cleanup * added never crit override + separate file per test + remove randomness in randBattleSeedInt * move folders * better org * renamed itemHeld folder to items * fix deploy.yml * cleanup * simplified the gameManager start battle and allow single pokemon in party * remove the need of mode development * added input handler to test inputs + remove time from phaser into inputController * added keyboard support * added fakeMobile support * added details * removed a console.log + added logUp * move test to folder * fixed canvas issue * added starter select tests * added some more test on starter-select * added battle-order tests * added battle-order tests * fixing Phaser RNG * ordering stats for better reading * fix tests for main * adapt battle-order test to be more readable * fix merge * fix some errors and silent all errors from gameWrapper since it's not possible to avoid them * fix mocks to manage childs & stuffs * added some docs * fix achievement test * removed an unused file * separate misc tests to clean battle.test file * added a basic french lokalization test * added i18n where it needs to be used only * revers extracted method * removed unused method * removed handler fetch since we do not test anything server related * fix test with handlers removed * added intrepid sword test * fix enum exp party --------- Co-authored-by: Frederico Santos <frederico.f.santos@tecnico.ulisboa.pt>
2024-06-08 00:33:45 +02:00
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;
2023-05-05 18:20:55 -04:00
}
}
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);
2024-05-24 01:45:04 +02:00
}
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);
}
applyMoveAttrs(VariableAtkAttr, source, this, move, sourceAtk);
applyMoveAttrs(VariableDefAttr, source, this, move, targetDef);
if (!isTypeImmune) {
damage.value = Math.ceil(((((2 * source.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2) * stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * ((this.scene.randBattleSeedInt(15) + 85) / 100) * criticalMultiplier.value);
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);
2024-05-24 01:45:04 +02:00
/**
* 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;
}
});
}
2023-04-13 12:16:36 -04:00
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;
}
2024-05-24 01:45:04 +02:00
if (!result) {
if (!typeMultiplier.value) {
result = move.id === Moves.SHEER_COLD ? HitResult.IMMUNE : HitResult.NO_EFFECT;
} else {
const oneHitKo = new Utils.BooleanHolder(false);
applyMoveAttrs(OneHitKOAttr, source, this, move, oneHitKo);
if (oneHitKo.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;
2023-11-07 22:23:42 -05:00
}
2023-04-26 23:33:13 -04:00
}
}
2023-04-15 01:32:16 -04:00
if (!fixedDamage.value) {
if (!source.isPlayer()) {
this.scene.applyModifiers(EnemyDamageBoosterModifier, false, damage);
}
if (!this.isPlayer()) {
this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage);
}
}
applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage);
applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, power);
if (power.value === 0) {
damage.value = 0;
}
console.log("damage", damage.value, move.name, power.value, 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);
const oneHitKo = result === HitResult.ONE_HIT_KO;
if (damage.value) {
if (this.getHpRatio() === 1) {
applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, move, cancelled, damage);
} else if (!this.isPlayer() && damage.value >= this.hp) {
this.scene.applyModifiers(EnemyEndureChanceModifier, false, this);
}
2024-03-09 23:49:00 +01:00
/**
* 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, oneHitKo, oneHitKo, true);
this.turnData.damageTaken += damage.value;
if (isCritical) {
this.scene.queueMessage(i18next.t("battle:hitResultCriticalHit"));
2023-04-26 23:33:13 -04:00
}
if (source.isPlayer()) {
this.scene.validateAchvs(DamageAchv, damage);
if (damage.value > this.scene.gameData.gameStats.highestDamage) {
this.scene.gameData.gameStats.highestDamage = damage.value;
2023-10-30 12:33:20 -04:00
}
2023-04-26 23:33:13 -04:00
}
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 };
this.turnData.attacksReceived.unshift(attackResult);
if (source.isPlayer() && !this.isPlayer()) {
this.scene.applyModifiers(DamageMoneyRewardModifier, true, source, damage);
}
}
if (source.turnData.hitsLeft === 1) {
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: this.name }));
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()) {
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), oneHitKo));
this.resetSummonData();
2023-04-15 01:32:16 -04:00
}
if (damage) {
this.scene.clearPhaseQueueSplice();
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: this.name }));
}
result = cancelled.value || !typeMultiplier.value ? HitResult.NO_EFFECT : HitResult.STATUS;
break;
2023-04-15 01:32:16 -04:00
}
return result;
2023-04-13 12:16:36 -04:00
}
damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false): integer {
if (this.isFainted()) {
2024-01-07 23:17:24 -05:00
return 0;
}
2024-04-06 01:48:42 -04:00
const surviveDamage = new Utils.BooleanHolder(false);
2023-04-16 18:40:32 -04:00
2024-04-06 01:48:42 -04:00
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) {
2023-04-23 21:31:06 -04:00
damage = this.hp - 1;
}
2023-04-23 21:31:06 -04:00
}
2024-01-07 23:17:24 -05:00
damage = Math.min(damage, this.hp);
this.hp = this.hp - damage;
if (this.isFainted() && !ignoreFaintPhase) {
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure));
2023-04-20 21:32:48 -04:00
this.resetSummonData();
2023-04-16 18:40:32 -04:00
}
2024-01-07 23:17:24 -05:00
return damage;
2023-04-16 18:40:32 -04:00
}
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;
}
2024-01-07 23:17:24 -05:00
heal(amount: integer): integer {
const healAmount = Math.min(amount, this.getMaxHp() - this.hp);
this.hp += healAmount;
return healAmount;
2023-10-21 20:52:19 -04:00
}
2023-12-18 21:53:28 -04:00
isBossImmune(): boolean {
2024-01-07 23:17:24 -05:00
return this.isBoss();
2023-12-18 21:53:28 -04:00
}
2024-04-24 01:57:59 +10:00
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 {
2023-04-15 01:32:16 -04:00
const existingTag = this.getTag(tagType);
if (existingTag) {
existingTag.onOverlap(this);
2023-04-13 12:16:36 -04:00
return false;
2023-04-15 01:32:16 -04:00
}
2023-04-13 12:16:36 -04:00
const newTag = getBattlerTag(tagType, turnCount, sourceMove, sourceId);
2023-04-21 19:30:04 -04:00
const cancelled = new Utils.BooleanHolder(false);
applyPreApplyBattlerTagAbAttrs(PreApplyBattlerTagAbAttr, this, newTag, cancelled);
if (!cancelled.value && newTag.canAdd(this)) {
2023-04-22 22:14:53 -04:00
this.summonData.tags.push(newTag);
newTag.onAdd(this);
return true;
}
return false;
2023-04-13 12:16:36 -04:00
}
getTag(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag {
if (!this.summonData) {
return null;
}
return typeof(tagType) === "string"
2023-04-15 01:32:16 -04:00
? this.summonData.tags.find(t => t.tagType === tagType)
: this.summonData.tags.find(t => t instanceof tagType);
2023-04-13 12:16:36 -04:00
}
2023-04-21 19:30:04 -04:00
findTag(tagFilter: ((tag: BattlerTag) => boolean)) {
if (!this.summonData) {
return null;
}
2023-04-15 01:32:16 -04:00
return this.summonData.tags.find(t => tagFilter(t));
2023-04-13 12:16:36 -04:00
}
getTags(tagType: BattlerTagType | Constructor<BattlerTag>): BattlerTag[] {
if (!this.summonData) {
return [];
}
return typeof(tagType) === "string"
2023-04-15 01:32:16 -04:00
? this.summonData.tags.filter(t => t.tagType === tagType)
: this.summonData.tags.filter(t => t instanceof tagType);
}
findTags(tagFilter: ((tag: BattlerTag) => boolean)): BattlerTag[] {
if (!this.summonData) {
return [];
}
2023-04-15 01:32:16 -04:00
return this.summonData.tags.filter(t => tagFilter(t));
}
2023-04-21 19:30:04 -04:00
lapseTag(tagType: BattlerTagType): boolean {
2023-04-13 12:16:36 -04:00
const tags = this.summonData.tags;
2023-04-15 01:32:16 -04:00
const tag = tags.find(t => t.tagType === tagType);
2023-04-21 19:30:04 -04:00
if (tag && !(tag.lapse(this, BattlerTagLapseType.CUSTOM))) {
2023-04-15 01:32:16 -04:00
tag.onRemove(this);
tags.splice(tags.indexOf(tag), 1);
}
2023-04-18 12:30:47 -04:00
return !!tag;
2023-04-15 01:32:16 -04:00
}
2023-04-21 19:30:04 -04:00
lapseTags(lapseType: BattlerTagLapseType): void {
2023-04-15 01:32:16 -04:00
const tags = this.summonData.tags;
2023-04-21 19:30:04 -04:00
tags.filter(t => lapseType === BattlerTagLapseType.FAINT || ((t.lapseType === lapseType) && !(t.lapse(this, lapseType))) || (lapseType === BattlerTagLapseType.TURN_END && t.turnCount < 1)).forEach(t => {
2023-04-15 01:32:16 -04:00
t.onRemove(this);
tags.splice(tags.indexOf(t), 1);
});
2023-04-13 12:16:36 -04:00
}
2024-02-20 12:27:38 -05:00
removeTag(tagType: BattlerTagType): boolean {
const tags = this.summonData.tags;
const tag = tags.find(t => t.tagType === tagType);
if (tag) {
tag.turnCount = 0;
tag.onRemove(this);
tags.splice(tags.indexOf(tag), 1);
}
return !!tag;
}
findAndRemoveTags(tagFilter: ((tag: BattlerTag) => boolean)): boolean {
if (!this.summonData) {
return false;
}
2023-04-22 22:14:53 -04:00
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 {
2024-03-29 15:43:36 -04:00
this.findAndRemoveTags(t => t.isSourceLinked() && t.sourceId === sourceId);
2023-04-22 22:14:53 -04:00
}
2023-04-28 19:26:41 -04:00
transferTagsBySourceId(sourceId: integer, newSourceId: integer): void {
if (!this.summonData) {
return;
}
2023-04-28 19:26:41 -04:00
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
*/
2023-04-28 19:26:41 -04:00
transferSummon(source: Pokemon): void {
const battleStats = Utils.getEnumValues(BattleStat);
for (const stat of battleStats) {
2023-04-28 19:26:41 -04:00
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;
}
2023-04-28 19:26:41 -04:00
this.summonData.tags.push(tag);
}
if (this instanceof PlayerPokemon && source.summonData.battleStats.find(bs => bs === 6)) {
this.scene.validateAchv(achvs.TRANSFER_MAX_BATTLE_STAT);
}
2024-04-30 23:23:32 -04:00
this.updateInfo();
2023-04-28 19:26:41 -04:00
}
2023-04-20 21:32:48 -04:00
getMoveHistory(): TurnMove[] {
2023-04-28 15:03:42 -04:00
return this.battleSummonData.moveHistory;
2023-04-20 21:32:48 -04:00
}
2023-04-25 01:32:48 -04:00
pushMoveHistory(turnMove: TurnMove) {
turnMove.turn = this.scene.currentBattle?.turn;
this.getMoveHistory().push(turnMove);
}
2023-04-13 12:16:36 -04:00
getLastXMoves(turnCount?: integer): TurnMove[] {
2023-04-20 21:32:48 -04:00
const moveHistory = this.getMoveHistory();
2023-04-18 12:30:47 -04:00
return moveHistory.slice(turnCount >= 0 ? Math.max(moveHistory.length - (turnCount || 1), 0) : 0, moveHistory.length).reverse();
2023-03-28 14:54:52 -04:00
}
2023-04-20 21:32:48 -04:00
getMoveQueue(): QueuedMove[] {
return this.summonData.moveQueue;
}
2024-01-09 23:34:43 -05:00
changeForm(formChange: SpeciesFormChange): Promise<void> {
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
2024-01-09 23:34:43 -05:00
this.abilityIndex = abilityCount - 1;
}
this.scene.gameData.setPokemonSeen(this, false);
this.setScale(this.getSpriteScale());
2024-01-09 23:34:43 -05:00
this.loadAssets().then(() => {
this.calculateStats();
this.scene.updateModifiers(this.isPlayer(), true);
2024-02-14 23:25:12 -05:00
Promise.all([ this.updateInfo(), this.scene.updateFieldScale() ]).then(() => resolve());
2024-01-09 23:34:43 -05:00
});
});
}
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound {
const scene = sceneOverride || this.scene;
const cry = this.getSpeciesForm().cry(scene, soundConfig);
2023-11-04 00:32:12 -04:00
let duration = cry.totalDuration * 1000;
if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) {
let fusionCry = this.getFusionSpeciesForm().cry(scene, soundConfig, true);
2023-11-04 00:32:12 -04:00
duration = Math.min(duration, fusionCry.totalDuration * 1000);
fusionCry.destroy();
scene.time.delayedCall(Utils.fixedInt(Math.ceil(duration * 0.4)), () => {
2023-11-04 00:32:12 -04:00
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);
2023-11-04 00:32:12 -04:00
} catch (err) {
console.error(err);
}
});
}
return cry;
2023-03-28 14:54:52 -04:00
}
2023-11-04 00:32:12 -04:00
faintCry(callback: Function): void {
if (this.fusionSpecies && this.getSpeciesForm() !== this.getFusionSpeciesForm()) {
2023-11-04 00:32:12 -04:00
return this.fusionFaintCry(callback);
}
2023-11-04 00:32:12 -04:00
2023-11-05 22:32:08 -05:00
const key = this.getSpeciesForm().getCryKey(this.formIndex);
//eslint-disable-next-line @typescript-eslint/no-unused-vars
2023-03-28 14:54:52 -04:00
let i = 0;
let rate = 0.85;
2023-11-04 00:32:12 -04:00
const cry = this.scene.playSound(key, { rate: rate }) as AnySound;
2023-03-28 14:54:52 -04:00
const sprite = this.getSprite();
2023-04-03 20:47:41 -04:00
const tintSprite = this.getTintSprite();
2023-03-28 14:54:52 -04:00
const delay = Math.max(this.scene.sound.get(key).totalDuration * 50, 25);
2023-11-04 00:32:12 -04:00
2023-03-28 14:54:52 -04:00
let frameProgress = 0;
let frameThreshold: number;
2024-05-24 01:45:04 +02:00
2023-03-28 14:54:52 -04:00
sprite.anims.pause();
2023-04-03 20:47:41 -04:00
tintSprite.anims.pause();
2023-11-04 00:32:12 -04:00
2023-03-28 14:54:52 -04:00
let faintCryTimer = this.scene.time.addEvent({
2023-10-26 16:33:59 -04:00
delay: Utils.fixedInt(delay),
2023-03-28 14:54:52 -04:00
repeat: -1,
callback: () => {
++i;
frameThreshold = sprite.anims.msPerFrame / rate;
frameProgress += delay;
while (frameProgress > frameThreshold) {
2023-04-03 20:47:41 -04:00
if (sprite.anims.duration) {
2023-03-28 14:54:52 -04:00
sprite.anims.nextFrame();
2023-04-03 20:47:41 -04:00
tintSprite.anims.nextFrame();
}
2023-03-28 14:54:52 -04:00
frameProgress -= frameThreshold;
}
2023-11-04 00:32:12 -04:00
if (cry && !cry.pendingRemove) {
2023-03-28 14:54:52 -04:00
rate *= 0.99;
2023-11-04 00:32:12 -04:00
cry.setRate(rate);
2023-11-10 16:41:02 -05:00
} else {
2023-03-28 14:54:52 -04:00
faintCryTimer.destroy();
faintCryTimer = null;
if (callback) {
2023-03-28 14:54:52 -04:00
callback();
}
2023-03-28 14:54:52 -04:00
}
}
});
2023-11-04 00:32:12 -04:00
2023-03-28 14:54:52 -04:00
// Failsafe
2023-10-26 16:33:59 -04:00
this.scene.time.delayedCall(Utils.fixedInt(3000), () => {
if (!faintCryTimer || !this.scene) {
2023-03-28 14:54:52 -04:00
return;
}
if (cry?.isPlaying) {
2023-11-04 00:32:12 -04:00
cry.stop();
}
2023-11-04 00:32:12 -04:00
faintCryTimer.destroy();
if (callback) {
2023-11-04 00:32:12 -04:00
callback();
}
2023-11-04 00:32:12 -04:00
});
}
private fusionFaintCry(callback: Function): void {
const key = this.getSpeciesForm().getCryKey(this.formIndex);
2023-11-04 00:32:12 -04:00
let i = 0;
let rate = 0.85;
const cry = this.scene.playSound(key, { rate: rate }) as AnySound;
2023-11-04 00:32:12 -04:00
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) {
2023-11-04 00:32:12 -04:00
cry.setRate(rate);
}
if (fusionCry && !fusionCry.pendingRemove) {
2023-11-04 00:32:12 -04:00
fusionCry.setRate(rate);
}
2023-11-04 00:32:12 -04:00
if ((!cry || cry.pendingRemove) && (!fusionCry || fusionCry.pendingRemove)) {
faintCryTimer.destroy();
faintCryTimer = null;
if (callback) {
2023-11-04 00:32:12 -04:00
callback();
}
2023-11-04 00:32:12 -04:00
}
}
});
// Failsafe
this.scene.time.delayedCall(Utils.fixedInt(3000), () => {
if (!faintCryTimer || !this.scene) {
2023-11-04 00:32:12 -04:00
return;
}
if (cry?.isPlaying) {
2023-11-04 00:32:12 -04:00
cry.stop();
}
if (fusionCry?.isPlaying) {
2023-11-04 00:32:12 -04:00
fusionCry.stop();
}
2023-03-28 14:54:52 -04:00
faintCryTimer.destroy();
if (callback) {
2023-03-28 14:54:52 -04:00
callback();
}
2023-03-28 14:54:52 -04:00
});
}
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 {
2024-03-13 20:36:25 -04:00
if (effect !== StatusEffect.FAINT) {
if (overrideStatus ? this.status?.effect === effect : this.status) {
2024-03-13 20:36:25 -04:00
return false;
}
if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.MISTY) {
2024-03-13 20:36:25 -04:00
return false;
}
2024-03-13 20:36:25 -04:00
}
const types = this.getTypes(true, true);
2023-04-11 19:08:03 -04:00
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)) {
2023-04-11 19:08:03 -04:00
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;
2023-04-11 19:08:03 -04:00
}
const cancelled = new Utils.BooleanHolder(false);
applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled, quiet);
if (cancelled.value) {
return false;
}
return true;
}
2024-05-24 01:45:04 +02:00
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 (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);
2024-01-16 00:28:03 -05:00
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);
2024-01-09 23:34:43 -05:00
if (effect !== StatusEffect.FAINT) {
2024-01-09 23:34:43 -05:00
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true);
}
2024-01-09 23:34:43 -05:00
2023-04-11 19:08:03 -04:00
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;
}
2023-04-11 19:08:03 -04:00
this.status = undefined;
2023-04-16 18:40:32 -04:00
if (lastStatus === StatusEffect.SLEEP) {
this.setFrameRate(12);
if (this.getTag(BattlerTagType.NIGHTMARE)) {
2023-04-21 19:30:04 -04:00
this.lapseTag(BattlerTagType.NIGHTMARE);
}
2023-04-16 18:40:32 -04:00
}
if (confusion) {
if (this.getTag(BattlerTagType.CONFUSED)) {
this.lapseTag(BattlerTagType.CONFUSED);
}
}
if (reloadAssets) {
this.loadAssets(false).then(() => this.playAnim());
}
2023-04-11 19:08:03 -04:00
}
primeSummonData(summonDataPrimer: PokemonSummonData): void {
this.summonDataPrimer = summonDataPrimer;
}
2023-04-11 19:08:03 -04:00
resetSummonData(): void {
if (this.summonData?.speciesForm) {
this.summonData.speciesForm = null;
this.updateFusionPalette();
}
2023-04-03 23:38:31 -04:00
this.summonData = new PokemonSummonData();
if (!this.battleData) {
2023-11-28 21:35:52 -05:00
this.resetBattleData();
}
2023-04-03 23:38:31 -04:00
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;
}
2024-04-30 23:23:32 -04:00
this.updateInfo();
2023-04-03 23:38:31 -04:00
}
2023-11-28 21:35:52 -05:00
resetBattleData(): void {
this.battleData = new PokemonBattleData();
}
2023-04-11 19:08:03 -04:00
resetBattleSummonData(): void {
2023-04-03 23:38:31 -04:00
this.battleSummonData = new PokemonBattleSummonData();
if (this.getTag(BattlerTagType.SEEDED)) {
2023-04-21 19:30:04 -04:00
this.lapseTag(BattlerTagType.SEEDED);
}
if (this.scene) {
this.scene.triggerPokemonFormChange(this, SpeciesFormChangePostMoveTrigger, true);
}
2023-04-03 23:38:31 -04:00
}
2023-04-11 19:08:03 -04:00
resetTurnData(): void {
2023-04-03 23:38:31 -04:00
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
2024-03-17 11:36:19 -04:00
return ((this.getSpeciesForm().getBaseExp() * this.level) / 5 + 1);
2023-03-28 14:54:52 -04:00
}
setFrameRate(frameRate: integer) {
this.scene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate;
this.getSprite().play(this.getBattleSpriteKey());
this.getTintSprite().play(this.getBattleSpriteKey());
}
2023-03-28 14:54:52 -04:00
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"
2023-03-28 14:54:52 -04:00
});
} else {
2023-03-28 14:54:52 -04:00
tintSprite.setAlpha(alpha);
}
2023-03-28 14:54:52 -04:00
}
untint(duration: integer, ease?: string) {
const tintSprite = this.getTintSprite();
if (duration) {
this.scene.tweens.add({
targets: tintSprite,
alpha: 0,
duration: duration,
ease: ease || "Linear",
2023-04-10 23:15:06 -04:00
onComplete: () => {
tintSprite.setVisible(false);
tintSprite.setAlpha(1);
}
2023-03-28 14:54:52 -04:00
});
2023-04-10 23:15:06 -04:00
} else {
2023-03-28 14:54:52 -04:00
tintSprite.setVisible(false);
2023-04-10 23:15:06 -04:00
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);
2023-04-10 23:15:06 -04:00
this.maskEnabled = true;
}
}
disableMask() {
if (this.maskEnabled) {
this.maskSprite.setVisible(false);
this.maskSprite.setPosition(0, 0);
this.maskSprite.setScale(this.getSpriteScale());
2023-04-10 23:15:06 -04:00
this.maskSprite = null;
this.maskEnabled = false;
}
2023-03-28 14:54:52 -04:00
}
sparkle(): void {
if (this.shinySparkle) {
this.shinySparkle.play(`sparkle${this.variant ? `_${this.variant + 1}` : ""}`);
this.scene.playSound("sparkle");
2023-03-28 14:54:52 -04:00
}
}
updateFusionPalette(ignoreOveride?: boolean): void {
if (!this.getFusionSpeciesForm(ignoreOveride)) {
2023-11-23 23:52:13 -05:00
[ this.getSprite(), this.getTintSprite() ].map(s => {
s.pipelineData[`spriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = [];
s.pipelineData[`fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = [];
2023-11-23 23:52:13 -05:00
});
return;
}
const speciesForm = this.getSpeciesForm(ignoreOveride);
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOveride);
2024-04-18 22:52:26 -04:00
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__");
2024-04-18 22:52:26 -04:00
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__");
2024-04-18 22:52:26 -04:00
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);
2023-11-23 23:52:13 -05:00
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);
2023-11-23 23:52:13 -05:00
const canvas = document.createElement("canvas");
const backCanvas = document.createElement("canvas");
const fusionCanvas = document.createElement("canvas");
const fusionBackCanvas = document.createElement("canvas");
2023-11-23 23:52:13 -05:00
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];
2023-11-23 23:52:13 -05:00
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);
2023-11-23 23:52:13 -05:00
const imageData = context.getImageData(frame.cutX, frame.cutY, frame.width, frame.height);
pixelData.push(imageData.data);
});
for (let f = 0; f < 2; f++) {
2024-04-18 22:52:26 -04:00
const variantColors = variantColorCache[!f ? spriteKey : backSpriteKey];
const variantColorSet = new Map<integer, integer[]>();
2024-04-20 16:08:33 -04:00
if (this.shiny && variantColors && variantColors[this.variant]) {
2024-04-18 22:52:26 -04:00
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);
2024-04-18 22:52:26 -04:00
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 ]);
}
}
2023-11-23 23:52:13 -05:00
}
}
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] }));
}
2023-11-23 23:52:13 -05:00
}
const fusionPixelColors = [];
for (let f = 0; f < 2; f++) {
2024-04-18 22:52:26 -04:00
const variantColors = variantColorCache[!f ? fusionSpriteKey : fusionBackSpriteKey];
const variantColorSet = new Map<integer, integer[]>();
2024-04-20 16:08:33 -04:00
if (this.fusionShiny && variantColors && variantColors[this.fusionVariant]) {
2024-04-18 22:52:26 -04:00
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;
}
2024-04-18 22:52:26 -04:00
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 }));
}
2023-11-23 23:52:13 -05:00
}
2024-05-24 01:45:04 +02:00
2023-11-23 23:52:13 -05:00
let paletteColors: Map<number, number>;
let fusionPaletteColors: Map<number, number>;
const originalRandom = Math.random;
Math.random = () => Phaser.Math.RND.realInRange(0, 1);
2024-05-24 01:45:04 +02:00
2023-11-23 23:52:13 -05:00
this.scene.executeWithSeedOffset(() => {
paletteColors = QuantizerCelebi.quantize(pixelColors, 4);
fusionPaletteColors = QuantizerCelebi.quantize(fusionPixelColors, 4);
}, 0, "This result should not vary");
2023-11-23 23:52:13 -05:00
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<number, integer[]>;
let hsvColors: Map<number, number[]>;
2024-05-24 01:45:04 +02:00
2023-11-23 23:52:13 -05:00
const mappedColors = new Map<integer, integer[]>();
do {
mappedColors.clear();
rgbaColors = keys.reduce((map: Map<number, integer[]>, k: number) => {
2024-05-24 01:45:04 +02:00
map.set(k, Object.values(rgbaFromArgb(k))); return map;
}, new Map<number, integer[]>());
2023-11-23 23:52:13 -05:00
hsvColors = Array.from(rgbaColors.keys()).reduce((map: Map<number, number[]>, 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<number, number[]>());
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])) {
2023-11-23 23:52:13 -05:00
mappedColors.get(keys[c]).push(keys[c2]);
} else {
2023-11-23 23:52:13 -05:00
mappedColors.set(keys[c], [ keys[c2] ]);
}
2023-11-23 23:52:13 -05:00
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);
2023-11-23 23:52:13 -05:00
let count = paletteColors.get(key);
for (const value of values) {
2023-11-23 23:52:13 -05:00
const valueCount = paletteColors.get(value);
if (!valueCount) {
2023-11-23 23:52:13 -05:00
continue;
}
2023-11-23 23:52:13 -05:00
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) {
2023-11-23 23:52:13 -05:00
paletteColors.delete(value);
if (mappedColors.has(value)) {
2023-11-23 23:52:13 -05:00
mappedColors.delete(value);
}
2023-11-23 23:52:13 -05:00
}
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)));
2023-11-23 23:52:13 -05:00
}
);
2023-11-23 23:52:13 -05:00
const paletteDeltas: number[][] = [];
spriteColors.forEach((sc: integer[], i: integer) => {
paletteDeltas.push([]);
for (let p = 0; p < palette.length; p++) {
2023-11-23 23:52:13 -05:00
paletteDeltas[i].push(Utils.deltaRgb(sc, palette[p]));
}
2023-11-23 23:52:13 -05:00
});
const easeFunc = Phaser.Tweens.Builders.GetEaseFunction("Cubic.easeIn");
2023-11-23 23:52:13 -05:00
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++) {
2023-11-23 23:52:13 -05:00
color[c] = Math.round((fusionSpriteColors[sc][c] * ratio) + (fusionPalette[paletteIndex][c] * (1 - ratio)));
}
fusionSpriteColors[sc] = color;
2023-11-23 23:52:13 -05:00
}
}
[ this.getSprite(), this.getTintSprite() ].map(s => {
s.pipelineData[`spriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = spriteColors;
s.pipelineData[`fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? "Base" : ""}`] = fusionSpriteColors;
2023-11-23 23:52:13 -05:00
});
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;
}
2023-03-28 14:54:52 -04:00
}
2023-04-14 18:21:33 -04:00
export default interface Pokemon {
scene: BattleScene
}
2023-03-28 14:54:52 -04:00
export class PlayerPokemon extends Pokemon {
2023-04-08 00:21:44 -04:00
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) {
2024-04-18 22:52:26 -04:00
super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource);
2024-05-24 01:45:04 +02:00
if (Overrides.STATUS_OVERRIDE) {
this.status = new Status(Overrides.STATUS_OVERRIDE);
2024-05-30 12:49:57 -04:00
}
if (Overrides.SHINY_OVERRIDE) {
this.shiny = true;
this.initShinySparkle();
if (Overrides.VARIANT_OVERRIDE) {
this.variant = Overrides.VARIANT_OVERRIDE;
}
}
if (!dataSource) {
this.generateAndPopulateMoveset();
}
2023-04-08 00:21:44 -04:00
this.generateCompatibleTms();
2023-03-28 14:54:52 -04:00
}
2024-01-07 23:17:24 -05:00
initBattleInfo(): void {
this.battleInfo = new PlayerBattleInfo(this.scene);
this.battleInfo.initInfo(this);
}
2023-04-08 00:21:44 -04:00
isPlayer(): boolean {
2023-03-28 14:54:52 -04:00
return true;
}
hasTrainer(): boolean {
return true;
}
2024-01-07 23:17:24 -05:00
isBoss(): boolean {
return false;
}
getFieldIndex(): integer {
return this.scene.getPlayerField().indexOf(this);
}
getBattlerIndex(): BattlerIndex {
return this.getFieldIndex();
}
2023-04-08 00:21:44 -04:00
generateCompatibleTms(): void {
this.compatibleTms = [];
const tms = Object.keys(tmSpecies);
for (const tm of tms) {
2023-04-08 00:21:44 -04:00
const moveId = parseInt(tm) as Moves;
let compatible = false;
for (const p of tmSpecies[tm]) {
2023-04-08 00:21:44 -04:00
if (Array.isArray(p)) {
2023-12-09 20:09:09 -05:00
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;
2023-04-08 00:21:44 -04:00
break;
}
} else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) {
compatible = true;
2023-04-08 00:21:44 -04:00
break;
}
}
if (reverseCompatibleTms.indexOf(moveId) > -1) {
compatible = !compatible;
}
if (compatible) {
this.compatibleTms.push(moveId);
}
2023-04-08 00:21:44 -04:00
}
}
2023-04-10 13:54:06 -04:00
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<void> {
2023-10-31 14:09:33 -04:00
return new Promise(resolve => {
this.resetTurnData();
if (!batonPass) {
2024-04-25 20:52:52 -04:00
this.resetSummonData();
}
2023-10-31 14:09:33 -04:00
this.hideInfo();
this.setVisible(false);
2024-05-24 01:45:04 +02:00
2023-10-31 14:09:33 -04:00
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.FAINT_SWITCH, this.getFieldIndex(), (slotIndex: integer, option: PartyOption) => {
if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6) {
2023-10-31 14:09:33 -04:00
this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, this.getFieldIndex(), slotIndex, false, batonPass));
}
if (removeFromField) {
this.setVisible(false);
this.scene.field.remove(this);
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true);
}
2023-10-31 14:09:33 -04:00
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 ? 2 : 1) / (fusionStarterSpeciesId ? 2 : 1)));
if (amount.value > 0) {
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount);
2024-05-24 01:45:04 +02:00
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);
}
}
}
2024-05-20 08:02:17 +09:00
/**
* Handles Revival Blessing when used by player.
* @returns Promise to revive a pokemon.
* @see {@linkcode RevivalBlessingAttr}
*/
revivalBlessing(): Promise<void> {
return new Promise(resolve => {
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.REVIVAL_BLESSING, this.getFieldIndex(), (slotIndex:integer, option: PartyOption) => {
if (slotIndex >= 0 && slotIndex<6) {
2024-05-20 08:02:17 +09:00
const pokemon = this.scene.getParty()[slotIndex];
if (!pokemon || !pokemon.isFainted()) {
2024-05-20 08:02:17 +09:00
resolve();
}
2024-05-20 08:02:17 +09:00
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) {
2024-05-20 08:02:17 +09:00
const allyPokemon = this.getAlly();
if (slotIndex<=1) {
2024-05-20 08:02:17 +09:00
// 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()) {
2024-05-20 08:02:17 +09:00
// 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);
});
2024-05-20 08:02:17 +09:00
}
2024-05-24 01:45:04 +02:00
getPossibleEvolution(evolution: SpeciesFormEvolution): Promise<Pokemon> {
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;
2024-04-18 22:52:26 -04:00
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;
2024-04-18 22:52:26 -04:00
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));
});
2023-12-14 11:54:56 -05:00
}
2023-10-31 14:09:33 -04:00
evolve(evolution: SpeciesFormEvolution): Promise<void> {
2023-04-10 13:54:06 -04:00
return new Promise(resolve => {
this.pauseEvolutions = false;
2023-04-14 18:21:33 -04:00
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();
if (this.abilityIndex >= abilityCount) { // Shouldn't happen
this.abilityIndex = abilityCount - 1;
}
} else {
const abilityCount = this.getFusionSpeciesForm().getAbilityCount();
if (this.fusionAbilityIndex >= abilityCount) {// Shouldn't happen
this.fusionAbilityIndex = abilityCount - 1;
}
}
2023-04-10 13:54:06 -04:00
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) {
2024-04-03 09:23:45 -04:00
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();
}
2023-04-10 13:54:06 -04:00
});
}
2023-04-14 18:21:33 -04:00
private handleSpecialEvolutions(evolution: SpeciesFormEvolution) {
const isFusion = evolution instanceof FusionSpeciesFormEvolution;
2024-05-24 01:45:04 +02:00
const evoSpecies = (!isFusion ? this.species : this.fusionSpecies);
if (evoSpecies.speciesId === Species.NINCADA && evolution.speciesId === Species.NINJASK) {
2024-05-24 01:45:04 +02:00
const newEvolution = pokemonEvolutions[evoSpecies.speciesId][1];
2023-04-14 18:21:33 -04:00
if (newEvolution.condition.predicate(this)) {
2024-04-18 22:52:26 -04:00
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;
2024-04-18 22:52:26 -04:00
newPokemon.fusionVariant = this.fusionVariant;
newPokemon.fusionGender = this.fusionGender;
2024-04-26 18:27:00 -04:00
newPokemon.fusionLuck = this.fusionLuck;
2023-04-14 18:21:33 -04:00
this.scene.getParty().push(newPokemon);
newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution));
2023-10-24 18:44:38 -04:00
const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier
&& (m as PokemonHeldItemModifier).pokemonId === this.id, true) as PokemonHeldItemModifier[];
modifiers.forEach(m => {
const clonedModifier = m.clone() as PokemonHeldItemModifier;
clonedModifier.pokemonId = newPokemon.id;
this.scene.addModifier(clonedModifier, true);
2023-10-24 18:44:38 -04:00
});
this.scene.updateModifiers(true);
2023-04-14 18:21:33 -04:00
}
}
}
2023-11-04 00:32:12 -04:00
2024-01-09 23:34:43 -05:00
getPossibleForm(formChange: SpeciesFormChange): Promise<Pokemon> {
return new Promise(resolve => {
const formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
2024-04-18 22:52:26 -04:00
const ret = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
2024-01-09 23:34:43 -05:00
ret.loadAssets().then(() => resolve(ret));
});
}
changeForm(formChange: SpeciesFormChange): Promise<void> {
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
2024-01-09 23:34:43 -05:00
this.abilityIndex = abilityCount - 1;
}
2024-01-09 23:34:43 -05:00
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();
}
2024-01-09 23:34:43 -05:00
});
}
clearFusionSpecies(): void {
super.clearFusionSpecies();
this.generateCompatibleTms();
}
/**
* Returns a Promise to fuse two PlayerPokemon together
* @param pokemon The PlayerPokemon to fuse to this one
*/
2023-11-04 00:32:12 -04:00
fuse(pokemon: PlayerPokemon): Promise<void> {
return new Promise(resolve => {
2024-02-27 21:34:21 -05:00
this.fusionSpecies = pokemon.species;
this.fusionFormIndex = pokemon.formIndex;
this.fusionAbilityIndex = pokemon.abilityIndex;
this.fusionShiny = pokemon.shiny;
this.fusionVariant = pokemon.variant;
2024-02-27 21:34:21 -05:00
this.fusionGender = pokemon.gender;
2024-04-26 18:27:00 -04:00
this.fusionLuck = pokemon.luck;
2023-11-04 00:32:12 -04:00
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();
2023-11-04 00:32:12 -04:00
this.calculateStats();
2024-05-24 01:45:04 +02:00
// 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 as PokemonHeldItemModifier).pokemonId === pokemon.id, true) as PokemonHeldItemModifier[];
const transferModifiers: Promise<boolean>[] = [];
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();
2023-11-04 00:32:12 -04:00
});
});
});
}
unfuse(): Promise<void> {
return new Promise(resolve => {
this.clearFusionSpecies();
2023-11-04 00:32:12 -04:00
this.updateInfo(true).then(() => resolve());
2023-11-23 23:52:13 -05:00
this.updateFusionPalette();
2023-11-04 00:32:12 -04:00
});
}
/** Returns a deep copy of this Pokemon's moveset array */
copyMoveset(): PokemonMove[] {
const newMoveset = [];
2024-05-24 01:45:04 +02:00
this.moveset.forEach(move =>
newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.virtual)));
return newMoveset;
}
2023-03-28 14:54:52 -04:00
}
export class EnemyPokemon extends Pokemon {
public trainerSlot: TrainerSlot;
2023-03-28 14:54:52 -04:00
public aiType: AiType;
2024-01-07 23:17:24 -05:00
public bossSegments: integer;
public bossSegmentIndex: integer;
2023-03-28 14:54:52 -04:00
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, dataSource: PokemonData) {
super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex,
2024-04-18 22:52:26 -04:00
dataSource?.gender, dataSource ? dataSource.shiny : false, dataSource ? dataSource.variant : undefined, null, dataSource ? dataSource.nature : undefined, dataSource);
2023-04-28 15:03:42 -04:00
this.trainerSlot = trainerSlot;
if (boss) {
2024-01-07 23:17:24 -05:00
this.setBoss();
}
2024-05-30 12:49:57 -04:00
if (Overrides.OPP_STATUS_OVERRIDE) {
this.status = new Status(Overrides.OPP_STATUS_OVERRIDE);
}
2023-04-28 15:03:42 -04:00
if (!dataSource) {
this.generateAndPopulateMoveset();
this.trySetShiny();
if (Overrides.OPP_SHINY_OVERRIDE) {
this.shiny = true;
this.initShinySparkle();
}
if (this.shiny) {
2024-04-18 22:52:26 -04:00
this.variant = this.generateVariant();
if (Overrides.OPP_VARIANT_OVERRIDE) {
this.variant = Overrides.OPP_VARIANT_OVERRIDE;
}
}
2024-04-18 22:52:26 -04:00
2024-04-26 19:36:27 -04:00
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
2023-04-28 15:03:42 -04:00
let prevolution: Species;
let speciesId = species.speciesId;
while ((prevolution = pokemonPrevolutions[speciesId])) {
2023-12-07 17:43:56 -05:00
const evolution = pokemonEvolutions[prevolution].find(pe => pe.speciesId === speciesId && (!pe.evoFormKey || pe.evoFormKey === this.getFormKey()));
if (evolution.condition?.enforceFunc) {
2023-04-28 15:03:42 -04:00
evolution.condition.enforceFunc(this);
}
2023-04-28 15:03:42 -04:00
speciesId = prevolution;
}
2023-04-18 15:07:10 -04:00
}
2024-04-10 12:49:23 -04:00
this.aiType = boss || this.hasTrainer() ? AiType.SMART : AiType.SMART_RANDOM;
2023-03-28 14:54:52 -04:00
}
2024-01-07 23:17:24 -05:00
initBattleInfo(): void {
if (!this.battleInfo) {
this.battleInfo = new EnemyBattleInfo(this.scene);
this.battleInfo.updateBossSegments(this);
this.battleInfo.initInfo(this);
} else {
this.battleInfo.updateBossSegments(this);
}
2024-01-07 23:17:24 -05:00
}
2024-05-24 01:45:04 +02:00
2024-03-28 14:05:15 -04:00
setBoss(boss: boolean = true, bossSegments: integer = 0): void {
2024-01-07 23:17:24 -05:00
if (boss) {
2024-03-28 14:05:15 -04:00
this.bossSegments = bossSegments || this.scene.getEncounterBossSegments(this.scene.currentBattle.waveIndex, this.level, this.species, true);
2024-01-07 23:17:24 -05:00
this.bossSegmentIndex = this.bossSegments - 1;
} else {
this.bossSegments = 0;
this.bossSegmentIndex = 0;
}
}
generateAndPopulateMoveset(formIndex?: integer): void {
2023-04-29 01:40:24 -04:00
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)
2023-12-25 20:43:56 -05:00
];
2023-04-29 01:40:24 -04:00
break;
default:
super.generateAndPopulateMoveset();
break;
2023-04-29 01:40:24 -04:00
}
}
getNextMove(): QueuedMove {
2023-04-20 21:32:48 -04:00
const queuedMove = this.getMoveQueue().length
? this.getMoveset().find(m => m.moveId === this.getMoveQueue()[0].move)
2023-04-13 12:16:36 -04:00
: null;
2023-04-19 18:19:55 -04:00
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 {
2023-04-20 21:32:48 -04:00
this.getMoveQueue().shift();
2023-04-19 18:19:55 -04:00
return this.getNextMove();
}
}
2023-04-13 12:16:36 -04:00
2023-10-25 09:41:37 -04:00
const movePool = this.getMoveset().filter(m => m.isUsable(this));
2023-03-28 14:54:52 -04:00
if (movePool.length) {
if (movePool.length === 1) {
return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) };
}
2023-11-16 00:58:57 -05:00
const encoreTag = this.getTag(EncoreTag) as EncoreTag;
if (encoreTag) {
const encoreMove = movePool.find(m => m.moveId === encoreTag.moveId);
if (encoreMove) {
2023-11-16 00:58:57 -05:00
return { move: encoreMove.moveId, targets: this.getNextTargets(encoreMove.moveId) };
}
2023-11-16 00:58:57 -05:00
}
2023-03-28 14:54:52 -04:00
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;
}
2023-04-10 23:15:06 -04:00
}
targetScores.push(targetScore);
}
2023-04-10 23:15:06 -04:00
moveScore += Math.max(...targetScores);
// could make smarter by checking opponent def/spdef
moveScores[m] = moveScore;
}
2023-03-29 00:31:25 -04:00
console.log(moveScores);
2023-03-29 00:31:25 -04:00
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++;
2023-03-29 00:31:25 -04:00
}
}
console.log(movePool.map(m => m.getName()), moveScores, r, sortedMovePool.map(m => m.getName()));
return { move: sortedMovePool[r].moveId, targets: moveTargets[sortedMovePool[r].moveId] };
2023-03-28 14:54:52 -04:00
}
}
2023-04-19 18:19:55 -04:00
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];
}
2024-05-24 01:45:04 +02:00
2023-05-23 10:44:33 -04:00
return [];
}
2023-05-23 10:44:33 -04:00
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] ];
2023-03-28 14:54:52 -04:00
}
isPlayer() {
return false;
}
hasTrainer(): boolean {
return !!this.trainerSlot;
}
2024-01-07 23:17:24 -05:00
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()) {
2024-01-07 23:17:24 -05:00
return 0;
}
2024-01-07 23:17:24 -05:00
2024-05-24 01:45:04 +02:00
let clearedBossSegmentIndex = this.isBoss()
? this.bossSegmentIndex + 1
: 0;
2024-03-01 11:35:13 -05:00
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)));
2024-02-20 00:02:44 -05:00
}
2024-03-01 11:35:13 -05:00
damage = hpRemainder + Math.round(segmentSize * segmentsBypassed);
clearedBossSegmentIndex = s - segmentsBypassed;
2024-01-07 23:17:24 -05:00
}
2024-03-01 11:35:13 -05:00
break;
2024-01-07 23:17:24 -05:00
}
}
}
2024-04-04 14:20:14 -04:00
switch (this.scene.currentBattle.battleSpec) {
case BattleSpec.FINAL_BOSS:
if (!this.formIndex && this.bossSegmentIndex < 1) {
damage = Math.min(damage, this.hp - 1);
}
2024-04-04 14:20:14 -04:00
}
const ret = super.damage(damage, ignoreSegments, preventEndure, ignoreFaintPhase);
2024-03-01 11:35:13 -05:00
if (this.isBoss()) {
if (ignoreSegments) {
const segmentSize = this.getMaxHp() / this.bossSegments;
clearedBossSegmentIndex = Math.ceil(this.hp / segmentSize);
}
if (clearedBossSegmentIndex <= this.bossSegmentIndex) {
this.handleBossSegmentCleared(clearedBossSegmentIndex);
}
2024-03-01 11:35:13 -05:00
this.battleInfo.updateBossSegments(this);
}
return ret;
2024-01-07 23:17:24 -05:00
}
canBypassBossSegments(segmentCount: integer = 1): boolean {
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
if (!this.formIndex && (this.bossSegmentIndex - segmentCount) < 1) {
return false;
}
}
return true;
}
2024-01-07 23:17:24 -05:00
handleBossSegmentCleared(segmentIndex: integer): void {
while (segmentIndex - 1 < this.bossSegmentIndex) {
let boostedStat = BattleStat.RAND;
const battleStats = Utils.getEnumValues(BattleStat).slice(0, -3);
2024-01-07 23:17:24 -05:00
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) {
2024-01-07 23:17:24 -05:00
totalWeight += statWeights[bs];
statThresholds.push(totalWeight);
}
const randInt = Utils.randSeedInt(totalWeight);
for (const bs of battleStats) {
2024-01-07 23:17:24 -05:00
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;
2024-01-07 23:17:24 -05:00
}
2024-04-11 12:11:55 -04:00
this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels, true, true));
2024-01-07 23:17:24 -05:00
this.bossSegmentIndex--;
}
}
heal(amount: integer): integer {
if (this.isBoss()) {
const amountRatio = amount / this.getMaxHp();
const segmentBypassCount = Math.floor(amountRatio / (1 / this.bossSegments));
2024-01-07 23:17:24 -05:00
const segmentSize = this.getMaxHp() / this.bossSegments;
for (let s = 1; s < this.bossSegments; s++) {
const hpThreshold = segmentSize * s;
if (this.hp <= Math.round(hpThreshold)) {
2024-01-07 23:17:24 -05:00
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);
}
2024-01-07 23:17:24 -05:00
}
}
return super.heal(amount);
}
getFieldIndex(): integer {
return this.scene.getEnemyField().indexOf(this);
}
getBattlerIndex(): BattlerIndex {
return BattlerIndex.ENEMY + this.getFieldIndex();
}
addToParty(pokeballType: PokeballType) {
2023-04-14 18:21:33 -04:00
const party = this.scene.getParty();
2023-03-31 20:19:57 -04:00
let ret: PlayerPokemon = null;
2023-03-28 14:54:52 -04:00
2023-03-31 20:19:57 -04:00
if (party.length < 6) {
this.pokeball = pokeballType;
2024-01-05 11:29:34 -05:00
this.metLevel = this.level;
this.metBiome = this.scene.arena.biomeType;
2024-04-18 22:52:26 -04:00
const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
2023-03-31 20:19:57 -04:00
party.push(newPokemon);
ret = newPokemon;
2024-01-09 23:53:31 -05:00
this.scene.triggerPokemonFormChange(newPokemon, SpeciesFormChangeActiveTrigger, true);
2023-03-31 20:19:57 -04:00
}
2023-03-31 22:31:20 -04:00
2023-03-31 20:19:57 -04:00
return ret;
2023-03-28 14:54:52 -04:00
}
}
2023-04-13 12:16:36 -04:00
export interface TurnMove {
move: Moves;
targets?: BattlerIndex[];
2023-04-13 12:16:36 -04:00
result: MoveResult;
2023-04-19 18:19:55 -04:00
virtual?: boolean;
2023-04-25 01:32:48 -04:00
turn?: integer;
2023-04-13 12:16:36 -04:00
}
export interface QueuedMove {
move: Moves;
targets: BattlerIndex[];
2023-04-13 12:16:36 -04:00
ignorePP?: boolean;
}
export interface AttackMoveResult {
move: Moves;
result: DamageResult;
damage: integer;
2023-10-26 21:12:53 -07:00
critical: boolean;
sourceId: integer;
}
2023-04-03 23:38:31 -04:00
export class PokemonSummonData {
2023-04-10 23:15:06 -04:00
public battleStats: integer[] = [ 0, 0, 0, 0, 0, 0, 0 ];
2023-04-13 12:16:36 -04:00
public moveQueue: QueuedMove[] = [];
2023-10-25 09:41:37 -04:00
public disabledMove: Moves = Moves.NONE;
public disabledTurns: integer = 0;
2023-04-21 19:30:04 -04:00
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;
2023-04-03 23:38:31 -04:00
}
2023-11-28 21:35:52 -05:00
export class PokemonBattleData {
public hitCount: integer = 0;
public endured: boolean = false;
2024-05-02 06:36:47 +10:00
public berriesEaten: BerryType[] = [];
public abilitiesApplied: Abilities[] = [];
public abilityRevealed: boolean = false;
2023-11-28 21:35:52 -05:00
}
2023-04-03 23:38:31 -04:00
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 */
2023-04-28 15:03:42 -04:00
public moveHistory: TurnMove[] = [];
2023-04-03 23:38:31 -04:00
}
export class PokemonTurnData {
public flinched: boolean;
public acted: boolean;
2023-04-10 16:17:25 -04:00
public hitCount: integer;
2023-04-03 23:38:31 -04:00
public hitsLeft: integer;
2023-04-14 01:08:44 -04:00
public damageDealt: integer = 0;
public currDamageDealt: integer = 0;
public damageTaken: integer = 0;
public attacksReceived: AttackMoveResult[] = [];
2023-04-03 23:38:31 -04:00
}
2023-03-29 00:31:25 -04:00
export enum AiType {
2023-03-28 14:54:52 -04:00
RANDOM,
2023-03-29 00:31:25 -04:00
SMART_RANDOM,
2023-03-28 14:54:52 -04:00
SMART
2023-04-29 01:40:24 -04:00
}
2023-03-28 14:54:52 -04:00
export enum MoveResult {
PENDING,
SUCCESS,
FAIL,
MISS,
OTHER
}
export enum HitResult {
EFFECTIVE = 1,
2023-03-28 14:54:52 -04:00
SUPER_EFFECTIVE,
NOT_VERY_EFFECTIVE,
2023-11-07 22:23:42 -05:00
ONE_HIT_KO,
2023-12-22 01:16:56 -05:00
NO_EFFECT,
2023-04-13 12:16:36 -04:00
STATUS,
HEAL,
FAIL,
MISS,
OTHER,
IMMUNE
2023-04-29 01:40:24 -04:00
}
2023-03-28 14:54:52 -04:00
2023-11-07 22:23:42 -05:00
export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO | HitResult.OTHER;
2023-04-15 01:32:16 -04:00
/**
* 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}.
**/
2023-03-28 14:54:52 -04:00
export class PokemonMove {
2023-04-13 12:16:36 -04:00
public moveId: Moves;
2023-03-28 14:54:52 -04:00
public ppUsed: integer;
public ppUp: integer;
2023-04-19 18:19:55 -04:00
public virtual: boolean;
2023-03-28 14:54:52 -04:00
2023-04-19 18:19:55 -04:00
constructor(moveId: Moves, ppUsed?: integer, ppUp?: integer, virtual?: boolean) {
2023-03-28 14:54:52 -04:00
this.moveId = moveId;
2023-04-13 23:04:51 -04:00
this.ppUsed = ppUsed || 0;
this.ppUp = ppUp || 0;
2023-04-19 18:19:55 -04:00
this.virtual = !!virtual;
2023-03-28 14:54:52 -04:00
}
2023-10-25 09:41:37 -04:00
isUsable(pokemon: Pokemon, ignorePp?: boolean): boolean {
if (this.moveId && pokemon.summonData?.disabledMove === this.moveId) {
2023-03-28 14:54:52 -04:00
return false;
}
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1) && !this.getMove().name.endsWith(" (N)");
2023-04-19 18:19:55 -04:00
}
2023-03-28 14:54:52 -04:00
getMove(): Move {
2023-04-20 21:32:48 -04:00
return allMoves[this.moveId];
2023-03-28 14:54:52 -04:00
}
/**
* 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());
}
2024-01-12 19:05:00 -05:00
getMovePp(): integer {
return this.getMove().pp + this.ppUp * Math.max(Math.floor(this.getMove().pp / 5), 1);
}
2023-04-28 00:25:33 -04:00
getPpRatio(): number {
2024-01-12 19:05:00 -05:00
return 1 - (this.ppUsed / this.getMovePp());
2023-04-28 00:25:33 -04:00
}
2023-03-28 14:54:52 -04:00
getName(): string {
return this.getMove().name;
2023-03-28 14:54:52 -04:00
}
/**
* 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);
}
}