mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-10-07 05:37:16 +02:00
Fixed an issue where two turn charge moves could be paused by status or Truant causing some infinite sequences if not careful.
3355 lines
132 KiB
TypeScript
3355 lines
132 KiB
TypeScript
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 { Moves } from "../data/enums/moves";
|
|
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveTypeAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr } from "../data/move";
|
|
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from '../data/pokemon-species';
|
|
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 } from '../phases';
|
|
import { BattleStat } from '../data/battle-stat';
|
|
import { BattlerTag, BattlerTagLapseType, EncoreTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, getBattlerTag } from '../data/battler-tags';
|
|
import { BattlerTagType } from "../data/enums/battler-tag-type";
|
|
import { Species } from '../data/enums/species';
|
|
import { WeatherType } from '../data/weather';
|
|
import { TempBattleStat } from '../data/temp-battle-stat';
|
|
import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from '../data/arena-tag';
|
|
import { ArenaTagType } from "../data/enums/arena-tag-type";
|
|
import { Biome } from "../data/enums/biome";
|
|
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, FieldVariableMovePowerAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, NonSuperEffectiveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, VariableMoveTypeAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPostDefendAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr } from '../data/ability';
|
|
import { Abilities } from "#app/data/enums/abilities";
|
|
import PokemonData from '../system/pokemon-data';
|
|
import Battle, { BattlerIndex } from '../battle';
|
|
import { BattleSpec } from "../enums/battle-spec";
|
|
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 { BerryType } from '../data/berry';
|
|
import i18next from '../plugins/i18n';
|
|
import { speciesEggMoves } from '../data/egg-moves';
|
|
import { ModifierTier } from '../modifier/modifier-tier';
|
|
|
|
export enum FieldPosition {
|
|
CENTER,
|
|
LEFT,
|
|
RIGHT
|
|
}
|
|
|
|
export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|
public id: integer;
|
|
public name: string;
|
|
public species: PokemonSpecies;
|
|
public formIndex: integer;
|
|
public abilityIndex: integer;
|
|
public passive: boolean;
|
|
public shiny: boolean;
|
|
public variant: Variant;
|
|
public pokeball: PokeballType;
|
|
protected battleInfo: BattleInfo;
|
|
public level: integer;
|
|
public exp: integer;
|
|
public levelExp: integer;
|
|
public gender: Gender;
|
|
public hp: integer;
|
|
public stats: integer[];
|
|
public ivs: integer[];
|
|
public nature: Nature;
|
|
public natureOverride: Nature | -1;
|
|
public moveset: PokemonMove[];
|
|
public status: Status;
|
|
public friendship: integer;
|
|
public metLevel: integer;
|
|
public metBiome: Biome | -1;
|
|
public luck: integer;
|
|
public pauseEvolutions: boolean;
|
|
public pokerus: boolean;
|
|
|
|
public fusionSpecies: PokemonSpecies;
|
|
public fusionFormIndex: integer;
|
|
public fusionAbilityIndex: integer;
|
|
public fusionShiny: boolean;
|
|
public fusionVariant: Variant;
|
|
public fusionGender: Gender;
|
|
public fusionLuck: integer;
|
|
|
|
private summonDataPrimer: PokemonSummonData;
|
|
|
|
public summonData: PokemonSummonData;
|
|
public battleData: PokemonBattleData;
|
|
public battleSummonData: PokemonBattleSummonData;
|
|
public turnData: PokemonTurnData;
|
|
|
|
public fieldPosition: FieldPosition;
|
|
|
|
public maskEnabled: boolean;
|
|
public maskSprite: Phaser.GameObjects.Sprite;
|
|
|
|
private shinySparkle: Phaser.GameObjects.Sprite;
|
|
|
|
constructor(scene: BattleScene, x: number, y: number, species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData) {
|
|
super(scene, x, y);
|
|
|
|
if (!species.isObtainable() && this.isPlayer())
|
|
throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`;
|
|
|
|
const hiddenAbilityChance = new Utils.IntegerHolder(256);
|
|
if (!this.hasTrainer())
|
|
this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
|
|
|
|
const hasHiddenAbility = !Utils.randSeedInt(hiddenAbilityChance.value);
|
|
const randAbilityIndex = Utils.randSeedInt(2);
|
|
|
|
this.species = species;
|
|
this.pokeball = dataSource?.pokeball || PokeballType.POKEBALL;
|
|
this.level = level;
|
|
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)
|
|
this.variant = variant;
|
|
this.exp = dataSource?.exp || getLevelTotalExp(this.level, species.growthRate);
|
|
this.levelExp = dataSource?.levelExp || 0;
|
|
if (dataSource) {
|
|
this.id = dataSource.id;
|
|
this.hp = dataSource.hp;
|
|
this.stats = dataSource.stats;
|
|
this.ivs = dataSource.ivs;
|
|
this.passive = !!dataSource.passive;
|
|
if (this.variant === undefined)
|
|
this.variant = 0;
|
|
this.nature = dataSource.nature || 0 as Nature;
|
|
this.natureOverride = dataSource.natureOverride !== undefined ? dataSource.natureOverride : -1;
|
|
this.moveset = dataSource.moveset;
|
|
this.status = dataSource.status;
|
|
this.friendship = dataSource.friendship !== undefined ? dataSource.friendship : this.species.baseFriendship;
|
|
this.metLevel = dataSource.metLevel || 5;
|
|
this.luck = dataSource.luck;
|
|
this.metBiome = dataSource.metBiome;
|
|
this.pauseEvolutions = dataSource.pauseEvolutions;
|
|
this.pokerus = !!dataSource.pokerus;
|
|
this.fusionSpecies = dataSource.fusionSpecies instanceof PokemonSpecies ? dataSource.fusionSpecies : getPokemonSpecies(dataSource.fusionSpecies);
|
|
this.fusionFormIndex = dataSource.fusionFormIndex;
|
|
this.fusionAbilityIndex = dataSource.fusionAbilityIndex;
|
|
this.fusionShiny = dataSource.fusionShiny;
|
|
this.fusionVariant = dataSource.fusionVariant || 0;
|
|
this.fusionGender = dataSource.fusionGender;
|
|
this.fusionLuck = dataSource.fusionLuck;
|
|
} else {
|
|
this.id = Utils.randSeedInt(4294967296);
|
|
this.ivs = ivs || Utils.getIvsFromId(this.id);
|
|
|
|
if (this.gender === undefined)
|
|
this.generateGender();
|
|
|
|
if (this.formIndex === undefined)
|
|
this.formIndex = this.scene.getSpeciesFormIndex(species, this.gender, this.nature, this.isPlayer());
|
|
|
|
if (this.shiny === undefined)
|
|
this.trySetShiny();
|
|
|
|
if (this.variant === undefined)
|
|
this.variant = this.shiny ? this.generateVariant() : 0;
|
|
|
|
if (nature !== undefined)
|
|
this.setNature(nature);
|
|
else
|
|
this.generateNature();
|
|
|
|
this.natureOverride = -1;
|
|
|
|
this.friendship = species.baseFriendship;
|
|
this.metLevel = level;
|
|
this.metBiome = scene.currentBattle ? scene.arena.biomeType : -1;
|
|
this.pokerus = false;
|
|
|
|
if (level > 1) {
|
|
const fused = new Utils.BooleanHolder(scene.gameMode.isSplicedOnly);
|
|
if (!fused.value && !this.isPlayer() && !this.hasTrainer())
|
|
this.scene.applyModifier(EnemyFusionChanceModifier, false, fused);
|
|
|
|
if (fused.value) {
|
|
this.calculateStats();
|
|
this.generateFusionSpecies();
|
|
}
|
|
}
|
|
|
|
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
|
|
}
|
|
|
|
this.generateName();
|
|
|
|
if (!species.isObtainable())
|
|
this.shiny = false;
|
|
|
|
this.calculateStats();
|
|
}
|
|
|
|
init(): void {
|
|
this.fieldPosition = FieldPosition.CENTER;
|
|
|
|
this.initBattleInfo();
|
|
|
|
this.scene.fieldUI.addAt(this.battleInfo, 0);
|
|
|
|
const getSprite = (hasShadow?: boolean) => {
|
|
const ret = this.scene.addPokemonSprite(this, 0, 0, `pkmn__${this.isPlayer() ? 'back__' : ''}sub`, undefined, true);
|
|
ret.setOrigin(0.5, 1);
|
|
ret.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: !!hasShadow, teraColor: getTypeRgb(this.getTeraType()) });
|
|
return ret;
|
|
};
|
|
|
|
this.setScale(this.getSpriteScale());
|
|
|
|
const sprite = getSprite(true);
|
|
const tintSprite = getSprite();
|
|
|
|
tintSprite.setVisible(false);
|
|
|
|
this.addAt(sprite, 0);
|
|
this.addAt(tintSprite, 1);
|
|
|
|
if (this.isShiny() && !this.shinySparkle)
|
|
this.initShinySparkle();
|
|
}
|
|
|
|
abstract initBattleInfo(): void;
|
|
|
|
isOnField(): boolean {
|
|
if (!this.scene)
|
|
return false;
|
|
return this.scene.field.getIndex(this) > -1;
|
|
}
|
|
|
|
isFainted(checkStatus?: boolean): boolean {
|
|
return !this.hp && (!checkStatus || this.status?.effect === StatusEffect.FAINT);
|
|
}
|
|
|
|
isActive(onField?: boolean): boolean {
|
|
if (!this.scene)
|
|
return false;
|
|
return !this.isFainted() && !!this.scene && (!onField || this.isOnField());
|
|
}
|
|
|
|
getDexAttr(): bigint {
|
|
let ret = 0n;
|
|
ret |= this.gender !== Gender.FEMALE ? DexAttr.MALE : DexAttr.FEMALE;
|
|
ret |= !this.shiny ? DexAttr.NON_SHINY : DexAttr.SHINY;
|
|
ret |= this.variant >= 2 ? DexAttr.VARIANT_3 : this.variant === 1 ? DexAttr.VARIANT_2 : DexAttr.DEFAULT_VARIANT;
|
|
ret |= this.scene.gameData.getFormAttr(this.formIndex);
|
|
return ret;
|
|
}
|
|
|
|
generateName(): void {
|
|
if (!this.fusionSpecies) {
|
|
this.name = this.species.getName(this.formIndex);
|
|
return;
|
|
}
|
|
this.name = getFusedSpeciesName(this.species.getName(this.formIndex), this.fusionSpecies.getName(this.fusionFormIndex));
|
|
if (this.battleInfo)
|
|
this.updateInfo(true);
|
|
}
|
|
|
|
abstract isPlayer(): boolean;
|
|
|
|
abstract hasTrainer(): boolean;
|
|
|
|
abstract getFieldIndex(): integer;
|
|
|
|
abstract getBattlerIndex(): BattlerIndex;
|
|
|
|
loadAssets(ignoreOverride: boolean = true): Promise<void> {
|
|
return new Promise(resolve => {
|
|
const moveIds = this.getMoveset().map(m => m.getMove().id);
|
|
Promise.allSettled(moveIds.map(m => initMoveAnim(this.scene, m)))
|
|
.then(() => {
|
|
loadMoveAnimAssets(this.scene, moveIds);
|
|
this.getSpeciesForm().loadAssets(this.scene, this.getGender() === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
|
|
if (this.isPlayer() || this.getFusionSpeciesForm())
|
|
this.scene.loadPokemonAtlas(this.getBattleSpriteKey(true, ignoreOverride), this.getBattleSpriteAtlasPath(true, ignoreOverride));
|
|
if (this.getFusionSpeciesForm()) {
|
|
this.getFusionSpeciesForm().loadAssets(this.scene, this.getFusionGender() === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant);
|
|
this.scene.loadPokemonAtlas(this.getFusionBattleSpriteKey(true, ignoreOverride), this.getFusionBattleSpriteAtlasPath(true, ignoreOverride));
|
|
}
|
|
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
|
|
if (this.isPlayer()) {
|
|
const originalWarn = console.warn;
|
|
// Ignore warnings for missing frames, because there will be a lot
|
|
console.warn = () => {};
|
|
const battleFrameNames = this.scene.anims.generateFrameNames(this.getBattleSpriteKey(), { zeroPad: 4, suffix: ".png", start: 1, end: 400 });
|
|
console.warn = originalWarn;
|
|
this.scene.anims.create({
|
|
key: this.getBattleSpriteKey(),
|
|
frames: battleFrameNames,
|
|
frameRate: 12,
|
|
repeat: -1
|
|
});
|
|
}
|
|
this.playAnim();
|
|
const updateFusionPaletteAndResolve = () => {
|
|
this.updateFusionPalette();
|
|
if (this.summonData?.speciesForm)
|
|
this.updateFusionPalette(true);
|
|
resolve();
|
|
};
|
|
if (this.shiny) {
|
|
const populateVariantColors = (key: string, back: boolean = false): Promise<void> => {
|
|
return new Promise(resolve => {
|
|
const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace('variant/', '').replace(/_[1-3]$/, '');
|
|
let variantSet: VariantSet;
|
|
let config = variantData;
|
|
const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride));
|
|
battleSpritePath.split('/').map(p => config ? config = config[p] : null);
|
|
variantSet = config as VariantSet;
|
|
if (variantSet && variantSet[this.variant] === 1) {
|
|
if (variantColorCache.hasOwnProperty(key))
|
|
return resolve();
|
|
this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? 'exp/' : ''}${battleSpritePath}.json`).then(res => res.json()).then(c => {
|
|
variantColorCache[key] = c;
|
|
resolve();
|
|
});
|
|
} else
|
|
resolve();
|
|
});
|
|
};
|
|
if (this.isPlayer())
|
|
Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve());
|
|
else
|
|
populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve());
|
|
} else
|
|
updateFusionPaletteAndResolve();
|
|
});
|
|
if (!this.scene.load.isLoading())
|
|
this.scene.load.start();
|
|
});
|
|
});
|
|
}
|
|
|
|
getFormKey(): string {
|
|
if (!this.species.forms.length || this.species.forms.length <= this.formIndex)
|
|
return '';
|
|
return this.species.forms[this.formIndex].formKey;
|
|
}
|
|
|
|
getFusionFormKey(): string {
|
|
if (!this.fusionSpecies)
|
|
return null;
|
|
if (!this.fusionSpecies.forms.length || this.fusionSpecies.forms.length <= this.fusionFormIndex)
|
|
return '';
|
|
return this.fusionSpecies.forms[this.fusionFormIndex].formKey;
|
|
}
|
|
|
|
getSpriteAtlasPath(ignoreOverride?: boolean): string {
|
|
const spriteId = this.getSpriteId(ignoreOverride).replace(/\_{2}/g, '/');
|
|
return `${/_[1-3]$/.test(spriteId) ? 'variant/' : ''}${spriteId}`;
|
|
}
|
|
|
|
getBattleSpriteAtlasPath(back?: boolean, ignoreOverride?: boolean): string {
|
|
const spriteId = this.getBattleSpriteId(back, ignoreOverride).replace(/\_{2}/g, '/');
|
|
return `${/_[1-3]$/.test(spriteId) ? 'variant/' : ''}${spriteId}`;
|
|
}
|
|
|
|
getSpriteId(ignoreOverride?: boolean): string {
|
|
return this.getSpeciesForm(ignoreOverride).getSpriteId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
|
|
}
|
|
|
|
getBattleSpriteId(back?: boolean, ignoreOverride?: boolean): string {
|
|
if (back === undefined)
|
|
back = this.isPlayer();
|
|
return this.getSpeciesForm(ignoreOverride).getSpriteId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant, back);
|
|
}
|
|
|
|
getSpriteKey(ignoreOverride?: boolean): string {
|
|
return this.getSpeciesForm(ignoreOverride).getSpriteKey(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
|
|
}
|
|
|
|
getBattleSpriteKey(back?: boolean, ignoreOverride?: boolean): string {
|
|
return `pkmn__${this.getBattleSpriteId(back, ignoreOverride)}`;
|
|
}
|
|
|
|
getFusionSpriteId(ignoreOverride?: boolean): string {
|
|
return this.getFusionSpeciesForm(ignoreOverride).getSpriteId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant);
|
|
}
|
|
|
|
getFusionBattleSpriteId(back?: boolean, ignoreOverride?: boolean): string {
|
|
if (back === undefined)
|
|
back = this.isPlayer();
|
|
return this.getFusionSpeciesForm(ignoreOverride).getSpriteId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.fusionVariant, back);
|
|
}
|
|
|
|
getFusionBattleSpriteKey(back?: boolean, ignoreOverride?: boolean): string {
|
|
return `pkmn__${this.getFusionBattleSpriteId(back, ignoreOverride)}`;
|
|
}
|
|
|
|
getFusionBattleSpriteAtlasPath(back?: boolean, ignoreOverride?: boolean): string {
|
|
return this.getFusionBattleSpriteId(back, ignoreOverride).replace(/\_{2}/g, '/');
|
|
}
|
|
|
|
getIconAtlasKey(ignoreOverride?: boolean): string {
|
|
return this.getSpeciesForm(ignoreOverride).getIconAtlasKey(this.formIndex, this.shiny, this.variant);
|
|
}
|
|
|
|
getFusionIconAtlasKey(ignoreOverride?: boolean): string {
|
|
return this.getFusionSpeciesForm(ignoreOverride).getIconAtlasKey(this.fusionFormIndex, this.fusionShiny, this.fusionVariant);
|
|
}
|
|
|
|
getIconId(ignoreOverride?: boolean): string {
|
|
return this.getSpeciesForm(ignoreOverride).getIconId(this.getGender(ignoreOverride) === Gender.FEMALE, this.formIndex, this.shiny, this.variant);
|
|
}
|
|
|
|
getFusionIconId(ignoreOverride?: boolean): string {
|
|
return this.getFusionSpeciesForm(ignoreOverride).getIconId(this.getFusionGender(ignoreOverride) === Gender.FEMALE, this.fusionFormIndex, this.fusionShiny, this.variant);
|
|
}
|
|
|
|
getSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm {
|
|
if (!ignoreOverride && this.summonData?.speciesForm)
|
|
return this.summonData.speciesForm;
|
|
if (!this.species.forms?.length)
|
|
return this.species;
|
|
return this.species.forms[this.formIndex];
|
|
}
|
|
|
|
getFusionSpeciesForm(ignoreOverride?: boolean): PokemonSpeciesForm {
|
|
if (!ignoreOverride && this.summonData?.speciesForm)
|
|
return this.summonData.fusionSpeciesForm;
|
|
if (!this.fusionSpecies?.forms?.length || this.fusionFormIndex >= this.fusionSpecies?.forms.length)
|
|
return this.fusionSpecies;
|
|
return this.fusionSpecies?.forms[this.fusionFormIndex];
|
|
}
|
|
|
|
getSprite(): Phaser.GameObjects.Sprite {
|
|
return this.getAt(0) as Phaser.GameObjects.Sprite;
|
|
}
|
|
|
|
getTintSprite(): Phaser.GameObjects.Sprite {
|
|
return !this.maskEnabled
|
|
? this.getAt(1) as Phaser.GameObjects.Sprite
|
|
: this.maskSprite;
|
|
}
|
|
|
|
getSpriteScale(): number {
|
|
const formKey = this.getFormKey();
|
|
if (formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 || formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1)
|
|
return 1.5;
|
|
return 1;
|
|
}
|
|
|
|
getHeldItems(): PokemonHeldItemModifier[] {
|
|
if (!this.scene)
|
|
return [];
|
|
return this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === this.id, this.isPlayer()) as PokemonHeldItemModifier[];
|
|
}
|
|
|
|
updateScale(): void {
|
|
this.setScale(this.getSpriteScale());
|
|
}
|
|
|
|
updateSpritePipelineData(): void {
|
|
[ this.getSprite(), this.getTintSprite() ].map(s => s.pipelineData['teraColor'] = getTypeRgb(this.getTeraType()));
|
|
this.updateInfo(true);
|
|
}
|
|
|
|
initShinySparkle(): void {
|
|
const keySuffix = this.variant ? `_${this.variant + 1}` : '';
|
|
const key = `shiny${keySuffix}`;
|
|
const shinySparkle = this.scene.addFieldSprite(0, 0, key);
|
|
shinySparkle.setVisible(false);
|
|
shinySparkle.setOrigin(0.5, 1);
|
|
const frameNames = this.scene.anims.generateFrameNames(key, { suffix: '.png', end: 34 });
|
|
this.scene.anims.create({
|
|
key: `sparkle${keySuffix}`,
|
|
frames: frameNames,
|
|
frameRate: 32,
|
|
showOnStart: true,
|
|
hideOnComplete: true,
|
|
});
|
|
this.add(shinySparkle);
|
|
|
|
this.shinySparkle = shinySparkle;
|
|
}
|
|
|
|
playAnim(): void {
|
|
this.getSprite().play(this.getBattleSpriteKey());
|
|
this.getTintSprite().play(this.getBattleSpriteKey());
|
|
}
|
|
|
|
getFieldPositionOffset(): [ number, number ] {
|
|
switch (this.fieldPosition) {
|
|
case FieldPosition.CENTER:
|
|
return [ 0, 0 ];
|
|
case FieldPosition.LEFT:
|
|
return [ -32, -8 ];
|
|
case FieldPosition.RIGHT:
|
|
return [ 32, 0 ];
|
|
}
|
|
}
|
|
|
|
setFieldPosition(fieldPosition: FieldPosition, duration?: integer): Promise<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();
|
|
|
|
let relX = newOffset[0] - initialOffset[0];
|
|
let relY = newOffset[1] - initialOffset[1];
|
|
|
|
if (duration) {
|
|
this.scene.tweens.add({
|
|
targets: this,
|
|
x: (_target, _key, value: number) => value + relX,
|
|
y: (_target, _key, value: number) => value + relY,
|
|
duration: duration,
|
|
ease: 'Sine.easeOut',
|
|
onComplete: () => resolve()
|
|
});
|
|
} else {
|
|
this.x += relX;
|
|
this.y += relY;
|
|
}
|
|
});
|
|
}
|
|
|
|
getStat(stat: Stat): integer {
|
|
return this.stats[stat];
|
|
}
|
|
|
|
getBattleStat(stat: Stat, opponent?: Pokemon, move?: Move, isCritical: boolean = false): integer {
|
|
if (stat === Stat.HP)
|
|
return this.getStat(Stat.HP);
|
|
const battleStat = (stat - 1) as BattleStat;
|
|
const statLevel = new Utils.IntegerHolder(this.summonData.battleStats[battleStat]);
|
|
if (opponent) {
|
|
if (isCritical) {
|
|
switch (stat) {
|
|
case Stat.ATK:
|
|
case Stat.SPATK:
|
|
statLevel.value = Math.max(statLevel.value, 0);
|
|
break;
|
|
case Stat.DEF:
|
|
case Stat.SPDEF:
|
|
statLevel.value = Math.min(statLevel.value, 0);
|
|
break;
|
|
}
|
|
}
|
|
applyAbAttrs(IgnoreOpponentStatChangesAbAttr, opponent, null, statLevel);
|
|
if (move)
|
|
applyMoveAttrs(IgnoreOpponentStatChangesAttr, this, opponent, move, statLevel);
|
|
}
|
|
if (this.isPlayer())
|
|
this.scene.applyModifiers(TempBattleStatBoosterModifier, this.isPlayer(), battleStat as integer as TempBattleStat, statLevel);
|
|
const statValue = new Utils.NumberHolder(this.getStat(stat));
|
|
applyBattleStatMultiplierAbAttrs(BattleStatMultiplierAbAttr, this, battleStat, statValue);
|
|
let ret = statValue.value * (Math.max(2, 2 + statLevel.value) / Math.max(2, 2 - statLevel.value));
|
|
switch (stat) {
|
|
case Stat.ATK:
|
|
if (this.getTag(BattlerTagType.SLOW_START))
|
|
ret >>= 1;
|
|
break;
|
|
case Stat.DEF:
|
|
if (this.isOfType(Type.ICE) && this.scene.arena.weather?.weatherType === WeatherType.SNOW)
|
|
ret *= 1.5;
|
|
break;
|
|
case Stat.SPATK:
|
|
break;
|
|
case Stat.SPDEF:
|
|
if (this.isOfType(Type.ROCK) && this.scene.arena.weather?.weatherType === WeatherType.SANDSTORM)
|
|
ret *= 1.5;
|
|
break;
|
|
case Stat.SPD:
|
|
// Check both the player and enemy to see if Tailwind should be multiplying the speed of the Pokemon
|
|
if ((this.isPlayer() && this.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.PLAYER))
|
|
|| (!this.isPlayer() && this.scene.arena.getTagOnSide(ArenaTagType.TAILWIND, ArenaTagSide.ENEMY)))
|
|
ret *= 2;
|
|
|
|
if (this.getTag(BattlerTagType.SLOW_START))
|
|
ret >>= 1;
|
|
if (this.status && this.status.effect === StatusEffect.PARALYSIS)
|
|
ret >>= 1;
|
|
break;
|
|
}
|
|
|
|
const highestStatBoost = this.findTag(t => t instanceof HighestStatBoostTag && (t as HighestStatBoostTag).stat === stat) as HighestStatBoostTag;
|
|
if (highestStatBoost)
|
|
ret *= highestStatBoost.multiplier;
|
|
|
|
return Math.floor(ret);
|
|
}
|
|
|
|
calculateStats(): void {
|
|
if (!this.stats)
|
|
this.stats = [ 0, 0, 0, 0, 0, 0 ];
|
|
const baseStats = this.getSpeciesForm().baseStats.slice(0);
|
|
if (this.fusionSpecies) {
|
|
const fusionBaseStats = this.getFusionSpeciesForm().baseStats;
|
|
for (let s = 0; s < this.stats.length; s++)
|
|
baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2);
|
|
} else if (this.scene.gameMode.isSplicedOnly) {
|
|
for (let s = 0; s < this.stats.length; s++)
|
|
baseStats[s] = Math.ceil(baseStats[s] / 2);
|
|
}
|
|
this.scene.applyModifiers(PokemonBaseStatModifier, this.isPlayer(), this, baseStats);
|
|
const stats = Utils.getEnumValues(Stat);
|
|
for (let s of stats) {
|
|
const isHp = s === Stat.HP;
|
|
let baseStat = baseStats[s];
|
|
let value = Math.floor(((2 * baseStat + this.ivs[s]) * this.level) * 0.01);
|
|
if (isHp) {
|
|
value = value + this.level + 10;
|
|
if (this.hasAbility(Abilities.WONDER_GUARD, false, true))
|
|
value = 1;
|
|
if (this.hp > value || this.hp === undefined)
|
|
this.hp = value;
|
|
else if (this.hp) {
|
|
const lastMaxHp = this.getMaxHp();
|
|
if (lastMaxHp && value > lastMaxHp)
|
|
this.hp += value - lastMaxHp;
|
|
}
|
|
} else {
|
|
value += 5;
|
|
const natureStatMultiplier = new Utils.NumberHolder(getNatureStatMultiplier(this.getNature(), s));
|
|
this.scene.applyModifier(PokemonNatureWeightModifier, this.isPlayer(), this, natureStatMultiplier);
|
|
if (natureStatMultiplier.value !== 1)
|
|
value = Math.max(Math[natureStatMultiplier.value > 1 ? 'ceil' : 'floor'](value * natureStatMultiplier.value), 1);
|
|
}
|
|
this.stats[s] = value;
|
|
}
|
|
}
|
|
|
|
getNature(): Nature {
|
|
return this.natureOverride !== -1 ? this.natureOverride : this.nature;
|
|
}
|
|
|
|
setNature(nature: Nature): void {
|
|
this.nature = nature;
|
|
this.calculateStats();
|
|
}
|
|
|
|
generateNature(naturePool?: Nature[]): void {
|
|
if (naturePool === undefined)
|
|
naturePool = Utils.getEnumValues(Nature);
|
|
const nature = naturePool[Utils.randSeedInt(naturePool.length)];
|
|
this.setNature(nature);
|
|
}
|
|
|
|
getMaxHp(): integer {
|
|
return this.getStat(Stat.HP);
|
|
}
|
|
|
|
getInverseHp(): integer {
|
|
return this.getMaxHp() - this.hp;
|
|
}
|
|
|
|
getHpRatio(precise: boolean = false): number {
|
|
return precise
|
|
? this.hp / this.getMaxHp()
|
|
: Math.round((this.hp / this.getMaxHp()) * 100) / 100;
|
|
}
|
|
|
|
generateGender(): void {
|
|
if (this.species.malePercent === null)
|
|
this.gender = Gender.GENDERLESS;
|
|
else {
|
|
const genderChance = (this.id % 256) * 0.390625;
|
|
if (genderChance < this.species.malePercent)
|
|
this.gender = Gender.MALE;
|
|
else
|
|
this.gender = Gender.FEMALE;
|
|
}
|
|
}
|
|
|
|
getGender(ignoreOverride?: boolean): Gender {
|
|
if (!ignoreOverride && this.summonData?.gender !== undefined)
|
|
return this.summonData.gender;
|
|
return this.gender;
|
|
}
|
|
|
|
getFusionGender(ignoreOverride?: boolean): Gender {
|
|
if (!ignoreOverride && this.summonData?.fusionGender !== undefined)
|
|
return this.summonData.fusionGender;
|
|
return this.fusionGender;
|
|
}
|
|
|
|
isShiny(): boolean {
|
|
return this.shiny || (this.isFusion() && this.fusionShiny);
|
|
}
|
|
|
|
getVariant(): Variant {
|
|
return !this.isFusion() ? this.variant : Math.max(this.variant, this.fusionVariant) as Variant;
|
|
}
|
|
|
|
getLuck(): integer {
|
|
return this.luck + (this.isFusion() ? this.fusionLuck : 0);
|
|
}
|
|
|
|
isFusion(): boolean {
|
|
return !!this.fusionSpecies;
|
|
}
|
|
|
|
abstract isBoss(): boolean;
|
|
|
|
getMoveset(ignoreOverride?: boolean): PokemonMove[] {
|
|
const ret = !ignoreOverride && this.summonData?.moveset
|
|
? this.summonData.moveset
|
|
: this.moveset;
|
|
|
|
// Overrides moveset based on arrays specified in overrides.ts
|
|
const overrideArray: Array<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))
|
|
});
|
|
}
|
|
|
|
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);
|
|
}
|
|
|
|
getTypes(includeTeraType = false, forDefend: boolean = false, ignoreOverride?: boolean): Type[] {
|
|
const types = [];
|
|
|
|
if (includeTeraType) {
|
|
const teraType = this.getTeraType();
|
|
if (teraType != Type.UNKNOWN)
|
|
types.push(teraType);
|
|
}
|
|
|
|
if (!types.length || !includeTeraType) {
|
|
if (!ignoreOverride && this.summonData?.types)
|
|
this.summonData.types.forEach(t => types.push(t));
|
|
else {
|
|
const speciesForm = this.getSpeciesForm();
|
|
|
|
types.push(speciesForm.type1);
|
|
|
|
const fusionSpeciesForm = this.getFusionSpeciesForm();
|
|
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);
|
|
}
|
|
}
|
|
|
|
if (forDefend && (this.getTag(BattlerTagType.IGNORE_FLYING) || this.scene.arena.getTag(ArenaTagType.GRAVITY) || this.getTag(BattlerTagType.GROUNDED))) {
|
|
const flyingIndex = types.indexOf(Type.FLYING);
|
|
if (flyingIndex > -1)
|
|
types.splice(flyingIndex, 1);
|
|
}
|
|
|
|
if (!types.length) // become UNKNOWN if no types are present
|
|
types.push(Type.UNKNOWN);
|
|
|
|
if (types.length > 1 && types.includes(Type.UNKNOWN)) { // remove UNKNOWN if other types are present
|
|
const index = types.indexOf(Type.UNKNOWN);
|
|
if (index !== -1) {
|
|
types.splice(index, 1);
|
|
}
|
|
}
|
|
|
|
return types;
|
|
}
|
|
|
|
isOfType(type: Type, forDefend: boolean = false): boolean {
|
|
return !!this.getTypes(true, forDefend).find(t => t === type);
|
|
}
|
|
|
|
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];
|
|
}
|
|
|
|
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]];
|
|
}
|
|
|
|
/**
|
|
* 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();
|
|
}
|
|
|
|
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).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));
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
hasAbilityWithAttr(attrType: { new(...args: any[]): 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;
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
getTeraType(): Type {
|
|
const teraModifier = this.scene.findModifier(m => m instanceof TerastallizeModifier
|
|
&& m.pokemonId === this.id && !!m.getBattlesLeft(), this.isPlayer()) as TerastallizeModifier;
|
|
if (teraModifier)
|
|
return teraModifier.teraType;
|
|
|
|
return Type.UNKNOWN;
|
|
}
|
|
|
|
isTerastallized(): boolean {
|
|
return this.getTeraType() !== Type.UNKNOWN;
|
|
}
|
|
|
|
isGrounded(): boolean {
|
|
return !this.isOfType(Type.FLYING, true) && this.getAbility().id !== Abilities.LEVITATE;
|
|
}
|
|
|
|
getAttackMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier {
|
|
const typeless = !!move.getMove().getAttrs(TypelessAttr).length;
|
|
const typeMultiplier = new Utils.NumberHolder(this.getAttackTypeEffectiveness(move.getMove().type, source));
|
|
const cancelled = new Utils.BooleanHolder(false);
|
|
if (!typeless)
|
|
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier, true);
|
|
if (!cancelled.value)
|
|
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, move, cancelled, typeMultiplier, true);
|
|
return (!cancelled.value ? typeMultiplier.value : 0) as TypeDamageMultiplier;
|
|
}
|
|
|
|
getAttackTypeEffectiveness(moveType: Type, source?: Pokemon): TypeDamageMultiplier {
|
|
if (moveType === Type.STELLAR)
|
|
return this.isTerastallized() ? 2 : 1;
|
|
const types = this.getTypes(true, true);
|
|
|
|
let multiplier = types.map(defType => {
|
|
if (source) {
|
|
const ignoreImmunity = new Utils.BooleanHolder(false);
|
|
applyAbAttrs(IgnoreTypeImmunityAbAttr, source, ignoreImmunity, moveType, defType);
|
|
if (ignoreImmunity.value)
|
|
return 1;
|
|
}
|
|
|
|
return getTypeDamageMultiplier(moveType, defType);
|
|
}).reduce((acc, cur) => acc * cur, 1) as TypeDamageMultiplier;
|
|
|
|
// Handle strong winds lowering effectiveness of types super effective against pure flying
|
|
if (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;
|
|
return multiplier;
|
|
}
|
|
|
|
getMatchupScore(pokemon: Pokemon): number {
|
|
const types = this.getTypes(true);
|
|
const enemyTypes = pokemon.getTypes(true, true);
|
|
const outspeed = (this.isActive(true) ? this.getBattleStat(Stat.SPD, pokemon) : this.getStat(Stat.SPD)) <= pokemon.getBattleStat(Stat.SPD, this);
|
|
let atkScore = pokemon.getAttackTypeEffectiveness(types[0], this) * (outspeed ? 1.25 : 1);
|
|
let defScore = 1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[0], pokemon), 0.25);
|
|
if (types.length > 1)
|
|
atkScore *= pokemon.getAttackTypeEffectiveness(types[1], this);
|
|
if (enemyTypes.length > 1)
|
|
defScore *= (1 / Math.max(this.getAttackTypeEffectiveness(enemyTypes[1], pokemon), 0.25));
|
|
let hpDiffRatio = this.getHpRatio() + (1 - pokemon.getHpRatio());
|
|
if (outspeed)
|
|
hpDiffRatio = Math.min(hpDiffRatio * 1.5, 1);
|
|
return (atkScore + defScore) * hpDiffRatio;
|
|
}
|
|
|
|
getEvolution(): SpeciesFormEvolution {
|
|
if (pokemonEvolutions.hasOwnProperty(this.species.speciesId)) {
|
|
const evolutions = pokemonEvolutions[this.species.speciesId];
|
|
for (let e of evolutions) {
|
|
if (!e.item && this.level >= e.level && (!e.preFormKey || this.getFormKey() === e.preFormKey)) {
|
|
if (e.condition === null || (e.condition as SpeciesEvolutionCondition).predicate(this))
|
|
return e;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.isFusion() && pokemonEvolutions.hasOwnProperty(this.fusionSpecies.speciesId)) {
|
|
const fusionEvolutions = pokemonEvolutions[this.fusionSpecies.speciesId].map(e => new FusionSpeciesFormEvolution(this.species.speciesId, e));
|
|
for (let fe of fusionEvolutions) {
|
|
if (!fe.item && this.level >= fe.level && (!fe.preFormKey || this.getFusionFormKey() === fe.preFormKey)) {
|
|
if (fe.condition === null || (fe.condition as SpeciesEvolutionCondition).predicate(this))
|
|
return fe;
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
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) {
|
|
const evolutionLevelMoves = levelMoves.slice(0, Math.max(levelMoves.findIndex(lm => !!lm[0]), 0));
|
|
const fusionLevelMoves = this.getFusionSpeciesForm(true).getLevelMoves();
|
|
const newLevelMoves: LevelMoves = [];
|
|
while (levelMoves.length && levelMoves[0][0] < startingLevel)
|
|
levelMoves.shift();
|
|
while (fusionLevelMoves.length && fusionLevelMoves[0][0] < startingLevel)
|
|
fusionLevelMoves.shift();
|
|
if (includeEvolutionMoves) {
|
|
for (let elm of evolutionLevelMoves.reverse())
|
|
levelMoves.unshift(elm);
|
|
}
|
|
for (let l = includeEvolutionMoves ? 0 : startingLevel; l <= this.level; l++) {
|
|
if (l === 1 && startingLevel > 1)
|
|
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 (let lm of levelMoves) {
|
|
const level = lm[0];
|
|
if ((!includeEvolutionMoves || level) && level < startingLevel)
|
|
continue;
|
|
else if (level > this.level)
|
|
break;
|
|
ret.push(lm);
|
|
}
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
setMove(moveIndex: integer, moveId: Moves): void {
|
|
const move = moveId ? new PokemonMove(moveId) : null;
|
|
this.moveset[moveIndex] = move;
|
|
if (this.summonData?.moveset)
|
|
this.summonData.moveset[moveIndex] = move;
|
|
}
|
|
|
|
trySetShiny(thresholdOverride?: integer): boolean {
|
|
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;
|
|
|
|
let shinyThreshold = new Utils.IntegerHolder(32);
|
|
if (thresholdOverride === undefined) {
|
|
if (!this.hasTrainer()) {
|
|
if (new Date() < new Date('2024-05-13'))
|
|
shinyThreshold.value *= 3;
|
|
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;
|
|
}
|
|
|
|
generateVariant(): Variant {
|
|
if (!this.shiny || !variantData.hasOwnProperty(this.species.speciesId))
|
|
return 0;
|
|
const rand = Utils.randSeedInt(10);
|
|
if (rand > 3)
|
|
return 0;
|
|
if (rand)
|
|
return 1;
|
|
return 2;
|
|
}
|
|
|
|
generateFusionSpecies(forStarter?: boolean): void {
|
|
const hiddenAbilityChance = new Utils.IntegerHolder(256);
|
|
if (!this.hasTrainer())
|
|
this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
|
|
|
|
const hasHiddenAbility = !Utils.randSeedInt(hiddenAbilityChance.value);
|
|
const randAbilityIndex = Utils.randSeedInt(2);
|
|
|
|
const filter = !forStarter ? this.species.getCompatibleFusionSpeciesFilter()
|
|
: species => {
|
|
return pokemonEvolutions.hasOwnProperty(species.speciesId)
|
|
&& !pokemonPrevolutions.hasOwnProperty(species.speciesId)
|
|
&& !species.pseudoLegendary
|
|
&& !species.legendary
|
|
&& !species.mythical
|
|
&& !species.isTrainerForbidden()
|
|
&& species.speciesId !== this.species.speciesId
|
|
};
|
|
|
|
this.fusionSpecies = this.scene.randomSpecies(this.scene.currentBattle?.waveIndex || 0, this.level, false, filter, true);
|
|
this.fusionAbilityIndex = (this.fusionSpecies.abilityHidden && hasHiddenAbility ? this.fusionSpecies.ability2 ? 2 : 1 : this.fusionSpecies.ability2 ? randAbilityIndex : 0);
|
|
this.fusionShiny = this.shiny;
|
|
this.fusionVariant = this.variant;
|
|
|
|
if (this.fusionSpecies.malePercent === null)
|
|
this.fusionGender = Gender.GENDERLESS;
|
|
else {
|
|
const genderChance = (this.id % 256) * 0.390625;
|
|
if (genderChance < this.fusionSpecies.malePercent)
|
|
this.fusionGender = Gender.MALE;
|
|
else
|
|
this.fusionGender = Gender.FEMALE;
|
|
}
|
|
|
|
this.fusionFormIndex = this.scene.getSpeciesFormIndex(this.fusionSpecies, this.fusionGender, this.getNature(), true);
|
|
this.fusionLuck = this.luck;
|
|
|
|
this.generateName();
|
|
}
|
|
|
|
clearFusionSpecies(): void {
|
|
this.fusionSpecies = undefined;
|
|
this.fusionFormIndex = 0;
|
|
this.fusionAbilityIndex = 0;
|
|
this.fusionShiny = false;
|
|
this.fusionVariant = 0;
|
|
this.fusionGender = 0;
|
|
this.fusionLuck = 0;
|
|
|
|
this.generateName();
|
|
this.calculateStats();
|
|
}
|
|
|
|
generateAndPopulateMoveset(): void {
|
|
this.moveset = [];
|
|
let movePool: [Moves, number][] = [];
|
|
const allLevelMoves = this.getLevelMoves(1, true, true);
|
|
if (!allLevelMoves) {
|
|
console.log(this.species.speciesId, 'ERROR');
|
|
return;
|
|
}
|
|
|
|
for (let m = 0; m < allLevelMoves.length; m++) {
|
|
const levelMove = allLevelMoves[m];
|
|
if (this.level < levelMove[0])
|
|
break;
|
|
let weight = levelMove[0];
|
|
if (weight === 0) // Evo Moves
|
|
weight = 50;
|
|
if (weight === 1 && allMoves[levelMove[1]].power >= 80) // Assume level 1 moves with 80+ BP are "move reminder" moves and bump their weight
|
|
weight = 40;
|
|
if (allMoves[levelMove[1]].name.endsWith(' (N)'))
|
|
weight /= 100; // Unimplemented level up moves are possible to generate, but 1% of their normal chance.
|
|
if (!movePool.some(m => m[0] === levelMove[1]))
|
|
movePool.push([levelMove[1], weight]);
|
|
}
|
|
|
|
if (this.hasTrainer()) {
|
|
const tms = Object.keys(tmSpecies);
|
|
for (let tm of tms) {
|
|
const moveId = parseInt(tm) as Moves;
|
|
let compatible = false;
|
|
for (let 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 >= 25) { // No egg moves below level 25
|
|
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, Math.min(this.level * 0.5, 40)]);
|
|
}
|
|
const moveId = speciesEggMoves[this.species.getRootSpeciesId()][3];
|
|
if (this.level >= 60 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(' (N)')) // No rare egg moves before level 60
|
|
movePool.push([moveId, Math.min(this.level * 0.2, 20)]);
|
|
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, Math.min(this.level * 0.5, 30)]);
|
|
}
|
|
const moveId = speciesEggMoves[this.fusionSpecies.getRootSpeciesId()][3];
|
|
if (this.level >= 60 && !movePool.some(m => m[0] === moveId) && !allMoves[moveId].name.endsWith(' (N)')) // No rare egg moves before level 60
|
|
movePool.push([moveId, Math.min(this.level * 0.2, 20)]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this.isBoss()) // Bosses never get self ko moves
|
|
movePool = movePool.filter(m => !allMoves[m[0]].getAttrs(SacrificialAttr).length);
|
|
if (this.hasTrainer()) {
|
|
// Trainers never get OHKO moves
|
|
movePool = movePool.filter(m => !allMoves[m[0]].getAttrs(OneHitKOAttr).length);
|
|
// Half the weight of self KO moves
|
|
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].getAttrs(SacrificialAttr).length ? 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 as StatChangeAttr).levels > 1 && (a as StatChangeAttr).selfTarget) ? 1.25 : 1)]);
|
|
// Trainers get a weight decrease to multiturn moves
|
|
movePool = movePool.map(m => [m[0], m[1] * (!!allMoves[m[0]].getAttrs(ChargeAttr).length || !!allMoves[m[0]].getAttrs(RechargeAttr).length ? 0.7 : 1)]);
|
|
}
|
|
|
|
// Weight towards higher power moves, by reducing the power of moves below the highest power.
|
|
// Caps max power at 90 to avoid something like hyper beam ruining the stats.
|
|
// This is a pretty soft weighting factor, although it is scaled with the weight multiplier.
|
|
const maxPower = Math.min(movePool.reduce((v, m) => Math.max(allMoves[m[0]].power, v), 40), 90);
|
|
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === MoveCategory.STATUS ? 1 : Math.max(Math.min(allMoves[m[0]].power/maxPower, 1), 0.5))]);
|
|
|
|
// Weight damaging moves against the lower stat
|
|
const worseCategory: MoveCategory = this.stats[Stat.ATK] > this.stats[Stat.SPATK] ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
|
|
const statRatio = worseCategory === MoveCategory.PHYSICAL ? this.stats[Stat.ATK]/this.stats[Stat.SPATK] : this.stats[Stat.SPATK]/this.stats[Stat.ATK];
|
|
movePool = movePool.map(m => [m[0], m[1] * (allMoves[m[0]].category === worseCategory ? statRatio : 1)]);
|
|
|
|
let weightMultiplier = 0.9; // The higher this is the more the game weights towards higher level moves. At 0 all moves are equal weight.
|
|
if (this.hasTrainer())
|
|
weightMultiplier += 0.7;
|
|
if (this.isBoss())
|
|
weightMultiplier += 0.4;
|
|
const baseWeights: [Moves, number][] = movePool.map(m => [m[0], Math.ceil(Math.pow(m[1], weightMultiplier)*100)]);
|
|
|
|
if (this.hasTrainer() || this.isBoss()) { // Trainers and bosses always force a stab move
|
|
const stabMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS && this.isOfType(allMoves[m[0]].type));
|
|
|
|
if (stabMovePool.length) {
|
|
const totalWeight = stabMovePool.reduce((v, m) => v + m[1], 0);
|
|
let rand = Utils.randSeedInt(totalWeight);
|
|
let index = 0;
|
|
while (rand > stabMovePool[index][1])
|
|
rand -= stabMovePool[index++][1];
|
|
this.moveset.push(new PokemonMove(stabMovePool[index][0], 0, 0));
|
|
}
|
|
} else { // Normal wild pokemon just force a random damaging move
|
|
const attackMovePool = baseWeights.filter(m => allMoves[m[0]].category !== MoveCategory.STATUS);
|
|
if (attackMovePool.length) {
|
|
const totalWeight = attackMovePool.reduce((v, m) => v + m[1], 0);
|
|
let rand = Utils.randSeedInt(totalWeight);
|
|
let index = 0;
|
|
while (rand > attackMovePool[index][1])
|
|
rand -= attackMovePool[index++][1];
|
|
this.moveset.push(new PokemonMove(attackMovePool[index][0], 0, 0));
|
|
}
|
|
}
|
|
|
|
while (baseWeights.length > this.moveset.length && this.moveset.length < 4) {
|
|
if (this.hasTrainer()) {
|
|
// Sqrt the weight of any damaging moves with overlapping types. This is about a 0.05 - 0.1 multiplier.
|
|
// Other damaging moves 2x weight if 0-1 damaging moves, 0.5x if 2, 0.125x if 3. These weights double if STAB.
|
|
// Status moves remain unchanged on weight, this encourages 1-2
|
|
movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo.moveId)).map(m => [m[0], this.moveset.some(mo => mo.getMove().category !== MoveCategory.STATUS && mo.getMove().type === allMoves[m[0]].type) ? Math.ceil(Math.sqrt(m[1])) : allMoves[m[0]].category !== MoveCategory.STATUS ? Math.ceil(m[1]/Math.max(Math.pow(4, this.moveset.filter(mo => mo.getMove().power > 1).length)/8,0.5) * (this.isOfType(allMoves[m[0]].type) ? 2 : 1)) : m[1]]);
|
|
} else { // Non-trainer pokemon just use normal weights
|
|
movePool = baseWeights.filter(m => !this.moveset.some(mo => m[0] === mo.moveId));
|
|
}
|
|
const totalWeight = movePool.reduce((v, m) => v + m[1], 0);
|
|
let rand = Utils.randSeedInt(totalWeight);
|
|
let index = 0;
|
|
while (rand > movePool[index][1])
|
|
rand -= movePool[index++][1];
|
|
this.moveset.push(new PokemonMove(movePool[index][0], 0, 0));
|
|
}
|
|
|
|
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeMoveLearnedTrigger);
|
|
}
|
|
|
|
trySelectMove(moveIndex: integer, ignorePp?: boolean): boolean {
|
|
const move = this.getMoveset().length > moveIndex
|
|
? this.getMoveset()[moveIndex]
|
|
: null;
|
|
return move?.isUsable(this, ignorePp);
|
|
}
|
|
|
|
showInfo(): void {
|
|
if (!this.battleInfo.visible) {
|
|
const otherBattleInfo = this.scene.fieldUI.getAll().slice(0, 4).filter(ui => ui instanceof BattleInfo && ((ui as BattleInfo) instanceof PlayerBattleInfo) === this.isPlayer()).find(() => true);
|
|
if (!otherBattleInfo || !this.getFieldIndex())
|
|
this.scene.fieldUI.sendToBack(this.battleInfo);
|
|
else
|
|
this.scene.fieldUI.moveAbove(this.battleInfo, otherBattleInfo);
|
|
this.battleInfo.setX(this.battleInfo.x + (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
|
|
this.battleInfo.setVisible(true);
|
|
if (this.isPlayer())
|
|
this.battleInfo.expMaskRect.x += 150;
|
|
this.scene.tweens.add({
|
|
targets: [ this.battleInfo, this.battleInfo.expMaskRect ],
|
|
x: this.isPlayer() ? '-=150' : `+=${!this.isBoss() ? 150 : 246}`,
|
|
duration: 1000,
|
|
ease: 'Cubic.easeOut'
|
|
});
|
|
}
|
|
}
|
|
|
|
hideInfo(): Promise<void> {
|
|
return new Promise(resolve => {
|
|
if (this.battleInfo.visible) {
|
|
this.scene.tweens.add({
|
|
targets: [ this.battleInfo, this.battleInfo.expMaskRect ],
|
|
x: this.isPlayer() ? '+=150' : `-=${!this.isBoss() ? 150 : 246}`,
|
|
duration: 500,
|
|
ease: 'Cubic.easeIn',
|
|
onComplete: () => {
|
|
if (this.isPlayer())
|
|
this.battleInfo.expMaskRect.x -= 150;
|
|
this.battleInfo.setVisible(false);
|
|
this.battleInfo.setX(this.battleInfo.x - (this.isPlayer() ? 150 : !this.isBoss() ? -150 : -198));
|
|
resolve();
|
|
}
|
|
});
|
|
} else
|
|
resolve();
|
|
});
|
|
}
|
|
|
|
updateInfo(instant?: boolean): Promise<void> {
|
|
return this.battleInfo.updateInfo(this, instant);
|
|
}
|
|
|
|
toggleStats(visible: boolean): void {
|
|
this.battleInfo.toggleStats(visible);
|
|
}
|
|
|
|
addExp(exp: integer) {
|
|
const maxExpLevel = this.scene.getMaxExpLevel();
|
|
const initialExp = this.exp;
|
|
this.exp += exp;
|
|
while (this.level < maxExpLevel && this.exp >= getLevelTotalExp(this.level + 1, this.species.growthRate))
|
|
this.level++;
|
|
if (this.level >= maxExpLevel) {
|
|
console.log(initialExp, this.exp, getLevelTotalExp(this.level, this.species.growthRate));
|
|
this.exp = Math.max(getLevelTotalExp(this.level, this.species.growthRate), initialExp);
|
|
}
|
|
this.levelExp = this.exp - getLevelTotalExp(this.level, this.species.growthRate);
|
|
}
|
|
|
|
getOpponent(targetIndex: integer): Pokemon {
|
|
const ret = this.getOpponents()[targetIndex];
|
|
if (ret.summonData)
|
|
return ret;
|
|
return null;
|
|
}
|
|
|
|
getOpponents(): Pokemon[] {
|
|
return ((this.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField()) as Pokemon[]).filter(p => p.isActive());
|
|
}
|
|
|
|
getOpponentDescriptor(): string {
|
|
const opponents = this.getOpponents();
|
|
if (opponents.length === 1)
|
|
return opponents[0].name;
|
|
return this.isPlayer() ? 'the opposing team' : 'your team';
|
|
}
|
|
|
|
getAlly(): Pokemon {
|
|
return (this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.getFieldIndex() ? 0 : 1];
|
|
}
|
|
|
|
apply(source: Pokemon, battlerMove: PokemonMove): HitResult {
|
|
let result: HitResult;
|
|
const move = battlerMove.getMove();
|
|
let damage = new Utils.NumberHolder(0);
|
|
const defendingSidePlayField = this.isPlayer() ? this.scene.getPlayerField() : this.scene.getEnemyField();
|
|
|
|
const variableCategory = new Utils.IntegerHolder(move.category);
|
|
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, variableCategory);
|
|
const moveCategory = variableCategory.value as MoveCategory;
|
|
|
|
const variableType = new Utils.IntegerHolder(move.type);
|
|
const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1);
|
|
applyMoveAttrs(VariableMoveTypeAttr, source, this, move, variableType);
|
|
// 2nd argument is for MoveTypeChangePowerMultiplierAbAttr
|
|
applyAbAttrs(VariableMoveTypeAbAttr, source, null, variableType, typeChangeMovePowerMultiplier);
|
|
applyPreAttackAbAttrs(MoveTypeChangeAttr, source, this, battlerMove, variableType, typeChangeMovePowerMultiplier);
|
|
const type = variableType.value as Type;
|
|
const types = this.getTypes(true, true);
|
|
|
|
const cancelled = new Utils.BooleanHolder(false);
|
|
const typeless = !!move.getAttrs(TypelessAttr).length;
|
|
const typeMultiplier = new Utils.NumberHolder(!typeless && (moveCategory !== MoveCategory.STATUS || move.getAttrs(StatusMoveTypeImmunityAttr).find(attr => types.includes((attr as StatusMoveTypeImmunityAttr).immuneType)))
|
|
? this.getAttackTypeEffectiveness(type, source)
|
|
: 1);
|
|
applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier);
|
|
if (typeless)
|
|
typeMultiplier.value = 1;
|
|
if (types.find(t => move.isTypeImmune(t)))
|
|
typeMultiplier.value = 0;
|
|
|
|
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 === type && power.value < 60 && move.priority <= 0 && !move.getAttrs(MultiHitAttr).length && !this.scene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id))
|
|
power.value = 60;
|
|
applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, this, battlerMove, power);
|
|
this.scene.getField(true).map(p => applyPreAttackAbAttrs(FieldVariableMovePowerAbAttr, this, source, battlerMove, power));
|
|
|
|
applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, battlerMove, cancelled, power);
|
|
|
|
power.value *= typeChangeMovePowerMultiplier.value;
|
|
|
|
if (!typeless)
|
|
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, battlerMove, cancelled, typeMultiplier);
|
|
if (!cancelled.value) {
|
|
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, battlerMove, cancelled, typeMultiplier);
|
|
defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, battlerMove, cancelled, typeMultiplier));
|
|
}
|
|
|
|
if (cancelled.value)
|
|
result = HitResult.NO_EFFECT;
|
|
else {
|
|
let typeBoost = source.findTag(t => t instanceof TypeBoostTag && (t as TypeBoostTag).boostedType === 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(type, source.isGrounded()));
|
|
applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier);
|
|
if (this.scene.arena.getTerrainType() === TerrainType.GRASSY && this.isGrounded() && 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, type, power);
|
|
this.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, 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);
|
|
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));
|
|
if (isCritical) {
|
|
const blockCrit = new Utils.BooleanHolder(false);
|
|
applyAbAttrs(BlockCritAbAttr, this, null, blockCrit);
|
|
if (blockCrit.value)
|
|
isCritical = false;
|
|
}
|
|
}
|
|
const sourceAtk = new Utils.IntegerHolder(source.getBattleStat(isPhysical ? Stat.ATK : Stat.SPATK, this, null, isCritical));
|
|
const targetDef = new Utils.IntegerHolder(this.getBattleStat(isPhysical ? Stat.DEF : Stat.SPDEF, source, move, isCritical));
|
|
const criticalMultiplier = new Utils.NumberHolder(isCritical ? 1.5 : 1);
|
|
applyAbAttrs(MultCritAbAttr, source, null, criticalMultiplier);
|
|
const screenMultiplier = new Utils.NumberHolder(1);
|
|
if (!isCritical) {
|
|
this.scene.arena.applyTagsForSide(WeakenMoveScreenTag, this.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY, move.category, this.scene.currentBattle.double, screenMultiplier);
|
|
}
|
|
const isTypeImmune = (typeMultiplier.value * arenaAttackTypeMultiplier.value) === 0;
|
|
const sourceTypes = source.getTypes();
|
|
const matchesSourceType = sourceTypes[0] === type || (sourceTypes.length > 1 && sourceTypes[1] === type);
|
|
let stabMultiplier = new Utils.NumberHolder(1);
|
|
if (sourceTeraType === Type.UNKNOWN && matchesSourceType)
|
|
stabMultiplier.value += 0.5;
|
|
else if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === 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) {
|
|
const burnDamageReductionCancelled = new Utils.BooleanHolder(false);
|
|
applyAbAttrs(BypassBurnDamageReductionAbAttr, source, burnDamageReductionCancelled);
|
|
if (!burnDamageReductionCancelled.value)
|
|
damage.value = Math.floor(damage.value / 2);
|
|
}
|
|
move.getAttrs(HitsTagAttr).map(hta => hta as HitsTagAttr).filter(hta => hta.doubleDamage).forEach(hta => {
|
|
if (this.getTag(hta.tagType))
|
|
damage.value *= 2;
|
|
});
|
|
}
|
|
|
|
if (this.scene.arena.terrain?.terrainType === TerrainType.MISTY && this.isGrounded() && type === Type.DRAGON)
|
|
damage.value = Math.floor(damage.value / 2);
|
|
|
|
const fixedDamage = new Utils.IntegerHolder(0);
|
|
applyMoveAttrs(FixedDamageAttr, source, this, move, fixedDamage);
|
|
if (!isTypeImmune && fixedDamage.value) {
|
|
damage.value = fixedDamage.value;
|
|
isCritical = false;
|
|
result = HitResult.EFFECTIVE;
|
|
}
|
|
|
|
if (!result) {
|
|
if (!typeMultiplier.value)
|
|
result = 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;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
if (power.value === 0) {
|
|
damage.value = 0;
|
|
}
|
|
|
|
console.log('damage', damage.value, move.name, power.value, sourceAtk, targetDef);
|
|
|
|
if (damage.value) {
|
|
if (this.getHpRatio() === 1)
|
|
applyPreDefendAbAttrs(PreDefendFullHpEndureAbAttr, this, source, battlerMove, cancelled, damage);
|
|
else if (!this.isPlayer() && damage.value >= this.hp)
|
|
this.scene.applyModifiers(EnemyEndureChanceModifier, false, this);
|
|
|
|
const oneHitKo = result === HitResult.ONE_HIT_KO;
|
|
damage.value = this.damageAndUpdate(damage.value, result as DamageResult, isCritical, oneHitKo, oneHitKo);
|
|
this.turnData.damageTaken += damage.value;
|
|
if (isCritical)
|
|
this.scene.queueMessage(i18next.t('battle:hitResultCriticalHit'));
|
|
this.scene.setPhaseQueueSplice();
|
|
if (source.isPlayer()) {
|
|
this.scene.validateAchvs(DamageAchv, damage);
|
|
if (damage.value > this.scene.gameData.gameStats.highestDamage)
|
|
this.scene.gameData.gameStats.highestDamage = damage.value;
|
|
}
|
|
source.turnData.damageDealt += damage.value;
|
|
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.ONE_HIT_KO:
|
|
this.scene.queueMessage(i18next.t('battle:hitResultOneHitKO'));
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (damage)
|
|
this.scene.clearPhaseQueueSplice();
|
|
}
|
|
break;
|
|
case MoveCategory.STATUS:
|
|
if (!typeless)
|
|
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, battlerMove, cancelled, typeMultiplier);
|
|
if (!cancelled.value) {
|
|
applyPreDefendAbAttrs(MoveImmunityAbAttr, this, source, battlerMove, cancelled, typeMultiplier);
|
|
defendingSidePlayField.forEach((p) => applyPreDefendAbAttrs(FieldPriorityMoveImmunityAbAttr, p, source, battlerMove, 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;
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false): integer {
|
|
if (this.isFainted())
|
|
return 0;
|
|
const surviveDamage = new Utils.BooleanHolder(false);
|
|
|
|
if (!preventEndure && this.hp - damage <= 0) {
|
|
if(this.hp >= 1 && this.getTag(BattlerTagType.ENDURING))
|
|
surviveDamage.value = this.lapseTag(BattlerTagType.ENDURING)
|
|
else if (this.hp > 1 && this.getTag(BattlerTagType.STURDY))
|
|
surviveDamage.value = this.lapseTag(BattlerTagType.STURDY)
|
|
if (!surviveDamage.value)
|
|
this.scene.applyModifiers(SurviveDamageModifier, this.isPlayer(), this, surviveDamage);
|
|
if (surviveDamage.value)
|
|
damage = this.hp - 1;
|
|
}
|
|
|
|
damage = Math.min(damage, this.hp);
|
|
|
|
this.hp = this.hp - damage;
|
|
if (this.isFainted()) {
|
|
this.scene.unshiftPhase(new FaintPhase(this.scene, this.getBattlerIndex(), preventEndure));
|
|
this.resetSummonData();
|
|
}
|
|
|
|
return damage;
|
|
}
|
|
|
|
damageAndUpdate(damage: integer, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: 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);
|
|
// Damage amount may have changed, but needed to be queued before calling damage function
|
|
damagePhase.updateAmount(damage);
|
|
return damage;
|
|
}
|
|
|
|
heal(amount: integer): integer {
|
|
const healAmount = Math.min(amount, this.getMaxHp() - this.hp);
|
|
this.hp += healAmount;
|
|
return healAmount;
|
|
}
|
|
|
|
isBossImmune(): boolean {
|
|
return this.isBoss();
|
|
}
|
|
|
|
isMax(): boolean {
|
|
const maxForms = [SpeciesFormKey.GIGANTAMAX, SpeciesFormKey.GIGANTAMAX_RAPID, SpeciesFormKey.GIGANTAMAX_SINGLE, SpeciesFormKey.ETERNAMAX] as string[];
|
|
return maxForms.includes(this.getFormKey()) || maxForms.includes(this.getFusionFormKey());
|
|
}
|
|
|
|
addTag(tagType: BattlerTagType, turnCount: integer = 0, sourceMove?: Moves, sourceId?: integer): boolean {
|
|
const existingTag = this.getTag(tagType);
|
|
if (existingTag) {
|
|
existingTag.onOverlap(this);
|
|
return false;
|
|
}
|
|
|
|
const newTag = getBattlerTag(tagType, turnCount, sourceMove, sourceId);
|
|
|
|
const cancelled = new Utils.BooleanHolder(false);
|
|
applyPreApplyBattlerTagAbAttrs(PreApplyBattlerTagAbAttr, this, newTag, cancelled);
|
|
|
|
if (!cancelled.value && newTag.canAdd(this)) {
|
|
this.summonData.tags.push(newTag);
|
|
newTag.onAdd(this);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
getTag(tagType: BattlerTagType | { new(...args: any[]): BattlerTag }): BattlerTag {
|
|
if (!this.summonData)
|
|
return null;
|
|
return typeof(tagType) === 'string'
|
|
? this.summonData.tags.find(t => t.tagType === tagType)
|
|
: this.summonData.tags.find(t => t instanceof tagType);
|
|
}
|
|
|
|
findTag(tagFilter: ((tag: BattlerTag) => boolean)) {
|
|
if (!this.summonData)
|
|
return null;
|
|
return this.summonData.tags.find(t => tagFilter(t));
|
|
}
|
|
|
|
getTags(tagType: BattlerTagType | { new(...args: any[]): BattlerTag }): BattlerTag[] {
|
|
if (!this.summonData)
|
|
return [];
|
|
return typeof(tagType) === 'string'
|
|
? 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 [];
|
|
return this.summonData.tags.filter(t => tagFilter(t));
|
|
}
|
|
|
|
lapseTag(tagType: BattlerTagType): boolean {
|
|
const tags = this.summonData.tags;
|
|
const tag = tags.find(t => t.tagType === tagType);
|
|
if (tag && !(tag.lapse(this, BattlerTagLapseType.CUSTOM))) {
|
|
tag.onRemove(this);
|
|
tags.splice(tags.indexOf(tag), 1);
|
|
}
|
|
return !!tag;
|
|
}
|
|
|
|
lapseTags(lapseType: BattlerTagLapseType): void {
|
|
const tags = this.summonData.tags;
|
|
tags.filter(t => lapseType === BattlerTagLapseType.FAINT || ((t.lapseType === lapseType) && !(t.lapse(this, lapseType))) || (lapseType === BattlerTagLapseType.TURN_END && t.turnCount < 1)).forEach(t => {
|
|
t.onRemove(this);
|
|
tags.splice(tags.indexOf(t), 1);
|
|
});
|
|
}
|
|
|
|
removeTag(tagType: BattlerTagType): boolean {
|
|
const tags = this.summonData.tags;
|
|
const tag = tags.find(t => t.tagType === tagType);
|
|
if (tag) {
|
|
tag.turnCount = 0;
|
|
tag.onRemove(this);
|
|
tags.splice(tags.indexOf(tag), 1);
|
|
}
|
|
return !!tag;
|
|
}
|
|
|
|
findAndRemoveTags(tagFilter: ((tag: BattlerTag) => boolean)): boolean {
|
|
if (!this.summonData)
|
|
return false;
|
|
const tags = this.summonData.tags;
|
|
const tagsToRemove = tags.filter(t => tagFilter(t));
|
|
for (let tag of tagsToRemove) {
|
|
tag.turnCount = 0;
|
|
tag.onRemove(this);
|
|
tags.splice(tags.indexOf(tag), 1);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
removeTagsBySourceId(sourceId: integer): void {
|
|
this.findAndRemoveTags(t => t.isSourceLinked() && t.sourceId === sourceId);
|
|
}
|
|
|
|
transferTagsBySourceId(sourceId: integer, newSourceId: integer): void {
|
|
if (!this.summonData)
|
|
return;
|
|
const tags = this.summonData.tags;
|
|
tags.filter(t => t.sourceId === sourceId).forEach(t => t.sourceId = newSourceId);
|
|
}
|
|
|
|
transferSummon(source: Pokemon): void {
|
|
const battleStats = Utils.getEnumValues(BattleStat);
|
|
for (let stat of battleStats)
|
|
this.summonData.battleStats[stat] = source.summonData.battleStats[stat];
|
|
for (let tag of source.summonData.tags)
|
|
this.summonData.tags.push(tag);
|
|
if (this instanceof PlayerPokemon && source.summonData.battleStats.find(bs => bs === 6))
|
|
this.scene.validateAchv(achvs.TRANSFER_MAX_BATTLE_STAT);
|
|
this.updateInfo();
|
|
}
|
|
|
|
getMoveHistory(): TurnMove[] {
|
|
return this.battleSummonData.moveHistory;
|
|
}
|
|
|
|
pushMoveHistory(turnMove: TurnMove) {
|
|
turnMove.turn = this.scene.currentBattle?.turn;
|
|
this.getMoveHistory().push(turnMove);
|
|
}
|
|
|
|
getLastXMoves(turnCount?: integer): TurnMove[] {
|
|
const moveHistory = this.getMoveHistory();
|
|
return moveHistory.slice(turnCount >= 0 ? Math.max(moveHistory.length - (turnCount || 1), 0) : 0, moveHistory.length).reverse();
|
|
}
|
|
|
|
getMoveQueue(): QueuedMove[] {
|
|
return this.summonData.moveQueue;
|
|
}
|
|
|
|
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
|
|
this.abilityIndex = abilityCount - 1;
|
|
this.scene.gameData.setPokemonSeen(this, false);
|
|
this.setScale(this.getSpriteScale());
|
|
this.loadAssets().then(() => {
|
|
this.calculateStats();
|
|
this.scene.updateModifiers(this.isPlayer(), true);
|
|
Promise.all([ this.updateInfo(), this.scene.updateFieldScale() ]).then(() => resolve());
|
|
});
|
|
});
|
|
}
|
|
|
|
cry(soundConfig?: Phaser.Types.Sound.SoundConfig, sceneOverride?: BattleScene): AnySound {
|
|
const scene = sceneOverride || this.scene;
|
|
const cry = this.getSpeciesForm().cry(scene, soundConfig);
|
|
let duration = cry.totalDuration * 1000;
|
|
if (this.fusionSpecies) {
|
|
let fusionCry = this.getFusionSpeciesForm().cry(scene, soundConfig, true);
|
|
duration = Math.min(duration, fusionCry.totalDuration * 1000);
|
|
fusionCry.destroy();
|
|
scene.time.delayedCall(Utils.fixedInt(Math.ceil(duration * 0.4)), () => {
|
|
try {
|
|
SoundFade.fadeOut(scene, cry, Utils.fixedInt(Math.ceil(duration * 0.2)));
|
|
fusionCry = this.getFusionSpeciesForm().cry(scene, Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0) }, soundConfig));
|
|
SoundFade.fadeIn(scene, fusionCry, Utils.fixedInt(Math.ceil(duration * 0.2)), scene.masterVolume * scene.seVolume, 0);
|
|
} catch (err) {
|
|
console.error(err);
|
|
}
|
|
});
|
|
}
|
|
|
|
return cry;
|
|
}
|
|
|
|
faintCry(callback: Function): void {
|
|
if (this.fusionSpecies)
|
|
return this.fusionFaintCry(callback);
|
|
|
|
const key = this.getSpeciesForm().getCryKey(this.formIndex);
|
|
let i = 0;
|
|
let rate = 0.85;
|
|
const cry = this.scene.playSound(key, { rate: rate }) as AnySound;
|
|
const sprite = this.getSprite();
|
|
const tintSprite = this.getTintSprite();
|
|
|
|
const delay = Math.max(this.scene.sound.get(key).totalDuration * 50, 25);
|
|
|
|
let frameProgress = 0;
|
|
let frameThreshold: number;
|
|
|
|
sprite.anims.pause();
|
|
tintSprite.anims.pause();
|
|
|
|
let faintCryTimer = this.scene.time.addEvent({
|
|
delay: Utils.fixedInt(delay),
|
|
repeat: -1,
|
|
callback: () => {
|
|
++i;
|
|
frameThreshold = sprite.anims.msPerFrame / rate;
|
|
frameProgress += delay;
|
|
while (frameProgress > frameThreshold) {
|
|
if (sprite.anims.duration) {
|
|
sprite.anims.nextFrame();
|
|
tintSprite.anims.nextFrame();
|
|
}
|
|
frameProgress -= frameThreshold;
|
|
}
|
|
if (cry && !cry.pendingRemove) {
|
|
rate *= 0.99;
|
|
cry.setRate(rate);
|
|
} else {
|
|
faintCryTimer.destroy();
|
|
faintCryTimer = null;
|
|
if (callback)
|
|
callback();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Failsafe
|
|
this.scene.time.delayedCall(Utils.fixedInt(3000), () => {
|
|
if (!faintCryTimer || !this.scene)
|
|
return;
|
|
if (cry?.isPlaying)
|
|
cry.stop();
|
|
faintCryTimer.destroy();
|
|
if (callback)
|
|
callback();
|
|
});
|
|
}
|
|
|
|
private fusionFaintCry(callback: Function): void {
|
|
const key = this.getSpeciesForm().getCryKey(this.formIndex);
|
|
let i = 0;
|
|
let rate = 0.85;
|
|
let cry = this.scene.playSound(key, { rate: rate }) as AnySound;
|
|
const sprite = this.getSprite();
|
|
const tintSprite = this.getTintSprite();
|
|
let duration = cry.totalDuration * 1000;
|
|
|
|
let fusionCry = this.scene.playSound(this.getFusionSpeciesForm().getCryKey(this.fusionFormIndex), { rate: rate }) as AnySound;
|
|
fusionCry.stop();
|
|
duration = Math.min(duration, fusionCry.totalDuration * 1000);
|
|
fusionCry.destroy();
|
|
|
|
const delay = Math.max(duration * 0.05, 25);
|
|
|
|
let transitionIndex = 0;
|
|
let durationProgress = 0;
|
|
|
|
const transitionThreshold = Math.ceil(duration * 0.4);
|
|
while (durationProgress < transitionThreshold) {
|
|
++i;
|
|
durationProgress += delay * rate;
|
|
rate *= 0.99;
|
|
}
|
|
|
|
transitionIndex = i;
|
|
|
|
i = 0;
|
|
rate = 0.85;
|
|
|
|
let frameProgress = 0;
|
|
let frameThreshold: number;
|
|
|
|
sprite.anims.pause();
|
|
tintSprite.anims.pause();
|
|
|
|
let faintCryTimer = this.scene.time.addEvent({
|
|
delay: Utils.fixedInt(delay),
|
|
repeat: -1,
|
|
callback: () => {
|
|
++i;
|
|
frameThreshold = sprite.anims.msPerFrame / rate;
|
|
frameProgress += delay;
|
|
while (frameProgress > frameThreshold) {
|
|
if (sprite.anims.duration) {
|
|
sprite.anims.nextFrame();
|
|
tintSprite.anims.nextFrame();
|
|
}
|
|
frameProgress -= frameThreshold;
|
|
}
|
|
if (i === transitionIndex) {
|
|
SoundFade.fadeOut(this.scene, cry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)));
|
|
fusionCry = this.scene.playSound(this.getFusionSpeciesForm().getCryKey(this.fusionFormIndex), Object.assign({ seek: Math.max(fusionCry.totalDuration * 0.4, 0), rate: rate }));
|
|
SoundFade.fadeIn(this.scene, fusionCry, Utils.fixedInt(Math.ceil((duration / rate) * 0.2)), this.scene.masterVolume * this.scene.seVolume, 0);
|
|
}
|
|
rate *= 0.99;
|
|
if (cry && !cry.pendingRemove)
|
|
cry.setRate(rate);
|
|
if (fusionCry && !fusionCry.pendingRemove)
|
|
fusionCry.setRate(rate);
|
|
if ((!cry || cry.pendingRemove) && (!fusionCry || fusionCry.pendingRemove)) {
|
|
faintCryTimer.destroy();
|
|
faintCryTimer = null;
|
|
if (callback)
|
|
callback();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Failsafe
|
|
this.scene.time.delayedCall(Utils.fixedInt(3000), () => {
|
|
if (!faintCryTimer || !this.scene)
|
|
return;
|
|
if (cry?.isPlaying)
|
|
cry.stop();
|
|
if (fusionCry?.isPlaying)
|
|
fusionCry.stop();
|
|
faintCryTimer.destroy();
|
|
if (callback)
|
|
callback();
|
|
});
|
|
}
|
|
|
|
isOppositeGender(pokemon: Pokemon): boolean {
|
|
return this.gender !== Gender.GENDERLESS && pokemon.gender === (this.gender === Gender.MALE ? Gender.FEMALE : Gender.MALE);
|
|
}
|
|
|
|
canSetStatus(effect: StatusEffect, quiet: boolean = false, overrideStatus: boolean = false): boolean {
|
|
if (effect !== StatusEffect.FAINT) {
|
|
if (overrideStatus ? this.status?.effect === effect : this.status)
|
|
return false;
|
|
if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.MISTY)
|
|
return false;
|
|
}
|
|
|
|
switch (effect) {
|
|
case StatusEffect.POISON:
|
|
case StatusEffect.TOXIC:
|
|
if (this.isOfType(Type.POISON) || this.isOfType(Type.STEEL))
|
|
return false;
|
|
break;
|
|
case StatusEffect.PARALYSIS:
|
|
if (this.isOfType(Type.ELECTRIC))
|
|
return false;
|
|
break;
|
|
case StatusEffect.SLEEP:
|
|
if (this.isGrounded() && this.scene.arena.terrain?.terrainType === TerrainType.ELECTRIC)
|
|
return false;
|
|
break;
|
|
case StatusEffect.FREEZE:
|
|
if (this.isOfType(Type.ICE) || [WeatherType.SUNNY, WeatherType.HARSH_SUN].includes(this.scene?.arena.weather?.weatherType))
|
|
return false;
|
|
break;
|
|
case StatusEffect.BURN:
|
|
if (this.isOfType(Type.FIRE))
|
|
return false;
|
|
break;
|
|
}
|
|
|
|
const cancelled = new Utils.BooleanHolder(false);
|
|
applyPreSetStatusAbAttrs(StatusEffectImmunityAbAttr, this, effect, cancelled, quiet);
|
|
|
|
if (cancelled.value)
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
trySetStatus(effect: StatusEffect, asPhase: boolean = false, cureTurn: integer = 0, sourceText: string = null): boolean {
|
|
if (!this.canSetStatus(effect, asPhase))
|
|
return false;
|
|
|
|
if (asPhase) {
|
|
this.scene.unshiftPhase(new ObtainStatusEffectPhase(this.scene, this.getBattlerIndex(), effect, cureTurn, sourceText));
|
|
return true;
|
|
}
|
|
|
|
let statusCureTurn: Utils.IntegerHolder;
|
|
|
|
if (effect === StatusEffect.SLEEP) {
|
|
statusCureTurn = new Utils.IntegerHolder(this.randSeedIntRange(2, 4));
|
|
applyAbAttrs(ReduceStatusEffectDurationAbAttr, this, null, effect, statusCureTurn);
|
|
|
|
this.setFrameRate(4);
|
|
|
|
// If the user is invulnerable, lets remove their invulnerability when they fall asleep
|
|
const invulnerableTags = [
|
|
BattlerTagType.UNDERGROUND,
|
|
BattlerTagType.UNDERWATER,
|
|
BattlerTagType.HIDDEN,
|
|
BattlerTagType.FLYING
|
|
];
|
|
|
|
const tag = invulnerableTags.find((t) => this.getTag(t));
|
|
|
|
if (tag) {
|
|
this.removeTag(tag);
|
|
this.getMoveQueue().pop();
|
|
}
|
|
}
|
|
|
|
this.status = new Status(effect, 0, statusCureTurn?.value);
|
|
|
|
if (effect !== StatusEffect.FAINT)
|
|
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeStatusEffectTrigger, true);
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Resets the status of a pokemon
|
|
* @param revive whether revive should be cured, defaults to true
|
|
*/
|
|
resetStatus(revive: boolean = true): void {
|
|
const lastStatus = this.status?.effect;
|
|
if (!revive && lastStatus === StatusEffect.FAINT) {
|
|
return;
|
|
}
|
|
this.status = undefined;
|
|
if (lastStatus === StatusEffect.SLEEP) {
|
|
this.setFrameRate(12);
|
|
if (this.getTag(BattlerTagType.NIGHTMARE))
|
|
this.lapseTag(BattlerTagType.NIGHTMARE);
|
|
}
|
|
}
|
|
|
|
primeSummonData(summonDataPrimer: PokemonSummonData): void {
|
|
this.summonDataPrimer = summonDataPrimer;
|
|
}
|
|
|
|
resetSummonData(): void {
|
|
if (this.summonData?.speciesForm) {
|
|
this.summonData.speciesForm = null;
|
|
this.updateFusionPalette();
|
|
}
|
|
this.summonData = new PokemonSummonData();
|
|
if (!this.battleData)
|
|
this.resetBattleData();
|
|
this.resetBattleSummonData();
|
|
if (this.summonDataPrimer) {
|
|
for (let k of Object.keys(this.summonData)) {
|
|
if (this.summonDataPrimer[k])
|
|
this.summonData[k] = this.summonDataPrimer[k];
|
|
}
|
|
this.summonDataPrimer = null;
|
|
}
|
|
this.updateInfo();
|
|
}
|
|
|
|
resetBattleData(): void {
|
|
this.battleData = new PokemonBattleData();
|
|
}
|
|
|
|
resetBattleSummonData(): void {
|
|
this.battleSummonData = new PokemonBattleSummonData();
|
|
if (this.getTag(BattlerTagType.SEEDED))
|
|
this.lapseTag(BattlerTagType.SEEDED);
|
|
if (this.scene)
|
|
this.scene.triggerPokemonFormChange(this, SpeciesFormChangePostMoveTrigger, true);
|
|
}
|
|
|
|
resetTurnData(): void {
|
|
this.turnData = new PokemonTurnData();
|
|
}
|
|
|
|
getExpValue(): integer {
|
|
// Logic to factor in victor level has been removed for balancing purposes, so the player doesn't have to focus on EXP maxxing
|
|
return ((this.getSpeciesForm().getBaseExp() * this.level) / 5 + 1);
|
|
}
|
|
|
|
setFrameRate(frameRate: integer) {
|
|
this.scene.anims.get(this.getBattleSpriteKey()).frameRate = frameRate;
|
|
this.getSprite().play(this.getBattleSpriteKey());
|
|
this.getTintSprite().play(this.getBattleSpriteKey());
|
|
}
|
|
|
|
tint(color: number, alpha?: number, duration?: integer, ease?: string) {
|
|
const tintSprite = this.getTintSprite();
|
|
tintSprite.setTintFill(color);
|
|
tintSprite.setVisible(true);
|
|
|
|
if (duration) {
|
|
tintSprite.setAlpha(0);
|
|
|
|
this.scene.tweens.add({
|
|
targets: tintSprite,
|
|
alpha: alpha || 1,
|
|
duration: duration,
|
|
ease: ease || 'Linear'
|
|
});
|
|
} else
|
|
tintSprite.setAlpha(alpha);
|
|
}
|
|
|
|
untint(duration: integer, ease?: string) {
|
|
const tintSprite = this.getTintSprite();
|
|
|
|
if (duration) {
|
|
this.scene.tweens.add({
|
|
targets: tintSprite,
|
|
alpha: 0,
|
|
duration: duration,
|
|
ease: ease || 'Linear',
|
|
onComplete: () => {
|
|
tintSprite.setVisible(false);
|
|
tintSprite.setAlpha(1);
|
|
}
|
|
});
|
|
} else {
|
|
tintSprite.setVisible(false);
|
|
tintSprite.setAlpha(1);
|
|
}
|
|
}
|
|
|
|
enableMask() {
|
|
if (!this.maskEnabled) {
|
|
this.maskSprite = this.getTintSprite();
|
|
this.maskSprite.setVisible(true);
|
|
this.maskSprite.setPosition(this.x * this.parentContainer.scale + this.parentContainer.x,
|
|
this.y * this.parentContainer.scale + this.parentContainer.y);
|
|
this.maskSprite.setScale(this.getSpriteScale() * this.parentContainer.scale);
|
|
this.maskEnabled = true;
|
|
}
|
|
}
|
|
|
|
disableMask() {
|
|
if (this.maskEnabled) {
|
|
this.maskSprite.setVisible(false);
|
|
this.maskSprite.setPosition(0, 0);
|
|
this.maskSprite.setScale(this.getSpriteScale());
|
|
this.maskSprite = null;
|
|
this.maskEnabled = false;
|
|
}
|
|
}
|
|
|
|
sparkle(): void {
|
|
if (this.shinySparkle) {
|
|
this.shinySparkle.play(`sparkle${this.variant ? `_${this.variant + 1}` : ''}`);
|
|
this.scene.playSound('sparkle');
|
|
}
|
|
}
|
|
|
|
updateFusionPalette(ignoreOveride?: boolean): void {
|
|
if (!this.getFusionSpeciesForm(ignoreOveride)) {
|
|
[ this.getSprite(), this.getTintSprite() ].map(s => {
|
|
s.pipelineData[`spriteColors${ignoreOveride && this.summonData?.speciesForm ? 'Base' : ''}`] = [];
|
|
s.pipelineData[`fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? 'Base' : ''}`] = [];
|
|
});
|
|
return;
|
|
}
|
|
|
|
const speciesForm = this.getSpeciesForm(ignoreOveride);
|
|
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOveride);
|
|
|
|
const spriteKey = speciesForm.getSpriteKey(this.getGender(ignoreOveride) === Gender.FEMALE, speciesForm.formIndex, this.shiny, this.variant);
|
|
const backSpriteKey = speciesForm.getSpriteKey(this.getGender(ignoreOveride) === Gender.FEMALE, speciesForm.formIndex, this.shiny, this.variant).replace('pkmn__', 'pkmn__back__');
|
|
const fusionSpriteKey = fusionSpeciesForm.getSpriteKey(this.getFusionGender(ignoreOveride) === Gender.FEMALE, fusionSpeciesForm.formIndex, this.fusionShiny, this.fusionVariant);
|
|
const fusionBackSpriteKey = fusionSpeciesForm.getSpriteKey(this.getFusionGender(ignoreOveride) === Gender.FEMALE, fusionSpeciesForm.formIndex, this.fusionShiny, this.fusionVariant).replace('pkmn__', 'pkmn__back__');
|
|
|
|
const sourceTexture = this.scene.textures.get(spriteKey);
|
|
const sourceBackTexture = this.scene.textures.get(backSpriteKey);
|
|
const fusionTexture = this.scene.textures.get(fusionSpriteKey);
|
|
const fusionBackTexture = this.scene.textures.get(fusionBackSpriteKey);
|
|
|
|
const [ sourceFrame, sourceBackFrame, fusionFrame, fusionBackFrame ] = [ sourceTexture, sourceBackTexture, fusionTexture, fusionBackTexture ].map(texture => texture.frames[texture.firstFrame]);
|
|
const [ sourceImage, sourceBackImage, fusionImage, fusionBackImage ] = [ sourceTexture, sourceBackTexture, fusionTexture, fusionBackTexture ].map(i => i.getSourceImage() as HTMLImageElement);
|
|
|
|
const canvas = document.createElement('canvas');
|
|
const backCanvas = document.createElement('canvas');
|
|
const fusionCanvas = document.createElement('canvas');
|
|
const fusionBackCanvas = document.createElement('canvas');
|
|
|
|
const spriteColors: integer[][] = [];
|
|
const pixelData: Uint8ClampedArray[] = [];
|
|
|
|
[ canvas, backCanvas, fusionCanvas, fusionBackCanvas ].forEach((canv: HTMLCanvasElement, c: integer) => {
|
|
const context = canv.getContext('2d');
|
|
const frame = [ sourceFrame, sourceBackFrame, fusionFrame, fusionBackFrame ][c];
|
|
canv.width = frame.width;
|
|
canv.height = frame.height;
|
|
context.drawImage([ sourceImage, sourceBackImage, fusionImage, fusionBackImage ][c], frame.cutX, frame.cutY, frame.width, frame.height, 0, 0, frame.width, frame.height);
|
|
const imageData = context.getImageData(frame.cutX, frame.cutY, frame.width, frame.height);
|
|
pixelData.push(imageData.data);
|
|
});
|
|
|
|
for (let f = 0; f < 2; f++) {
|
|
const variantColors = variantColorCache[!f ? spriteKey : backSpriteKey];
|
|
let variantColorSet = new Map<integer, integer[]>();
|
|
if (this.shiny && variantColors && variantColors[this.variant]) {
|
|
Object.keys(variantColors[this.variant]).forEach(k => {
|
|
variantColorSet.set(Utils.rgbaToInt(Array.from(Object.values(Utils.rgbHexToRgba(k)))), Array.from(Object.values(Utils.rgbHexToRgba(variantColors[this.variant][k]))));
|
|
});
|
|
}
|
|
|
|
for (let i = 0; i < pixelData[f].length; i += 4) {
|
|
if (pixelData[f][i + 3]) {
|
|
const pixel = pixelData[f].slice(i, i + 4);
|
|
let [ r, g, b, a ] = pixel;
|
|
if (variantColors) {
|
|
const color = Utils.rgbaToInt([r, g, b, a]);
|
|
if (variantColorSet.has(color)) {
|
|
const mappedPixel = variantColorSet.get(color);
|
|
[ r, g, b, a ] = mappedPixel;
|
|
}
|
|
}
|
|
if (!spriteColors.find(c => c[0] === r && c[1] === g && c[2] === b))
|
|
spriteColors.push([ r, g, b, a ]);
|
|
}
|
|
}
|
|
}
|
|
|
|
const fusionSpriteColors = JSON.parse(JSON.stringify(spriteColors));
|
|
|
|
const pixelColors = [];
|
|
for (let f = 0; f < 2; f++) {
|
|
for (let i = 0; i < pixelData[f].length; i += 4) {
|
|
const total = pixelData[f].slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0);
|
|
if (!total)
|
|
continue;
|
|
pixelColors.push(argbFromRgba({ r: pixelData[f][i], g: pixelData[f][i + 1], b: pixelData[f][i + 2], a: pixelData[f][i + 3] }));
|
|
}
|
|
}
|
|
|
|
const fusionPixelColors = [];
|
|
for (let f = 0; f < 2; f++) {
|
|
const variantColors = variantColorCache[!f ? fusionSpriteKey : fusionBackSpriteKey];
|
|
let variantColorSet = new Map<integer, integer[]>();
|
|
if (this.fusionShiny && variantColors && variantColors[this.fusionVariant]) {
|
|
Object.keys(variantColors[this.fusionVariant]).forEach(k => {
|
|
variantColorSet.set(Utils.rgbaToInt(Array.from(Object.values(Utils.rgbHexToRgba(k)))), Array.from(Object.values(Utils.rgbHexToRgba(variantColors[this.fusionVariant][k]))));
|
|
});
|
|
}
|
|
for (let i = 0; i < pixelData[2 + f].length; i += 4) {
|
|
const total = pixelData[2 + f].slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0);
|
|
if (!total)
|
|
continue;
|
|
let [ r, g, b, a ] = [ pixelData[2 + f][i], pixelData[2 + f][i + 1], pixelData[2 + f][i + 2], pixelData[2 + f][i + 3] ];
|
|
if (variantColors) {
|
|
const color = Utils.rgbaToInt([r, g, b, a]);
|
|
if (variantColorSet.has(color)) {
|
|
const mappedPixel = variantColorSet.get(color);
|
|
[ r, g, b, a ] = mappedPixel;
|
|
}
|
|
}
|
|
fusionPixelColors.push(argbFromRgba({ r, g, b, a }));
|
|
}
|
|
}
|
|
|
|
let paletteColors: Map<number, number>;
|
|
let fusionPaletteColors: Map<number, number>;
|
|
|
|
const originalRandom = Math.random;
|
|
Math.random = () => Phaser.Math.RND.realInRange(0, 1);
|
|
|
|
this.scene.executeWithSeedOffset(() => {
|
|
paletteColors = QuantizerCelebi.quantize(pixelColors, 4);
|
|
fusionPaletteColors = QuantizerCelebi.quantize(fusionPixelColors, 4);
|
|
}, 0, 'This result should not vary');
|
|
|
|
Math.random = originalRandom;
|
|
|
|
const [ palette, fusionPalette ] = [ paletteColors, fusionPaletteColors ]
|
|
.map(paletteColors => {
|
|
let keys = Array.from(paletteColors.keys()).sort((a: integer, b: integer) => paletteColors.get(a) < paletteColors.get(b) ? 1 : -1);
|
|
let rgbaColors: Map<number, integer[]>;
|
|
let hsvColors: Map<number, number[]>;
|
|
|
|
const mappedColors = new Map<integer, integer[]>();
|
|
|
|
do {
|
|
mappedColors.clear();
|
|
|
|
rgbaColors = keys.reduce((map: Map<number, integer[]>, k: number) => { map.set(k, Object.values(rgbaFromArgb(k))); return map; }, new Map<number, integer[]>());
|
|
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]))
|
|
mappedColors.get(keys[c]).push(keys[c2]);
|
|
else
|
|
mappedColors.set(keys[c], [ keys[c2] ]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
mappedColors.forEach((values: integer[], key: integer) => {
|
|
const keyColor = rgbaColors.get(key);
|
|
const valueColors = values.map(v => rgbaColors.get(v));
|
|
let color = keyColor.slice(0);
|
|
let count = paletteColors.get(key);
|
|
for (let value of values) {
|
|
const valueCount = paletteColors.get(value);
|
|
if (!valueCount)
|
|
continue;
|
|
count += valueCount;
|
|
}
|
|
|
|
for (let c = 0; c < 3; c++) {
|
|
color[c] *= (paletteColors.get(key) / count);
|
|
values.forEach((value: integer, i: integer) => {
|
|
if (paletteColors.has(value)) {
|
|
const valueCount = paletteColors.get(value);
|
|
color[c] += valueColors[i][c] * (valueCount / count);
|
|
}
|
|
});
|
|
color[c] = Math.round(color[c]);
|
|
}
|
|
|
|
paletteColors.delete(key);
|
|
for (let value of values) {
|
|
paletteColors.delete(value);
|
|
if (mappedColors.has(value))
|
|
mappedColors.delete(value);
|
|
}
|
|
|
|
paletteColors.set(argbFromRgba({ r: color[0], g: color[1], b: color[2], a: color[3] }), count);
|
|
});
|
|
|
|
keys = Array.from(paletteColors.keys()).sort((a: integer, b: integer) => paletteColors.get(a) < paletteColors.get(b) ? 1 : -1);
|
|
} while (mappedColors.size);
|
|
|
|
return keys.map(c => Object.values(rgbaFromArgb(c)))
|
|
}
|
|
);
|
|
|
|
const paletteDeltas: number[][] = [];
|
|
|
|
spriteColors.forEach((sc: integer[], i: integer) => {
|
|
paletteDeltas.push([]);
|
|
for (let p = 0; p < palette.length; p++)
|
|
paletteDeltas[i].push(Utils.deltaRgb(sc, palette[p]));
|
|
});
|
|
|
|
const easeFunc = Phaser.Tweens.Builders.GetEaseFunction('Cubic.easeIn');
|
|
|
|
for (let sc = 0; sc < spriteColors.length; sc++) {
|
|
const delta = Math.min(...paletteDeltas[sc]);
|
|
const paletteIndex = Math.min(paletteDeltas[sc].findIndex(pd => pd === delta), fusionPalette.length - 1);
|
|
if (delta < 255) {
|
|
const ratio = easeFunc(delta / 255);
|
|
let color = [ 0, 0, 0, fusionSpriteColors[sc][3] ];
|
|
for (let c = 0; c < 3; c++)
|
|
color[c] = Math.round((fusionSpriteColors[sc][c] * ratio) + (fusionPalette[paletteIndex][c] * (1 - ratio)));
|
|
fusionSpriteColors[sc] = color;
|
|
}
|
|
}
|
|
|
|
[ this.getSprite(), this.getTintSprite() ].map(s => {
|
|
s.pipelineData[`spriteColors${ignoreOveride && this.summonData?.speciesForm ? 'Base' : ''}`] = spriteColors;
|
|
s.pipelineData[`fusionSpriteColors${ignoreOveride && this.summonData?.speciesForm ? 'Base' : ''}`] = fusionSpriteColors;
|
|
});
|
|
|
|
canvas.remove();
|
|
fusionCanvas.remove();
|
|
}
|
|
|
|
randSeedInt(range: integer, min: integer = 0): integer {
|
|
return this.scene.currentBattle
|
|
? this.scene.randBattleSeedInt(range, min)
|
|
: Utils.randSeedInt(range, min);
|
|
}
|
|
|
|
randSeedIntRange(min: integer, max: integer): integer {
|
|
return this.randSeedInt((max - min) + 1, min);
|
|
}
|
|
|
|
destroy(): void {
|
|
this.battleInfo?.destroy();
|
|
super.destroy();
|
|
}
|
|
}
|
|
|
|
export default interface Pokemon {
|
|
scene: BattleScene
|
|
}
|
|
|
|
export class PlayerPokemon extends Pokemon {
|
|
public compatibleTms: Moves[];
|
|
|
|
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, abilityIndex: integer, formIndex: integer, gender: Gender, shiny: boolean, variant: Variant, ivs: integer[], nature: Nature, dataSource: Pokemon | PokemonData) {
|
|
super(scene, 106, 148, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource);
|
|
|
|
if (!dataSource)
|
|
this.generateAndPopulateMoveset();
|
|
this.generateCompatibleTms();
|
|
}
|
|
|
|
initBattleInfo(): void {
|
|
this.battleInfo = new PlayerBattleInfo(this.scene);
|
|
this.battleInfo.initInfo(this);
|
|
}
|
|
|
|
isPlayer(): boolean {
|
|
return true;
|
|
}
|
|
|
|
hasTrainer(): boolean {
|
|
return true;
|
|
}
|
|
|
|
isBoss(): boolean {
|
|
return false;
|
|
}
|
|
|
|
getFieldIndex(): integer {
|
|
return this.scene.getPlayerField().indexOf(this);
|
|
}
|
|
|
|
getBattlerIndex(): BattlerIndex {
|
|
return this.getFieldIndex();
|
|
}
|
|
|
|
generateCompatibleTms(): void {
|
|
this.compatibleTms = [];
|
|
|
|
const tms = Object.keys(tmSpecies);
|
|
for (let tm of tms) {
|
|
const moveId = parseInt(tm) as Moves;
|
|
let compatible = false;
|
|
for (let p of tmSpecies[tm]) {
|
|
if (Array.isArray(p)) {
|
|
if (p[0] === this.species.speciesId || (this.fusionSpecies && p[0] === this.fusionSpecies.speciesId) && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) {
|
|
compatible = true;
|
|
break;
|
|
}
|
|
} else if (p === this.species.speciesId || (this.fusionSpecies && p === this.fusionSpecies.speciesId)) {
|
|
compatible = true;
|
|
break;
|
|
}
|
|
}
|
|
if (reverseCompatibleTms.indexOf(moveId) > -1)
|
|
compatible = !compatible;
|
|
if (compatible)
|
|
this.compatibleTms.push(moveId);
|
|
}
|
|
}
|
|
|
|
tryPopulateMoveset(moveset: StarterMoveset): boolean {
|
|
if (!this.getSpeciesForm().validateStarterMoveset(moveset, this.scene.gameData.starterData[this.species.getRootSpeciesId()].eggMoves))
|
|
return false;
|
|
|
|
this.moveset = moveset.map(m => new PokemonMove(m));
|
|
|
|
return true;
|
|
}
|
|
|
|
switchOut(batonPass: boolean, removeFromField: boolean = false): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.resetTurnData();
|
|
if (!batonPass)
|
|
this.resetSummonData();
|
|
this.hideInfo();
|
|
this.setVisible(false);
|
|
|
|
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.FAINT_SWITCH, this.getFieldIndex(), (slotIndex: integer, option: PartyOption) => {
|
|
if (slotIndex >= this.scene.currentBattle.getBattlerCount() && slotIndex < 6)
|
|
this.scene.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);
|
|
}
|
|
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);
|
|
|
|
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 (let sd of starterData)
|
|
sd.friendship = Math.max((sd.friendship || 0) + starterAmount.value, 0);
|
|
}
|
|
}
|
|
|
|
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;
|
|
ret = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
|
|
this.fusionSpecies = originalFusionSpecies;
|
|
this.fusionFormIndex = originalFusionFormIndex;
|
|
} else {
|
|
const formIndex = evolution.evoFormKey !== null && !isFusion ? Math.max(evolutionSpecies.forms.findIndex(f => f.formKey === evolution.evoFormKey), 0) : this.formIndex;
|
|
ret = this.scene.addPlayerPokemon(!isFusion ? evolutionSpecies : this.species, this.level, this.abilityIndex, formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
|
|
}
|
|
ret.loadAssets().then(() => resolve(ret));
|
|
});
|
|
}
|
|
|
|
evolve(evolution: SpeciesFormEvolution): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.pauseEvolutions = false;
|
|
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;
|
|
}
|
|
this.compatibleTms.splice(0, this.compatibleTms.length);
|
|
this.generateCompatibleTms();
|
|
const updateAndResolve = () => {
|
|
this.loadAssets().then(() => {
|
|
this.calculateStats();
|
|
this.updateInfo(true).then(() => resolve());
|
|
});
|
|
};
|
|
if (!this.scene.gameMode.isDaily || this.metBiome > -1) {
|
|
this.scene.gameData.updateSpeciesDexIvs(this.species.speciesId, this.ivs);
|
|
this.scene.gameData.setPokemonSeen(this, false);
|
|
this.scene.gameData.setPokemonCaught(this, false).then(() => updateAndResolve());
|
|
} else
|
|
updateAndResolve();
|
|
});
|
|
}
|
|
|
|
private handleSpecialEvolutions(evolution: SpeciesFormEvolution) {
|
|
const isFusion = evolution instanceof FusionSpeciesFormEvolution;
|
|
|
|
const evoSpecies = (!isFusion ? this.species : this.fusionSpecies)
|
|
if (evoSpecies.speciesId === Species.NINCADA && evolution.speciesId === Species.NINJASK) {
|
|
const newEvolution = pokemonEvolutions[evoSpecies.speciesId][1];
|
|
|
|
if (newEvolution.condition.predicate(this)) {
|
|
const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, undefined, this.shiny, this.variant, this.ivs, this.nature);
|
|
newPokemon.natureOverride = this.natureOverride;
|
|
newPokemon.passive = this.passive;
|
|
newPokemon.moveset = this.moveset.slice();
|
|
newPokemon.moveset = this.copyMoveset();
|
|
newPokemon.luck = this.luck;
|
|
newPokemon.fusionSpecies = this.fusionSpecies;
|
|
newPokemon.fusionFormIndex = this.fusionFormIndex;
|
|
newPokemon.fusionAbilityIndex = this.fusionAbilityIndex;
|
|
newPokemon.fusionShiny = this.fusionShiny;
|
|
newPokemon.fusionVariant = this.fusionVariant;
|
|
newPokemon.fusionGender = this.fusionGender;
|
|
newPokemon.fusionLuck = this.fusionLuck;
|
|
|
|
this.scene.getParty().push(newPokemon);
|
|
newPokemon.evolve(!isFusion ? newEvolution : new FusionSpeciesFormEvolution(this.id, newEvolution));
|
|
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);
|
|
});
|
|
this.scene.updateModifiers(true);
|
|
}
|
|
}
|
|
}
|
|
|
|
getPossibleForm(formChange: SpeciesFormChange): Promise<Pokemon> {
|
|
return new Promise(resolve => {
|
|
const formIndex = Math.max(this.species.forms.findIndex(f => f.formKey === formChange.formKey), 0);
|
|
const ret = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
|
|
ret.loadAssets().then(() => resolve(ret));
|
|
});
|
|
}
|
|
|
|
changeForm(formChange: SpeciesFormChange): Promise<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
|
|
this.abilityIndex = abilityCount - 1;
|
|
this.compatibleTms.splice(0, this.compatibleTms.length);
|
|
this.generateCompatibleTms();
|
|
const updateAndResolve = () => {
|
|
this.loadAssets().then(() => {
|
|
this.calculateStats();
|
|
this.scene.updateModifiers(true, true);
|
|
this.updateInfo(true).then(() => resolve());
|
|
});
|
|
};
|
|
if (!this.scene.gameMode.isDaily || this.metBiome > -1) {
|
|
this.scene.gameData.setPokemonSeen(this, false);
|
|
this.scene.gameData.setPokemonCaught(this, false).then(() => updateAndResolve());
|
|
} else
|
|
updateAndResolve();
|
|
});
|
|
}
|
|
|
|
clearFusionSpecies(): void {
|
|
super.clearFusionSpecies();
|
|
this.generateCompatibleTms();
|
|
}
|
|
|
|
/**
|
|
* Returns a Promise to fuse two PlayerPokemon together
|
|
* @param pokemon The PlayerPokemon to fuse to this one
|
|
*/
|
|
fuse(pokemon: PlayerPokemon): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.fusionSpecies = pokemon.species;
|
|
this.fusionFormIndex = pokemon.formIndex;
|
|
this.fusionAbilityIndex = pokemon.abilityIndex;
|
|
this.fusionShiny = pokemon.shiny;
|
|
this.fusionVariant = pokemon.variant;
|
|
this.fusionGender = pokemon.gender;
|
|
this.fusionLuck = pokemon.luck;
|
|
|
|
this.scene.validateAchv(achvs.SPLICE);
|
|
this.scene.gameData.gameStats.pokemonFused++;
|
|
|
|
// Store the average HP% that each Pokemon has
|
|
const newHpPercent = ((pokemon.hp / pokemon.stats[Stat.HP]) + (this.hp / this.stats[Stat.HP])) / 2;
|
|
|
|
this.generateName();
|
|
this.calculateStats();
|
|
|
|
// Set this Pokemon's HP to the average % of both fusion components
|
|
this.hp = Math.round(this.stats[Stat.HP] * newHpPercent);
|
|
if (!this.isFainted()) {
|
|
// If this Pokemon hasn't fainted, make sure the HP wasn't set over the new maximum
|
|
this.hp = Math.min(this.hp, this.stats[Stat.HP]);
|
|
this.status = getRandomStatus(this.status, pokemon.status); // Get a random valid status between the two
|
|
}
|
|
else if (!pokemon.isFainted()) {
|
|
// If this Pokemon fainted but the other hasn't, make sure the HP wasn't set to zero
|
|
this.hp = Math.max(this.hp, 1);
|
|
this.status = pokemon.status; // Inherit the other Pokemon's status
|
|
}
|
|
|
|
this.generateCompatibleTms();
|
|
this.updateInfo(true);
|
|
const fusedPartyMemberIndex = this.scene.getParty().indexOf(pokemon);
|
|
let partyMemberIndex = this.scene.getParty().indexOf(this);
|
|
if (partyMemberIndex > fusedPartyMemberIndex)
|
|
partyMemberIndex--;
|
|
const fusedPartyMemberHeldModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier
|
|
&& (m as PokemonHeldItemModifier).pokemonId === pokemon.id, true) as PokemonHeldItemModifier[];
|
|
const transferModifiers: Promise<boolean>[] = [];
|
|
for (let modifier of fusedPartyMemberHeldModifiers)
|
|
transferModifiers.push(this.scene.tryTransferHeldItemModifier(modifier, this, true, false, true, true));
|
|
Promise.allSettled(transferModifiers).then(() => {
|
|
this.scene.updateModifiers(true, true).then(() => {
|
|
this.scene.removePartyMemberModifiers(fusedPartyMemberIndex);
|
|
this.scene.getParty().splice(fusedPartyMemberIndex, 1)[0];
|
|
const newPartyMemberIndex = this.scene.getParty().indexOf(this);
|
|
pokemon.getMoveset(true).map(m => this.scene.unshiftPhase(new LearnMovePhase(this.scene, newPartyMemberIndex, m.getMove().id)));
|
|
pokemon.destroy();
|
|
this.updateFusionPalette();
|
|
resolve();
|
|
});
|
|
});
|
|
});
|
|
}
|
|
|
|
unfuse(): Promise<void> {
|
|
return new Promise(resolve => {
|
|
this.clearFusionSpecies();
|
|
|
|
this.updateInfo(true).then(() => resolve());
|
|
this.updateFusionPalette();
|
|
});
|
|
}
|
|
|
|
/** Returns a deep copy of this Pokemon's moveset array */
|
|
copyMoveset(): PokemonMove[] {
|
|
let newMoveset = [];
|
|
this.moveset.forEach(move =>
|
|
newMoveset.push(new PokemonMove(move.moveId, 0, move.ppUp, move.virtual)));
|
|
|
|
return newMoveset;
|
|
}
|
|
}
|
|
|
|
export class EnemyPokemon extends Pokemon {
|
|
public trainerSlot: TrainerSlot;
|
|
public aiType: AiType;
|
|
public bossSegments: integer;
|
|
public bossSegmentIndex: integer;
|
|
|
|
constructor(scene: BattleScene, species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean, dataSource: PokemonData) {
|
|
super(scene, 236, 84, species, level, dataSource?.abilityIndex, dataSource?.formIndex,
|
|
dataSource?.gender, dataSource ? dataSource.shiny : false, dataSource ? dataSource.variant : undefined, null, dataSource ? dataSource.nature : undefined, dataSource);
|
|
|
|
this.trainerSlot = trainerSlot;
|
|
if (boss)
|
|
this.setBoss();
|
|
|
|
if (!dataSource) {
|
|
this.generateAndPopulateMoveset();
|
|
|
|
this.trySetShiny();
|
|
if (Overrides.OPP_SHINY_OVERRIDE) {
|
|
this.shiny = true;
|
|
this.initShinySparkle();
|
|
}
|
|
if (this.shiny) {
|
|
this.variant = this.generateVariant();
|
|
if (Overrides.OPP_VARIANT_OVERRIDE)
|
|
this.variant = Overrides.OPP_VARIANT_OVERRIDE;
|
|
}
|
|
|
|
this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0);
|
|
|
|
let prevolution: Species;
|
|
let speciesId = species.speciesId;
|
|
while ((prevolution = pokemonPrevolutions[speciesId])) {
|
|
const evolution = pokemonEvolutions[prevolution].find(pe => pe.speciesId === speciesId && (!pe.evoFormKey || pe.evoFormKey === this.getFormKey()));
|
|
if (evolution.condition?.enforceFunc)
|
|
evolution.condition.enforceFunc(this);
|
|
speciesId = prevolution;
|
|
}
|
|
}
|
|
|
|
this.aiType = boss || this.hasTrainer() ? AiType.SMART : AiType.SMART_RANDOM;
|
|
}
|
|
|
|
initBattleInfo(): void {
|
|
if (!this.battleInfo) {
|
|
this.battleInfo = new EnemyBattleInfo(this.scene);
|
|
this.battleInfo.updateBossSegments(this);
|
|
this.battleInfo.initInfo(this);
|
|
} else
|
|
this.battleInfo.updateBossSegments(this);
|
|
}
|
|
|
|
setBoss(boss: boolean = true, bossSegments: integer = 0): void {
|
|
if (boss) {
|
|
this.bossSegments = bossSegments || this.scene.getEncounterBossSegments(this.scene.currentBattle.waveIndex, this.level, this.species, true);
|
|
this.bossSegmentIndex = this.bossSegments - 1;
|
|
} else {
|
|
this.bossSegments = 0;
|
|
this.bossSegmentIndex = 0;
|
|
}
|
|
}
|
|
|
|
generateAndPopulateMoveset(formIndex?: integer): void {
|
|
switch (true) {
|
|
case (this.species.speciesId === Species.SMEARGLE):
|
|
this.moveset = [
|
|
new PokemonMove(Moves.SKETCH),
|
|
new PokemonMove(Moves.SKETCH),
|
|
new PokemonMove(Moves.SKETCH),
|
|
new PokemonMove(Moves.SKETCH)
|
|
];
|
|
break;
|
|
case (this.species.speciesId === Species.ETERNATUS):
|
|
this.moveset = (formIndex !== undefined ? formIndex : this.formIndex)
|
|
? [
|
|
new PokemonMove(Moves.DYNAMAX_CANNON),
|
|
new PokemonMove(Moves.CROSS_POISON),
|
|
new PokemonMove(Moves.FLAMETHROWER),
|
|
new PokemonMove(Moves.RECOVER, 0, -4)
|
|
]
|
|
: [
|
|
new PokemonMove(Moves.ETERNABEAM),
|
|
new PokemonMove(Moves.SLUDGE_BOMB),
|
|
new PokemonMove(Moves.DRAGON_DANCE),
|
|
new PokemonMove(Moves.COSMIC_POWER)
|
|
];
|
|
break;
|
|
default:
|
|
super.generateAndPopulateMoveset();
|
|
break;
|
|
}
|
|
}
|
|
|
|
getNextMove(): QueuedMove {
|
|
const queuedMove = this.getMoveQueue().length
|
|
? this.getMoveset().find(m => m.moveId === this.getMoveQueue()[0].move)
|
|
: null;
|
|
if (queuedMove) {
|
|
if (queuedMove.isUsable(this, this.getMoveQueue()[0].ignorePP))
|
|
return { move: queuedMove.moveId, targets: this.getMoveQueue()[0].targets, ignorePP: this.getMoveQueue()[0].ignorePP };
|
|
else {
|
|
this.getMoveQueue().shift();
|
|
return this.getNextMove();
|
|
}
|
|
}
|
|
|
|
const movePool = this.getMoveset().filter(m => m.isUsable(this));
|
|
if (movePool.length) {
|
|
if (movePool.length === 1)
|
|
return { move: movePool[0].moveId, targets: this.getNextTargets(movePool[0].moveId) };
|
|
const encoreTag = this.getTag(EncoreTag) as EncoreTag;
|
|
if (encoreTag) {
|
|
const encoreMove = movePool.find(m => m.moveId === encoreTag.moveId);
|
|
if (encoreMove)
|
|
return { move: encoreMove.moveId, targets: this.getNextTargets(encoreMove.moveId) };
|
|
}
|
|
switch (this.aiType) {
|
|
case AiType.RANDOM:
|
|
const moveId = movePool[this.scene.randBattleSeedInt(movePool.length)].moveId;
|
|
return { move: moveId, targets: this.getNextTargets(moveId) };
|
|
case AiType.SMART_RANDOM:
|
|
case AiType.SMART:
|
|
const moveScores = movePool.map(() => 0);
|
|
const moveTargets = Object.fromEntries(movePool.map(m => [ m.moveId, this.getNextTargets(m.moveId) ]));
|
|
for (let m in movePool) {
|
|
const pokemonMove = movePool[m];
|
|
const move = pokemonMove.getMove();
|
|
|
|
const variableType = new Utils.IntegerHolder(move.type);
|
|
applyAbAttrs(VariableMoveTypeAbAttr, this, null, variableType);
|
|
const moveType = variableType.value as Type;
|
|
|
|
let moveScore = moveScores[m];
|
|
let targetScores: integer[] = [];
|
|
|
|
for (let 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 (move.name.endsWith(' (N)') || !move.applyConditions(this, target, move))
|
|
targetScore = -20;
|
|
else if (move instanceof AttackMove) {
|
|
const effectiveness = target.getAttackMoveEffectiveness(this, pokemonMove);
|
|
if (target.isPlayer() !== this.isPlayer()) {
|
|
targetScore *= effectiveness;
|
|
if (this.isOfType(moveType))
|
|
targetScore *= 1.5;
|
|
} else if (effectiveness) {
|
|
targetScore /= effectiveness;
|
|
if (this.isOfType(moveType))
|
|
targetScore /= 1.5;
|
|
}
|
|
if (!targetScore)
|
|
targetScore = -20;
|
|
}
|
|
targetScores.push(targetScore);
|
|
}
|
|
|
|
moveScore += Math.max(...targetScores);
|
|
|
|
// could make smarter by checking opponent def/spdef
|
|
moveScores[m] = moveScore;
|
|
}
|
|
|
|
console.log(moveScores);
|
|
|
|
const sortedMovePool = movePool.slice(0);
|
|
sortedMovePool.sort((a, b) => {
|
|
const scoreA = moveScores[movePool.indexOf(a)];
|
|
const scoreB = moveScores[movePool.indexOf(b)];
|
|
return scoreA < scoreB ? 1 : scoreA > scoreB ? -1 : 0;
|
|
});
|
|
let r = 0;
|
|
if (this.aiType === AiType.SMART_RANDOM) {
|
|
while (r < sortedMovePool.length - 1 && this.scene.randBattleSeedInt(8) >= 5)
|
|
r++;
|
|
} else if (this.aiType === AiType.SMART) {
|
|
while (r < sortedMovePool.length - 1 && (moveScores[movePool.indexOf(sortedMovePool[r + 1])] / moveScores[movePool.indexOf(sortedMovePool[r])]) >= 0
|
|
&& this.scene.randBattleSeedInt(100) < Math.round((moveScores[movePool.indexOf(sortedMovePool[r + 1])] / moveScores[movePool.indexOf(sortedMovePool[r])]) * 50))
|
|
r++;
|
|
}
|
|
console.log(movePool.map(m => m.getName()), moveScores, r, sortedMovePool.map(m => m.getName()));
|
|
return { move: sortedMovePool[r].moveId, targets: moveTargets[sortedMovePool[r].moveId] };
|
|
}
|
|
}
|
|
|
|
return { move: Moves.STRUGGLE, targets: this.getNextTargets(Moves.STRUGGLE) };
|
|
}
|
|
|
|
getNextTargets(moveId: Moves): BattlerIndex[] {
|
|
const moveTargets = getMoveTargets(this, moveId);
|
|
const targets = this.scene.getField(true).filter(p => moveTargets.targets.indexOf(p.getBattlerIndex()) > -1);
|
|
if (moveTargets.multiple)
|
|
return targets.map(p => p.getBattlerIndex());
|
|
|
|
const move = allMoves[moveId];
|
|
|
|
const benefitScores = targets
|
|
.map(p => [ p.getBattlerIndex(), move.getTargetBenefitScore(this, p, move) * (p.isPlayer() === this.isPlayer() ? 1 : -1) ]);
|
|
|
|
const sortedBenefitScores = benefitScores.slice(0);
|
|
sortedBenefitScores.sort((a, b) => {
|
|
const scoreA = a[1];
|
|
const scoreB = b[1];
|
|
return scoreA < scoreB ? 1 : scoreA > scoreB ? -1 : 0;
|
|
});
|
|
|
|
if (!sortedBenefitScores.length) {
|
|
// Set target to BattlerIndex.ATTACKER when using a counter move
|
|
// This is the same as when the player does so
|
|
if (!!move.findAttr(attr => attr instanceof CounterDamageAttr))
|
|
return [BattlerIndex.ATTACKER];
|
|
|
|
return [];
|
|
}
|
|
|
|
let targetWeights = sortedBenefitScores.map(s => s[1]);
|
|
const lowestWeight = targetWeights[targetWeights.length - 1];
|
|
|
|
if (lowestWeight < 1) {
|
|
for (let w = 0; w < targetWeights.length; w++)
|
|
targetWeights[w] += Math.abs(lowestWeight - 1);
|
|
}
|
|
|
|
const benefitCutoffIndex = targetWeights.findIndex(s => s < targetWeights[0] / 2);
|
|
if (benefitCutoffIndex > -1)
|
|
targetWeights = targetWeights.slice(0, benefitCutoffIndex);
|
|
|
|
const thresholds: integer[] = [];
|
|
let totalWeight: integer;
|
|
targetWeights.reduce((total: integer, w: integer) => {
|
|
total += w;
|
|
thresholds.push(total);
|
|
totalWeight = total;
|
|
return total;
|
|
}, 0);
|
|
|
|
const randValue = this.scene.randBattleSeedInt(totalWeight);
|
|
let targetIndex: integer;
|
|
|
|
thresholds.every((t, i) => {
|
|
if (randValue >= t)
|
|
return true;
|
|
|
|
targetIndex = i;
|
|
return false;
|
|
});
|
|
|
|
return [ sortedBenefitScores[targetIndex][0] ];
|
|
}
|
|
|
|
isPlayer() {
|
|
return false;
|
|
}
|
|
|
|
hasTrainer(): boolean {
|
|
return !!this.trainerSlot;
|
|
}
|
|
|
|
isBoss(): boolean {
|
|
return !!this.bossSegments;
|
|
}
|
|
|
|
getBossSegmentIndex(): integer {
|
|
const segments = (this as EnemyPokemon).bossSegments;
|
|
const segmentSize = this.getMaxHp() / segments;
|
|
for (let s = segments - 1; s > 0; s--) {
|
|
const hpThreshold = Math.round(segmentSize * s);
|
|
if (this.hp > hpThreshold) {
|
|
return s;
|
|
}
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
damage(damage: integer, ignoreSegments: boolean = false, preventEndure: boolean = false): integer {
|
|
if (this.isFainted())
|
|
return 0;
|
|
|
|
let clearedBossSegmentIndex = this.isBoss()
|
|
? this.bossSegmentIndex + 1
|
|
: 0;
|
|
|
|
if (this.isBoss() && !ignoreSegments) {
|
|
const segmentSize = this.getMaxHp() / this.bossSegments;
|
|
for (let s = this.bossSegmentIndex; s > 0; s--) {
|
|
const hpThreshold = segmentSize * s;
|
|
const roundedHpThreshold = Math.round(hpThreshold);
|
|
if (this.hp >= roundedHpThreshold) {
|
|
if (this.hp - damage <= roundedHpThreshold) {
|
|
const hpRemainder = this.hp - roundedHpThreshold;
|
|
let segmentsBypassed = 0;
|
|
while (segmentsBypassed < this.bossSegmentIndex && this.canBypassBossSegments(segmentsBypassed + 1) && (damage - hpRemainder) >= Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1))) {
|
|
segmentsBypassed++;
|
|
//console.log('damage', damage, 'segment', segmentsBypassed + 1, 'segment size', segmentSize, 'damage needed', Math.round(segmentSize * Math.pow(2, segmentsBypassed + 1)));
|
|
}
|
|
|
|
damage = hpRemainder + Math.round(segmentSize * segmentsBypassed);
|
|
clearedBossSegmentIndex = s - segmentsBypassed;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
switch (this.scene.currentBattle.battleSpec) {
|
|
case BattleSpec.FINAL_BOSS:
|
|
if (!this.formIndex && this.bossSegmentIndex < 1)
|
|
damage = Math.min(damage, this.hp - 1);
|
|
}
|
|
|
|
let ret = super.damage(damage, ignoreSegments, preventEndure);
|
|
|
|
if (this.isBoss()) {
|
|
if (ignoreSegments) {
|
|
const segmentSize = this.getMaxHp() / this.bossSegments;
|
|
clearedBossSegmentIndex = Math.ceil(this.hp / segmentSize);
|
|
}
|
|
if (clearedBossSegmentIndex <= this.bossSegmentIndex)
|
|
this.handleBossSegmentCleared(clearedBossSegmentIndex);
|
|
this.battleInfo.updateBossSegments(this);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
canBypassBossSegments(segmentCount: integer = 1): boolean {
|
|
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
|
|
if (!this.formIndex && (this.bossSegmentIndex - segmentCount) < 1)
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
handleBossSegmentCleared(segmentIndex: integer): void {
|
|
while (segmentIndex - 1 < this.bossSegmentIndex) {
|
|
let boostedStat = BattleStat.RAND;
|
|
|
|
const battleStats = Utils.getEnumValues(BattleStat).slice(0, -3);
|
|
const statWeights = new Array().fill(battleStats.length).filter((bs: BattleStat) => this.summonData.battleStats[bs] < 6).map((bs: BattleStat) => this.getStat(bs + 1));
|
|
const statThresholds: integer[] = [];
|
|
let totalWeight = 0;
|
|
for (let bs of battleStats) {
|
|
totalWeight += statWeights[bs];
|
|
statThresholds.push(totalWeight);
|
|
}
|
|
|
|
const randInt = Utils.randSeedInt(totalWeight);
|
|
|
|
for (let bs of battleStats) {
|
|
if (randInt < statThresholds[bs]) {
|
|
boostedStat = bs;
|
|
break;
|
|
}
|
|
}
|
|
|
|
let statLevels = 1;
|
|
|
|
switch (segmentIndex) {
|
|
case 1:
|
|
if (this.bossSegments >= 3)
|
|
statLevels++;
|
|
break;
|
|
case 2:
|
|
if (this.bossSegments >= 5)
|
|
statLevels++;
|
|
break;
|
|
}
|
|
|
|
this.scene.unshiftPhase(new StatChangePhase(this.scene, this.getBattlerIndex(), true, [ boostedStat ], statLevels, true, true));
|
|
|
|
this.bossSegmentIndex--;
|
|
}
|
|
}
|
|
|
|
heal(amount: integer): integer {
|
|
if (this.isBoss()) {
|
|
let amountRatio = amount / this.getMaxHp();
|
|
let segmentBypassCount = Math.floor(amountRatio / (1 / this.bossSegments));
|
|
const segmentSize = this.getMaxHp() / this.bossSegments;
|
|
for (let s = 1; s < this.bossSegments; s++) {
|
|
const hpThreshold = segmentSize * s;
|
|
if (this.hp <= Math.round(hpThreshold)) {
|
|
const healAmount = Math.min(amount, this.getMaxHp() - this.hp, Math.round(hpThreshold + (segmentSize * segmentBypassCount) - this.hp));
|
|
this.hp += healAmount;
|
|
return healAmount;
|
|
} else if (s >= this.bossSegmentIndex)
|
|
return super.heal(amount);
|
|
}
|
|
}
|
|
|
|
return super.heal(amount);
|
|
}
|
|
|
|
getFieldIndex(): integer {
|
|
return this.scene.getEnemyField().indexOf(this);
|
|
}
|
|
|
|
getBattlerIndex(): BattlerIndex {
|
|
return BattlerIndex.ENEMY + this.getFieldIndex();
|
|
}
|
|
|
|
addToParty(pokeballType: PokeballType) {
|
|
const party = this.scene.getParty();
|
|
let ret: PlayerPokemon = null;
|
|
|
|
if (party.length < 6) {
|
|
this.pokeball = pokeballType;
|
|
this.metLevel = this.level;
|
|
this.metBiome = this.scene.arena.biomeType;
|
|
const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, this.gender, this.shiny, this.variant, this.ivs, this.nature, this);
|
|
party.push(newPokemon);
|
|
ret = newPokemon;
|
|
this.scene.triggerPokemonFormChange(newPokemon, SpeciesFormChangeActiveTrigger, true);
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
export interface TurnMove {
|
|
move: Moves;
|
|
targets?: BattlerIndex[];
|
|
result: MoveResult;
|
|
virtual?: boolean;
|
|
turn?: integer;
|
|
}
|
|
|
|
export interface QueuedMove {
|
|
move: Moves;
|
|
targets: BattlerIndex[];
|
|
ignorePP?: boolean;
|
|
}
|
|
|
|
export interface AttackMoveResult {
|
|
move: Moves;
|
|
result: DamageResult;
|
|
damage: integer;
|
|
critical: boolean;
|
|
sourceId: integer;
|
|
}
|
|
|
|
export class PokemonSummonData {
|
|
public battleStats: integer[] = [ 0, 0, 0, 0, 0, 0, 0 ];
|
|
public moveQueue: QueuedMove[] = [];
|
|
public disabledMove: Moves = Moves.NONE;
|
|
public disabledTurns: integer = 0;
|
|
public tags: BattlerTag[] = [];
|
|
public abilitySuppressed: boolean = false;
|
|
|
|
public speciesForm: PokemonSpeciesForm;
|
|
public fusionSpeciesForm: PokemonSpeciesForm;
|
|
public ability: Abilities = Abilities.NONE;
|
|
public gender: Gender;
|
|
public fusionGender: Gender;
|
|
public stats: integer[];
|
|
public moveset: PokemonMove[];
|
|
public types: Type[];
|
|
}
|
|
|
|
export class PokemonBattleData {
|
|
public hitCount: integer = 0;
|
|
public endured: boolean = false;
|
|
public berriesEaten: BerryType[] = [];
|
|
public abilitiesApplied: Abilities[] = [];
|
|
}
|
|
|
|
export class PokemonBattleSummonData {
|
|
public turnCount: integer = 1;
|
|
public moveHistory: TurnMove[] = [];
|
|
}
|
|
|
|
export class PokemonTurnData {
|
|
public flinched: boolean;
|
|
public acted: boolean;
|
|
public hitCount: integer;
|
|
public hitsLeft: integer;
|
|
public damageDealt: integer = 0;
|
|
public damageTaken: integer = 0;
|
|
public attacksReceived: AttackMoveResult[] = [];
|
|
}
|
|
|
|
export enum AiType {
|
|
RANDOM,
|
|
SMART_RANDOM,
|
|
SMART
|
|
}
|
|
|
|
export enum MoveResult {
|
|
PENDING,
|
|
SUCCESS,
|
|
FAIL,
|
|
MISS,
|
|
OTHER
|
|
}
|
|
|
|
export enum HitResult {
|
|
EFFECTIVE = 1,
|
|
SUPER_EFFECTIVE,
|
|
NOT_VERY_EFFECTIVE,
|
|
ONE_HIT_KO,
|
|
NO_EFFECT,
|
|
STATUS,
|
|
HEAL,
|
|
FAIL,
|
|
MISS,
|
|
OTHER
|
|
}
|
|
|
|
export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO | HitResult.OTHER;
|
|
|
|
export class PokemonMove {
|
|
public moveId: Moves;
|
|
public ppUsed: integer;
|
|
public ppUp: integer;
|
|
public virtual: boolean;
|
|
|
|
constructor(moveId: Moves, ppUsed?: integer, ppUp?: integer, virtual?: boolean) {
|
|
this.moveId = moveId;
|
|
this.ppUsed = ppUsed || 0;
|
|
this.ppUp = ppUp || 0;
|
|
this.virtual = !!virtual;
|
|
}
|
|
|
|
isUsable(pokemon: Pokemon, ignorePp?: boolean): boolean {
|
|
if (this.moveId && pokemon.summonData?.disabledMove === this.moveId)
|
|
return false;
|
|
return (ignorePp || this.ppUsed < this.getMovePp() || this.getMove().pp === -1) && !this.getMove().name.endsWith(' (N)');
|
|
}
|
|
|
|
getMove(): Move {
|
|
return allMoves[this.moveId];
|
|
}
|
|
|
|
/**
|
|
* Sets {@link ppUsed} for this move and ensures the value does not exceed {@link getMovePp}
|
|
* @param {number} count Amount of PP to use
|
|
*/
|
|
usePp(count: number = 1) {
|
|
this.ppUsed = Math.min(this.ppUsed + count, this.getMovePp());
|
|
}
|
|
|
|
getMovePp(): integer {
|
|
return this.getMove().pp + this.ppUp * Math.max(Math.floor(this.getMove().pp / 5), 1);
|
|
}
|
|
|
|
getPpRatio(): number {
|
|
return 1 - (this.ppUsed / this.getMovePp());
|
|
}
|
|
|
|
getName(): string {
|
|
return this.getMove().name;
|
|
}
|
|
|
|
/**
|
|
* Copies an existing move or creates a valid PokemonMove object from json representing one
|
|
* @param {PokemonMove | any} source The data for the move to copy
|
|
* @return {PokemonMove} A valid pokemonmove object
|
|
*/
|
|
static loadMove(source: PokemonMove | any): PokemonMove {
|
|
return new PokemonMove(source.moveId, source.ppUsed, source.ppUp, source.virtual);
|
|
}
|
|
}
|