Merge remote-tracking branch 'origin/beta' into ability/wimpout

This commit is contained in:
muscode13 2024-11-03 00:44:25 -06:00
commit 21fc752e66
10 changed files with 425 additions and 239 deletions

@ -1 +1 @@
Subproject commit 71390cba88f4103d0d2273d59a6dd8340a4fa54f Subproject commit fc4a1effd5170def3c8314208a52cd0d8e6913ef

View File

@ -5306,7 +5306,8 @@ export function initAbilities() {
.attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1) .attr(TypeImmunityAddBattlerTagAbAttr, Type.FIRE, BattlerTagType.FIRE_BOOST, 1)
.ignorable(), .ignorable(),
new Ability(Abilities.SHIELD_DUST, 3) new Ability(Abilities.SHIELD_DUST, 3)
.attr(IgnoreMoveEffectsAbAttr), .attr(IgnoreMoveEffectsAbAttr)
.ignorable(),
new Ability(Abilities.OWN_TEMPO, 3) new Ability(Abilities.OWN_TEMPO, 3)
.attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED) .attr(BattlerTagImmunityAbAttr, BattlerTagType.CONFUSED)
.attr(IntimidateImmunityAbAttr) .attr(IntimidateImmunityAbAttr)
@ -5424,11 +5425,9 @@ export function initAbilities() {
new Ability(Abilities.CUTE_CHARM, 3) new Ability(Abilities.CUTE_CHARM, 3)
.attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED), .attr(PostDefendContactApplyTagChanceAbAttr, 30, BattlerTagType.INFATUATED),
new Ability(Abilities.PLUS, 3) new Ability(Abilities.PLUS, 3)
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
.ignorable(),
new Ability(Abilities.MINUS, 3) new Ability(Abilities.MINUS, 3)
.conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5) .conditionalAttr(p => p.scene.currentBattle.double && [ Abilities.PLUS, Abilities.MINUS ].some(a => p.getAlly().hasAbility(a)), StatMultiplierAbAttr, Stat.SPATK, 1.5),
.ignorable(),
new Ability(Abilities.FORECAST, 3) new Ability(Abilities.FORECAST, 3)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(NoFusionAbilityAbAttr) .attr(NoFusionAbilityAbAttr)
@ -5766,7 +5765,8 @@ export function initAbilities() {
.attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonTeravolt", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }))
.attr(MoveAbilityBypassAbAttr), .attr(MoveAbilityBypassAbAttr),
new Ability(Abilities.AROMA_VEIL, 6) new Ability(Abilities.AROMA_VEIL, 6)
.attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ]), .attr(UserFieldBattlerTagImmunityAbAttr, [ BattlerTagType.INFATUATED, BattlerTagType.TAUNT, BattlerTagType.DISABLED, BattlerTagType.TORMENT, BattlerTagType.HEAL_BLOCK ])
.ignorable(),
new Ability(Abilities.FLOWER_VEIL, 6) new Ability(Abilities.FLOWER_VEIL, 6)
.ignorable() .ignorable()
.unimplemented(), .unimplemented(),
@ -6217,16 +6217,14 @@ export function initAbilities() {
.ignorable(), .ignorable(),
new Ability(Abilities.SWORD_OF_RUIN, 9) new Ability(Abilities.SWORD_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75) .attr(FieldMultiplyStatAbAttr, Stat.DEF, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonSwordOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.DEF)) })),
.ignorable(),
new Ability(Abilities.TABLETS_OF_RUIN, 9) new Ability(Abilities.TABLETS_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75) .attr(FieldMultiplyStatAbAttr, Stat.ATK, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) })) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonTabletsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.ATK)) }))
.ignorable(), .ignorable(),
new Ability(Abilities.BEADS_OF_RUIN, 9) new Ability(Abilities.BEADS_OF_RUIN, 9)
.attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75) .attr(FieldMultiplyStatAbAttr, Stat.SPDEF, 0.75)
.attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })) .attr(PostSummonMessageAbAttr, (user) => i18next.t("abilityTriggers:postSummonBeadsOfRuin", { pokemonNameWithAffix: getPokemonNameWithAffix(user), statName: i18next.t(getStatKey(Stat.SPDEF)) })),
.ignorable(),
new Ability(Abilities.ORICHALCUM_PULSE, 9) new Ability(Abilities.ORICHALCUM_PULSE, 9)
.attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostSummonWeatherChangeAbAttr, WeatherType.SUNNY)
.attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY) .attr(PostBiomeChangeWeatherChangeAbAttr, WeatherType.SUNNY)

View File

@ -4465,7 +4465,7 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr {
} }
if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) { if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(Species.SILVALLY)) {
const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; // TODO: is this bang correct? const form = user.species.speciesId === Species.ARCEUS || user.species.speciesId === Species.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!;
moveType.value = Type[Type[form]]; moveType.value = Type[Type[form]];
return true; return true;

View File

@ -47,7 +47,7 @@ export function getPokemonSpecies(species: Species | Species[] | undefined): Pok
return allSpecies[species - 1]; return allSpecies[species - 1];
} }
export function getPokemonSpeciesForm(species: Species, formIndex: integer): PokemonSpeciesForm { export function getPokemonSpeciesForm(species: Species, formIndex: number): PokemonSpeciesForm {
const retSpecies: PokemonSpecies = species >= 2000 const retSpecies: PokemonSpecies = species >= 2000
? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct? ? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct?
: allSpecies[species - 1]; : allSpecies[species - 1];
@ -129,26 +129,27 @@ export type PokemonSpeciesFilter = (species: PokemonSpecies) => boolean;
export abstract class PokemonSpeciesForm { export abstract class PokemonSpeciesForm {
public speciesId: Species; public speciesId: Species;
public formIndex: integer; protected _formIndex: number;
public generation: integer; protected _generation: number;
public type1: Type; readonly type1: Type;
public type2: Type | null; readonly type2: Type | null;
public height: number; readonly height: number;
public weight: number; readonly weight: number;
public ability1: Abilities; readonly ability1: Abilities;
public ability2: Abilities; readonly ability2: Abilities;
public abilityHidden: Abilities; readonly abilityHidden: Abilities;
public baseTotal: integer; readonly baseTotal: number;
public baseStats: integer[]; readonly baseStats: number[];
public catchRate: integer; readonly catchRate: number;
public baseFriendship: integer; readonly baseFriendship: number;
public baseExp: integer; readonly baseExp: number;
public genderDiffs: boolean; readonly genderDiffs: boolean;
public isStarterSelectable: boolean; readonly isStarterSelectable: boolean;
constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, constructor(type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs: boolean, isStarterSelectable: boolean) { catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean, isStarterSelectable: boolean
) {
this.type1 = type1; this.type1 = type1;
this.type2 = type2; this.type2 = type2;
this.height = height; this.height = height;
@ -180,7 +181,23 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
isOfType(type: integer): boolean { get generation(): number {
return this._generation;
}
set generation(generation: number) {
this._generation = generation;
}
get formIndex(): number {
return this._formIndex;
}
set formIndex(formIndex: number) {
this._formIndex = formIndex;
}
isOfType(type: number): boolean {
return this.type1 === type || (this.type2 !== null && this.type2 === type); return this.type1 === type || (this.type2 !== null && this.type2 === type);
} }
@ -188,7 +205,7 @@ export abstract class PokemonSpeciesForm {
* Method to get the total number of abilities a Pokemon species has. * Method to get the total number of abilities a Pokemon species has.
* @returns Number of abilities * @returns Number of abilities
*/ */
getAbilityCount(): integer { getAbilityCount(): number {
return this.abilityHidden !== Abilities.NONE ? 3 : 2; return this.abilityHidden !== Abilities.NONE ? 3 : 2;
} }
@ -197,7 +214,7 @@ export abstract class PokemonSpeciesForm {
* @param abilityIndex Which ability to get (should only be 0-2) * @param abilityIndex Which ability to get (should only be 0-2)
* @returns The id of the Ability * @returns The id of the Ability
*/ */
getAbility(abilityIndex: integer): Abilities { getAbility(abilityIndex: number): Abilities {
let ret: Abilities; let ret: Abilities;
if (abilityIndex === 0) { if (abilityIndex === 0) {
ret = this.ability1; ret = this.ability1;
@ -277,12 +294,12 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
getSpriteAtlasPath(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { getSpriteAtlasPath(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/"); const spriteId = this.getSpriteId(female, formIndex, shiny, variant).replace(/\_{2}/g, "/");
return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`; return `${/_[1-3]$/.test(spriteId) ? "variant/" : ""}${spriteId}`;
} }
getSpriteId(female: boolean, formIndex?: integer, shiny?: boolean, variant: integer = 0, back?: boolean): string { getSpriteId(female: boolean, formIndex?: number, shiny?: boolean, variant: number = 0, back?: boolean): string {
if (formIndex === undefined || this instanceof PokemonForm) { if (formIndex === undefined || this instanceof PokemonForm) {
formIndex = this.formIndex; formIndex = this.formIndex;
} }
@ -299,11 +316,11 @@ export abstract class PokemonSpeciesForm {
return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`; return `${back ? "back__" : ""}${shiny && (!variantSet || (!variant && !variantSet[variant || 0])) ? "shiny__" : ""}${baseSpriteKey}${shiny && variantSet && variantSet[variant] === 2 ? `_${variant + 1}` : ""}`;
} }
getSpriteKey(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { getSpriteKey(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`; return `pkmn__${this.getSpriteId(female, formIndex, shiny, variant)}`;
} }
abstract getFormSpriteKey(formIndex?: integer): string; abstract getFormSpriteKey(formIndex?: number): string;
/** /**
@ -311,9 +328,9 @@ export abstract class PokemonSpeciesForm {
* @param formIndex optional form index for pokemon with different forms * @param formIndex optional form index for pokemon with different forms
* @returns species id if no additional forms, index with formkey if a pokemon with a form * @returns species id if no additional forms, index with formkey if a pokemon with a form
*/ */
getVariantDataIndex(formIndex?: integer) { getVariantDataIndex(formIndex?: number) {
let formkey: string | null = null; let formkey: string | null = null;
let variantDataIndex: integer | string = this.speciesId; let variantDataIndex: number | string = this.speciesId;
const species = getPokemonSpecies(this.speciesId); const species = getPokemonSpecies(this.speciesId);
if (species.forms.length > 0 && formIndex !== undefined) { if (species.forms.length > 0 && formIndex !== undefined) {
formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex); formkey = species.forms[formIndex]?.getFormSpriteKey(formIndex);
@ -324,13 +341,13 @@ export abstract class PokemonSpeciesForm {
return variantDataIndex; return variantDataIndex;
} }
getIconAtlasKey(formIndex?: integer, shiny?: boolean, variant?: integer): string { getIconAtlasKey(formIndex?: number, shiny?: boolean, variant?: number): string {
const variantDataIndex = this.getVariantDataIndex(formIndex); const variantDataIndex = this.getVariantDataIndex(formIndex);
const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]); const isVariant = shiny && variantData[variantDataIndex] && (variant !== undefined && variantData[variantDataIndex][variant]);
return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`; return `pokemon_icons_${this.generation}${isVariant ? "v" : ""}`;
} }
getIconId(female: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): string { getIconId(female: boolean, formIndex?: number, shiny?: boolean, variant?: number): string {
if (formIndex === undefined) { if (formIndex === undefined) {
formIndex = this.formIndex; formIndex = this.formIndex;
} }
@ -379,7 +396,7 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
getCryKey(formIndex?: integer): string { getCryKey(formIndex?: number): string {
let speciesId = this.speciesId; let speciesId = this.speciesId;
if (this.speciesId > 2000) { if (this.speciesId > 2000) {
switch (this.speciesId) { switch (this.speciesId) {
@ -446,7 +463,7 @@ export abstract class PokemonSpeciesForm {
return ret; return ret;
} }
validateStarterMoveset(moveset: StarterMoveset, eggMoves: integer): boolean { validateStarterMoveset(moveset: StarterMoveset, eggMoves: number): boolean {
const rootSpeciesId = this.getRootSpeciesId(); const rootSpeciesId = this.getRootSpeciesId();
for (const moveId of moveset) { for (const moveId of moveset) {
if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) { if (speciesEggMoves.hasOwnProperty(rootSpeciesId)) {
@ -467,7 +484,7 @@ export abstract class PokemonSpeciesForm {
return true; return true;
} }
loadAssets(scene: BattleScene, female: boolean, formIndex?: integer, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise<void> { loadAssets(scene: BattleScene, female: boolean, formIndex?: number, shiny?: boolean, variant?: Variant, startLoad?: boolean): Promise<void> {
return new Promise(resolve => { return new Promise(resolve => {
const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant); const spriteKey = this.getSpriteKey(female, formIndex, shiny, variant);
scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant)); scene.loadPokemonAtlas(spriteKey, this.getSpriteAtlasPath(female, formIndex, shiny, variant));
@ -536,7 +553,7 @@ export abstract class PokemonSpeciesForm {
return cry; return cry;
} }
generateCandyColors(scene: BattleScene): integer[][] { generateCandyColors(scene: BattleScene): number[][] {
const sourceTexture = scene.textures.get(this.getSpriteKey(false)); const sourceTexture = scene.textures.get(this.getSpriteKey(false));
const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame]; const sourceFrame = sourceTexture.frames[sourceTexture.firstFrame];
@ -544,7 +561,7 @@ export abstract class PokemonSpeciesForm {
const canvas = document.createElement("canvas"); const canvas = document.createElement("canvas");
const spriteColors: integer[][] = []; const spriteColors: number[][] = [];
const context = canvas.getContext("2d"); const context = canvas.getContext("2d");
const frame = sourceFrame; const frame = sourceFrame;
@ -567,7 +584,7 @@ export abstract class PokemonSpeciesForm {
} }
for (let i = 0; i < pixelData.length; i += 4) { for (let i = 0; i < pixelData.length; i += 4) {
const total = pixelData.slice(i, i + 3).reduce((total: integer, value: integer) => total + value, 0); const total = pixelData.slice(i, i + 3).reduce((total: number, value: number) => total + value, 0);
if (!total) { if (!total) {
continue; continue;
} }
@ -586,27 +603,28 @@ export abstract class PokemonSpeciesForm {
Math.random = originalRandom; Math.random = originalRandom;
return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as integer[]); return Array.from(paletteColors.keys()).map(c => Object.values(rgbaFromArgb(c)) as number[]);
} }
} }
export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable { export default class PokemonSpecies extends PokemonSpeciesForm implements Localizable {
public name: string; public name: string;
public subLegendary: boolean; readonly subLegendary: boolean;
public legendary: boolean; readonly legendary: boolean;
public mythical: boolean; readonly mythical: boolean;
public species: string; readonly species: string;
public growthRate: GrowthRate; readonly growthRate: GrowthRate;
public malePercent: number | null; readonly malePercent: number | null;
public genderDiffs: boolean; readonly genderDiffs: boolean;
public canChangeForm: boolean; readonly canChangeForm: boolean;
public forms: PokemonForm[]; readonly forms: PokemonForm[];
constructor(id: Species, generation: integer, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string, constructor(id: Species, generation: number, subLegendary: boolean, legendary: boolean, mythical: boolean, species: string,
type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: integer, baseFriendship: integer, baseExp: integer, growthRate: GrowthRate, malePercent: number | null, catchRate: number, baseFriendship: number, baseExp: number, growthRate: GrowthRate, malePercent: number | null,
genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]) { genderDiffs: boolean, canChangeForm?: boolean, ...forms: PokemonForm[]
) {
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
catchRate, baseFriendship, baseExp, genderDiffs, false); catchRate, baseFriendship, baseExp, genderDiffs, false);
this.speciesId = id; this.speciesId = id;
@ -631,7 +649,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
}); });
} }
getName(formIndex?: integer): string { getName(formIndex?: number): string {
if (formIndex !== undefined && this.forms.length) { if (formIndex !== undefined && this.forms.length) {
const form = this.forms[formIndex]; const form = this.forms[formIndex];
let key: string | null; let key: string | null;
@ -662,11 +680,11 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`); this.name = i18next.t(`pokemon:${Species[this.speciesId].toLowerCase()}`);
} }
getWildSpeciesForLevel(level: integer, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species { getWildSpeciesForLevel(level: number, allowEvolving: boolean, isBoss: boolean, gameMode: GameMode): Species {
return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0)); return this.getSpeciesForLevel(level, allowEvolving, false, (isBoss ? PartyMemberStrength.WEAKER : PartyMemberStrength.AVERAGE) + (gameMode?.isEndless ? 1 : 0));
} }
getTrainerSpeciesForLevel(level: integer, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species { getTrainerSpeciesForLevel(level: number, allowEvolving: boolean = false, strength: PartyMemberStrength, currentWave: number = 0): Species {
return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave); return this.getSpeciesForLevel(level, allowEvolving, true, strength, currentWave);
} }
@ -688,7 +706,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
* @param strength {@linkcode PartyMemberStrength} The strength of the party member in question * @param strength {@linkcode PartyMemberStrength} The strength of the party member in question
* @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance. * @returns {@linkcode integer} The level difference from expected evolution level tolerated for a mon to be unevolved. Lower value = higher evolution chance.
*/ */
private getStrengthLevelDiff(strength: PartyMemberStrength): integer { private getStrengthLevelDiff(strength: PartyMemberStrength): number {
switch (Math.min(strength, PartyMemberStrength.STRONGER)) { switch (Math.min(strength, PartyMemberStrength.STRONGER)) {
case PartyMemberStrength.WEAKEST: case PartyMemberStrength.WEAKEST:
return 60; return 60;
@ -705,7 +723,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
} }
} }
getSpeciesForLevel(level: integer, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species { getSpeciesForLevel(level: number, allowEvolving: boolean = false, forTrainer: boolean = false, strength: PartyMemberStrength = PartyMemberStrength.WEAKER, currentWave: number = 0): Species {
const prevolutionLevels = this.getPrevolutionLevels(); const prevolutionLevels = this.getPrevolutionLevels();
if (prevolutionLevels.length) { if (prevolutionLevels.length) {
@ -847,7 +865,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
} }
// This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon // This could definitely be written better and more accurate to the getSpeciesForLevel logic, but it is only for generating movesets for evolved Pokemon
getSimulatedEvolutionChain(currentLevel: integer, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] { getSimulatedEvolutionChain(currentLevel: number, forTrainer: boolean = false, isBoss: boolean = false, player: boolean = false): EvolutionLevel[] {
const ret: EvolutionLevel[] = []; const ret: EvolutionLevel[] = [];
if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) { if (pokemonPrevolutions.hasOwnProperty(this.speciesId)) {
const prevolutionLevels = this.getPrevolutionLevels().reverse(); const prevolutionLevels = this.getPrevolutionLevels().reverse();
@ -899,7 +917,7 @@ export default class PokemonSpecies extends PokemonSpeciesForm implements Locali
return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId); return variantData.hasOwnProperty(variantDataIndex) || variantData.hasOwnProperty(this.speciesId);
} }
getFormSpriteKey(formIndex?: integer) { getFormSpriteKey(formIndex?: number) {
if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) { if (this.forms.length && (formIndex !== undefined && formIndex >= this.forms.length)) {
console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`); console.warn(`Attempted accessing form with index ${formIndex} of species ${this.getName()} with only ${this.forms.length || 0} forms`);
formIndex = Math.min(formIndex, this.forms.length - 1); formIndex = Math.min(formIndex, this.forms.length - 1);
@ -919,16 +937,17 @@ export class PokemonForm extends PokemonSpeciesForm {
private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ]; private starterSelectableKeys: string[] = [ "10", "50", "10-pc", "50-pc", "red", "orange", "yellow", "green", "blue", "indigo", "violet" ];
constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities, constructor(formName: string, formKey: string, type1: Type, type2: Type | null, height: number, weight: number, ability1: Abilities, ability2: Abilities, abilityHidden: Abilities,
baseTotal: integer, baseHp: integer, baseAtk: integer, baseDef: integer, baseSpatk: integer, baseSpdef: integer, baseSpd: integer, baseTotal: number, baseHp: number, baseAtk: number, baseDef: number, baseSpatk: number, baseSpdef: number, baseSpd: number,
catchRate: integer, baseFriendship: integer, baseExp: integer, genderDiffs?: boolean, formSpriteKey?: string | null, isStarterSelectable?: boolean, ) { catchRate: number, baseFriendship: number, baseExp: number, genderDiffs: boolean = false, formSpriteKey: string | null = null, isStarterSelectable: boolean = false
) {
super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd, super(type1, type2, height, weight, ability1, ability2, abilityHidden, baseTotal, baseHp, baseAtk, baseDef, baseSpatk, baseSpdef, baseSpd,
catchRate, baseFriendship, baseExp, !!genderDiffs, (!!isStarterSelectable || !formKey)); catchRate, baseFriendship, baseExp, genderDiffs, (isStarterSelectable || !formKey));
this.formName = formName; this.formName = formName;
this.formKey = formKey; this.formKey = formKey;
this.formSpriteKey = formSpriteKey !== undefined ? formSpriteKey : null; this.formSpriteKey = formSpriteKey;
} }
getFormSpriteKey(_formIndex?: integer) { getFormSpriteKey(_formIndex?: number) {
return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey; return this.formSpriteKey !== null ? this.formSpriteKey : this.formKey;
} }
} }

View File

@ -428,38 +428,26 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
resolve(); resolve();
}; };
if (this.shiny) { if (this.shiny) {
const populateVariantColors = (key: string, back: boolean = false): Promise<void> => { const populateVariantColors = (isBackSprite: boolean = false): Promise<void> => {
return new Promise(resolve => { return new Promise(resolve => {
const battleSpritePath = this.getBattleSpriteAtlasPath(back, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, ""); const battleSpritePath = this.getBattleSpriteAtlasPath(isBackSprite, ignoreOverride).replace("variant/", "").replace(/_[1-3]$/, "");
let config = variantData; let config = variantData;
const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(back, ignoreOverride)); const useExpSprite = this.scene.experimentalSprites && this.scene.hasExpSprite(this.getBattleSpriteKey(isBackSprite, ignoreOverride));
battleSpritePath.split("/").map(p => config ? config = config[p] : null); battleSpritePath.split("/").map(p => config ? config = config[p] : null);
const variantSet: VariantSet = config as VariantSet; const variantSet: VariantSet = config as VariantSet;
if (variantSet && variantSet[this.variant] === 1) { if (variantSet && variantSet[this.variant] === 1) {
if (variantColorCache.hasOwnProperty(key)) { const cacheKey = this.getBattleSpriteKey(isBackSprite);
return resolve(); if (!variantColorCache.hasOwnProperty(cacheKey)) {
this.populateVariantColorCache(cacheKey, useExpSprite, battleSpritePath);
} }
this.scene.cachedFetch(`./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`).
then(res => {
// Prevent the JSON from processing if it failed to load
if (!res.ok) {
console.error(`Could not load ${res.url}!`);
return;
} }
return res.json();
}).then(c => {
variantColorCache[key] = c;
resolve(); resolve();
}); });
} else {
resolve();
}
});
}; };
if (this.isPlayer()) { if (this.isPlayer()) {
Promise.all([ populateVariantColors(this.getBattleSpriteKey(false)), populateVariantColors(this.getBattleSpriteKey(true), true) ]).then(() => updateFusionPaletteAndResolve()); Promise.all([ populateVariantColors(false), populateVariantColors(true) ]).then(() => updateFusionPaletteAndResolve());
} else { } else {
populateVariantColors(this.getBattleSpriteKey(false)).then(() => updateFusionPaletteAndResolve()); populateVariantColors(false).then(() => updateFusionPaletteAndResolve());
} }
} else { } else {
updateFusionPaletteAndResolve(); updateFusionPaletteAndResolve();
@ -472,6 +460,45 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}); });
} }
/**
* Gracefully handle errors loading a variant sprite. Log if it fails and attempt to fall back on
* non-experimental sprites before giving up.
*
* @param cacheKey the cache key for the variant color sprite
* @param attemptedSpritePath the sprite path that failed to load
* @param useExpSprite was the attempted sprite experimental
* @param battleSpritePath the filename of the sprite
* @param optionalParams any additional params to log
*/
fallbackVariantColor(cacheKey: string, attemptedSpritePath: string, useExpSprite: boolean, battleSpritePath: string, ...optionalParams: any[]) {
console.warn(`Could not load ${attemptedSpritePath}!`, ...optionalParams);
if (useExpSprite) {
this.populateVariantColorCache(cacheKey, false, battleSpritePath);
}
}
/**
* Attempt to process variant sprite.
*
* @param cacheKey the cache key for the variant color sprite
* @param useExpSprite should the experimental sprite be used
* @param battleSpritePath the filename of the sprite
*/
populateVariantColorCache(cacheKey: string, useExpSprite: boolean, battleSpritePath: string) {
const spritePath = `./images/pokemon/variant/${useExpSprite ? "exp/" : ""}${battleSpritePath}.json`;
this.scene.cachedFetch(spritePath).then(res => {
// Prevent the JSON from processing if it failed to load
if (!res.ok) {
return this.fallbackVariantColor(cacheKey, res.url, useExpSprite, battleSpritePath, res.status, res.statusText);
}
return res.json();
}).catch(error => {
this.fallbackVariantColor(cacheKey, spritePath, useExpSprite, battleSpritePath, error);
}).then(c => {
variantColorCache[cacheKey] = c;
});
}
getFormKey(): string { getFormKey(): string {
if (!this.species.forms.length || this.species.forms.length <= this.formIndex) { if (!this.species.forms.length || this.species.forms.length <= this.formIndex) {
return ""; return "";

View File

@ -1,20 +1,62 @@
import BattleScene from "#app/battle-scene";
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr, TypeImmunityAbAttr } from "#app/data/ability"; import BattleScene from "#app/battle-scene";
import {
AddSecondStrikeAbAttr,
AlwaysHitAbAttr,
applyPostAttackAbAttrs,
applyPostDefendAbAttrs,
applyPreAttackAbAttrs,
IgnoreMoveEffectsAbAttr,
MaxMultiHitAbAttr,
PostAttackAbAttr,
PostDefendAbAttr,
TypeImmunityAbAttr,
} from "#app/data/ability";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag"; import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag";
import { MoveAnim } from "#app/data/battle-anims"; import { MoveAnim } from "#app/data/battle-anims";
import { BattlerTagLapseType, DamageProtectedTag, ProtectedTag, SemiInvulnerableTag, SubstituteTag } from "#app/data/battler-tags"; import {
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, OneHitKOAttr, MoveEffectTrigger, MoveCategory, NoEffectAttr, HitsTagAttr, ToxicAccuracyAttr } from "#app/data/move"; BattlerTagLapseType,
DamageProtectedTag,
ProtectedTag,
SemiInvulnerableTag,
SubstituteTag,
} from "#app/data/battler-tags";
import {
applyFilteredMoveAttrs,
applyMoveAttrs,
AttackMove,
FixedDamageAttr,
HitsTagAttr,
MissEffectAttr,
MoveAttr,
MoveCategory,
MoveEffectAttr,
MoveEffectTrigger,
MoveFlags,
MoveTarget,
MultiHitAttr,
NoEffectAttr,
OneHitKOAttr,
OverrideMoveEffectAttr,
ToxicAccuracyAttr,
VariableTargetAttr,
} from "#app/data/move";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms"; import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Moves } from "#app/enums/moves";
import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier";
import i18next from "i18next";
import * as Utils from "#app/utils";
import { PokemonPhase } from "./pokemon-phase";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import Pokemon, { HitResult, MoveResult, PokemonMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import {
ContactHeldItemTransferChanceModifier,
EnemyAttackStatusEffectChanceModifier,
FlinchChanceModifier,
HitHealModifier,
PokemonMultiHitModifier,
} from "#app/modifier/modifier";
import { BooleanHolder, executeIf, NumberHolder } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves";
import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
export class MoveEffectPhase extends PokemonPhase { export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove; public move: PokemonMove;
@ -35,7 +77,7 @@ export class MoveEffectPhase extends PokemonPhase {
this.targets = targets; this.targets = targets;
} }
start() { public override start(): void {
super.start(); super.start();
/** The Pokemon using this phase's invoked move */ /** The Pokemon using this phase's invoked move */
@ -52,12 +94,12 @@ export class MoveEffectPhase extends PokemonPhase {
* Does an effect from this move override other effects on this turn? * Does an effect from this move override other effects on this turn?
* e.g. Charging moves (Fly, etc.) on their first turn of use. * e.g. Charging moves (Fly, etc.) on their first turn of use.
*/ */
const overridden = new Utils.BooleanHolder(false); const overridden = new BooleanHolder(false);
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */ /** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
const move = this.move.getMove(); const move = this.move.getMove();
// Assume single target for override // Assume single target for override
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => { applyMoveAttrs(OverrideMoveEffectAttr, user, this.getFirstTarget() ?? null, move, overridden, this.move.virtual).then(() => {
// If other effects were overriden, stop this phase before they can be applied // If other effects were overriden, stop this phase before they can be applied
if (overridden.value) { if (overridden.value) {
return this.end(); return this.end();
@ -71,14 +113,14 @@ export class MoveEffectPhase extends PokemonPhase {
* effects of the move itself, Parental Bond, and Multi-Lens to do so. * effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/ */
if (user.turnData.hitsLeft === -1) { if (user.turnData.hitsLeft === -1) {
const hitCount = new Utils.IntegerHolder(1); const hitCount = new NumberHolder(1);
// Assume single target for multi hit // Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount); applyMoveAttrs(MultiHitAttr, user, this.getFirstTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, double the hit count // If Parental Bond is applicable, double the hit count
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new Utils.IntegerHolder(0)); applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, false, targets.length, hitCount, new NumberHolder(0));
// If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user // If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) { if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0)); this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new NumberHolder(0));
} }
// Set the user's relevant turnData fields to reflect the final hit count // Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value; user.turnData.hitCount = hitCount.value;
@ -100,7 +142,8 @@ export class MoveEffectPhase extends PokemonPhase {
const hasActiveTargets = targets.some(t => t.isActive(true)); const hasActiveTargets = targets.some(t => t.isActive(true));
/** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */ /** Check if the target is immune via ability to the attacking move, and NOT in semi invulnerable state */
const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr) && (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) const isImmune = targets[0].hasAbilityWithAttr(TypeImmunityAbAttr)
&& (targets[0].getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !targets[0].getTag(SemiInvulnerableTag); && !targets[0].getTag(SemiInvulnerableTag);
/** /**
@ -111,7 +154,7 @@ export class MoveEffectPhase extends PokemonPhase {
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) { if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]] && !targets[0].getTag(ProtectedTag) && !isImmune)) {
this.stopMultiHit(); this.stopMultiHit();
if (hasActiveTargets) { if (hasActiveTargets) {
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget() ? getPokemonNameWithAffix(this.getTarget()!) : "" })); this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getFirstTarget() ? getPokemonNameWithAffix(this.getFirstTarget()!) : "" }));
moveHistoryEntry.result = MoveResult.MISS; moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, move); applyMoveAttrs(MissEffectAttr, user, null, move);
} else { } else {
@ -127,29 +170,39 @@ export class MoveEffectPhase extends PokemonPhase {
const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false; const playOnEmptyField = this.scene.currentBattle?.mysteryEncounter?.hasBattleAnimationsWithoutTargets ?? false;
// Move animation only needs one target // Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getTarget()!), () => { new MoveAnim(move.id as Moves, user, this.getFirstTarget()!.getBattlerIndex()!, playOnEmptyField).play(this.scene, move.hitsSubstitute(user, this.getFirstTarget()!), () => {
/** Has the move successfully hit a target (for damage) yet? */ /** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false; let hasHit: boolean = false;
for (const target of targets) { for (const target of targets) {
// Prevent ENEMY_SIDE targeted moves from occurring twice in double battles
if (move.moveTarget === MoveTarget.ENEMY_SIDE && target !== targets[targets.length - 1]) {
continue;
}
/** The {@linkcode ArenaTagSide} to which the target belongs */ /** The {@linkcode ArenaTagSide} to which the target belongs */
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY; const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */ /** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
const hasConditionalProtectApplied = new Utils.BooleanHolder(false); const hasConditionalProtectApplied = new BooleanHolder(false);
/** Does the applied conditional protection bypass Protect-ignoring effects? */ /** Does the applied conditional protection bypass Protect-ignoring effects? */
const bypassIgnoreProtect = new Utils.BooleanHolder(false); const bypassIgnoreProtect = new BooleanHolder(false);
/** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */ /** If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects */
if (!this.move.getMove().isAllyTarget()) { if (!this.move.getMove().isAllyTarget()) {
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect); this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, false, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
} }
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */ /** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target)) const isProtected = (
&& (hasConditionalProtectApplied.value || (!target.findTags(t => t instanceof DamageProtectedTag).length && target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType))) bypassIgnoreProtect.value
|| (this.move.getMove().category !== MoveCategory.STATUS && target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType)))); || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
&& (hasConditionalProtectApplied.value
|| (!target.findTags(t => t instanceof DamageProtectedTag).length
&& target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)))
|| (this.move.getMove().category !== MoveCategory.STATUS
&& target.findTags(t => t instanceof DamageProtectedTag).find(t => target.lapseTag(t.tagType))));
/** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */ /** Is the pokemon immune due to an ablility, and also not in a semi invulnerable state? */
const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr) && (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move)) const isImmune = target.hasAbilityWithAttr(TypeImmunityAbAttr)
&& (target.getAbility()?.getAttrs(TypeImmunityAbAttr)?.[0]?.getImmuneType() === user.getMoveType(move))
&& !target.getTag(SemiInvulnerableTag); && !target.getTag(SemiInvulnerableTag);
/** /**
@ -218,7 +271,7 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** Does this phase represent the invoked move's last strike? */ /** Does this phase represent the invoked move's last strike? */
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()); const lastHit = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive());
/** /**
* If the user can change forms by using the invoked move, * If the user can change forms by using the invoked move,
@ -234,85 +287,48 @@ export class MoveEffectPhase extends PokemonPhase {
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger * These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
* type requires different conditions to be met with respect to the move's hit result. * type requires different conditions to be met with respect to the move's hit result.
*/ */
applyAttrs.push(new Promise(resolve => { const k = new Promise<void>((resolve) => {
// Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move) //Start promise chain and apply PRE_APPLY move attributes
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT, let promiseChain: Promise<void | null> = applyFilteredMoveAttrs((attr: MoveAttr) =>
user, target, move).then(() => { attr instanceof MoveEffectAttr
// All other effects require the move to not have failed or have been cancelled to trigger && attr.trigger === MoveEffectTrigger.PRE_APPLY
if (hitResult !== HitResult.FAIL) { && (!attr.firstHitOnly || firstHit)
/** && (!attr.lastHitOnly || lastHit)
* If the invoked move's effects are meant to trigger during the move's "charge turn," && hitResult !== HitResult.NO_EFFECT, user, target, move);
* ignore all effects after this point.
* Otherwise, apply all self-targeted POST_APPLY effects. /** Don't complete if the move failed */
*/ if (hitResult === HitResult.FAIL) {
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move).then(() => {
// All effects past this point require the move to have hit the target
if (hitResult !== HitResult.NO_EFFECT) {
// Apply all non-self-targeted POST_APPLY effects
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
/**
* If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock
*/
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !move.hitsSubstitute(user, target)) {
const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
}
// If the move was not protected against, apply all HIT effects
Utils.executeIf(!isProtected, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
// Only apply the following effects if the move was not deflected by a substitute
if (move.hitsSubstitute(user, target)) {
return resolve(); return resolve();
} }
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tokens /** Apply Move/Ability Effects in correct order */
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) { promiseChain = promiseChain
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target); .then(this.applySelfTargetEffects(user, target, firstHit, lastHit));
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
})).then(() => { if (hitResult !== HitResult.NO_EFFECT) {
// Apply the user's post-attack ability effects promiseChain
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => { .then(this.applyPostApplyEffects(user, target, firstHit, lastHit))
/** .then(this.applyHeldItemFlinchCheck(user, target, dealsDamage))
* If the invoked move is an attack, apply the user's chance to .then(this.applySuccessfulAttackEffects(user, target, firstHit, lastHit, !!isProtected, hitResult, firstTarget))
* steal an item from the target granted by Grip Claw .then(() => resolve());
*/
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
resolve();
});
});
})
).then(() => resolve());
});
} else { } else {
applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve()); promiseChain
.then(() => applyMoveAttrs(NoEffectAttr, user, null, move))
.then(resolve);
} }
}); });
} else {
resolve(); applyAttrs.push(k);
}
});
}));
} }
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved // Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ? const postTarget = (user.turnData.hitsLeft === 1 || !this.getFirstTarget()?.isActive()) ?
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) : applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
null; null;
if (!!postTarget) { if (postTarget) {
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget); applyAttrs[applyAttrs.length - 1].then(() => postTarget);
} else { // Otherwise, push a new asynchronous move effect } else { // Otherwise, push a new asynchronous move effect
applyAttrs.push(postTarget); applyAttrs.push(postTarget);
} }
@ -327,7 +343,7 @@ export class MoveEffectPhase extends PokemonPhase {
*/ */
targets.forEach(target => { targets.forEach(target => {
const substitute = target.getTag(SubstituteTag); const substitute = target.getTag(SubstituteTag);
if (!!substitute && substitute.hp <= 0) { if (substitute && substitute.hp <= 0) {
target.lapseTag(BattlerTagType.SUBSTITUTE); target.lapseTag(BattlerTagType.SUBSTITUTE);
} }
}); });
@ -337,7 +353,7 @@ export class MoveEffectPhase extends PokemonPhase {
}); });
} }
end() { public override end(): void {
const user = this.getUserPokemon(); const user = this.getUserPokemon();
/** /**
* If this phase isn't for the invoked move's last strike, * If this phase isn't for the invoked move's last strike,
@ -347,7 +363,7 @@ export class MoveEffectPhase extends PokemonPhase {
* to the user. * to the user.
*/ */
if (user) { if (user) {
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) { if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getFirstTarget()?.isActive()) {
this.scene.unshiftPhase(this.getNewHitPhase()); this.scene.unshiftPhase(this.getNewHitPhase());
} else { } else {
// Queue message for number of hits made by multi-move // Queue message for number of hits made by multi-move
@ -367,11 +383,135 @@ export class MoveEffectPhase extends PokemonPhase {
} }
/** /**
* Resolves whether this phase's invoked move hits or misses the given target * Apply self-targeted effects that trigger `POST_APPLY`
* @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move *
* @returns `true` if the move does not miss the target; `false` otherwise * @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call.
*/ */
hitCheck(target: Pokemon): boolean { protected applySelfTargetEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
}
/**
* Applies non-self-targeted effects that trigger `POST_APPLY`
* (i.e. Smelling Salts curing Paralysis, and the forced switch from U-Turn, Dragon Tail, etc)
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @returns a function intended to be passed into a `then()` call.
*/
protected applyPostApplyEffects(user: Pokemon, target: Pokemon, firstHit: boolean, lastHit: boolean): () => Promise<void | null> {
return () => applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.POST_APPLY
&& !attr.selfTarget
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit), user, target, this.move.getMove());
}
/**
* Applies effects that trigger on HIT
* (i.e. Final Gambit, Power-Up Punch, Drain Punch)
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call.
*/
protected applyOnHitEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, firstTarget: boolean): Promise<void> {
return applyFilteredMoveAttrs((attr: MoveAttr) =>
attr instanceof MoveEffectAttr
&& attr.trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit)
&& (!attr.lastHitOnly || lastHit)
&& (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove());
}
/**
* Applies reactive effects that occur when a Pokémon is hit.
* (i.e. Effect Spore, Disguise, Liquid Ooze, Beak Blast)
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param hitResult - The {@linkcode HitResult} of the attempted move
* @returns a `Promise` intended to be passed into a `then()` call.
*/
protected applyOnGetHitAbEffects(user: Pokemon, target: Pokemon, hitResult: HitResult): Promise<void | null> {
return executeIf(!target.isFainted() || target.canApplyAbility(), () =>
applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult)
.then(() => {
if (!this.move.getMove().hitsSubstitute(user, target)) {
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
target.lapseTags(BattlerTagLapseType.AFTER_HIT);
}
})
);
}
/**
* Applies all effects and attributes that require a move to connect with a target,
* namely reactive effects like Weak Armor, on-hit effects like that of Power-Up Punch, and item stealing effects
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param firstHit - `true` if this is the first hit in a multi-hit attack
* @param lastHit - `true` if this is the last hit in a multi-hit attack
* @param isProtected - `true` if the target is protected by effects such as Protect
* @param hitResult - The {@linkcode HitResult} of the attempted move
* @param firstTarget - `true` if {@linkcode target} is the first target hit by this strike of {@linkcode move}
* @returns a function intended to be passed into a `then()` call.
*/
protected applySuccessfulAttackEffects(user: Pokemon, target: Pokemon, firstHit : boolean, lastHit: boolean, isProtected : boolean, hitResult: HitResult, firstTarget: boolean) : () => Promise<void | null> {
return () => executeIf(!isProtected, () =>
this.applyOnHitEffects(user, target, firstHit, lastHit, firstTarget).then(() =>
this.applyOnGetHitAbEffects(user, target, hitResult)).then(() =>
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult)).then(() => { // Item Stealing Effects
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
})
);
}
/**
* Handles checking for and applying Flinches
* @param user - The {@linkcode Pokemon} using this phase's invoked move
* @param target - {@linkcode Pokemon} the current target of this phase's invoked move
* @param dealsDamage - `true` if the attempted move successfully dealt damage
* @returns a function intended to be passed into a `then()` call.
*/
protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void {
return () => {
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) {
const flinched = new BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
}
};
}
/**
* Resolves whether this phase's invoked move hits the given target
* @param target - The {@linkcode Pokemon} targeted by the invoked move
* @returns `true` if the move hits the target
*/
public hitCheck(target: Pokemon): boolean {
// Moves targeting the user and entry hazards can't miss // Moves targeting the user and entry hazards can't miss
if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) { if ([ MoveTarget.USER, MoveTarget.ENEMY_SIDE ].includes(this.move.getMove().moveTarget)) {
return true; return true;
@ -425,29 +565,29 @@ export class MoveEffectPhase extends PokemonPhase {
return rand < (moveAccuracy * accuracyMultiplier); return rand < (moveAccuracy * accuracyMultiplier);
} }
/** Returns the {@linkcode Pokemon} using this phase's invoked move */ /** @returns The {@linkcode Pokemon} using this phase's invoked move */
getUserPokemon(): Pokemon | undefined { public getUserPokemon(): Pokemon | undefined {
if (this.battlerIndex > BattlerIndex.ENEMY_2) { if (this.battlerIndex > BattlerIndex.ENEMY_2) {
return this.scene.getPokemonById(this.battlerIndex) ?? undefined; return this.scene.getPokemonById(this.battlerIndex) ?? undefined;
} }
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex]; return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
} }
/** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */ /** @returns An array of all {@linkcode Pokemon} targeted by this phase's invoked move */
getTargets(): Pokemon[] { public getTargets(): Pokemon[] {
return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1); return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
} }
/** Returns the first target of this phase's invoked move */ /** @returns The first target of this phase's invoked move */
getTarget(): Pokemon | undefined { public getFirstTarget(): Pokemon | undefined {
return this.getTargets()[0]; return this.getTargets()[0];
} }
/** /**
* Removes the given {@linkcode Pokemon} from this phase's target list * Removes the given {@linkcode Pokemon} from this phase's target list
* @param target {@linkcode Pokemon} the Pokemon to be removed * @param target - The {@linkcode Pokemon} to be removed
*/ */
removeTarget(target: Pokemon): void { protected removeTarget(target: Pokemon): void {
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex()); const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
if (targetIndex !== -1) { if (targetIndex !== -1) {
this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1); this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1);
@ -459,23 +599,25 @@ export class MoveEffectPhase extends PokemonPhase {
* @param target {@linkcode Pokemon} if defined, only stop subsequent * @param target {@linkcode Pokemon} if defined, only stop subsequent
* strikes against this Pokemon * strikes against this Pokemon
*/ */
stopMultiHit(target?: Pokemon): void { public stopMultiHit(target?: Pokemon): void {
/** If given a specific target, remove the target from subsequent strikes */ // If given a specific target, remove the target from subsequent strikes
if (target) { if (target) {
this.removeTarget(target); this.removeTarget(target);
} }
/** const user = this.getUserPokemon();
* If no target specified, or the specified target was the last of this move's if (!user) {
* targets, completely cancel all subsequent strikes. return;
*/ }
// If no target specified, or the specified target was the last of this move's
// targets, completely cancel all subsequent strikes.
if (!target || this.targets.length === 0 ) { if (!target || this.targets.length === 0 ) {
this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here? user.turnData.hitCount = 1;
this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here? user.turnData.hitsLeft = 1;
} }
} }
/** Returns a new MoveEffectPhase with the same properties as this phase */ /** @returns A new `MoveEffectPhase` with the same properties as this phase */
getNewHitPhase() { protected getNewHitPhase(): MoveEffectPhase {
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move); return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
} }
} }

View File

@ -57,7 +57,7 @@ describe("Abilities - Serene Grace", () => {
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name); console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(30); expect(chance.value).toBe(30);
}, 20000); }, 20000);
@ -83,7 +83,7 @@ describe("Abilities - Serene Grace", () => {
expect(move.id).toBe(Moves.AIR_SLASH); expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(60); expect(chance.value).toBe(60);
}, 20000); }, 20000);

View File

@ -60,8 +60,8 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(0); expect(chance.value).toBe(0);
expect(power.value).toBe(move.power * 5461 / 4096); expect(power.value).toBe(move.power * 5461 / 4096);
@ -93,8 +93,8 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1); expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power); expect(power.value).toBe(move.power);
@ -126,8 +126,8 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getTarget()!, move, false, power); applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1); expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power); expect(power.value).toBe(move.power);
@ -161,7 +161,7 @@ describe("Abilities - Sheer Force", () => {
const power = new Utils.IntegerHolder(move.power); const power = new Utils.IntegerHolder(move.power);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
const user = phase.getUserPokemon()!; const user = phase.getUserPokemon()!;
const target = phase.getTarget()!; const target = phase.getFirstTarget()!;
const opponentType = target.getTypes()[0]; const opponentType = target.getTypes()[0];
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false);

View File

@ -57,8 +57,8 @@ describe("Abilities - Shield Dust", () => {
expect(move.id).toBe(Moves.AIR_SLASH); expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance); const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getTarget(), false); applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getTarget()!, phase.getUserPokemon()!, null, null, false, chance); applyPreDefendAbAttrs(IgnoreMoveEffectsAbAttr, phase.getFirstTarget()!, phase.getUserPokemon()!, null, null, false, chance);
expect(chance.value).toBe(0); expect(chance.value).toBe(0);
}, 20000); }, 20000);

View File

@ -81,7 +81,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(120);
}, 20000); }, 20000);
@ -98,7 +98,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(140);
}, 20000); }, 20000);
@ -115,7 +115,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(160);
}, 20000); }, 20000);
@ -132,7 +132,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(180);
}, 20000); }, 20000);
@ -149,7 +149,7 @@ describe("Moves - Dynamax Cannon", () => {
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
expect(phase.move.moveId).toBe(dynamaxCannon.id); expect(phase.move.moveId).toBe(dynamaxCannon.id);
// Force level cap to be 100 // Force level cap to be 100
vi.spyOn(phase.getTarget()!.scene, "getMaxExpLevel").mockReturnValue(100); vi.spyOn(phase.getFirstTarget()!.scene, "getMaxExpLevel").mockReturnValue(100);
await game.phaseInterceptor.to(DamagePhase, false); await game.phaseInterceptor.to(DamagePhase, false);
expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200); expect(dynamaxCannon.calculateBattlePower).toHaveLastReturnedWith(200);
}, 20000); }, 20000);