[Ability] Implement Tera Shell (#3856)
* Implement Tera Shell * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Update src/data/ability.ts Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com> * Add comments and fixed damage condition to `applyPreDefend` * Fix speed tie breaking things in tera shell test * Change deprecated `startBattle` calls --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Adrian T. <68144167+torranx@users.noreply.github.com>
This commit is contained in:
parent
a41133a55a
commit
7755f798fd
|
@ -8,7 +8,7 @@ import { Weather, WeatherType } from "./weather";
|
|||
import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
|
||||
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
|
||||
import { Gender } from "./gender";
|
||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr } from "./move";
|
||||
import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move";
|
||||
import { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
|
||||
import { Stat, getStatName } from "./pokemon-stat";
|
||||
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier";
|
||||
|
@ -475,6 +475,47 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attribute implementing the effects of {@link https://bulbapedia.bulbagarden.net/wiki/Tera_Shell_(Ability) | Tera Shell}
|
||||
* When the source is at full HP, incoming attacks will have a maximum 0.5x type effectiveness multiplier.
|
||||
* @extends PreDefendAbAttr
|
||||
*/
|
||||
export class FullHpResistTypeAbAttr extends PreDefendAbAttr {
|
||||
/**
|
||||
* Reduces a type multiplier to 0.5 if the source is at full HP.
|
||||
* @param pokemon {@linkcode Pokemon} the Pokemon with this ability
|
||||
* @param passive n/a
|
||||
* @param simulated n/a (this doesn't change game state)
|
||||
* @param attacker n/a
|
||||
* @param move {@linkcode Move} the move being used on the source
|
||||
* @param cancelled n/a
|
||||
* @param args `[0]` a container for the move's current type effectiveness multiplier
|
||||
* @returns `true` if the move's effectiveness is reduced; `false` otherwise
|
||||
*/
|
||||
applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move | null, cancelled: Utils.BooleanHolder | null, args: any[]): boolean | Promise<boolean> {
|
||||
const typeMultiplier = args[0];
|
||||
if (!(typeMultiplier && typeMultiplier instanceof Utils.NumberHolder)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (move && move.hasAttr(FixedDamageAttr)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pokemon.isFullHp() && typeMultiplier.value > 0.5) {
|
||||
typeMultiplier.value = 0.5;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
getTriggerMessage(pokemon: Pokemon, abilityName: string, ...args: any[]): string {
|
||||
return i18next.t("abilityTriggers:fullHpResistType", {
|
||||
pokemonNameWithAffix: getPokemonNameWithAffix(pokemon)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class PostDefendAbAttr extends AbAttr {
|
||||
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean | Promise<boolean> {
|
||||
return false;
|
||||
|
@ -5761,10 +5802,10 @@ export function initAbilities() {
|
|||
.attr(NoTransformAbilityAbAttr)
|
||||
.attr(NoFusionAbilityAbAttr),
|
||||
new Ability(Abilities.TERA_SHELL, 9)
|
||||
.attr(FullHpResistTypeAbAttr)
|
||||
.attr(UncopiableAbilityAbAttr)
|
||||
.attr(UnswappableAbilityAbAttr)
|
||||
.ignorable()
|
||||
.unimplemented(),
|
||||
.ignorable(),
|
||||
new Ability(Abilities.TERAFORM_ZERO, 9)
|
||||
.attr(UncopiableAbilityAbAttr)
|
||||
.attr(UnswappableAbilityAbAttr)
|
||||
|
|
|
@ -22,7 +22,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo
|
|||
import { WeatherType } from "../data/weather";
|
||||
import { TempBattleStat } from "../data/temp-battle-stat";
|
||||
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag";
|
||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, 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, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr } from "../data/ability";
|
||||
import { Ability, AbAttr, BattleStatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatChangesAbAttr, MoveImmunityAbAttr, 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, IgnoreOpponentEvasionAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr } from "../data/ability";
|
||||
import PokemonData from "../system/pokemon-data";
|
||||
import { BattlerIndex } from "../battle";
|
||||
import { Mode } from "../ui/ui";
|
||||
|
@ -1276,6 +1276,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
|||
}
|
||||
}
|
||||
|
||||
// Apply Tera Shell's effect to attacks after all immunities are accounted for
|
||||
if (!ignoreAbility && move.category !== MoveCategory.STATUS) {
|
||||
applyPreDefendAbAttrs(FullHpResistTypeAbAttr, this, source, move, cancelledHolder, simulated, typeMultiplier);
|
||||
}
|
||||
|
||||
return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier;
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"blockItemTheft": "{{pokemonNameWithAffix}}'s {{abilityName}}\nprevents item theft!",
|
||||
"typeImmunityHeal": "{{pokemonNameWithAffix}}'s {{abilityName}}\nrestored its HP a little!",
|
||||
"nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} avoided damage\nwith {{abilityName}}!",
|
||||
"fullHpResistType": "{{pokemonNameWithAffix}} made its shell gleam!\nIt's distorting type matchups!",
|
||||
"moveImmunity": "It doesn't affect {{pokemonNameWithAffix}}!",
|
||||
"reverseDrain": "{{pokemonNameWithAffix}} sucked up the liquid ooze!",
|
||||
"postDefendTypeChange": "{{pokemonNameWithAffix}}'s {{abilityName}}\nmade it the {{typeName}} type!",
|
||||
|
|
|
@ -0,0 +1,111 @@
|
|||
import { Abilities } from "#app/enums/abilities";
|
||||
import { Moves } from "#app/enums/moves";
|
||||
import { Species } from "#app/enums/species";
|
||||
import { HitResult } from "#app/field/pokemon.js";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
const TIMEOUT = 10 * 1000; // 10 second timeout
|
||||
|
||||
describe("Abilities - Tera Shell", () => {
|
||||
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("single")
|
||||
.ability(Abilities.TERA_SHELL)
|
||||
.moveset([Moves.SPLASH])
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyAbility(Abilities.INSOMNIA)
|
||||
.enemyMoveset(Array(4).fill(Moves.MACH_PUNCH))
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100);
|
||||
});
|
||||
|
||||
it(
|
||||
"should change the effectiveness of non-resisted attacks when the source is at full HP",
|
||||
async () => {
|
||||
await game.classicMode.startBattle([Species.SNORLAX]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0.5);
|
||||
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(2);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should not override type immunities",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.SHADOW_SNEAK));
|
||||
|
||||
await game.classicMode.startBattle([Species.SNORLAX]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should not override type multipliers less than 0.5x",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.QUICK_ATTACK));
|
||||
|
||||
await game.classicMode.startBattle([Species.AGGRON]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "getMoveEffectiveness");
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(playerPokemon.getMoveEffectiveness).toHaveLastReturnedWith(0.25);
|
||||
}, TIMEOUT
|
||||
);
|
||||
|
||||
it(
|
||||
"should not affect the effectiveness of fixed-damage moves",
|
||||
async () => {
|
||||
game.override.enemyMoveset(Array(4).fill(Moves.DRAGON_RAGE));
|
||||
|
||||
await game.classicMode.startBattle([Species.CHARIZARD]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
vi.spyOn(playerPokemon, "apply");
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
|
||||
await game.phaseInterceptor.to("BerryPhase", false);
|
||||
expect(playerPokemon.apply).toHaveLastReturnedWith(HitResult.EFFECTIVE);
|
||||
expect(playerPokemon.hp).toBe(playerPokemon.getMaxHp() - 40);
|
||||
}, TIMEOUT
|
||||
);
|
||||
});
|
Loading…
Reference in New Issue