mirror of
https://github.com/pagefaultgames/pokerogue.git
synced 2025-07-31 12:42:22 +02:00
Merge branch 'beta' into instruct
This commit is contained in:
commit
ce609944fb
@ -5953,7 +5953,7 @@ export function initAbilities() {
|
||||
.bypassFaint()
|
||||
.partial(), // Meteor form should protect against status effects and yawn
|
||||
new Ability(Abilities.STAKEOUT, 7)
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => user?.scene.currentBattle.turnCommands[target?.getBattlerIndex() ?? BattlerIndex.ATTACKER]?.command === Command.POKEMON, 2),
|
||||
.attr(MovePowerBoostAbAttr, (user, target, move) => !!target?.turnData.switchedInThisTurn, 2),
|
||||
new Ability(Abilities.WATER_BUBBLE, 7)
|
||||
.attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5)
|
||||
.attr(MoveTypePowerBoostAbAttr, Type.WATER, 2)
|
||||
|
@ -1085,10 +1085,6 @@ export class OctolockTag extends TrappedTag {
|
||||
super(BattlerTagType.OCTOLOCK, BattlerTagLapseType.TURN_END, 1, Moves.OCTOLOCK, sourceId);
|
||||
}
|
||||
|
||||
canAdd(pokemon: Pokemon): boolean {
|
||||
return !pokemon.getTag(BattlerTagType.OCTOLOCK);
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);
|
||||
|
||||
|
@ -7661,6 +7661,8 @@ const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Po
|
||||
return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField());
|
||||
};
|
||||
|
||||
const failIfGhostTypeCondition: MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => !target.isOfType(Type.GHOST);
|
||||
|
||||
export type MoveAttrFilter = (attr: MoveAttr) => boolean;
|
||||
|
||||
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> {
|
||||
@ -8407,6 +8409,7 @@ export function initMoves() {
|
||||
new AttackMove(Moves.THIEF, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2)
|
||||
.attr(StealHeldItemChanceAttr, 0.3),
|
||||
new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2)
|
||||
.condition(failIfGhostTypeCondition)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
|
||||
new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2)
|
||||
.attr(IgnoreAccuracyAttr),
|
||||
@ -8543,6 +8546,7 @@ export function initMoves() {
|
||||
new AttackMove(Moves.STEEL_WING, Type.STEEL, MoveCategory.PHYSICAL, 70, 90, 25, 10, 0, 2)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
|
||||
new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2)
|
||||
.condition(failIfGhostTypeCondition)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
|
||||
new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.INFATUATED)
|
||||
@ -8922,6 +8926,7 @@ export function initMoves() {
|
||||
new SelfStatusMove(Moves.IRON_DEFENSE, Type.STEEL, -1, 15, -1, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
|
||||
new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3)
|
||||
.condition(failIfGhostTypeCondition)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
|
||||
new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3)
|
||||
.attr(StatStageChangeAttr, [ Stat.ATK ], 1)
|
||||
@ -10217,6 +10222,7 @@ export function initMoves() {
|
||||
.attr(EatBerryAttr)
|
||||
.target(MoveTarget.ALL),
|
||||
new StatusMove(Moves.OCTOLOCK, Type.FIGHTING, 100, 15, -1, 0, 8)
|
||||
.condition(failIfGhostTypeCondition)
|
||||
.attr(AddBattlerTagAttr, BattlerTagType.OCTOLOCK, false, true, 1),
|
||||
new AttackMove(Moves.BOLT_BEAK, Type.ELECTRIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8)
|
||||
.attr(FirstAttackDoublePowerAttr),
|
||||
|
@ -1730,8 +1730,14 @@ const modifierPool: ModifierPool = {
|
||||
new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => {
|
||||
const { gameMode, gameData } = party[0].scene;
|
||||
if (gameMode.isDaily || (!gameMode.isFreshStartChallenge() && gameData.isUnlocked(Unlockables.EVIOLITE))) {
|
||||
return party.some(p => ((p.getSpeciesForm(true).speciesId in pokemonEvolutions) || (p.isFusion() && (p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)))
|
||||
&& !p.getHeldItems().some(i => i instanceof EvolutionStatBoosterModifier) && !p.isMax()) ? 10 : 0;
|
||||
return party.some(p => {
|
||||
// Check if Pokemon's species (or fusion species, if applicable) can evolve or if they're G-Max'd
|
||||
if (!p.isMax() && ((p.getSpeciesForm(true).speciesId in pokemonEvolutions) || (p.isFusion() && (p.getFusionSpeciesForm(true).speciesId in pokemonEvolutions)))) {
|
||||
// Check if Pokemon is already holding an Eviolite
|
||||
return !p.getHeldItems().some(i => i.type.id === "EVIOLITE");
|
||||
}
|
||||
return false;
|
||||
}) ? 10 : 0;
|
||||
}
|
||||
return 0;
|
||||
}),
|
||||
|
85
src/test/abilities/stakeout.test.ts
Normal file
85
src/test/abilities/stakeout.test.ts
Normal file
@ -0,0 +1,85 @@
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { isBetween } from "#app/utils";
|
||||
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("Abilities - Stakeout", () => {
|
||||
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.SURF ])
|
||||
.ability(Abilities.STAKEOUT)
|
||||
.battleType("single")
|
||||
.disableCrits()
|
||||
.startingLevel(100)
|
||||
.enemyLevel(100)
|
||||
.enemySpecies(Species.SNORLAX)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.enemyMoveset([ Moves.SPLASH, Moves.FLIP_TURN ])
|
||||
.startingWave(5);
|
||||
});
|
||||
|
||||
it("should do double damage to a pokemon that switched out", async () => {
|
||||
await game.classicMode.startBattle([ Species.MILOTIC ]);
|
||||
|
||||
const [ enemy1, ] = game.scene.getEnemyParty();
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
const damage1 = enemy1.getInverseHp();
|
||||
enemy1.hp = enemy1.getMaxHp();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
game.forceEnemyToSwitch();
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
game.forceEnemyToSwitch();
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy1.isFainted()).toBe(false);
|
||||
expect(isBetween(enemy1.getInverseHp(), (damage1 * 2) - 5, (damage1 * 2) + 5)).toBe(true);
|
||||
});
|
||||
|
||||
it("should do double damage to a pokemon that switched out via U-Turn/etc", async () => {
|
||||
await game.classicMode.startBattle([ Species.MILOTIC ]);
|
||||
|
||||
const [ enemy1, ] = game.scene.getEnemyParty();
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.forceEnemyMove(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
const damage1 = enemy1.getInverseHp();
|
||||
enemy1.hp = enemy1.getMaxHp();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.forceEnemyMove(Moves.FLIP_TURN);
|
||||
await game.toNextTurn();
|
||||
|
||||
game.move.select(Moves.SURF);
|
||||
await game.forceEnemyMove(Moves.FLIP_TURN);
|
||||
await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemy1.isFainted()).toBe(false);
|
||||
expect(isBetween(enemy1.getInverseHp(), (damage1 * 2) - 5, (damage1 * 2) + 5)).toBe(true);
|
||||
});
|
||||
});
|
@ -1,9 +1,8 @@
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import { BattlerTag, BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags";
|
||||
import { BattlerTagLapseType, OctolockTag, TrappedTag } from "#app/data/battler-tags";
|
||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
import { BattlerTagType } from "#app/enums/battler-tag-type";
|
||||
import { Stat } from "#enums/stat";
|
||||
|
||||
vi.mock("#app/battle-scene.js");
|
||||
@ -33,30 +32,4 @@ describe("BattlerTag - OctolockTag", () => {
|
||||
it ("traps its target (extends TrappedTag)", async () => {
|
||||
expect(new OctolockTag(1)).toBeInstanceOf(TrappedTag);
|
||||
});
|
||||
|
||||
it("can be added to pokemon who are not octolocked", async => {
|
||||
const mockPokemon = {
|
||||
getTag: vi.fn().mockReturnValue(undefined) as Pokemon["getTag"],
|
||||
} as Pokemon;
|
||||
|
||||
const subject = new OctolockTag(1);
|
||||
|
||||
expect(subject.canAdd(mockPokemon)).toBeTruthy();
|
||||
|
||||
expect(mockPokemon.getTag).toHaveBeenCalledTimes(1);
|
||||
expect(mockPokemon.getTag).toHaveBeenCalledWith(BattlerTagType.OCTOLOCK);
|
||||
});
|
||||
|
||||
it("cannot be added to pokemon who are octolocked", async => {
|
||||
const mockPokemon = {
|
||||
getTag: vi.fn().mockReturnValue(new BattlerTag(null!, null!, null!, null!)) as Pokemon["getTag"],
|
||||
} as Pokemon;
|
||||
|
||||
const subject = new OctolockTag(1);
|
||||
|
||||
expect(subject.canAdd(mockPokemon)).toBeFalsy();
|
||||
|
||||
expect(mockPokemon.getTag).toHaveBeenCalledTimes(1);
|
||||
expect(mockPokemon.getTag).toHaveBeenCalledWith(BattlerTagType.OCTOLOCK);
|
||||
});
|
||||
});
|
||||
|
@ -1,11 +1,8 @@
|
||||
import { Stat } from "#enums/stat";
|
||||
import { TrappedTag } from "#app/data/battler-tags";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { MoveEndPhase } from "#app/phases/move-end-phase";
|
||||
import { TurnInitPhase } from "#app/phases/turn-init-phase";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { Stat } from "#enums/stat";
|
||||
import GameManager from "#test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
|
||||
@ -27,12 +24,13 @@ describe("Moves - Octolock", () => {
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
game.override.battleType("single")
|
||||
.enemySpecies(Species.RATTATA)
|
||||
game.override
|
||||
.battleType("single")
|
||||
.enemySpecies(Species.MAGIKARP)
|
||||
.enemyMoveset(Moves.SPLASH)
|
||||
.enemyAbility(Abilities.BALL_FETCH)
|
||||
.startingLevel(2000)
|
||||
.moveset([ Moves.OCTOLOCK, Moves.SPLASH ])
|
||||
.moveset([ Moves.OCTOLOCK, Moves.SPLASH, Moves.TRICK_OR_TREAT ])
|
||||
.ability(Abilities.BALL_FETCH);
|
||||
});
|
||||
|
||||
@ -43,16 +41,15 @@ describe("Moves - Octolock", () => {
|
||||
|
||||
// use Octolock and advance to init phase of next turn to check for stat changes
|
||||
game.move.select(Moves.OCTOLOCK);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
|
||||
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
|
||||
|
||||
// take a second turn to make sure stat changes occur again
|
||||
await game.phaseInterceptor.to(CommandPhase);
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2);
|
||||
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-2);
|
||||
});
|
||||
@ -65,7 +62,7 @@ describe("Moves - Octolock", () => {
|
||||
|
||||
// use Octolock and advance to init phase of next turn to check for stat changes
|
||||
game.move.select(Moves.OCTOLOCK);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
|
||||
@ -79,7 +76,7 @@ describe("Moves - Octolock", () => {
|
||||
|
||||
// use Octolock and advance to init phase of next turn to check for stat changes
|
||||
game.move.select(Moves.OCTOLOCK);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
|
||||
@ -93,7 +90,7 @@ describe("Moves - Octolock", () => {
|
||||
|
||||
// use Octolock and advance to init phase of next turn to check for stat changes
|
||||
game.move.select(Moves.OCTOLOCK);
|
||||
await game.phaseInterceptor.to(TurnInitPhase);
|
||||
await game.toNextTurn();
|
||||
|
||||
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
|
||||
@ -110,7 +107,44 @@ describe("Moves - Octolock", () => {
|
||||
game.move.select(Moves.OCTOLOCK);
|
||||
|
||||
// after Octolock - enemy should be trapped
|
||||
await game.phaseInterceptor.to(MoveEndPhase);
|
||||
await game.phaseInterceptor.to("MoveEndPhase");
|
||||
expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not work on ghost type pokemon", async () => {
|
||||
game.override.enemyMoveset(Moves.OCTOLOCK);
|
||||
await game.classicMode.startBattle([ Species.GASTLY ]);
|
||||
|
||||
const playerPokemon = game.scene.getPlayerPokemon()!;
|
||||
|
||||
// before Octolock - player should not be trapped
|
||||
expect(playerPokemon.findTag(t => t instanceof TrappedTag)).toBeUndefined();
|
||||
|
||||
game.move.select(Moves.SPLASH);
|
||||
await game.toNextTurn();
|
||||
|
||||
// after Octolock - player should still not be trapped, and no stat loss
|
||||
expect(playerPokemon.findTag(t => t instanceof TrappedTag)).toBeUndefined();
|
||||
expect(playerPokemon.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(playerPokemon.getStatStage(Stat.SPDEF)).toBe(0);
|
||||
});
|
||||
|
||||
it("does not work on pokemon with added ghost type via Trick-or-Treat", async () => {
|
||||
await game.classicMode.startBattle([ Species.FEEBAS ]);
|
||||
|
||||
const enemy = game.scene.getEnemyPokemon()!;
|
||||
|
||||
// before Octolock - pokemon should not be trapped
|
||||
expect(enemy.findTag(t => t instanceof TrappedTag)).toBeUndefined();
|
||||
|
||||
game.move.select(Moves.TRICK_OR_TREAT);
|
||||
await game.toNextTurn();
|
||||
game.move.select(Moves.OCTOLOCK);
|
||||
await game.toNextTurn();
|
||||
|
||||
// after Octolock - pokemon should still not be trapped, and no stat loss
|
||||
expect(enemy.findTag(t => t instanceof TrappedTag)).toBeUndefined();
|
||||
expect(enemy.getStatStage(Stat.DEF)).toBe(0);
|
||||
expect(enemy.getStatStage(Stat.SPDEF)).toBe(0);
|
||||
});
|
||||
});
|
||||
|
@ -313,6 +313,11 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container {
|
||||
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
|
||||
const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0));
|
||||
this.pokemonShinyNewIcon.setVisible(!!newShinyOrVariant);
|
||||
} else if ((caughtAttr & DexAttr.NON_SHINY) === BigInt(0) && ((caughtAttr & DexAttr.SHINY) === DexAttr.SHINY)) { //If the player has *only* caught any shiny variant of this species, not a non-shiny
|
||||
this.pokemonShinyNewIcon.setVisible(true);
|
||||
this.pokemonShinyNewIcon.setText("(+)");
|
||||
this.pokemonShinyNewIcon.setColor(getTextColor(TextStyle.SUMMARY_BLUE, false, this.scene.uiTheme));
|
||||
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
|
||||
} else {
|
||||
this.pokemonShinyNewIcon.setVisible(false);
|
||||
}
|
||||
|
@ -51,6 +51,7 @@ import { Abilities } from "#enums/abilities";
|
||||
import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters";
|
||||
import { BooleanHolder, capitalizeString, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils";
|
||||
import type { Nature } from "#enums/nature";
|
||||
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
|
||||
|
||||
export type StarterSelectCallback = (starters: Starter[]) => void;
|
||||
|
||||
@ -1462,7 +1463,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler {
|
||||
|
||||
const currentPartyValue = this.starterSpecies.map(s => s.generation).reduce((total: number, gen: number, i: number) => total += this.scene.gameData.getSpeciesStarterValue(this.starterSpecies[i].speciesId), 0);
|
||||
const newCost = this.scene.gameData.getSpeciesStarterValue(this.lastSpecies.speciesId);
|
||||
if (!isDupe && isValidForChallenge.value && currentPartyValue + newCost <= this.getValueLimit() && this.starterSpecies.length < 6) { // this checks to make sure the pokemon doesn't exist in your party, it's valid for the challenge and that it won't go over the cost limit; if it meets all these criteria it will add it to your party
|
||||
if (!isDupe && isValidForChallenge.value && currentPartyValue + newCost <= this.getValueLimit() && this.starterSpecies.length < PLAYER_PARTY_MAX_SIZE) { // this checks to make sure the pokemon doesn't exist in your party, it's valid for the challenge and that it won't go over the cost limit; if it meets all these criteria it will add it to your party
|
||||
options = [
|
||||
{
|
||||
label: i18next.t("starterSelectUiHandler:addToParty"),
|
||||
|
Loading…
Reference in New Issue
Block a user