[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:
innerthunder 2024-09-01 18:21:11 -07:00 committed by GitHub
parent a41133a55a
commit 7755f798fd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 162 additions and 4 deletions

View File

@ -8,7 +8,7 @@ import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags"; import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect";
import { Gender } from "./gender"; 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 { ArenaTagSide, ArenaTrapTag } from "./arena-tag";
import { Stat, getStatName } from "./pokemon-stat"; import { Stat, getStatName } from "./pokemon-stat";
import { BerryModifier, PokemonHeldItemModifier } from "../modifier/modifier"; 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 { export class PostDefendAbAttr extends AbAttr {
applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean | Promise<boolean> { applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult | null, args: any[]): boolean | Promise<boolean> {
return false; return false;
@ -5761,10 +5802,10 @@ export function initAbilities() {
.attr(NoTransformAbilityAbAttr) .attr(NoTransformAbilityAbAttr)
.attr(NoFusionAbilityAbAttr), .attr(NoFusionAbilityAbAttr),
new Ability(Abilities.TERA_SHELL, 9) new Ability(Abilities.TERA_SHELL, 9)
.attr(FullHpResistTypeAbAttr)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr) .attr(UnswappableAbilityAbAttr)
.ignorable() .ignorable(),
.unimplemented(),
new Ability(Abilities.TERAFORM_ZERO, 9) new Ability(Abilities.TERAFORM_ZERO, 9)
.attr(UncopiableAbilityAbAttr) .attr(UncopiableAbilityAbAttr)
.attr(UnswappableAbilityAbAttr) .attr(UnswappableAbilityAbAttr)

View File

@ -22,7 +22,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo
import { WeatherType } from "../data/weather"; import { WeatherType } from "../data/weather";
import { TempBattleStat } from "../data/temp-battle-stat"; import { TempBattleStat } from "../data/temp-battle-stat";
import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "../data/arena-tag"; 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 PokemonData from "../system/pokemon-data";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
import { Mode } from "../ui/ui"; 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; return (!cancelledHolder.value ? typeMultiplier.value : 0) as TypeDamageMultiplier;
} }

View File

@ -12,6 +12,7 @@
"blockItemTheft": "{{pokemonNameWithAffix}}'s {{abilityName}}\nprevents item theft!", "blockItemTheft": "{{pokemonNameWithAffix}}'s {{abilityName}}\nprevents item theft!",
"typeImmunityHeal": "{{pokemonNameWithAffix}}'s {{abilityName}}\nrestored its HP a little!", "typeImmunityHeal": "{{pokemonNameWithAffix}}'s {{abilityName}}\nrestored its HP a little!",
"nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} avoided damage\nwith {{abilityName}}!", "nonSuperEffectiveImmunity": "{{pokemonNameWithAffix}} avoided damage\nwith {{abilityName}}!",
"fullHpResistType": "{{pokemonNameWithAffix}} made its shell gleam!\nIt's distorting type matchups!",
"moveImmunity": "It doesn't affect {{pokemonNameWithAffix}}!", "moveImmunity": "It doesn't affect {{pokemonNameWithAffix}}!",
"reverseDrain": "{{pokemonNameWithAffix}} sucked up the liquid ooze!", "reverseDrain": "{{pokemonNameWithAffix}} sucked up the liquid ooze!",
"postDefendTypeChange": "{{pokemonNameWithAffix}}'s {{abilityName}}\nmade it the {{typeName}} type!", "postDefendTypeChange": "{{pokemonNameWithAffix}}'s {{abilityName}}\nmade it the {{typeName}} type!",

View File

@ -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
);
});