[Bug Fix] Correct PostSummonPhase Timing for Abilities and Entry Hazards (#2108)

* added test for spikes + forceOpponentToSwitch

* added conditionalQueue && pushConditionalPhase to fix entry hasard and abilities at post summon

* reduce timeout time to default
This commit is contained in:
Greenlamp2 2024-06-12 00:55:16 +02:00 committed by GitHub
parent 6d1fbf92cc
commit f57798fd1d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 184 additions and 14 deletions

View File

@ -168,6 +168,7 @@ export default class BattleScene extends SceneBase {
public sessionSlotId: integer; public sessionSlotId: integer;
public phaseQueue: Phase[]; public phaseQueue: Phase[];
public conditionalQueue: Array<[() => boolean, Phase]>;
private phaseQueuePrepend: Phase[]; private phaseQueuePrepend: Phase[];
private phaseQueuePrependSpliceIndex: integer; private phaseQueuePrependSpliceIndex: integer;
private nextCommandPhaseQueue: Phase[]; private nextCommandPhaseQueue: Phase[];
@ -252,6 +253,7 @@ export default class BattleScene extends SceneBase {
super("battle"); super("battle");
this.phaseQueue = []; this.phaseQueue = [];
this.phaseQueuePrepend = []; this.phaseQueuePrepend = [];
this.conditionalQueue = [];
this.phaseQueuePrependSpliceIndex = -1; this.phaseQueuePrependSpliceIndex = -1;
this.nextCommandPhaseQueue = []; this.nextCommandPhaseQueue = [];
this.updateGameInfo(); this.updateGameInfo();
@ -1843,6 +1845,21 @@ export default class BattleScene extends SceneBase {
return this.standbyPhase; return this.standbyPhase;
} }
/**
* Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met.
*
* This method allows deferring the execution of a phase until certain conditions are met, which is useful for handling
* situations like abilities and entry hazards that depend on specific game states.
*
* @param {Phase} phase - The phase to be added to the conditional queue.
* @param {() => boolean} condition - A function that returns a boolean indicating whether the phase should be executed.
*
*/
pushConditionalPhase(phase: Phase, condition: () => boolean): void {
this.conditionalQueue.push([condition, phase]);
}
pushPhase(phase: Phase, defer: boolean = false): void { pushPhase(phase: Phase, defer: boolean = false): void {
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase); (!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
} }
@ -1886,6 +1903,21 @@ export default class BattleScene extends SceneBase {
this.populatePhaseQueue(); this.populatePhaseQueue();
} }
this.currentPhase = this.phaseQueue.shift(); this.currentPhase = this.phaseQueue.shift();
// Check if there are any conditional phases queued
if (this.conditionalQueue?.length) {
// Retrieve the first conditional phase from the queue
const conditionalPhase = this.conditionalQueue.shift();
// Evaluate the condition associated with the phase
if (conditionalPhase[0]()) {
// If the condition is met, add the phase to the front of the phase queue
this.unshiftPhase(conditionalPhase[1]);
} else {
// If the condition is not met, re-add the phase back to the front of the conditional queue
this.conditionalQueue.unshift(conditionalPhase);
}
}
this.currentPhase.start(); this.currentPhase.start();
} }

View File

@ -1024,6 +1024,15 @@ export class EncounterPhase extends BattlePhase {
}); });
if (this.scene.currentBattle.battleType !== BattleType.TRAINER) { if (this.scene.currentBattle.battleType !== BattleType.TRAINER) {
enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => {
// is the player party initialized ?
const a = !!this.scene.getParty()?.length;
// how many player pokemon are on the field ?
const amountOnTheField = this.scene.getParty().filter(p => p.isOnField()).length;
// if it's a double, there should be 2, otherwise 1
const b = this.scene.currentBattle.double ? amountOnTheField === 2 : amountOnTheField === 1;
return a && b;
}));
const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier); const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier);
if (ivScannerModifier) { if (ivScannerModifier) {
enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6)))); enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))));
@ -1479,10 +1488,6 @@ export class SummonPhase extends PartyMemberPokemonPhase {
if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1) { if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1) {
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true); this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
}
if (pokemon.isPlayer()) {
// postSummon for player only here, since we want the postSummon from opponent to be call in the turnInitPhase
// covering both wild & trainer battles
this.queuePostSummon(); this.queuePostSummon();
} }
} }
@ -1797,13 +1802,6 @@ export class TurnInitPhase extends FieldPhase {
start() { start() {
super.start(); super.start();
const enemyField = this.scene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
enemyField.map(p => {
if (p.battleSummonData.turnCount !== 1) {
return;
}
return this.scene.unshiftPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()));
});
this.scene.getPlayerField().forEach(p => { this.scene.getPlayerField().forEach(p => {
// If this pokemon is in play and evolved into something illegal under the current challenge, force a switch // If this pokemon is in play and evolved into something illegal under the current challenge, force a switch

View File

@ -0,0 +1,128 @@
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
import Phaser from "phaser";
import GameManager from "#app/test/utils/gameManager";
import * as overrides from "#app/overrides";
import {Abilities} from "#app/data/enums/abilities";
import {Species} from "#app/data/enums/species";
import {
CommandPhase
} from "#app/phases";
import {Moves} from "#app/data/enums/moves";
describe("Moves - Spikes", () => {
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.scene.battleStyle = 1;
vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA);
vi.spyOn(overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION);
vi.spyOn(overrides, "OPP_PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION);
vi.spyOn(overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION);
vi.spyOn(overrides, "PASSIVE_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.HYDRATION);
vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3);
vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH,Moves.SPLASH,Moves.SPLASH,Moves.SPLASH]);
vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPIKES,Moves.SPLASH, Moves.ROAR]);
});
it("single - wild - stay on field - no damage", async() => {
// player set spikes on the field and do splash for 3 turns
// opponent do splash for 4 turns
// nobody should take damage
await game.runToSummon([
Species.MIGHTYENA,
Species.POOCHYENA,
]);
await game.phaseInterceptor.to(CommandPhase, true);
const initialHp = game.scene.getParty()[0].hp;
expect(game.scene.getParty()[0].hp).toBe(initialHp);
game.doAttack(0);
await game.toNextTurn();
game.doAttack(1);
await game.toNextTurn();
game.doAttack(1);
await game.toNextTurn();
game.doAttack(1);
await game.toNextTurn();
game.doAttack(1);
await game.toNextTurn();
expect(game.scene.getParty()[0].hp).toBe(initialHp);
console.log(game.textInterceptor.logs);
}, 20000);
it("single - wild - take some damage", async() => {
// player set spikes on the field and switch back to back
// opponent do splash for 2 turns
// nobody should take damage
await game.runToSummon([
Species.MIGHTYENA,
Species.POOCHYENA,
]);
await game.phaseInterceptor.to(CommandPhase, false);
const initialHp = game.scene.getParty()[0].hp;
await game.switchPokemon(1, false);
await game.phaseInterceptor.run(CommandPhase);
await game.phaseInterceptor.to(CommandPhase, false);
await game.switchPokemon(1, false);
await game.phaseInterceptor.run(CommandPhase);
await game.phaseInterceptor.to(CommandPhase, false);
expect(game.scene.getParty()[0].hp).toBe(initialHp);
}, 20000);
it("trainer - wild - force switch opponent - should take damage", async() => {
vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5);
// player set spikes on the field and do splash for 3 turns
// opponent do splash for 4 turns
// nobody should take damage
await game.runToSummon([
Species.MIGHTYENA,
Species.POOCHYENA,
]);
await game.phaseInterceptor.to(CommandPhase, true);
const initialHpOpponent = game.scene.currentBattle.enemyParty[1].hp;
game.doAttack(0);
await game.toNextTurn();
game.doAttack(2);
await game.toNextTurn();
expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(initialHpOpponent);
}, 20000);
it("trainer - wild - force switch by himself opponent - should take damage", async() => {
vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5);
vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(5000);
vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(0);
// turn 1: player set spikes, opponent do splash
// turn 2: player do splash, opponent switch pokemon
// opponent pokemon should trigger spikes and lose HP
await game.runToSummon([
Species.MIGHTYENA,
Species.POOCHYENA,
]);
await game.phaseInterceptor.to(CommandPhase, true);
const initialHpOpponent = game.scene.currentBattle.enemyParty[1].hp;
game.doAttack(0);
await game.toNextTurn();
game.forceOpponentToSwitch();
game.doAttack(1);
await game.toNextTurn();
expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(initialHpOpponent);
}, 20000);
});

View File

@ -7,6 +7,7 @@ export default class TextInterceptor {
} }
showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer): void { showText(text: string, delay?: integer, callback?: Function, callbackDelay?: integer, prompt?: boolean, promptDelay?: integer): void {
console.log(text);
this.logs.push(text); this.logs.push(text);
} }

View File

@ -29,6 +29,7 @@ import {Command} from "#app/ui/command-ui-handler";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import {Button} from "#app/enums/buttons"; import {Button} from "#app/enums/buttons";
import PartyUiHandler, {PartyUiMode} from "#app/ui/party-ui-handler"; import PartyUiHandler, {PartyUiMode} from "#app/ui/party-ui-handler";
import Trainer from "#app/field/trainer";
/** /**
* Class to manage the game state and transitions between phases. * Class to manage the game state and transitions between phases.
@ -192,6 +193,14 @@ export default class GameManager {
}, () => this.isCurrentPhase(CommandPhase) || this.isCurrentPhase(NewBattlePhase)); }, () => this.isCurrentPhase(CommandPhase) || this.isCurrentPhase(NewBattlePhase));
} }
forceOpponentToSwitch() {
const originalMatchupScore = Trainer.prototype.getPartyMemberMatchupScores;
Trainer.prototype.getPartyMemberMatchupScores = () => {
Trainer.prototype.getPartyMemberMatchupScores = originalMatchupScore;
return [[1, 100], [1, 100]];
};
}
/** Transition to the next upcoming {@linkcode CommandPhase} */ /** Transition to the next upcoming {@linkcode CommandPhase} */
async toNextTurn() { async toNextTurn() {
await this.phaseInterceptor.to(CommandPhase); await this.phaseInterceptor.to(CommandPhase);
@ -281,14 +290,16 @@ export default class GameManager {
}); });
} }
async switchPokemon(pokemonIndex: number) { async switchPokemon(pokemonIndex: number, toNext: boolean = true) {
this.onNextPrompt("CommandPhase", Mode.COMMAND, () => { this.onNextPrompt("CommandPhase", Mode.COMMAND, () => {
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.SWITCH, (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getFieldIndex(), null, PartyUiHandler.FilterNonFainted); this.scene.ui.setMode(Mode.PARTY, PartyUiMode.SWITCH, (this.scene.getCurrentPhase() as CommandPhase).getPokemon().getFieldIndex(), null, PartyUiHandler.FilterNonFainted);
}); });
this.onNextPrompt("CommandPhase", Mode.PARTY, () => { this.onNextPrompt("CommandPhase", Mode.PARTY, () => {
(this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.POKEMON, pokemonIndex, false); (this.scene.getCurrentPhase() as CommandPhase).handleCommand(Command.POKEMON, pokemonIndex, false);
}); });
if (toNext) {
await this.phaseInterceptor.run(CommandPhase); await this.phaseInterceptor.run(CommandPhase);
await this.phaseInterceptor.to(CommandPhase); await this.phaseInterceptor.to(CommandPhase);
} }
} }
}