diff --git a/README.md b/README.md index 24dc3490d1f..2920daba98f 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,10 @@ This is a mod for PokéRogue, for use with the offline version. It's used to help with our routing project. -Feature progress: +This program is for Windows - it does not have installers for Mac or Linux right now. +(You can still do run validation without this mod, of course) + +## Feature progress - [ ] Logs all the steps you take while playing - [x] Logs the wild Pokémon you encounter and their stats - [x] Logs the category of trainers you encounter @@ -11,3 +14,33 @@ Feature progress: - [ ] Show damage values for attacks (present, but incomplete) - [x] Show catch rates - [x] Show attributes of wild Pokémon (max IVs, nature, abilities) + +# Instructions +### Installation +- Make sure you have the app (download v1.3.1 [here](https://github.com/Admiral-Billy/Pokerogue-App/releases) - v2.0.0 and up will not work!) +- Look on the `record-path` channel for the modified installer that allows downloading different versions +- Replace `resources/update-game.js` in the offline version's files with the modified installer +- Run the installer, typing `y` and pressing enter to confirm you want to install offline mode +- Select Pokerogue-Projects/Pathing-Tool (option 2 by default) and press enter again +- Wait (it will take a few minutes to install no matter which version you selected) +- Choose whether you want the offline version of the `pkmn.help` type calculator, then press enter one final time when prompted to close the terminal +### Setting up a run +- Open PokéRogue online (you can use [PokeRogue](https://pokerogue.net/) or the online mode of the app) +- Start a new Daily Run +- Save & Quit +- Open the menu +- Go to Manage Data, select Export Session, and select the slot you saved the Daily Run to - you will download a `.prsv` file +- Open the app in offline mode by running `Pokerogue Offine.bat` +- Open the menu, go to Manage Data, and instead *import* a session +- Select the `.prsv` you downloaded, and select a slot to save it to. When the game reloads, you'll see that the newly imported run has appeared as an option on the title screen. +- Open Manage Logs on the title screen. +- If you played a run already, be sure to export your files first. +- Select `Clear All (3)` to delete any previous run data. +### Playing the Daily Run +- All Daily Run saves will appear as buttons on the title screen. Selecting them will load the file as if you had opened the Load Game menu. (Selecting them in that menu still works, of course.) +- Play! The game will automatically log your run as you go. + - **Warning**: The logs do not discriminate between saves, and if you open another save file, it will **overwrite** any data in Steps (`instructions.txt`) or Encounters (`encounters.csv`). +- When you're done, go to the title screen and open Manage Logs. + - Select a log to save it to your device (the number in parenthases indicates the file size) + - Select "Export All" to save all logs to your device at once (the number in parenthases indicates how many logs will be exported) + - Select "Reset All" to delete all existing run data diff --git a/src/data/move.ts b/src/data/move.ts index d87d7f918bd..321fbc6d097 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4218,7 +4218,7 @@ export class AddArenaTagAttr extends MoveEffectAttr { public selfSideTarget: boolean; constructor(tagType: ArenaTagType, turnCount?: integer, failOnOverlap: boolean = false, selfSideTarget: boolean = false) { - super(true, MoveEffectTrigger.POST_APPLY, true); + super(true, MoveEffectTrigger.POST_APPLY); this.tagType = tagType; this.turnCount = turnCount; diff --git a/src/test/abilities/costar.test.ts b/src/test/abilities/costar.test.ts index 1b7eb3f7b90..ecd70088aa2 100644 --- a/src/test/abilities/costar.test.ts +++ b/src/test/abilities/costar.test.ts @@ -29,7 +29,7 @@ describe("Abilities - COSTAR", () => { game = new GameManager(phaserGame); vi.spyOn(Overrides, "DOUBLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); vi.spyOn(Overrides, "ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.COSTAR); - vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.NASTY_PLOT, Moves.CURSE]); + vi.spyOn(Overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.NASTY_PLOT]); vi.spyOn(Overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH, Moves.SPLASH, Moves.SPLASH, Moves.SPLASH]); }); @@ -37,15 +37,17 @@ describe("Abilities - COSTAR", () => { test( "ability copies positive stat changes", async () => { + vi.spyOn(Overrides, "OPP_ABILITY_OVERRIDE", "get").mockReturnValue(Abilities.BALL_FETCH); + await game.startBattle([Species.MAGIKARP, Species.MAGIKARP, Species.FLAMIGO]); let [leftPokemon, rightPokemon] = game.scene.getPlayerField(); - expect(leftPokemon).not.toBe(undefined); - expect(rightPokemon).not.toBe(undefined); + expect(leftPokemon).toBeDefined(); + expect(rightPokemon).toBeDefined(); game.doAttack(getMovePosition(game.scene, 0, Moves.NASTY_PLOT)); await game.phaseInterceptor.to(CommandPhase); - game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + game.doAttack(getMovePosition(game.scene, 1, Moves.SPLASH)); await game.toNextTurn(); expect(leftPokemon.summonData.battleStats[BattleStat.SPATK]).toBe(+2); @@ -71,8 +73,8 @@ describe("Abilities - COSTAR", () => { await game.startBattle([Species.MAGIKARP, Species.MAGIKARP, Species.FLAMIGO]); let [leftPokemon, rightPokemon] = game.scene.getPlayerField(); - expect(leftPokemon).not.toBe(undefined); - expect(rightPokemon).not.toBe(undefined); + expect(leftPokemon).toBeDefined(); + expect(rightPokemon).toBeDefined(); expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); expect(leftPokemon.summonData.battleStats[BattleStat.ATK]).toBe(-2); diff --git a/src/test/moves/ceaseless_edge.test.ts b/src/test/moves/ceaseless_edge.test.ts new file mode 100644 index 00000000000..de47027ccd5 --- /dev/null +++ b/src/test/moves/ceaseless_edge.test.ts @@ -0,0 +1,135 @@ +import {afterEach, beforeAll, beforeEach, describe, expect, test, vi} from "vitest"; +import Phaser from "phaser"; +import GameManager from "#app/test/utils/gameManager"; +import * as overrides from "#app/overrides"; +import { + MoveEffectPhase, + TurnEndPhase +} from "#app/phases"; +import {getMovePosition} from "#app/test/utils/gameManagerUtils"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { ArenaTagType } from "#app/enums/arena-tag-type.js"; +import { allMoves } from "#app/data/move.js"; +import { ArenaTagSide, ArenaTrapTag } from "#app/data/arena-tag.js"; + +const TIMEOUT = 20 * 1000; + +describe("Moves - Ceaseless Edge", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + vi.spyOn(overrides, "SINGLE_BATTLE_OVERRIDE", "get").mockReturnValue(true); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(Species.RATTATA); + vi.spyOn(overrides, "STARTING_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "OPP_LEVEL_OVERRIDE", "get").mockReturnValue(100); + vi.spyOn(overrides, "MOVESET_OVERRIDE", "get").mockReturnValue([ Moves.CEASELESS_EDGE, Moves.SPLASH, Moves.ROAR ]); + vi.spyOn(overrides, "OPP_MOVESET_OVERRIDE", "get").mockReturnValue([Moves.SPLASH,Moves.SPLASH,Moves.SPLASH,Moves.SPLASH]); + vi.spyOn(allMoves[Moves.CEASELESS_EDGE], "accuracy", "get").mockReturnValue(100); + + }); + + test( + "move should hit and apply spikes", + async () => { + await game.startBattle([ Species.ILLUMISE ]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(enemyPokemon).toBeDefined(); + + const enemyStartingHp = enemyPokemon.hp; + + game.doAttack(getMovePosition(game.scene, 0, Moves.CEASELESS_EDGE)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + // Spikes should not have any layers before move effect is applied + const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; + expect(tagBefore instanceof ArenaTrapTag).toBeFalsy(); + + await game.phaseInterceptor.to(TurnEndPhase); + const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; + expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); + expect(tagAfter.layers).toBe(1); + expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); + }, TIMEOUT + ); + + test( + "move should hit twice with multi lens and apply two layers of spikes", + async () => { + vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "MULTI_LENS"}]); + await game.startBattle([ Species.ILLUMISE ]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(enemyPokemon).toBeDefined(); + + const enemyStartingHp = enemyPokemon.hp; + + game.doAttack(getMovePosition(game.scene, 0, Moves.CEASELESS_EDGE)); + + await game.phaseInterceptor.to(MoveEffectPhase, false); + // Spikes should not have any layers before move effect is applied + const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; + expect(tagBefore instanceof ArenaTrapTag).toBeFalsy(); + + await game.phaseInterceptor.to(TurnEndPhase); + const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; + expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); + expect(tagAfter.layers).toBe(2); + expect(enemyPokemon.hp).toBeLessThan(enemyStartingHp); + }, TIMEOUT + ); + + test( + "trainer - move should hit twice, apply two layers of spikes, force switch opponent - opponent takes damage", + async () => { + vi.spyOn(overrides, "STARTING_HELD_ITEMS_OVERRIDE", "get").mockReturnValue([{name: "MULTI_LENS"}]); + vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(5); + vi.spyOn(overrides, "OPP_SPECIES_OVERRIDE", "get").mockReturnValue(0); + + await game.startBattle([ Species.SNORLAX, Species.MUNCHLAX ]); + + const leadPokemon = game.scene.getPlayerPokemon(); + expect(leadPokemon).toBeDefined(); + + const enemyPokemon = game.scene.getEnemyPokemon(); + expect(enemyPokemon).toBeDefined(); + + game.doAttack(getMovePosition(game.scene, 0, Moves.CEASELESS_EDGE)); + await game.phaseInterceptor.to(MoveEffectPhase, false); + // Spikes should not have any layers before move effect is applied + const tagBefore = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; + expect(tagBefore instanceof ArenaTrapTag).toBeFalsy(); + + await game.phaseInterceptor.to(TurnEndPhase, false); + const tagAfter = game.scene.arena.getTagOnSide(ArenaTagType.SPIKES, ArenaTagSide.ENEMY) as ArenaTrapTag; + expect(tagAfter instanceof ArenaTrapTag).toBeTruthy(); + expect(tagAfter.layers).toBe(2); + + const hpBeforeSpikes = game.scene.currentBattle.enemyParty[1].hp; + // Check HP of pokemon that WILL BE switched in (index 1) + game.forceOpponentToSwitch(); + game.doAttack(getMovePosition(game.scene, 0, Moves.SPLASH)); + await game.phaseInterceptor.to(TurnEndPhase, false); + expect(game.scene.currentBattle.enemyParty[0].hp).toBeLessThan(hpBeforeSpikes); + }, TIMEOUT + ); +});