[Enhancement] Decouple move power calculation from Pokemon.apply(), Fixes Power Spot & Battery not boosting ally's move (#2984)
* refactor power calc, fix battery & power spot * fix hard press unit test * fix hard press * refactor tests * use sypOn hp instead * rename method * cleanup tests * improve tests * use slow vs fast pokemon * fix steely spirit test * fix steely spirit for real this time * remove unnecessary test * address pr feedback * add removed code
This commit is contained in:
parent
eedabbf17c
commit
8d5bfa51e8
|
@ -1,7 +1,7 @@
|
|||
import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims";
|
||||
import { BattleEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchSummonPhase } from "../phases";
|
||||
import { BattleStat, getBattleStatName } from "./battle-stat";
|
||||
import { EncoreTag, SemiInvulnerableTag } from "./battler-tags";
|
||||
import { EncoreTag, HelpingHandTag, SemiInvulnerableTag, TypeBoostTag } from "./battler-tags";
|
||||
import { getPokemonMessage, getPokemonNameWithAffix } from "../messages";
|
||||
import Pokemon, { AttackMoveResult, EnemyPokemon, HitResult, MoveResult, PlayerPokemon, PokemonMove, TurnMove } from "../field/pokemon";
|
||||
import { StatusEffect, getStatusEffectHealText, isNonVolatileStatusEffect, getNonVolatileStatusEffects} from "./status-effect";
|
||||
|
@ -9,10 +9,10 @@ import { Type } from "./type";
|
|||
import { Constructor } from "#app/utils";
|
||||
import * as Utils from "../utils";
|
||||
import { WeatherType } from "./weather";
|
||||
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||
import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr } from "./ability";
|
||||
import { ArenaTagSide, ArenaTrapTag, WeakenMoveTypeTag } from "./arena-tag";
|
||||
import { UnswappableAbilityAbAttr, UncopiableAbilityAbAttr, UnsuppressableAbilityAbAttr, BlockRecoilDamageAttr, BlockOneHitKOAbAttr, IgnoreContactAbAttr, MaxMultiHitAbAttr, applyAbAttrs, BlockNonDirectDamageAbAttr, applyPreSwitchOutAbAttrs, PreSwitchOutAbAttr, applyPostDefendAbAttrs, PostDefendContactApplyStatusEffectAbAttr, MoveAbilityBypassAbAttr, ReverseDrainAbAttr, FieldPreventExplosiveMovesAbAttr, ForceSwitchOutImmunityAbAttr, BlockItemTheftAbAttr, applyPostAttackAbAttrs, ConfusionOnStatusEffectAbAttr, HealFromBerryUseAbAttr, IgnoreProtectOnContactAbAttr, IgnoreMoveEffectsAbAttr, applyPreDefendAbAttrs, MoveEffectChanceMultiplierAbAttr, applyPreAttackAbAttrs, MoveTypeChangeAttr, UserFieldMoveTypePowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AllyMoveCategoryPowerBoostAbAttr, VariableMovePowerAbAttr } from "./ability";
|
||||
import { allAbilities } from "./ability";
|
||||
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier } from "../modifier/modifier";
|
||||
import { PokemonHeldItemModifier, BerryModifier, PreserveBerryModifier, AttackTypeBoosterModifier, PokemonMultiHitModifier } from "../modifier/modifier";
|
||||
import { BattlerIndex } from "../battle";
|
||||
import { Stat } from "./pokemon-stat";
|
||||
import { TerrainType } from "./terrain";
|
||||
|
@ -655,6 +655,70 @@ export default class Move implements Localizable {
|
|||
|
||||
return score;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculates the power of a move in battle based on various conditions and attributes.
|
||||
*
|
||||
* @param source {@linkcode Pokemon} The Pokémon using the move.
|
||||
* @param target {@linkcode Pokemon} The Pokémon being targeted by the move.
|
||||
* @returns The calculated power of the move.
|
||||
*/
|
||||
calculateBattlePower(source: Pokemon, target: Pokemon): number {
|
||||
const power = new Utils.NumberHolder(this.power);
|
||||
|
||||
const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1);
|
||||
applyPreAttackAbAttrs(MoveTypeChangeAttr, source, target, this, typeChangeMovePowerMultiplier);
|
||||
|
||||
const sourceTeraType = source.getTeraType();
|
||||
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === this.type && power.value < 60 && this.priority <= 0 && !this.hasAttr(MultiHitAttr) && !source.scene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
|
||||
power.value = 60;
|
||||
}
|
||||
|
||||
applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, target, this, power);
|
||||
|
||||
if (source.getAlly()) {
|
||||
applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, source.getAlly(), target, this, power);
|
||||
}
|
||||
|
||||
const fieldAuras = new Set(
|
||||
source.scene.getField(true)
|
||||
.map((p) => p.getAbilityAttrs(FieldMoveTypePowerBoostAbAttr) as FieldMoveTypePowerBoostAbAttr[])
|
||||
.flat(),
|
||||
);
|
||||
for (const aura of fieldAuras) {
|
||||
// The only relevant values are `move` and the `power` holder
|
||||
aura.applyPreAttack(null, null, null, this, [power]);
|
||||
}
|
||||
|
||||
const alliedField: Pokemon[] = source instanceof PlayerPokemon ? source.scene.getPlayerField() : source.scene.getEnemyField();
|
||||
alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, target, this, power));
|
||||
|
||||
power.value *= typeChangeMovePowerMultiplier.value;
|
||||
|
||||
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === this.type) as TypeBoostTag;
|
||||
if (typeBoost) {
|
||||
power.value *= typeBoost.boostValue;
|
||||
}
|
||||
|
||||
if (source.scene.arena.getTerrainType() === TerrainType.GRASSY && target.isGrounded() && this.type === Type.GROUND && this.moveTarget === MoveTarget.ALL_NEAR_OTHERS) {
|
||||
power.value /= 2;
|
||||
}
|
||||
|
||||
applyMoveAttrs(VariablePowerAttr, source, target, this, power);
|
||||
|
||||
source.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power);
|
||||
|
||||
if (!this.hasAttr(TypelessAttr)) {
|
||||
source.scene.arena.applyTags(WeakenMoveTypeTag, this.type, power);
|
||||
source.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, this.type, power);
|
||||
}
|
||||
|
||||
if (source.getTag(HelpingHandTag)) {
|
||||
power.value *= 1.5;
|
||||
}
|
||||
|
||||
return power.value;
|
||||
}
|
||||
}
|
||||
|
||||
export class AttackMove extends Move {
|
||||
|
|
|
@ -3,14 +3,14 @@ import BattleScene, { AnySound } from "../battle-scene";
|
|||
import { Variant, VariantSet, variantColorCache } from "#app/data/variant";
|
||||
import { variantData } from "#app/data/variant";
|
||||
import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "../ui/battle-info";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, VariablePowerAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, MultiHitAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, MoveTarget, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr } from "../data/move";
|
||||
import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, StatusMoveTypeImmunityAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatChangesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatChangeAttr, RechargeAttr, ChargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, MoveFlags, NeutralDamageAgainstFlyingTypeMultiplierAttr } from "../data/move";
|
||||
import { default as PokemonSpecies, PokemonSpeciesForm, SpeciesFormKey, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm, getStarterValueFriendshipCap, speciesStarters, starterPassiveAbilities } from "../data/pokemon-species";
|
||||
import { Constructor } from "#app/utils";
|
||||
import * as Utils from "../utils";
|
||||
import { Type, TypeDamageMultiplier, getTypeDamageMultiplier, getTypeRgb } from "../data/type";
|
||||
import { getLevelTotalExp } from "../data/exp";
|
||||
import { Stat } from "../data/pokemon-stat";
|
||||
import { AttackTypeBoosterModifier, DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonMultiHitModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, TerastallizeModifier } from "../modifier/modifier";
|
||||
import { DamageMoneyRewardModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, HiddenAbilityRateBoosterModifier, PokemonBaseStatModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonNatureWeightModifier, ShinyRateBoosterModifier, SurviveDamageModifier, TempBattleStatBoosterModifier, StatBoosterModifier, TerastallizeModifier } from "../modifier/modifier";
|
||||
import { PokeballType } from "../data/pokeball";
|
||||
import { Gender } from "../data/gender";
|
||||
import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
|
||||
|
@ -19,11 +19,11 @@ import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEv
|
|||
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
|
||||
import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase } from "../phases";
|
||||
import { BattleStat } from "../data/battle-stat";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HelpingHandTag, HighestStatBoostTag, TypeBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag } from "../data/battler-tags";
|
||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag } from "../data/battler-tags";
|
||||
import { WeatherType } from "../data/weather";
|
||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||
import { ArenaTagSide, WeakenMoveScreenTag, WeakenMoveTypeTag } from "../data/arena-tag";
|
||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, MoveTypeChangeAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, VariableMovePowerAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AllyMoveCategoryPowerBoostAbAttr, FieldMoveTypePowerBoostAbAttr, AddSecondStrikeAbAttr, UserFieldMoveTypePowerBoostAbAttr } from "../data/ability";
|
||||
import { ArenaTagSide, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, PreApplyBattlerTagAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, ReduceStatusEffectDurationAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyBattleStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldBattleStatMultiplierAbAttrs, FieldMultiplyBattleStatAbAttr, AddSecondStrikeAbAttr } from "../data/ability";
|
||||
import PokemonData from "../system/pokemon-data";
|
||||
import { BattlerIndex } from "../battle";
|
||||
import { Mode } from "../ui/ui";
|
||||
|
@ -1747,9 +1747,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
applyMoveAttrs(VariableMoveCategoryAttr, source, this, move, variableCategory);
|
||||
const moveCategory = variableCategory.value as MoveCategory;
|
||||
|
||||
const typeChangeMovePowerMultiplier = new Utils.NumberHolder(1);
|
||||
applyMoveAttrs(VariableMoveTypeAttr, source, this, move);
|
||||
applyPreAttackAbAttrs(MoveTypeChangeAttr, source, this, move, typeChangeMovePowerMultiplier);
|
||||
const types = this.getTypes(true, true);
|
||||
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
|
@ -1778,31 +1776,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
case MoveCategory.PHYSICAL:
|
||||
case MoveCategory.SPECIAL:
|
||||
const isPhysical = moveCategory === MoveCategory.PHYSICAL;
|
||||
const power = new Utils.NumberHolder(move.power);
|
||||
const power = move.calculateBattlePower(source, this);
|
||||
const sourceTeraType = source.getTeraType();
|
||||
if (sourceTeraType !== Type.UNKNOWN && sourceTeraType === move.type && power.value < 60 && move.priority <= 0 && !move.hasAttr(MultiHitAttr) && !this.scene.findModifier(m => m instanceof PokemonMultiHitModifier && m.pokemonId === source.id)) {
|
||||
power.value = 60;
|
||||
}
|
||||
applyPreAttackAbAttrs(VariableMovePowerAbAttr, source, this, move, power);
|
||||
|
||||
if (source.getAlly()?.hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) {
|
||||
applyPreAttackAbAttrs(AllyMoveCategoryPowerBoostAbAttr, source, this, move, power);
|
||||
}
|
||||
|
||||
const fieldAuras = new Set(
|
||||
this.scene.getField(true)
|
||||
.map((p) => p.getAbilityAttrs(FieldMoveTypePowerBoostAbAttr) as FieldMoveTypePowerBoostAbAttr[])
|
||||
.flat(),
|
||||
);
|
||||
for (const aura of fieldAuras) {
|
||||
// The only relevant values are `move` and the `power` holder
|
||||
aura.applyPreAttack(null, null, null, move, [power]);
|
||||
}
|
||||
|
||||
const alliedField: Pokemon[] = source instanceof PlayerPokemon ? this.scene.getPlayerField() : this.scene.getEnemyField();
|
||||
alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, this, move, power));
|
||||
|
||||
power.value *= typeChangeMovePowerMultiplier.value;
|
||||
|
||||
if (!typeless) {
|
||||
applyPreDefendAbAttrs(TypeImmunityAbAttr, this, source, move, cancelled, typeMultiplier);
|
||||
|
@ -1818,28 +1793,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
result = HitResult.NO_EFFECT;
|
||||
} else {
|
||||
const typeBoost = source.findTag(t => t instanceof TypeBoostTag && t.boostedType === move.type) as TypeBoostTag;
|
||||
if (typeBoost) {
|
||||
power.value *= typeBoost.boostValue;
|
||||
if (typeBoost.oneUse) {
|
||||
source.removeTag(typeBoost.tagType);
|
||||
}
|
||||
if (typeBoost?.oneUse) {
|
||||
source.removeTag(typeBoost.tagType);
|
||||
}
|
||||
|
||||
const arenaAttackTypeMultiplier = new Utils.NumberHolder(this.scene.arena.getAttackTypeMultiplier(move.type, source.isGrounded()));
|
||||
applyMoveAttrs(IgnoreWeatherTypeDebuffAttr, source, this, move, arenaAttackTypeMultiplier);
|
||||
if (this.scene.arena.getTerrainType() === TerrainType.GRASSY && this.isGrounded() && move.type === Type.GROUND && move.moveTarget === MoveTarget.ALL_NEAR_OTHERS) {
|
||||
power.value /= 2;
|
||||
}
|
||||
|
||||
applyMoveAttrs(VariablePowerAttr, source, this, move, power);
|
||||
|
||||
this.scene.applyModifiers(PokemonMultiHitModifier, source.isPlayer(), source, new Utils.IntegerHolder(0), power);
|
||||
if (!typeless) {
|
||||
this.scene.arena.applyTags(WeakenMoveTypeTag, move.type, power);
|
||||
this.scene.applyModifiers(AttackTypeBoosterModifier, source.isPlayer(), source, move.type, power);
|
||||
}
|
||||
if (source.getTag(HelpingHandTag)) {
|
||||
power.value *= 1.5;
|
||||
}
|
||||
let isCritical: boolean;
|
||||
const critOnly = new Utils.BooleanHolder(false);
|
||||
const critAlways = source.getTag(BattlerTagType.ALWAYS_CRIT);
|
||||
|
@ -1910,7 +1870,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
|
||||
if (!isTypeImmune) {
|
||||
damage.value = Math.ceil(
|
||||
((((2 * source.level / 5 + 2) * power.value * sourceAtk.value / targetDef.value) / 50) + 2)
|
||||
((((2 * source.level / 5 + 2) * power * sourceAtk.value / targetDef.value) / 50) + 2)
|
||||
* stabMultiplier.value * typeMultiplier.value * arenaAttackTypeMultiplier.value * screenMultiplier.value * twoStrikeMultiplier.value * ((this.scene.randBattleSeedInt(16) + 85) / 100) * criticalMultiplier.value);
|
||||
if (isPhysical && source.status && source.status.effect === StatusEffect.BURN) {
|
||||
if (!move.hasAttr(BypassBurnDamageReductionAttr)) {
|
||||
|
@ -1981,7 +1941,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
applyMoveAttrs(ModifiedDamageAttr, source, this, move, damage);
|
||||
applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, damage);
|
||||
|
||||
console.log("damage", damage.value, move.name, power.value, sourceAtk, targetDef);
|
||||
console.log("damage", damage.value, move.name, power, sourceAtk, targetDef);
|
||||
|
||||
// In case of fatal damage, this tag would have gotten cleared before we could lapse it.
|
||||
const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND);
|
||||
|
|
|
@ -7,15 +7,13 @@ import { MoveEffectPhase } from "#app/phases";
|
|||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import Move, { allMoves } from "#app/data/move.js";
|
||||
import Pokemon from "#app/field/pokemon.js";
|
||||
import { FieldMoveTypePowerBoostAbAttr } from "#app/data/ability.js";
|
||||
import { NumberHolder } from "#app/utils.js";
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
|
||||
describe("Abilities - Aura Break", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
const multiplier = 9 / 16;
|
||||
|
||||
const auraBreakMultiplier = 9/16 * 4/3;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
|
@ -33,63 +31,34 @@ describe("Abilities - Aura Break", () => {
|
|||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.MOONBLAST, Moves.DARK_PULSE, Moves.MOONBLAST, Moves.DARK_PULSE]);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.AURA_BREAK);
|
||||
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE);
|
||||
});
|
||||
|
||||
it("reverses the effect of fairy aura", async () => {
|
||||
const moveToCheck = allMoves[Moves.MOONBLAST];
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.FAIRY_AURA);
|
||||
const basePower = allMoves[Moves.MOONBLAST].power;
|
||||
await game.startBattle([Species.MAGIKARP]);
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
|
||||
await game.startBattle([Species.PIKACHU]);
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.MOONBLAST));
|
||||
|
||||
const appliedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[Moves.MOONBLAST]);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
expect(appliedPower).not.toBe(undefined);
|
||||
expect(appliedPower).not.toBe(basePower);
|
||||
expect(appliedPower).toBe(basePower * multiplier);
|
||||
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(expect.closeTo(basePower * auraBreakMultiplier));
|
||||
});
|
||||
|
||||
it("reverses the effect of dark aura", async () => {
|
||||
const moveToCheck = allMoves[Moves.DARK_PULSE];
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.DARK_AURA);
|
||||
const basePower = allMoves[Moves.DARK_PULSE].power;
|
||||
await game.startBattle([Species.MAGIKARP]);
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
|
||||
await game.startBattle([Species.PIKACHU]);
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.DARK_PULSE));
|
||||
|
||||
const appliedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[Moves.DARK_PULSE]);
|
||||
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
expect(appliedPower).not.toBe(undefined);
|
||||
expect(appliedPower).not.toBe(basePower);
|
||||
expect(appliedPower).toBe(basePower * multiplier);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(expect.closeTo(basePower * auraBreakMultiplier));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Calculates the mocked power of a move in a Pokémon battle, taking into account certain abilities.
|
||||
*
|
||||
* @param defender - The defending Pokémon.
|
||||
* @param attacker - The attacking Pokémon.
|
||||
* @param move - The move being used in the attack.
|
||||
* @returns The calculated power of the move after applying any relevant ability effects.
|
||||
*
|
||||
* @remarks
|
||||
* This function creates a NumberHolder with the initial power of the move.
|
||||
* It then checks if the defender has an ability with the FieldMoveTypePowerBoostAbAttr.
|
||||
* If so, it applies a power modification of 9/16 using an instance of FieldMoveTypePowerBoostAbAttr.
|
||||
* The final calculated power is then returned.
|
||||
*/
|
||||
const getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move): number => {
|
||||
const powerHolder = new NumberHolder(move.power);
|
||||
|
||||
if (defender.hasAbilityWithAttr(FieldMoveTypePowerBoostAbAttr)) {
|
||||
const auraBreakInstance = new FieldMoveTypePowerBoostAbAttr(move.type, 9 / 16);
|
||||
auraBreakInstance.applyPreAttack(attacker, false, defender, move, [powerHolder]);
|
||||
}
|
||||
|
||||
return powerHolder.value;
|
||||
};
|
||||
|
|
|
@ -5,15 +5,16 @@ import overrides from "#app/overrides";
|
|||
import { Species } from "#enums/species";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import Move, { allMoves, MoveCategory } from "#app/data/move.js";
|
||||
import { AllyMoveCategoryPowerBoostAbAttr } from "#app/data/ability.js";
|
||||
import { NumberHolder } from "#app/utils.js";
|
||||
import Pokemon from "#app/field/pokemon.js";
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
import { Abilities } from "#app/enums/abilities.js";
|
||||
import { MoveEffectPhase, TurnEndPhase } from "#app/phases.js";
|
||||
|
||||
describe("Abilities - Battery", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
const batteryMultiplier = 1.3;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
|
@ -27,94 +28,54 @@ describe("Abilities - Battery", () => {
|
|||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.ROCK_SLIDE, Moves.SPLASH, Moves.HEAT_WAVE]);
|
||||
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.BREAKING_SWIPE, Moves.SPLASH, Moves.DAZZLING_GLEAM]);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
});
|
||||
|
||||
it("raises the power of allies' special moves by 30%", async () => {
|
||||
const moveToBeUsed = Moves.HEAT_WAVE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
const moveToCheck = allMoves[Moves.DAZZLING_GLEAM];
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
await game.startBattle([Species.MAGIKARP, Species.CHARJABUG]);
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
await game.startBattle([Species.PIKACHU, Species.CHARJABUG]);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.DAZZLING_GLEAM));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]);
|
||||
const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
expect(mockedPower).not.toBe(undefined);
|
||||
expect(mockedPower).not.toBe(basePower);
|
||||
expect(mockedPower).toBe(basePower * multiplier);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower * batteryMultiplier);
|
||||
});
|
||||
|
||||
it("does not raise the power of allies' non-special moves", async () => {
|
||||
const moveToBeUsed = Moves.ROCK_SLIDE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
const moveToCheck = allMoves[Moves.BREAKING_SWIPE];
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
await game.startBattle([Species.MAGIKARP, Species.CHARJABUG]);
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
await game.startBattle([Species.PIKACHU, Species.CHARJABUG]);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BREAKING_SWIPE));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]);
|
||||
const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
expect(mockedPower).not.toBe(undefined);
|
||||
expect(mockedPower).toBe(basePower);
|
||||
expect(mockedPower).not.toBe(basePower * multiplier);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower);
|
||||
});
|
||||
|
||||
it("does not raise the power of the ability owner's special moves", async () => {
|
||||
const moveToBeUsed = Moves.HEAT_WAVE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
const moveToCheck = allMoves[Moves.DAZZLING_GLEAM];
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
await game.startBattle([Species.CHARJABUG, Species.MAGIKARP]);
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
await game.startBattle([Species.CHARJABUG, Species.PIKACHU]);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.DAZZLING_GLEAM));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[0]);
|
||||
const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
expect(mockedPower).not.toBe(undefined);
|
||||
expect(mockedPower).toBe(basePower);
|
||||
expect(mockedPower).not.toBe(basePower * multiplier);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Calculates the mocked power of a move.
|
||||
* Note this does not consider other damage calculations
|
||||
* except the power multiplier from Battery.
|
||||
*
|
||||
* @param defender - The defending Pokémon.
|
||||
* @param attacker - The attacking Pokémon.
|
||||
* @param move - The move being used by the attacker.
|
||||
* @returns The adjusted power of the move.
|
||||
*/
|
||||
const getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => {
|
||||
const powerHolder = new NumberHolder(move.power);
|
||||
|
||||
/**
|
||||
* @see AllyMoveCategoryPowerBoostAbAttr
|
||||
*/
|
||||
if (attacker.getAlly().hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) {
|
||||
const batteryInstance = new AllyMoveCategoryPowerBoostAbAttr([MoveCategory.SPECIAL], 1.3);
|
||||
batteryInstance.applyPreAttack(attacker, false, defender, move, [ powerHolder ]);
|
||||
}
|
||||
|
||||
return powerHolder.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the power multiplier from a Pokémon's ability attribute.
|
||||
*
|
||||
* @param pokemon - The Pokémon whose ability attributes are being queried.
|
||||
* @returns The power multiplier of the `AllyMoveCategoryPowerBoostAbAttr` attribute.
|
||||
*/
|
||||
const getAttrPowerMultiplier = (pokemon: Pokemon) => {
|
||||
const attr = pokemon.getAbilityAttrs(AllyMoveCategoryPowerBoostAbAttr);
|
||||
|
||||
return (attr[0] as AllyMoveCategoryPowerBoostAbAttr)["powerMultiplier"];
|
||||
};
|
||||
|
|
|
@ -5,15 +5,16 @@ import overrides from "#app/overrides";
|
|||
import { Species } from "#enums/species";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import Move, { allMoves, MoveCategory } from "#app/data/move.js";
|
||||
import { AllyMoveCategoryPowerBoostAbAttr } from "#app/data/ability.js";
|
||||
import { NumberHolder } from "#app/utils.js";
|
||||
import Pokemon from "#app/field/pokemon.js";
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
import { MoveEffectPhase, TurnEndPhase } from "#app/phases.js";
|
||||
import { Abilities } from "#app/enums/abilities.js";
|
||||
|
||||
describe("Abilities - Power Spot", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
const powerSpotMultiplier = 1.3;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
|
@ -27,94 +28,51 @@ describe("Abilities - Power Spot", () => {
|
|||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.ROCK_SLIDE, Moves.SPLASH, Moves.HEAT_WAVE]);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.TACKLE, Moves.BREAKING_SWIPE, Moves.SPLASH, Moves.DAZZLING_GLEAM]);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||
});
|
||||
|
||||
it("raises the power of allies' special moves by 30%", async () => {
|
||||
const moveToBeUsed = Moves.HEAT_WAVE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
const moveToCheck = allMoves[Moves.DAZZLING_GLEAM];
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
await game.startBattle([Species.MAGIKARP, Species.STONJOURNER]);
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
await game.startBattle([Species.PIKACHU, Species.STONJOURNER]);
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.DAZZLING_GLEAM));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]);
|
||||
const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
expect(mockedPower).not.toBe(undefined);
|
||||
expect(mockedPower).not.toBe(basePower);
|
||||
expect(mockedPower).toBe(basePower * multiplier);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower * powerSpotMultiplier);
|
||||
});
|
||||
|
||||
it("raises the power of allies' physical moves by 30%", async () => {
|
||||
const moveToBeUsed = Moves.ROCK_SLIDE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
const moveToCheck = allMoves[Moves.BREAKING_SWIPE];
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
await game.startBattle([Species.MAGIKARP, Species.STONJOURNER]);
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
await game.startBattle([Species.PIKACHU, Species.STONJOURNER]);
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BREAKING_SWIPE));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[1]);
|
||||
const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
expect(mockedPower).not.toBe(undefined);
|
||||
expect(mockedPower).not.toBe(basePower);
|
||||
expect(mockedPower).toBe(basePower * multiplier);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower * powerSpotMultiplier);
|
||||
});
|
||||
|
||||
it("does not raise the power of the ability owner's moves", async () => {
|
||||
const moveToBeUsed = Moves.HEAT_WAVE;
|
||||
const basePower = allMoves[moveToBeUsed].power;
|
||||
const moveToCheck = allMoves[Moves.BREAKING_SWIPE];
|
||||
const basePower = moveToCheck.power;
|
||||
|
||||
await game.startBattle([Species.STONJOURNER, Species.MAGIKARP]);
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToBeUsed));
|
||||
await game.startBattle([Species.STONJOURNER, Species.PIKACHU]);
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.BREAKING_SWIPE));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(TurnEndPhase);
|
||||
|
||||
const multiplier = getAttrPowerMultiplier(game.scene.getPlayerField()[0]);
|
||||
const mockedPower = getMockedMovePower(game.scene.getEnemyField()[0], game.scene.getPlayerField()[0], allMoves[moveToBeUsed]);
|
||||
|
||||
expect(mockedPower).not.toBe(undefined);
|
||||
expect(mockedPower).toBe(basePower);
|
||||
expect(mockedPower).not.toBe(basePower * multiplier);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(basePower);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Calculates the mocked power of a move.
|
||||
* Note this does not consider other damage calculations
|
||||
* except the power multiplier from Power Spot.
|
||||
*
|
||||
* @param defender - The defending Pokémon.
|
||||
* @param attacker - The attacking Pokémon.
|
||||
* @param move - The move being used by the attacker.
|
||||
* @returns The adjusted power of the move.
|
||||
*/
|
||||
const getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => {
|
||||
const powerHolder = new NumberHolder(move.power);
|
||||
|
||||
/**
|
||||
* @see AllyMoveCategoryPowerBoostAbAttr
|
||||
*/
|
||||
if (attacker.getAlly().hasAbilityWithAttr(AllyMoveCategoryPowerBoostAbAttr)) {
|
||||
const powerSpotInstance = new AllyMoveCategoryPowerBoostAbAttr([MoveCategory.SPECIAL, MoveCategory.PHYSICAL], 1.3);
|
||||
powerSpotInstance.applyPreAttack(attacker, false, defender, move, [ powerHolder ]);
|
||||
}
|
||||
|
||||
return powerHolder.value;
|
||||
};
|
||||
|
||||
/**
|
||||
* Retrieves the power multiplier from a Pokémon's ability attribute.
|
||||
*
|
||||
* @param pokemon - The Pokémon whose ability attributes are being queried.
|
||||
* @returns The power multiplier of the `AllyMoveCategoryPowerBoostAbAttr` attribute.
|
||||
*/
|
||||
const getAttrPowerMultiplier = (pokemon: Pokemon) => {
|
||||
const attr = pokemon.getAbilityAttrs(AllyMoveCategoryPowerBoostAbAttr);
|
||||
|
||||
return (attr[0] as AllyMoveCategoryPowerBoostAbAttr)["powerMultiplier"];
|
||||
};
|
||||
|
|
|
@ -5,17 +5,17 @@ import overrides from "#app/overrides";
|
|||
import { Species } from "#enums/species";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import Pokemon, { PlayerPokemon } from "#app/field/pokemon.js";
|
||||
import Move, { allMoves } from "#app/data/move.js";
|
||||
import { NumberHolder } from "#app/utils.js";
|
||||
import { allAbilities, applyPreAttackAbAttrs, UserFieldMoveTypePowerBoostAbAttr } from "#app/data/ability.js";
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
import { allAbilities } from "#app/data/ability.js";
|
||||
import { Abilities } from "#app/enums/abilities.js";
|
||||
import { MoveEffectPhase, SelectTargetPhase } from "#app/phases.js";
|
||||
|
||||
describe("Abilities - Steely Spirit", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
const steelySpiritMultiplier = 1.5;
|
||||
const moveToCheck = Moves.IRON_HEAD;
|
||||
const ironHeadPower = allMoves[moveToCheck].power;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
|
@ -30,29 +30,34 @@ describe("Abilities - Steely Spirit", () => {
|
|||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
vi.spyOn(overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MAGIKARP);
|
||||
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SHUCKLE);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.IRON_HEAD, Moves.SPLASH]);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
vi.spyOn(allMoves[moveToCheck], "calculateBattlePower");
|
||||
});
|
||||
|
||||
it("increases Steel-type moves used by the user and its allies", async () => {
|
||||
await game.startBattle([Species.MAGIKARP, Species.PERRSERKER]);
|
||||
const perserrker = game.scene.getPlayerField()[1];
|
||||
it("increases Steel-type moves' power used by the user and its allies by 50%", async () => {
|
||||
await game.startBattle([Species.PIKACHU, Species.SHUCKLE]);
|
||||
const boostSource = game.scene.getPlayerField()[1];
|
||||
const enemyToCheck = game.scene.getEnemyPokemon();
|
||||
|
||||
vi.spyOn(perserrker, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]);
|
||||
vi.spyOn(boostSource, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]);
|
||||
|
||||
expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true);
|
||||
expect(boostSource.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToCheck));
|
||||
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||
game.doSelectTarget(enemyToCheck.getBattlerIndex());
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), perserrker, allMoves[moveToCheck]);
|
||||
|
||||
expect(mockedMovePower).toBe(allMoves[moveToCheck].power * steelySpiritMultiplier);
|
||||
expect(allMoves[moveToCheck].calculateBattlePower).toHaveReturnedWith(ironHeadPower * steelySpiritMultiplier);
|
||||
});
|
||||
|
||||
it("stacks if multiple users with this ability are on the field.", async () => {
|
||||
await game.startBattle([Species.PERRSERKER, Species.PERRSERKER]);
|
||||
await game.startBattle([Species.PIKACHU, Species.PIKACHU]);
|
||||
const enemyToCheck = game.scene.getEnemyPokemon();
|
||||
|
||||
game.scene.getPlayerField().forEach(p => {
|
||||
vi.spyOn(p, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]);
|
||||
|
@ -61,55 +66,35 @@ describe("Abilities - Steely Spirit", () => {
|
|||
expect(game.scene.getPlayerField().every(p => p.hasAbility(Abilities.STEELY_SPIRIT))).toBe(true);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToCheck));
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||
game.doSelectTarget(enemyToCheck.getBattlerIndex());
|
||||
game.doAttack(getMovePosition(game.scene, 1, moveToCheck));
|
||||
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||
game.doSelectTarget(enemyToCheck.getBattlerIndex());
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), game.scene.getPlayerPokemon(), allMoves[moveToCheck]);
|
||||
|
||||
expect(mockedMovePower).toBe(allMoves[moveToCheck].power * Math.pow(steelySpiritMultiplier, 2));
|
||||
expect(allMoves[moveToCheck].calculateBattlePower).toHaveReturnedWith(ironHeadPower * Math.pow(steelySpiritMultiplier, 2));
|
||||
});
|
||||
|
||||
it("does not take effect when suppressed", async () => {
|
||||
await game.startBattle([Species.MAGIKARP, Species.PERRSERKER]);
|
||||
const perserrker = game.scene.getPlayerField()[1];
|
||||
await game.startBattle([Species.PIKACHU, Species.SHUCKLE]);
|
||||
const boostSource = game.scene.getPlayerField()[1];
|
||||
const enemyToCheck = game.scene.getEnemyPokemon();
|
||||
|
||||
vi.spyOn(perserrker, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]);
|
||||
expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true);
|
||||
vi.spyOn(boostSource, "getAbility").mockReturnValue(allAbilities[Abilities.STEELY_SPIRIT]);
|
||||
expect(boostSource.hasAbility(Abilities.STEELY_SPIRIT)).toBe(true);
|
||||
|
||||
perserrker.summonData.abilitySuppressed = true;
|
||||
boostSource.summonData.abilitySuppressed = true;
|
||||
|
||||
expect(perserrker.hasAbility(Abilities.STEELY_SPIRIT)).toBe(false);
|
||||
expect(perserrker.summonData.abilitySuppressed).toBe(true);
|
||||
expect(boostSource.hasAbility(Abilities.STEELY_SPIRIT)).toBe(false);
|
||||
expect(boostSource.summonData.abilitySuppressed).toBe(true);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToCheck));
|
||||
await game.phaseInterceptor.to(SelectTargetPhase, false);
|
||||
game.doSelectTarget(enemyToCheck.getBattlerIndex());
|
||||
game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
const mockedMovePower = getMockedMovePower(game.scene.getEnemyPokemon(), perserrker, allMoves[moveToCheck]);
|
||||
|
||||
expect(mockedMovePower).toBe(allMoves[moveToCheck].power);
|
||||
expect(allMoves[moveToCheck].calculateBattlePower).toHaveReturnedWith(ironHeadPower);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Calculates the mocked power of a move.
|
||||
* Note this does not consider other damage calculations
|
||||
* except the power multiplier from Steely Spirit.
|
||||
*
|
||||
* @param defender - The defending Pokémon.
|
||||
* @param attacker - The attacking Pokémon.
|
||||
* @param move - The move being used by the attacker.
|
||||
* @returns The adjusted power of the move.
|
||||
*/
|
||||
const getMockedMovePower = (defender: Pokemon, attacker: Pokemon, move: Move) => {
|
||||
const powerHolder = new NumberHolder(move.power);
|
||||
|
||||
/**
|
||||
* Check if pokemon has the specified ability and is in effect.
|
||||
* See Pokemon.hasAbility {@linkcode Pokemon.hasAbility}
|
||||
*/
|
||||
if (attacker.hasAbility(Abilities.STEELY_SPIRIT)) {
|
||||
const alliedField: Pokemon[] = attacker instanceof PlayerPokemon ? attacker.scene.getPlayerField() : attacker.scene.getEnemyField();
|
||||
alliedField.forEach(p => applyPreAttackAbAttrs(UserFieldMoveTypePowerBoostAbAttr, p, this, move, powerHolder));
|
||||
}
|
||||
|
||||
return powerHolder.value;
|
||||
};
|
||||
|
|
|
@ -4,39 +4,19 @@ import GameManager from "#app/test/utils/gameManager";
|
|||
import overrides from "#app/overrides";
|
||||
import { Species } from "#enums/species";
|
||||
import {
|
||||
MoveEffectPhase,
|
||||
MoveEffectPhase
|
||||
} from "#app/phases";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { getMovePosition } from "#app/test/utils/gameManagerUtils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { applyMoveAttrs, VariablePowerAttr } from "#app/data/move";
|
||||
import * as Utils from "#app/utils";
|
||||
import { Stat } from "#enums/stat";
|
||||
|
||||
/**
|
||||
* Checks the base power of the {@linkcode intendedMove} before and after any
|
||||
* {@linkcode VariablePowerAttr}s have been applied.
|
||||
* @param phase current {@linkcode MoveEffectPhase}
|
||||
* @param intendedMove Expected move during this {@linkcode phase}
|
||||
* @param before Expected base power before any base power changes
|
||||
* @param after Expected base power after any base power changes
|
||||
*/
|
||||
const checkBasePowerChanges = (phase: MoveEffectPhase, intendedMove: Moves, before: number, after: number) => {
|
||||
// Double check if the intended move was used and verify its initial base power
|
||||
const move = phase.move.getMove();
|
||||
expect(move.id).toBe(intendedMove);
|
||||
expect(move.power).toBe(before);
|
||||
|
||||
/** Mocking application of {@linkcode VariablePowerAttr} */
|
||||
const power = new Utils.IntegerHolder(move.power);
|
||||
applyMoveAttrs(VariablePowerAttr, phase.getUserPokemon(), phase.getTarget(), move, power);
|
||||
expect(power.value).toBe(after);
|
||||
};
|
||||
import { allMoves } from "#app/data/move.js";
|
||||
|
||||
describe("Moves - Hard Press", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
const moveToCheck = allMoves[Moves.HARD_PRESS];
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
|
@ -50,98 +30,59 @@ describe("Moves - Hard Press", () => {
|
|||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
|
||||
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.SNORLAX);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.NONE);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.HARD_PRESS ]);
|
||||
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.MUNCHLAX);
|
||||
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH);
|
||||
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]);
|
||||
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.HARD_PRESS]);
|
||||
vi.spyOn(moveToCheck, "calculateBattlePower");
|
||||
});
|
||||
|
||||
it("HARD_PRESS varies based on target health ratio (100%)", async () => {
|
||||
await game.startBattle([ Species.GRAVELER ]);
|
||||
const moveToUse = Moves.HARD_PRESS;
|
||||
it("should return 100 power if target HP ratio is at 100%", async () => {
|
||||
await game.startBattle([Species.PIKACHU]);
|
||||
|
||||
// Force party to go first
|
||||
game.scene.getParty()[0].stats[Stat.SPD] = 100;
|
||||
game.scene.getEnemyParty()[0].stats[Stat.SPD] = 1;
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.HARD_PRESS));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, 100);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(100);
|
||||
});
|
||||
|
||||
it("HARD_PRESS varies based on target health ratio (50%)", async () => {
|
||||
await game.startBattle([ Species.GRAVELER ]);
|
||||
const moveToUse = Moves.HARD_PRESS;
|
||||
const enemy = game.scene.getEnemyParty()[0];
|
||||
it("should return 50 power if target HP ratio is at 50%", async () => {
|
||||
await game.startBattle([Species.PIKACHU]);
|
||||
const targetHpRatio = .5;
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
|
||||
// Force party to go first
|
||||
game.scene.getParty()[0].stats[Stat.SPD] = 100;
|
||||
enemy.stats[Stat.SPD] = 1;
|
||||
vi.spyOn(enemy, "getHpRatio").mockReturnValue(targetHpRatio);
|
||||
|
||||
// Halve the enemy's HP
|
||||
enemy.hp /= 2;
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.HARD_PRESS));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, 50);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(50);
|
||||
});
|
||||
|
||||
it("HARD_PRESS varies based on target health ratio (1%)", async () => {
|
||||
await game.startBattle([ Species.GRAVELER ]);
|
||||
const moveToUse = Moves.HARD_PRESS;
|
||||
const enemy = game.scene.getEnemyParty()[0];
|
||||
it("should return 1 power if target HP ratio is at 1%", async () => {
|
||||
await game.startBattle([Species.PIKACHU]);
|
||||
const targetHpRatio = .01;
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
|
||||
// Force party to go first
|
||||
game.scene.getParty()[0].stats[Stat.SPD] = 100;
|
||||
enemy.stats[Stat.SPD] = 1;
|
||||
vi.spyOn(enemy, "getHpRatio").mockReturnValue(targetHpRatio);
|
||||
|
||||
// Force enemy to have 1% of full health
|
||||
enemy.stats[Stat.HP] = 100;
|
||||
enemy.hp = 1;
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.HARD_PRESS));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, 1);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(1);
|
||||
});
|
||||
|
||||
it("HARD_PRESS varies based on target health ratio, (<1%)", async () => {
|
||||
await game.startBattle([ Species.GRAVELER ]);
|
||||
const moveToUse = Moves.HARD_PRESS;
|
||||
const enemy = game.scene.getEnemyParty()[0];
|
||||
it("should return 1 power if target HP ratio is less than 1%", async () => {
|
||||
await game.startBattle([Species.PIKACHU]);
|
||||
const targetHpRatio = .005;
|
||||
const enemy = game.scene.getEnemyPokemon();
|
||||
|
||||
// Force party to go first
|
||||
game.scene.getParty()[0].stats[Stat.SPD] = 100;
|
||||
enemy.stats[Stat.SPD] = 1;
|
||||
vi.spyOn(enemy, "getHpRatio").mockReturnValue(targetHpRatio);
|
||||
|
||||
// Force enemy to have less than 1% of full health
|
||||
enemy.stats[Stat.HP] = 1000;
|
||||
enemy.hp = 1;
|
||||
game.doAttack(getMovePosition(game.scene, 0, Moves.HARD_PRESS));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, 1);
|
||||
});
|
||||
|
||||
it("HARD_PRESS varies based on target health ratio, random", async () => {
|
||||
await game.startBattle([ Species.GRAVELER ]);
|
||||
const moveToUse = Moves.HARD_PRESS;
|
||||
const enemy = game.scene.getEnemyParty()[0];
|
||||
|
||||
// Force party to go first
|
||||
game.scene.getParty()[0].stats[Stat.SPD] = 100;
|
||||
enemy.stats[Stat.SPD] = 1;
|
||||
|
||||
// Force a random n / 100 ratio with the enemy's HP
|
||||
enemy.stats[Stat.HP] = 100;
|
||||
enemy.hp = Utils.randInt(99, 1);
|
||||
|
||||
game.doAttack(getMovePosition(game.scene, 0, moveToUse));
|
||||
await game.phaseInterceptor.to(MoveEffectPhase, false);
|
||||
|
||||
// Because the ratio is n / 100 and the max base power of the move is 100, the resultant base power should just be n
|
||||
checkBasePowerChanges(game.scene.getCurrentPhase() as MoveEffectPhase, moveToUse, -1, enemy.hp);
|
||||
expect(moveToCheck.calculateBattlePower).toHaveReturnedWith(1);
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue