Merge 0ce30aad5f
into a9ffe03804
This commit is contained in:
commit
a3188905ea
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue