Merge branch 'beta' into refactor/api-requests
This commit is contained in:
commit
29807381b9
|
@ -330,6 +330,30 @@ export class ReceivedMoveDamageMultiplierAbAttr extends PreDefendAbAttr {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduces the damage dealt to an allied Pokemon. Used by Friend Guard.
|
||||||
|
* @see {@linkcode applyPreDefend}
|
||||||
|
*/
|
||||||
|
export class AlliedFieldDamageReductionAbAttr extends PreDefendAbAttr {
|
||||||
|
private damageMultiplier: number;
|
||||||
|
|
||||||
|
constructor(damageMultiplier: number) {
|
||||||
|
super();
|
||||||
|
this.damageMultiplier = damageMultiplier;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the damage reduction
|
||||||
|
* @param args
|
||||||
|
* - `[0]` {@linkcode Utils.NumberHolder} - The damage being dealt
|
||||||
|
*/
|
||||||
|
override applyPreDefend(_pokemon: Pokemon, _passive: boolean, _simulated: boolean, _attacker: Pokemon, _move: Move, _cancelled: Utils.BooleanHolder, args: any[]): boolean {
|
||||||
|
const damage = args[0] as Utils.NumberHolder;
|
||||||
|
damage.value = Utils.toDmgValue(damage.value * this.damageMultiplier);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ReceivedTypeDamageMultiplierAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
|
export class ReceivedTypeDamageMultiplierAbAttr extends ReceivedMoveDamageMultiplierAbAttr {
|
||||||
constructor(moveType: Type, damageMultiplier: number) {
|
constructor(moveType: Type, damageMultiplier: number) {
|
||||||
super((target, user, move) => user.getMoveType(move) === moveType, damageMultiplier);
|
super((target, user, move) => user.getMoveType(move) === moveType, damageMultiplier);
|
||||||
|
@ -5310,8 +5334,8 @@ export function initAbilities() {
|
||||||
new Ability(Abilities.HEALER, 5)
|
new Ability(Abilities.HEALER, 5)
|
||||||
.conditionalAttr(pokemon => pokemon.getAlly() && Utils.randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true),
|
.conditionalAttr(pokemon => pokemon.getAlly() && Utils.randSeedInt(10) < 3, PostTurnResetStatusAbAttr, true),
|
||||||
new Ability(Abilities.FRIEND_GUARD, 5)
|
new Ability(Abilities.FRIEND_GUARD, 5)
|
||||||
.ignorable()
|
.attr(AlliedFieldDamageReductionAbAttr, 0.75)
|
||||||
.unimplemented(),
|
.ignorable(),
|
||||||
new Ability(Abilities.WEAK_ARMOR, 5)
|
new Ability(Abilities.WEAK_ARMOR, 5)
|
||||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1)
|
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.DEF, -1)
|
||||||
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2),
|
.attr(PostDefendStatStageChangeAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL, Stat.SPD, 2),
|
||||||
|
|
|
@ -22,7 +22,7 @@ import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "#app/data/balance/
|
||||||
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
|
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, SubstituteTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, MoveRestrictionBattlerTag, ExposedTag, DragonCheerTag, CritBoostTag, TrappedTag, TarShotTag, AutotomizedTag, PowerTrickTag } from "../data/battler-tags";
|
||||||
import { WeatherType } from "#app/data/weather";
|
import { WeatherType } from "#app/data/weather";
|
||||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
|
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag";
|
||||||
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr } from "#app/data/ability";
|
import { Ability, AbAttr, StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr } from "#app/data/ability";
|
||||||
import PokemonData from "#app/system/pokemon-data";
|
import PokemonData from "#app/system/pokemon-data";
|
||||||
import { BattlerIndex } from "#app/battle";
|
import { BattlerIndex } from "#app/battle";
|
||||||
import { Mode } from "#app/ui/ui";
|
import { Mode } from "#app/ui/ui";
|
||||||
|
@ -2667,9 +2667,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||||
this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage);
|
this.scene.applyModifiers(EnemyDamageReducerModifier, false, damage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */
|
/** Apply this Pokemon's post-calc defensive modifiers (e.g. Fur Coat) */
|
||||||
if (!ignoreAbility) {
|
if (!ignoreAbility) {
|
||||||
applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damage);
|
applyPreDefendAbAttrs(ReceivedMoveDamageMultiplierAbAttr, this, source, move, cancelled, simulated, damage);
|
||||||
|
|
||||||
|
/** Additionally apply friend guard damage reduction if ally has it. */
|
||||||
|
if (this.scene.currentBattle.double && this.getAlly()?.isActive(true)) {
|
||||||
|
applyPreDefendAbAttrs(AlliedFieldDamageReductionAbAttr, this.getAlly(), source, move, cancelled, simulated, damage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// This attribute may modify damage arbitrarily, so be careful about changing its order of application.
|
// This attribute may modify damage arbitrarily, so be careful about changing its order of application.
|
||||||
|
|
|
@ -502,45 +502,25 @@ export class BerryModifierType extends PokemonHeldItemModifierType implements Ge
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getAttackTypeBoosterItemName(type: Type) {
|
enum AttackTypeBoosterItem {
|
||||||
switch (type) {
|
SILK_SCARF,
|
||||||
case Type.NORMAL:
|
BLACK_BELT,
|
||||||
return "Silk Scarf";
|
SHARP_BEAK,
|
||||||
case Type.FIGHTING:
|
POISON_BARB,
|
||||||
return "Black Belt";
|
SOFT_SAND,
|
||||||
case Type.FLYING:
|
HARD_STONE,
|
||||||
return "Sharp Beak";
|
SILVER_POWDER,
|
||||||
case Type.POISON:
|
SPELL_TAG,
|
||||||
return "Poison Barb";
|
METAL_COAT,
|
||||||
case Type.GROUND:
|
CHARCOAL,
|
||||||
return "Soft Sand";
|
MYSTIC_WATER,
|
||||||
case Type.ROCK:
|
MIRACLE_SEED,
|
||||||
return "Hard Stone";
|
MAGNET,
|
||||||
case Type.BUG:
|
TWISTED_SPOON,
|
||||||
return "Silver Powder";
|
NEVER_MELT_ICE,
|
||||||
case Type.GHOST:
|
DRAGON_FANG,
|
||||||
return "Spell Tag";
|
BLACK_GLASSES,
|
||||||
case Type.STEEL:
|
FAIRY_FEATHER
|
||||||
return "Metal Coat";
|
|
||||||
case Type.FIRE:
|
|
||||||
return "Charcoal";
|
|
||||||
case Type.WATER:
|
|
||||||
return "Mystic Water";
|
|
||||||
case Type.GRASS:
|
|
||||||
return "Miracle Seed";
|
|
||||||
case Type.ELECTRIC:
|
|
||||||
return "Magnet";
|
|
||||||
case Type.PSYCHIC:
|
|
||||||
return "Twisted Spoon";
|
|
||||||
case Type.ICE:
|
|
||||||
return "Never-Melt Ice";
|
|
||||||
case Type.DRAGON:
|
|
||||||
return "Dragon Fang";
|
|
||||||
case Type.DARK:
|
|
||||||
return "Black Glasses";
|
|
||||||
case Type.FAIRY:
|
|
||||||
return "Fairy Feather";
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType {
|
export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType {
|
||||||
|
@ -548,7 +528,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i
|
||||||
public boostPercent: integer;
|
public boostPercent: integer;
|
||||||
|
|
||||||
constructor(moveType: Type, boostPercent: integer) {
|
constructor(moveType: Type, boostPercent: integer) {
|
||||||
super("", `${getAttackTypeBoosterItemName(moveType)?.replace(/[ \-]/g, "_").toLowerCase()}`,
|
super("", `${AttackTypeBoosterItem[moveType]?.toLowerCase()}`,
|
||||||
(_type, args) => new AttackTypeBoosterModifier(this, (args[0] as Pokemon).id, moveType, boostPercent));
|
(_type, args) => new AttackTypeBoosterModifier(this, (args[0] as Pokemon).id, moveType, boostPercent));
|
||||||
|
|
||||||
this.moveType = moveType;
|
this.moveType = moveType;
|
||||||
|
@ -556,7 +536,7 @@ export class AttackTypeBoosterModifierType extends PokemonHeldItemModifierType i
|
||||||
}
|
}
|
||||||
|
|
||||||
get name(): string {
|
get name(): string {
|
||||||
return i18next.t(`modifierType:AttackTypeBoosterItem.${getAttackTypeBoosterItemName(this.moveType)?.replace(/[ \-]/g, "_").toLowerCase()}`);
|
return i18next.t(`modifierType:AttackTypeBoosterItem.${AttackTypeBoosterItem[this.moveType]?.toLowerCase()}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
getDescription(scene: BattleScene): string {
|
getDescription(scene: BattleScene): string {
|
||||||
|
|
|
@ -0,0 +1,120 @@
|
||||||
|
import { Moves } from "#enums/moves";
|
||||||
|
import { Species } from "#enums/species";
|
||||||
|
import { Abilities } from "#enums/abilities";
|
||||||
|
import GameManager from "#test/utils/gameManager";
|
||||||
|
import Phaser from "phaser";
|
||||||
|
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { BattlerIndex } from "#app/battle";
|
||||||
|
import { allAbilities } from "#app/data/ability";
|
||||||
|
import { allMoves, MoveCategory } from "#app/data/move";
|
||||||
|
|
||||||
|
describe("Moves - Friend Guard", () => {
|
||||||
|
let phaserGame: Phaser.Game;
|
||||||
|
let game: GameManager;
|
||||||
|
|
||||||
|
beforeAll(() => {
|
||||||
|
phaserGame = new Phaser.Game({
|
||||||
|
type: Phaser.HEADLESS,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
game.phaseInterceptor.restoreOg();
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
game = new GameManager(phaserGame);
|
||||||
|
game.override
|
||||||
|
.battleType("double")
|
||||||
|
.enemyAbility(Abilities.BALL_FETCH)
|
||||||
|
.enemyMoveset([ Moves.TACKLE, Moves.SPLASH, Moves.DRAGON_RAGE ])
|
||||||
|
.enemySpecies(Species.SHUCKLE)
|
||||||
|
.moveset([ Moves.SPLASH ])
|
||||||
|
.startingLevel(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should reduce damage that other allied Pokémon receive from attacks (from any Pokémon) by 25%", async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
|
||||||
|
const [ player1, player2 ] = game.scene.getPlayerField();
|
||||||
|
const spy = vi.spyOn(player1, "getAttackDamage");
|
||||||
|
|
||||||
|
const enemy1 = game.scene.getEnemyField()[0];
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Get the last return value from `getAttackDamage`
|
||||||
|
const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
|
||||||
|
// Making sure the test is controlled; turn 1 damage is equal to base damage (after rounding)
|
||||||
|
expect(turn1Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL)));
|
||||||
|
|
||||||
|
vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
// Get the last return value from `getAttackDamage`
|
||||||
|
const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
|
||||||
|
// With the ally's Friend Guard, damage should have been reduced from base damage by 25%
|
||||||
|
expect(turn2Damage).toBe(Math.floor(player1.getBaseDamage(enemy1, allMoves[Moves.TACKLE], MoveCategory.PHYSICAL) * 0.75));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT reduce damage to pokemon with friend guard", async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
|
||||||
|
|
||||||
|
const player2 = game.scene.getPlayerField()[1];
|
||||||
|
const spy = vi.spyOn(player2, "getAttackDamage");
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
|
||||||
|
|
||||||
|
vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
|
||||||
|
expect(turn2Damage).toBe(turn1Damage);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should NOT reduce damage from fixed damage attacks", async () => {
|
||||||
|
await game.classicMode.startBattle([ Species.BULBASAUR, Species.CHARMANDER ]);
|
||||||
|
|
||||||
|
const [ player1, player2 ] = game.scene.getPlayerField();
|
||||||
|
const spy = vi.spyOn(player1, "getAttackDamage");
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
await game.forceEnemyMove(Moves.DRAGON_RAGE, BattlerIndex.PLAYER);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const turn1Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
|
||||||
|
expect(turn1Damage).toBe(40);
|
||||||
|
|
||||||
|
vi.spyOn(player2, "getAbility").mockReturnValue(allAbilities[Abilities.FRIEND_GUARD]);
|
||||||
|
|
||||||
|
game.move.select(Moves.SPLASH);
|
||||||
|
game.move.select(Moves.SPLASH, 1);
|
||||||
|
await game.forceEnemyMove(Moves.DRAGON_RAGE, BattlerIndex.PLAYER);
|
||||||
|
await game.forceEnemyMove(Moves.SPLASH);
|
||||||
|
await game.toNextTurn();
|
||||||
|
|
||||||
|
const turn2Damage = spy.mock.results[spy.mock.results.length - 1].value.damage;
|
||||||
|
expect(turn2Damage).toBe(40);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue