This commit is contained in:
schmidtc1 2025-01-29 20:44:27 -07:00 committed by GitHub
commit a3188905ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 170 additions and 10 deletions

View File

@ -665,7 +665,7 @@ export class ConfusedTag extends BattlerTag {
const def = pokemon.getEffectiveStat(Stat.DEF);
const damage = toDmgValue(((((2 * pokemon.level / 5 + 2) * 40 * atk / def) / 50) + 2) * (pokemon.randSeedIntRange(85, 100) / 100));
globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself"));
pokemon.damageAndUpdate(damage);
pokemon.damageAndUpdate(damage, HitResult.SELF);
pokemon.battleData.hitCount++;
(globalScene.getCurrentPhase() as MovePhase).cancel();
}

View File

@ -2946,7 +2946,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* We explicitly require to ignore the faint phase here, as we want to show the messages
* about the critical hit and the super effective/not very effective messages before the faint phase.
*/
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, isOneHitKo, true, source);
const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, result as DamageResult, isCritical, isOneHitKo, false, true, source);
if (damage > 0) {
if (source.isPlayer()) {
@ -2990,7 +2990,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (this.isFainted()) {
// set splice index here, so future scene queues happen before FaintedPhase
globalScene.setPhaseQueueSplice();
globalScene.unshiftPhase(new FaintPhase(this.getBattlerIndex(), isOneHitKo, destinyTag, grudgeTag, source));
globalScene.unshiftPhase(new FaintPhase(this.getBattlerIndex(), false, destinyTag, grudgeTag, source));
this.destroySubstitute();
this.lapseTag(BattlerTagType.COMMANDED);
@ -3015,7 +3015,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
const surviveDamage = new Utils.BooleanHolder(false);
if (!preventEndure && this.hp - damage <= 0) {
if (!preventEndure && this.hp - damage <= 0 && this.turnData?.damageSources?.at(0) !== HitResult.OTHER) {
if (this.hp >= 1 && this.getTag(BattlerTagType.ENDURING)) {
surviveDamage.value = this.lapseTag(BattlerTagType.ENDURING);
} else if (this.hp > 1 && this.getTag(BattlerTagType.STURDY)) {
@ -3042,7 +3042,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Once the MoveEffectPhase is over (and calls it's .end() function, shiftPhase() will reset the PhaseQueueSplice via clearPhaseQueueSplice() )
*/
globalScene.setPhaseQueueSplice();
globalScene.unshiftPhase(new FaintPhase(this.getBattlerIndex(), preventEndure));
globalScene.unshiftPhase(new FaintPhase(this.getBattlerIndex()));
this.destroySubstitute();
this.lapseTag(BattlerTagType.COMMANDED);
this.resetSummonData();
@ -3061,6 +3061,12 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @returns integer of damage done
*/
damageAndUpdate(damage: number, result?: DamageResult, critical: boolean = false, ignoreSegments: boolean = false, preventEndure: boolean = false, ignoreFaintPhase: boolean = false, source?: Pokemon): number {
// When damage is done from any source (Move or Indirect damage, e.g. weather), store latest occurrence in damageSources[0]
if (result !== undefined) {
this.turnData?.damageSources?.unshift(result);
} else {
this.turnData?.damageSources?.unshift(HitResult.OTHER);
}
const damagePhase = new DamageAnimPhase(this.getBattlerIndex(), damage, result as DamageResult, critical);
globalScene.unshiftPhase(damagePhase);
if (this.switchOutStatus && source) {
@ -5366,6 +5372,10 @@ export class PokemonTurnData {
* forced to act again in the same turn
*/
public extraTurns: number = 0;
/**
* Used to track damage sources from HitResult.OTHER
*/
public damageSources: DamageResult[] = [];
}
export enum AiType {
@ -5393,10 +5403,11 @@ export enum HitResult {
FAIL,
MISS,
OTHER,
IMMUNE
IMMUNE,
SELF
}
export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO | HitResult.OTHER;
export type DamageResult = HitResult.EFFECTIVE | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO | HitResult.SELF | HitResult.OTHER;
/** Interface containing the results of a damage calculation for a given move */
export interface DamageCalculationResult {

View File

@ -5,7 +5,7 @@ import { allMoves } from "#app/data/move";
import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball";
import { type FormChangeItem, SpeciesFormChangeItemTrigger, SpeciesFormChangeLapseTeraTrigger, SpeciesFormChangeTeraTrigger } from "#app/data/pokemon-forms";
import { getStatusEffectHealText } from "#app/data/status-effect";
import Pokemon, { type PlayerPokemon } from "#app/field/pokemon";
import Pokemon, { HitResult, type PlayerPokemon } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import Overrides from "#app/overrides";
import { EvolutionPhase } from "#app/phases/evolution-phase";
@ -1927,6 +1927,10 @@ export class PokemonInstantReviveModifier extends PokemonHeldItemModifier {
* @returns always `true`
*/
override apply(pokemon: Pokemon): boolean {
// Do not revive if damage is indirect
if (pokemon.turnData?.damageSources?.at(0) === HitResult.OTHER) {
return false;
}
// Restore the Pokemon to half HP
globalScene.unshiftPhase(new PokemonHealPhase(pokemon.getBattlerIndex(),
toDmgValue(pokemon.getMaxHp() / 2), i18next.t("modifier:pokemonInstantReviveApply", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), typeName: this.type.name }), false, false, true));

View File

@ -42,6 +42,7 @@ export class DamageAnimPhase extends PokemonPhase {
applyDamage() {
switch (this.damageResult) {
case HitResult.EFFECTIVE:
case HitResult.SELF:
globalScene.playSound("se/hit");
break;
case HitResult.SUPER_EFFECTIVE:

View File

@ -68,7 +68,7 @@ export class PokemonHealPhase extends CommonAnimPhase {
}
const healAmount = new Utils.NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value));
if (healAmount.value < 0) {
pokemon.damageAndUpdate(healAmount.value * -1, HitResult.HEAL as DamageResult);
pokemon.damageAndUpdate(healAmount.value * -1, HitResult.OTHER as DamageResult);
healAmount.value = 0;
}
// Prevent healing to full if specified (in case of healing tokens so Sturdy doesn't cause a softlock)

View File

@ -49,7 +49,7 @@ export class WeatherEffectPhase extends CommonAnimPhase {
const damage = Utils.toDmgValue(pokemon.getMaxHp() / 16);
globalScene.queueMessage(getWeatherDamageMessage(this.weather?.weatherType!, pokemon)!); // TODO: are those bangs correct?
pokemon.damageAndUpdate(damage, HitResult.EFFECTIVE, false, false, true);
pokemon.damageAndUpdate(damage, HitResult.OTHER, false, false, true);
};
this.executeForAll((pokemon: Pokemon) => {

View File

@ -0,0 +1,144 @@
import { allMoves } from "#app/data/move";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import type { PokemonInstantReviveModifier } from "#app/modifier/modifier";
import { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import GameManager from "#test/utils/gameManager";
import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
describe("Items - Reviver Seed", () => {
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
.moveset([ Moves.SPLASH, Moves.TACKLE, Moves.ENDURE ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.startingHeldItems([{ name: "REVIVER_SEED" }])
.enemyHeldItems([{ name: "REVIVER_SEED" }])
.enemyMoveset(Moves.SPLASH);
vi.spyOn(allMoves[Moves.SHEER_COLD], "accuracy", "get").mockReturnValue(100);
vi.spyOn(allMoves[Moves.LEECH_SEED], "accuracy", "get").mockReturnValue(100);
vi.spyOn(allMoves[Moves.WHIRLPOOL], "accuracy", "get").mockReturnValue(100);
vi.spyOn(allMoves[Moves.WILL_O_WISP], "accuracy", "get").mockReturnValue(100);
});
it.each([
{ moveType: "Special Move", move: Moves.WATER_GUN },
{ moveType: "Physical Move", move: Moves.TACKLE },
{ moveType: "Fixed Damage Move", move: Moves.SEISMIC_TOSS },
{ moveType: "Final Gambit", move: Moves.FINAL_GAMBIT },
{ moveType: "Counter", move: Moves.COUNTER },
{ moveType: "OHKO", move: Moves.SHEER_COLD }
])("should activate the holder's reviver seed from a $moveType", async ({ move }) => {
game.override
.enemyLevel(100)
.startingLevel(1)
.enemyMoveset(move);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.hp - 1);
const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier;
vi.spyOn(reviverSeed, "apply");
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("BerryPhase");
expect(reviverSeed.apply).toHaveReturnedWith(true); // Reviver Seed triggers
expect(player.isFainted()).toBeFalsy();
});
it("should activate the holder's reviver seed from confusion self-hit", async () => {
game.override
.enemyLevel(1)
.startingLevel(100)
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.hp - 1);
player.addTag(BattlerTagType.CONFUSED, 3);
const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier;
vi.spyOn(reviverSeed, "apply");
vi.spyOn(player, "randSeedInt").mockReturnValue(0); // Force confusion self-hit
game.move.select(Moves.TACKLE);
await game.phaseInterceptor.to("BerryPhase");
expect(reviverSeed.apply).toHaveReturnedWith(true); // Reviver Seed triggers
expect(player.isFainted()).toBeFalsy();
});
// Damaging opponents tests
it.each([
{ moveType: "Damaging Move Chip Damage", move: Moves.SALT_CURE },
{ moveType: "Chip Damage", move: Moves.LEECH_SEED },
{ moveType: "Trapping Chip Damage", move: Moves.WHIRLPOOL },
{ moveType: "Status Effect Damage", move: Moves.WILL_O_WISP },
{ moveType: "Weather", move: Moves.SANDSTORM },
])("should not activate the holder's reviver seed from $moveType", async ({ move }) => {
game.override
.enemyLevel(1)
.startingLevel(100)
.enemySpecies(Species.MAGIKARP)
.moveset(move)
.enemyMoveset(Moves.ENDURE);
await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]);
const enemy = game.scene.getEnemyPokemon()!;
enemy.damageAndUpdate(enemy.hp - 1);
const enemySeed = enemy.getHeldItems()[0] as PokemonInstantReviveModifier;
vi.spyOn(enemySeed, "apply");
game.move.select(move);
await game.phaseInterceptor.to("TurnEndPhase");
expect(enemy.isFainted()).toBeTruthy();
});
// Self-damage tests
it.each([
{ moveType: "Recoil", move: Moves.DOUBLE_EDGE },
{ moveType: "Self-KO", move: Moves.EXPLOSION },
{ moveType: "Self-Deduction", move: Moves.CURSE },
{ moveType: "Liquid Ooze", move: Moves.GIGA_DRAIN },
])("should not activate the holder's reviver seed from $moveType", async ({ move }) => {
game.override
.enemyLevel(100)
.startingLevel(1)
.enemySpecies(Species.MAGIKARP)
.moveset(move)
.enemyAbility(Abilities.LIQUID_OOZE)
.enemyMoveset(Moves.SPLASH);
await game.classicMode.startBattle([ Species.GASTLY, Species.FEEBAS ]);
const player = game.scene.getPlayerPokemon()!;
player.damageAndUpdate(player.hp - 1);
const playerSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier;
vi.spyOn(playerSeed, "apply");
game.move.select(move);
await game.phaseInterceptor.to("TurnEndPhase");
expect(playerSeed.apply).toHaveReturnedWith(false); // Reviver Seed triggers
expect(player.isFainted()).toBeTruthy();
});
});