Compare commits

...

67 Commits

Author SHA1 Message Date
schmidtc1 96864bd35c
Merge 9d5e84dd90 into 10e0f9f0de 2024-12-20 23:41:58 -08:00
NightKev 10e0f9f0de
Merge pull request #5024 from pagefaultgames/main
Merge main to beta
2024-12-20 19:32:30 -08:00
AJ Fontaine 8457fb96fe
[Hotfix] Fix off-by-one error for event encounters (#5022)
* Fix off-by-one error for event encounters

* Increment version to 1.4.1

---------

Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2024-12-20 18:41:07 -08:00
damocleas f95a5d41cb
Merge pull request #5019 from pagefaultgames/beta
Release 1.4.0
2024-12-20 19:06:24 -05:00
Tempoanon 6d903440b4
Release 1.3.0
Release 1.3.0
2024-12-03 18:05:48 -05:00
Christopher Schmidt 9d5e84dd90 Attempt to fix merge conflicts 2024-12-01 17:03:39 -05:00
Christopher Schmidt 068b0189db Linter 2024-12-01 16:46:22 -05:00
Christopher Schmidt c558c7f5fc Merge branch 'beta' into assistbug2 2024-12-01 16:43:14 -05:00
Christopher Schmidt c2293716b5 Attempt to fix merge conflicts 2024-10-30 21:15:20 -04:00
Christopher Schmidt 77c9fb0daa Merge changes and resolve conflicts 2024-10-30 21:02:51 -04:00
Christopher Schmidt 7d1553320d Merge branch 'assistbug2' of https://github.com/schmidtc1/pokerogue into assistbug2 2024-10-26 20:35:43 -04:00
Christopher Schmidt 468275ba23 Corrects mirror move implementation, rewrite unit test to adjust 2024-10-26 20:34:10 -04:00
schmidtc1 83bafbe153
Merge branch 'beta' into assistbug2 2024-10-26 15:40:36 -04:00
Christopher Schmidt 365e77d328 Creates moveHistory in Battle to track all moves used, adjusts mirror move to use this, writes unit tests 2024-10-26 15:34:38 -04:00
Christopher Schmidt 29c76294fc Adjusts move-phase to better track last move for copycat, writes and updates unit tests for assist/copycat 2024-10-26 13:32:22 -04:00
Christopher Schmidt df04e31886 Merge beta and resolve conflicts 2024-10-26 11:50:43 -04:00
NightKev 33809f4add Merge branch 'beta' into assistbug2 2024-10-21 15:42:06 -07:00
Christopher Schmidt ce717b2db8 Adds sleep talk unit test coverage 2024-10-17 19:59:23 -04:00
Christopher Schmidt 25e4037e45 Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into assistbug2 2024-10-17 19:32:14 -04:00
Christopher Schmidt c9a4e8894e Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into assistbug2 2024-10-15 18:42:09 -04:00
Christopher Schmidt fb29682b0b Adds test for assist failing 2024-10-10 17:47:45 -04:00
Christopher Schmidt b1fb1bd5d9 Creates test for Assist 2024-10-10 17:40:02 -04:00
Christopher Schmidt a50984217f Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into assistbug2 2024-10-10 17:08:15 -04:00
Christopher Schmidt 463f86fb83 Add test for Roar, remove test for Acupressure 2024-10-04 19:22:27 -04:00
Christopher Schmidt 931025d3d2 Adds tests for secondary effects and recharge moves for metronome 2024-10-04 19:06:51 -04:00
Christopher Schmidt 0ac69d1ee6 eslint fixes 2024-10-04 19:00:24 -04:00
Christopher Schmidt dfdbe133d6 Merge beta into assistbug2 and fix conflicts 2024-10-04 18:56:40 -04:00
Christopher Schmidt 3c209f32b1 Adds unit test for ally targeting with Aromatic Mist 2024-09-29 13:58:01 -04:00
Christopher Schmidt 3206fa8e65 Refactors Copycat and Mirror Move, adjusts move targeting 2024-09-26 19:02:09 -04:00
Christopher Schmidt e37805d53c Adds unit test for recharge moves 2024-09-24 21:32:59 -04:00
Christopher Schmidt 6b5436fe44 Creates metronome test, refactors RandomMoveAttr for easier testing 2024-09-22 18:58:19 -04:00
schmidtc1 259f06c324
Merge branch 'beta' into assistbug2 2024-09-21 09:39:01 -04:00
Christopher Schmidt 9c60d1d6fd Adjust command phase args handling 2024-09-19 18:29:21 -04:00
Christopher Schmidt 56ea4be3cc Merge beta into assistbug2 2024-09-19 17:35:15 -04:00
schmidtc1 c42b9da403
Merge branch 'beta' into assistbug2 2024-09-16 19:08:48 -04:00
Christopher Schmidt f86b348afc Minor fixes 2024-09-15 21:26:29 -04:00
Christopher Schmidt 76dba4c300 Revert clarity change due to causing bug with move selection 2024-09-15 20:11:02 -04:00
Christopher Schmidt 68ecc3dc19 Clarity in if statement 2024-09-15 20:03:55 -04:00
Christopher Schmidt 6c656d81de Replaces nested ternary with if-else block per DayKev's comment 2024-09-15 20:01:05 -04:00
Christopher Schmidt 0008881eca Merge beta and fix conflicts 2024-09-15 19:37:58 -04:00
Christopher Schmidt ae0504b451 Fixes metronome two turn moves for enemy pokemon 2024-09-08 11:41:06 -04:00
Christopher Schmidt bfef09ecb3 Further fixes for TurnMove refactor 2024-09-07 11:58:36 -04:00
Christopher Schmidt 364e4cdb1b Fix Swallow test due to TurnMove refactor 2024-09-07 11:45:00 -04:00
schmidtc1 85c6a49e8d
Update src/data/move.ts
Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com>
2024-09-07 11:40:32 -04:00
Christopher Schmidt 714211d381 Replaces QueuedMove with TurnMove, refactors to attempt two-turn move fix for metronome 2024-09-07 11:37:56 -04:00
Christopher Schmidt d874f98bf6 Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into assistbug2 2024-09-07 10:17:36 -04:00
NightKev 4c9bcdc0e6
Merge branch 'beta' into assistbug2 2024-09-06 02:38:26 -07:00
Christopher Schmidt fbb2a44c92 Refactors other attributes to extend CallMoveAttr 2024-08-30 15:57:37 -04:00
Christopher Schmidt 70dcc75a7c Experimental async/await to be tested 2024-08-28 22:01:25 -04:00
Christopher Schmidt 1e64ef0c90 Fixes bug with metronome/copycat/assist/sleep talk targeting ally 2024-08-28 18:11:58 -04:00
Christopher Schmidt b854a37e05 Merge branch 'assistbug2' of https://github.com/schmidtc1/pokerogue into assistbug2 2024-08-28 16:36:31 -04:00
Christopher Schmidt 15ff2c7f7e Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into assistbug2 2024-08-28 16:36:23 -04:00
schmidtc1 e588c14a63
Merge branch 'beta' into assistbug2 2024-08-28 10:58:49 -04:00
Christopher Schmidt df5da77dd7 Removes Max/Z moves per frutescens' comment 2024-08-27 13:13:46 -04:00
Christopher Schmidt eb1d0714f1 Attempts to resolve merge conflicts 2024-08-26 12:00:47 -04:00
Christopher Schmidt 040a8778ee Merge branch 'assistbug2' of https://github.com/schmidtc1/pokerogue into assistbug2 2024-08-26 11:58:45 -04:00
Christopher Schmidt 42ec6297c5 Merge branch 'beta' of https://github.com/pagefaultgames/pokerogue into assistbug2 2024-08-26 11:55:56 -04:00
schmidtc1 e66b75aa6d
Merge branch 'beta' into assistbug2 2024-08-26 11:14:41 -04:00
Christopher Schmidt c1d018ef29 Removes ignoresVirtual from beta merge 2024-08-07 09:51:35 -04:00
Tempoanon 86b9f88335
Merge branch 'beta' into assistbug2 2024-08-06 23:57:35 -04:00
Christopher Schmidt e10ed4320f Corrects invalid move lists, adds Max/Z moves to metronome's list 2024-08-06 12:02:05 -04:00
Christopher Schmidt 67a04e171c Cherrypicks due to beta branch issue 2024-08-06 11:20:52 -04:00
Christopher Schmidt d712c3fbc0 Refactors RandomMoveAttr to set moveId in condition, places reused code in callMove in RandomMoveAttr 2024-08-06 11:16:11 -04:00
Christopher Schmidt 8352f1a5c8 Refactors move filtering in RandomMovesetMoveAttr, creates arrays with invalid moves for assist/sleep talk 2024-08-06 11:15:52 -04:00
Christopher Schmidt f9bd2df312 Refactor assist and sleep talk to use metronome's attribute for calling a move 2024-08-06 11:15:22 -04:00
Christopher Schmidt ad26883309 Clearer implementation of combining user and teammates' moves 2024-08-06 11:15:08 -04:00
Christopher Schmidt bd60de6624 Combines moveset from allies and uses it to get a move 2024-08-06 11:14:51 -04:00
19 changed files with 1014 additions and 383 deletions

4
package-lock.json generated
View File

@ -1,12 +1,12 @@
{
"name": "pokemon-rogue-battle",
"version": "1.4.0",
"version": "1.4.1",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "pokemon-rogue-battle",
"version": "1.4.0",
"version": "1.4.1",
"hasInstallScript": true,
"dependencies": {
"@material/material-color-utilities": "^0.2.7",

View File

@ -1,7 +1,7 @@
{
"name": "pokemon-rogue-battle",
"private": true,
"version": "1.4.0",
"version": "1.4.1",
"type": "module",
"scripts": {
"start": "vite",

View File

@ -7,7 +7,7 @@ import { MoneyMultiplierModifier, PokemonHeldItemModifier } from "./modifier/mod
import { PokeballType } from "#enums/pokeball";
import { trainerConfigs } from "#app/data/trainer-config";
import { SpeciesFormKey } from "#enums/species-form-key";
import Pokemon, { EnemyPokemon, PlayerPokemon, QueuedMove } from "#app/field/pokemon";
import Pokemon, { EnemyPokemon, PlayerPokemon, TurnMove } from "#app/field/pokemon";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleSpec } from "#enums/battle-spec";
import { Moves } from "#enums/moves";
@ -44,12 +44,12 @@ export enum BattlerIndex {
}
export interface TurnCommand {
command: Command;
cursor?: number;
move?: QueuedMove;
targets?: BattlerIndex[];
skip?: boolean;
args?: any[];
command: Command;
cursor?: number;
move?: TurnMove;
targets?: BattlerIndex[];
skip?: boolean;
args?: any[];
}
export interface FaintLogEntry {
@ -91,6 +91,7 @@ export default class Battle {
public playerFaintsHistory: FaintLogEntry[] = [];
public enemyFaintsHistory: FaintLogEntry[] = [];
public mysteryEncounterType?: MysteryEncounterType;
/** If the current battle is a Mystery Encounter, this will always be defined */
public mysteryEncounter?: MysteryEncounter;

View File

@ -610,7 +610,7 @@ export class InterruptedTag extends BattlerTag {
super.onAdd(pokemon);
pokemon.getMoveQueue().shift();
pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.OTHER });
pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.OTHER, targets: []});
}
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {

File diff suppressed because it is too large Load Diff

View File

@ -31,7 +31,7 @@ import { BerryType } from "#enums/berry-type";
import { PERMANENT_STATS, Stat } from "#enums/stat";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import PokemonSpecies, { allSpecies } from "#app/data/pokemon-species";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/berriesAbound";
@ -62,8 +62,8 @@ export const BerriesAboundEncounter: MysteryEncounter =
let bossSpecies: PokemonSpecies;
if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!);
bossSpecies = allSpecies[eventEncounter.species];
bossSpecies.speciesId = bossSpecies.getSpeciesForLevel(level, eventEncounter.allowEvolution);
const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode);
bossSpecies = getPokemonSpecies( levelSpecies );
} else {
bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
}

View File

@ -29,7 +29,7 @@ import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encoun
import { randSeedInt, randSeedItem } from "#app/utils";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import PokemonSpecies, { allSpecies } from "#app/data/pokemon-species";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/fightOrFlight";
@ -60,8 +60,8 @@ export const FightOrFlightEncounter: MysteryEncounter =
let bossSpecies: PokemonSpecies;
if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!);
bossSpecies = allSpecies[eventEncounter.species];
bossSpecies.speciesId = bossSpecies.getSpeciesForLevel(level, eventEncounter.allowEvolution);
const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode);
bossSpecies = getPokemonSpecies( levelSpecies );
} else {
bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
}

View File

@ -23,7 +23,7 @@ import { BerryModifier } from "#app/modifier/modifier";
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import PokemonSpecies, { allSpecies } from "#app/data/pokemon-species";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounters/uncommonBreed";
@ -55,8 +55,8 @@ export const UncommonBreedEncounter: MysteryEncounter =
let species: PokemonSpecies;
if (scene.eventManager.isEventActive() && scene.eventManager.activeEvent()?.uncommonBreedEncounters && randSeedInt(2) === 1) {
const eventEncounter = randSeedItem(scene.eventManager.activeEvent()!.uncommonBreedEncounters!);
species = allSpecies[eventEncounter.species];
species.speciesId = species.getSpeciesForLevel(level, eventEncounter.allowEvolution);
const levelSpecies = getPokemonSpecies(eventEncounter.species).getWildSpeciesForLevel(level, eventEncounter.allowEvolution ?? false, true, scene.gameMode);
species = getPokemonSpecies( levelSpecies );
} else {
species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getPlayerParty()), true);
}

View File

@ -3286,7 +3286,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
}
}
getMoveQueue(): QueuedMove[] {
getMoveQueue(): TurnMove[] {
return this.summonData.moveQueue;
}
@ -4801,17 +4801,19 @@ export class EnemyPokemon extends Pokemon {
* the Pokemon the move will target.
* @returns this Pokemon's next move in the format {move, moveTargets}
*/
getNextMove(): QueuedMove {
getNextMove(): TurnMove {
// If this Pokemon has a move already queued, return it.
const queuedMove = this.getMoveQueue().length
? this.getMoveset().find(m => m?.moveId === this.getMoveQueue()[0].move)
: null;
if (queuedMove) {
if (queuedMove.isUsable(this, this.getMoveQueue()[0].ignorePP)) {
return { move: queuedMove.moveId, targets: this.getMoveQueue()[0].targets, ignorePP: this.getMoveQueue()[0].ignorePP };
} else {
this.getMoveQueue().shift();
return this.getNextMove();
const moveQueue = this.getMoveQueue();
if (moveQueue.length !== 0) {
const queuedMove = moveQueue[0];
if (queuedMove) {
const moveIndex = this.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move);
if ((moveIndex > -1 && this.getMoveset()[moveIndex]!.isUsable(this, queuedMove.ignorePP)) || queuedMove.virtual) {
return queuedMove;
} else {
this.getMoveQueue().shift();
return this.getNextMove();
}
}
}
@ -5233,15 +5235,10 @@ export class EnemyPokemon extends Pokemon {
export interface TurnMove {
move: Moves;
targets?: BattlerIndex[];
result: MoveResult;
targets: BattlerIndex[];
result?: MoveResult;
virtual?: boolean;
turn?: number;
}
export interface QueuedMove {
move: Moves;
targets: BattlerIndex[];
ignorePP?: boolean;
}
@ -5257,7 +5254,7 @@ export interface AttackMoveResult {
export class PokemonSummonData {
/** [Atk, Def, SpAtk, SpDef, Spd, Acc, Eva] */
public statStages: number[] = [ 0, 0, 0, 0, 0, 0, 0 ];
public moveQueue: QueuedMove[] = [];
public moveQueue: TurnMove[] = [];
public tags: BattlerTag[] = [];
public abilitySuppressed: boolean = false;
public abilitiesApplied: Abilities[] = [];

View File

@ -8,7 +8,7 @@ import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Biome } from "#app/enums/biome";
import { Moves } from "#app/enums/moves";
import { PokeballType } from "#enums/pokeball";
import { FieldPosition, PlayerPokemon } from "#app/field/pokemon";
import { FieldPosition, PlayerPokemon, TurnMove } from "#app/field/pokemon";
import { getPokemonNameWithAffix } from "#app/messages";
import { Command } from "#app/ui/command-ui-handler";
import { Mode } from "#app/ui/ui";
@ -76,19 +76,19 @@ export class CommandPhase extends FieldPhase {
const moveQueue = playerPokemon.getMoveQueue();
while (moveQueue.length && moveQueue[0]
&& moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move)
&& moveQueue[0].move && !moveQueue[0].virtual && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move)
|| !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move)]!.isUsable(playerPokemon, moveQueue[0].ignorePP))) { // TODO: is the bang correct?
moveQueue.shift();
}
if (moveQueue.length) {
if (moveQueue.length !== 0) {
const queuedMove = moveQueue[0];
if (!queuedMove.move) {
this.handleCommand(Command.FIGHT, -1, false);
this.handleCommand(Command.FIGHT, -1, undefined);
} else {
const moveIndex = playerPokemon.getMoveset().findIndex(m => m?.moveId === queuedMove.move);
if (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex]!.isUsable(playerPokemon, queuedMove.ignorePP)) { // TODO: is the bang correct?
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, { targets: queuedMove.targets, multiple: queuedMove.targets.length > 1 });
if ((moveIndex > -1 && playerPokemon.getMoveset()[moveIndex]!.isUsable(playerPokemon, queuedMove.ignorePP)) || queuedMove.virtual) { // TODO: is the bang correct?
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, queuedMove);
} else {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
@ -110,12 +110,24 @@ export class CommandPhase extends FieldPhase {
switch (command) {
case Command.FIGHT:
let useStruggle = false;
const turnMove: TurnMove | undefined = (args.length === 2 ? (args[1] as TurnMove) : undefined);
if (cursor === -1 ||
playerPokemon.trySelectMove(cursor, args[0] as boolean) ||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m?.isUsable(playerPokemon)).length)) {
const moveId = !useStruggle ? cursor > -1 ? playerPokemon.getMoveset()[cursor]!.moveId : Moves.NONE : Moves.STRUGGLE; // TODO: is the bang correct?
let moveId: Moves;
if (useStruggle) {
moveId = Moves.STRUGGLE;
} else if (turnMove !== undefined) {
moveId = turnMove.move;
} else if (cursor > -1) {
moveId = playerPokemon.getMoveset()[cursor]!.moveId;
} else {
moveId = Moves.NONE;
}
const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args };
const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, moveId) : args[2];
const moveTargets: MoveTargetSet = turnMove === undefined ? getMoveTargets(playerPokemon, moveId) : { targets: turnMove.targets, multiple: turnMove.targets.length > 1 };
if (!moveId) {
turnCommand.targets = [ this.fieldIndex ];
}

View File

@ -294,11 +294,6 @@ export class MovePhase extends BattlePhase {
this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.ppUsed));
}
// Update the battle's "last move" pointer, unless we're currently mimicking a move.
if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
this.scene.currentBattle.lastMove = this.move.moveId;
}
/**
* Determine if the move is successful (meaning that its damage/effects can be attempted)
* by checking that all of the following are true:
@ -322,6 +317,14 @@ export class MovePhase extends BattlePhase {
const success = passesConditions && !failedDueToWeather && !failedDueToTerrain;
// Update the battle's "last move" pointer, unless we're currently mimicking a move.
if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
// The last move used is unaffected by moves that fail
if (success) {
this.scene.currentBattle.lastMove = this.move.moveId;
}
}
/**
* If the move has not failed, trigger ability-based user type changes and then execute it.
*
@ -516,7 +519,7 @@ export class MovePhase extends BattlePhase {
frenzyMissFunc(this.pokemon, this.move.getMove());
}
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL });
this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL, targets: this.targets });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
this.pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);

View File

@ -0,0 +1,70 @@
import { BattlerIndex } from "#app/battle";
import { Stat } from "#app/enums/stat";
import { MoveResult } from "#app/field/pokemon";
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 } from "vitest";
describe("Moves - Assist", () => {
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.ASSIST, Moves.SKETCH, Moves.FLY, Moves.DRAGON_TAIL ]) // These are all moves Assist cannot call; Sketch will be used to test that it can call other moves properly
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyLevel(100)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should be able to use an ally's moves", async () => {
game.override
.battleType("double")
.enemyMoveset(Moves.SWORDS_DANCE);
await game.classicMode.startBattle([ Species.FEEBAS, Species.SHUCKLE ]);
game.move.select(Moves.ASSIST, 0);
game.move.select(Moves.SKETCH, 1);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER ]);
// Player_2 uses Sketch, copies Swords Dance, Player_1 uses Assist, uses Player_2's Sketched Swords Dance
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(2); // Stat raised from Assist -> Swords Dance
});
it("should fail if there are no usable moves", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
// Moves above are already unusable by Assist
game.move.select(Moves.ASSIST, 0);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should apply secondary effects of a move", async () => {
game.override.moveset([ Moves.ASSIST, Moves.WOOD_HAMMER, Moves.WOOD_HAMMER, Moves.WOOD_HAMMER ]);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.ASSIST, 0);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // should receive recoil damage from Wood Hammer
});
});

View File

@ -0,0 +1,88 @@
import { BattlerIndex } from "#app/battle";
import { Stat } from "#app/enums/stat";
import { MoveResult } from "#app/field/pokemon";
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("Moves - Copycat", () => {
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.COPYCAT, Moves.SPIKY_SHIELD, Moves.SWORDS_DANCE, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.starterSpecies(Species.FEEBAS)
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should copy the last move successfully executed", async () => {
game.override.enemyMoveset(Moves.SUCKER_PUNCH);
await game.classicMode.startBattle();
game.move.select(Moves.SWORDS_DANCE);
await game.toNextTurn();
game.move.select(Moves.COPYCAT); // Last successful move should be Swords Dance
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK)).toBe(4);
});
it("should fail when the last move used is not a valid Copycat move", async () => {
game.override.enemyMoveset(Moves.PROTECT); // Protect is not a valid move for Copycat to copy
await game.classicMode.startBattle();
game.move.select(Moves.SPIKY_SHIELD); // Spiky Shield is not a valid move for Copycat to copy
await game.toNextTurn();
game.move.select(Moves.COPYCAT);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should copy the called move when the last move successfully calls another", async () => {
game.override
.moveset([ Moves.SPLASH, Moves.METRONOME ])
.enemyMoveset(Moves.COPYCAT);
await game.classicMode.startBattle();
vi.spyOn(game.scene.getPlayerPokemon()!, "randSeedInt").mockReturnValue(Moves.SWORDS_DANCE);
game.move.select(Moves.METRONOME);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); // Player moves first, so enemy can copy Swords Dance
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(2);
});
it("should apply secondary effects of a move", async () => {
game.override.enemyMoveset(Moves.ACID_SPRAY); // Secondary effect lowers SpDef by 2 stages
await game.classicMode.startBattle();
game.move.select(Moves.COPYCAT);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPDEF)).toBe(-2);
});
});

View File

@ -0,0 +1,117 @@
import { RechargingTag, SemiInvulnerableTag } from "#app/data/battler-tags";
import { Abilities } from "#app/enums/abilities";
import { Stat } from "#app/enums/stat";
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, it, expect, vi } from "vitest";
describe("Moves - Metronome", () => {
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.METRONOME, Moves.SPLASH ])
.battleType("single")
.startingLevel(100)
.starterSpecies(Species.REGIELEKI)
.enemyLevel(100)
.enemySpecies(Species.SHUCKLE)
.enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH);
});
it("should have one semi-invulnerable turn and deal damage on the second turn when a semi-invulnerable move is called", async () => {
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
const enemy = game.scene.getEnemyPokemon()!;
vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.DIVE);
game.move.select(Moves.METRONOME);
await game.toNextTurn();
expect(player.getTag(SemiInvulnerableTag)).toBeTruthy();
await game.move.forceHit(); // Force hit on Dive, required due to randSeedInt mock making hitCheck return false every time.
await game.toNextTurn();
expect(player.getTag(SemiInvulnerableTag)).toBeFalsy();
expect(enemy.isFullHp()).toBeFalsy();
});
it("should apply secondary effects of a move", async () => {
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.WOOD_HAMMER);
game.move.select(Moves.METRONOME);
await game.phaseInterceptor.to("MoveEffectPhase"); // Metronome has its own MoveEffectPhase, followed by Wood Hammer's MoveEffectPhase
await game.move.forceHit(); // Calls forceHit on Wood Hammer's MoveEffectPhase, required due to randSeedInt mock making hitCheck return false every time.
await game.toNextTurn();
expect(player.isFullHp()).toBeFalsy();
});
it("should recharge after using recharge move", async () => {
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.HYPER_BEAM);
game.move.select(Moves.METRONOME);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceHit();
await game.toNextTurn();
expect(player.getTag(RechargingTag)).toBeTruthy();
});
it("should only target ally for Aromatic Mist", async () => {
game.override.battleType("double");
await game.classicMode.startBattle([ Species.REGIELEKI, Species.RATTATA ]);
const [ leftPlayer, rightPlayer ] = game.scene.getPlayerField();
const [ leftOpp, rightOpp ] = game.scene.getEnemyField();
vi.spyOn(leftPlayer, "randSeedInt").mockReturnValue(Moves.AROMATIC_MIST);
game.move.select(Moves.METRONOME);
game.move.select(Moves.SPLASH, 1);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceHit();
await game.toNextTurn();
expect(rightPlayer.getStatStage(Stat.SPDEF)).toBe(1);
expect(leftPlayer.getStatStage(Stat.SPDEF)).toBe(0);
expect(leftOpp.getStatStage(Stat.SPDEF)).toBe(0);
expect(rightOpp.getStatStage(Stat.SPDEF)).toBe(0);
});
it("should cause opponent to flee, and not crash for Roar", async () => {
await game.classicMode.startBattle();
const player = game.scene.getPlayerPokemon()!;
vi.spyOn(player, "randSeedInt").mockReturnValue(Moves.ROAR);
const enemyPokemon = game.scene.getEnemyPokemon()!;
game.move.select(Moves.METRONOME);
await game.phaseInterceptor.to("MoveEffectPhase");
await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase");
const isVisible = enemyPokemon.visible;
const hasFled = enemyPokemon.switchOutStatus;
expect(!isVisible && hasFled).toBe(true);
await game.phaseInterceptor.to("BattleEndPhase");
});
});

View File

@ -0,0 +1,84 @@
import { BattlerIndex } from "#app/battle";
import { Stat } from "#app/enums/stat";
import { MoveResult } from "#app/field/pokemon";
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 } from "vitest";
describe("Moves - Mirror Move", () => {
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.MIRROR_MOVE, Moves.SPLASH ])
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH);
});
it("should use the last move that the target", async () => {
game.override
.battleType("double")
.enemyMoveset([ Moves.TACKLE, Moves.GROWL ]);
await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]);
game.move.select(Moves.MIRROR_MOVE, 0, BattlerIndex.ENEMY); // target's last move is Tackle, enemy should receive damage from Mirror Move copying Tackle
game.move.select(Moves.SPLASH, 1);
await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2);
await game.forceEnemyMove(Moves.GROWL, BattlerIndex.PLAYER_2);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.ENEMY_2, BattlerIndex.PLAYER_2, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyField()[0].isFullHp()).toBeFalsy();
});
it("should apply secondary effects of a move", async () => {
game.override.enemyMoveset(Moves.ACID_SPRAY);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.MIRROR_MOVE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.SPDEF)).toBe(-2);
});
it("should be able to copy status moves", async () => {
game.override.enemyMoveset(Moves.GROWL);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.MIRROR_MOVE);
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
await game.toNextTurn();
expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.ATK)).toBe(-1);
});
it("should fail if the target has not used any moves", async () => {
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.MIRROR_MOVE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
});

View File

@ -0,0 +1,75 @@
import { Stat } from "#app/enums/stat";
import { StatusEffect } from "#app/enums/status-effect";
import { MoveResult } from "#app/field/pokemon";
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 } from "vitest";
describe("Moves - Sleep Talk", () => {
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.SLEEP_TALK ])
.statusEffect(StatusEffect.SLEEP)
.ability(Abilities.BALL_FETCH)
.battleType("single")
.disableCrits()
.enemySpecies(Species.MAGIKARP)
.enemyAbility(Abilities.BALL_FETCH)
.enemyMoveset(Moves.SPLASH)
.enemyLevel(100);
});
it("should fail when the user is not asleep", async () => {
game.override.statusEffect(StatusEffect.NONE);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SLEEP_TALK);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should fail if the user has no valid moves", async () => {
game.override.moveset([ Moves.SLEEP_TALK, Moves.DIG, Moves.METRONOME, Moves.SOLAR_BEAM, Moves.SLEEP_TALK ]);
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SLEEP_TALK);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].result).toBe(MoveResult.FAIL);
});
it("should call a random valid move if the user is asleep", async () => {
game.override.moveset([ Moves.SLEEP_TALK, Moves.DIG, Moves.FLY, Moves.SWORDS_DANCE ]); // Dig and Fly are invalid moves, Swords Dance should always be called
await game.classicMode.startBattle([ Species.FEEBAS ]);
game.move.select(Moves.SLEEP_TALK);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.getStatStage(Stat.ATK));
});
it("should apply secondary effects of a move", async () => {
game.override.moveset([ Moves.SLEEP_TALK, Moves.DIG, Moves.FLY, Moves.WOOD_HAMMER ]); // Dig and Fly are invalid moves, Wood Hammer should always be called
await game.classicMode.startBattle();
game.move.select(Moves.SLEEP_TALK);
await game.toNextTurn();
expect(game.scene.getPlayerPokemon()!.isFullHp()).toBeFalsy(); // Wood Hammer recoil effect should be applied
});
});

View File

@ -124,7 +124,7 @@ describe("Moves - Spit Up", () => {
game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.FAIL });
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.FAIL, targets: [ game.scene.getEnemyPokemon()!.getBattlerIndex() ]});
expect(spitUp.calculateBattlePower).not.toHaveBeenCalled();
});
@ -147,7 +147,7 @@ describe("Moves - Spit Up", () => {
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS });
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS, targets: [ game.scene.getEnemyPokemon()!.getBattlerIndex() ]});
expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce();
@ -175,7 +175,7 @@ describe("Moves - Spit Up", () => {
game.move.select(Moves.SPIT_UP);
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS });
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SPIT_UP, result: MoveResult.SUCCESS, targets: [ game.scene.getEnemyPokemon()!.getBattlerIndex() ]});
expect(spitUp.calculateBattlePower).toHaveBeenCalledOnce();

View File

@ -71,7 +71,7 @@ describe("Moves - Stockpile", () => {
expect(user.getStatStage(Stat.SPDEF)).toBe(3);
expect(stockpilingTag).toBeDefined();
expect(stockpilingTag.stockpiledCount).toBe(3);
expect(user.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ result: MoveResult.FAIL, move: Moves.STOCKPILE });
expect(user.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ result: MoveResult.FAIL, move: Moves.STOCKPILE, targets: [ user.getBattlerIndex() ]});
}
}
});

View File

@ -134,7 +134,7 @@ describe("Moves - Swallow", () => {
game.move.select(Moves.SWALLOW);
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.FAIL });
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.FAIL, targets: [ pokemon.getBattlerIndex() ]});
});
describe("restores stat stage boosts granted by stacks", () => {
@ -155,7 +155,7 @@ describe("Moves - Swallow", () => {
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS });
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS, targets: [ pokemon.getBattlerIndex() ]});
expect(pokemon.getStatStage(Stat.DEF)).toBe(0);
expect(pokemon.getStatStage(Stat.SPDEF)).toBe(0);
@ -182,7 +182,7 @@ describe("Moves - Swallow", () => {
await game.phaseInterceptor.to(TurnInitPhase);
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS });
expect(pokemon.getMoveHistory().at(-1)).toMatchObject<TurnMove>({ move: Moves.SWALLOW, result: MoveResult.SUCCESS, targets: [ pokemon.getBattlerIndex() ]});
expect(pokemon.getStatStage(Stat.DEF)).toBe(1);
expect(pokemon.getStatStage(Stat.SPDEF)).toBe(-2);