Merge branch 'beta' into linger-arena-trap-fix

This commit is contained in:
Moka 2024-11-30 20:28:54 +01:00 committed by GitHub
commit 9a4cf6bc6a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 181 additions and 67 deletions

View File

@ -5953,7 +5953,7 @@ export function initAbilities() {
.bypassFaint() .bypassFaint()
.partial(), // Meteor form should protect against status effects and yawn .partial(), // Meteor form should protect against status effects and yawn
new Ability(Abilities.STAKEOUT, 7) 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) new Ability(Abilities.WATER_BUBBLE, 7)
.attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5)
.attr(MoveTypePowerBoostAbAttr, Type.WATER, 2) .attr(MoveTypePowerBoostAbAttr, Type.WATER, 2)

View File

@ -3,10 +3,10 @@ import { Species } from "#enums/species";
export const POKERUS_STARTER_COUNT = 5; export const POKERUS_STARTER_COUNT = 5;
// #region Friendship constants // #region Friendship constants
export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 2; export const CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER = 3;
export const FRIENDSHIP_GAIN_FROM_BATTLE = 2; export const FRIENDSHIP_GAIN_FROM_BATTLE = 3;
export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 5; export const FRIENDSHIP_GAIN_FROM_RARE_CANDY = 6;
export const FRIENDSHIP_LOSS_FROM_FAINT = 10; export const FRIENDSHIP_LOSS_FROM_FAINT = 5;
/** /**
* Function to get the cumulative friendship threshold at which a candy is earned * Function to get the cumulative friendship threshold at which a candy is earned
@ -16,19 +16,19 @@ export const FRIENDSHIP_LOSS_FROM_FAINT = 10;
export function getStarterValueFriendshipCap(starterCost: number): number { export function getStarterValueFriendshipCap(starterCost: number): number {
switch (starterCost) { switch (starterCost) {
case 1: case 1:
return 20; return 25;
case 2: case 2:
return 40; return 50;
case 3: case 3:
return 60; return 75;
case 4: case 4:
return 100; return 100;
case 5: case 5:
return 140; return 150;
case 6: case 6:
return 200; return 200;
case 7: case 7:
return 280; return 300;
case 8: case 8:
case 9: case 9:
return 450; return 450;

View File

@ -1085,10 +1085,6 @@ export class OctolockTag extends TrappedTag {
super(BattlerTagType.OCTOLOCK, BattlerTagLapseType.TURN_END, 1, Moves.OCTOLOCK, sourceId); 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 { lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType); const shouldLapse = lapseType !== BattlerTagLapseType.CUSTOM || super.lapse(pokemon, lapseType);

View File

@ -7541,6 +7541,8 @@ const failIfLastInPartyCondition: MoveConditionFunc = (user: Pokemon, target: Po
return party.some(pokemon => pokemon.isActive() && !pokemon.isOnField()); 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; export type MoveAttrFilter = (attr: MoveAttr) => boolean;
function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> { function applyMoveAttrsInternal(attrFilter: MoveAttrFilter, user: Pokemon | null, target: Pokemon | null, move: Move, args: any[]): Promise<void> {
@ -8287,6 +8289,7 @@ export function initMoves() {
new AttackMove(Moves.THIEF, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2) new AttackMove(Moves.THIEF, Type.DARK, MoveCategory.PHYSICAL, 60, 100, 25, -1, 0, 2)
.attr(StealHeldItemChanceAttr, 0.3), .attr(StealHeldItemChanceAttr, 0.3),
new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2) new StatusMove(Moves.SPIDER_WEB, Type.BUG, -1, 10, -1, 0, 2)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2) new StatusMove(Moves.MIND_READER, Type.NORMAL, -1, 5, -1, 0, 2)
.attr(IgnoreAccuracyAttr), .attr(IgnoreAccuracyAttr),
@ -8423,6 +8426,7 @@ export function initMoves() {
new AttackMove(Moves.STEEL_WING, Type.STEEL, MoveCategory.PHYSICAL, 70, 90, 25, 10, 0, 2) new AttackMove(Moves.STEEL_WING, Type.STEEL, MoveCategory.PHYSICAL, 70, 90, 25, 10, 0, 2)
.attr(StatStageChangeAttr, [ Stat.DEF ], 1, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 1, true),
new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2) new StatusMove(Moves.MEAN_LOOK, Type.NORMAL, -1, 5, -1, 0, 2)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2) new StatusMove(Moves.ATTRACT, Type.NORMAL, 100, 15, -1, 0, 2)
.attr(AddBattlerTagAttr, BattlerTagType.INFATUATED) .attr(AddBattlerTagAttr, BattlerTagType.INFATUATED)
@ -8802,6 +8806,7 @@ export function initMoves() {
new SelfStatusMove(Moves.IRON_DEFENSE, Type.STEEL, -1, 15, -1, 0, 3) new SelfStatusMove(Moves.IRON_DEFENSE, Type.STEEL, -1, 15, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.DEF ], 2, true), .attr(StatStageChangeAttr, [ Stat.DEF ], 2, true),
new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3) new StatusMove(Moves.BLOCK, Type.NORMAL, -1, 5, -1, 0, 3)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.TRAPPED, false, true, 1),
new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3) new StatusMove(Moves.HOWL, Type.NORMAL, -1, 40, -1, 0, 3)
.attr(StatStageChangeAttr, [ Stat.ATK ], 1) .attr(StatStageChangeAttr, [ Stat.ATK ], 1)
@ -10096,6 +10101,7 @@ export function initMoves() {
.attr(EatBerryAttr) .attr(EatBerryAttr)
.target(MoveTarget.ALL), .target(MoveTarget.ALL),
new StatusMove(Moves.OCTOLOCK, Type.FIGHTING, 100, 15, -1, 0, 8) new StatusMove(Moves.OCTOLOCK, Type.FIGHTING, 100, 15, -1, 0, 8)
.condition(failIfGhostTypeCondition)
.attr(AddBattlerTagAttr, BattlerTagType.OCTOLOCK, false, true, 1), .attr(AddBattlerTagAttr, BattlerTagType.OCTOLOCK, false, true, 1),
new AttackMove(Moves.BOLT_BEAK, Type.ELECTRIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8) new AttackMove(Moves.BOLT_BEAK, Type.ELECTRIC, MoveCategory.PHYSICAL, 85, 100, 10, -1, 0, 8)
.attr(FirstAttackDoublePowerAttr), .attr(FirstAttackDoublePowerAttr),

View File

@ -276,6 +276,8 @@ export const ClowningAroundEncounter: MysteryEncounter =
generateItemsOfTier(scene, mostHeldItemsPokemon, numBerries, "Berries"); generateItemsOfTier(scene, mostHeldItemsPokemon, numBerries, "Berries");
// Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm)
// For the purpose of this ME, Soothe Bells and Lucky Eggs are counted as Ultra tier
// And Golden Eggs as Rogue tier
let numUltra = 0; let numUltra = 0;
let numRogue = 0; let numRogue = 0;
items.filter(m => m.isTransferable && !(m instanceof BerryModifier)) items.filter(m => m.isTransferable && !(m instanceof BerryModifier))
@ -285,7 +287,7 @@ export const ClowningAroundEncounter: MysteryEncounter =
if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) {
numRogue += m.stackCount; numRogue += m.stackCount;
scene.removeModifier(m); scene.removeModifier(m);
} else if (type.id === "LUCKY_EGG" || tier === ModifierTier.ULTRA) { } else if (type.id === "LUCKY_EGG" || type.id === "SOOTHE_BELL" || tier === ModifierTier.ULTRA) {
numUltra += m.stackCount; numUltra += m.stackCount;
scene.removeModifier(m); scene.removeModifier(m);
} }
@ -456,7 +458,6 @@ function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItem
[ modifierTypes.LEFTOVERS, 4 ], [ modifierTypes.LEFTOVERS, 4 ],
[ modifierTypes.SHELL_BELL, 4 ], [ modifierTypes.SHELL_BELL, 4 ],
[ modifierTypes.SOUL_DEW, 10 ], [ modifierTypes.SOUL_DEW, 10 ],
[ modifierTypes.SOOTHE_BELL, 3 ],
[ modifierTypes.SCOPE_LENS, 1 ], [ modifierTypes.SCOPE_LENS, 1 ],
[ modifierTypes.BATON, 1 ], [ modifierTypes.BATON, 1 ],
[ modifierTypes.FOCUS_BAND, 5 ], [ modifierTypes.FOCUS_BAND, 5 ],

View File

@ -1702,7 +1702,8 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.EVOLUTION_ITEM, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.EVOLUTION_ITEM, (party: Pokemon[]) => {
return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8); return Math.min(Math.ceil(party[0].scene.currentBattle.waveIndex / 15), 8);
}, 8), }, 8),
new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 1 : 0, 1), new WeightedModifierType(modifierTypes.MAP, (party: Pokemon[]) => party[0].scene.gameMode.isClassic && party[0].scene.currentBattle.waveIndex < 180 ? 2 : 0, 2),
new WeightedModifierType(modifierTypes.SOOTHE_BELL, 2),
new WeightedModifierType(modifierTypes.TM_GREAT, 3), new WeightedModifierType(modifierTypes.TM_GREAT, 3),
new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.MEMORY_MUSHROOM, (party: Pokemon[]) => {
if (!party.find(p => p.getLearnableLevelMoves().length)) { if (!party.find(p => p.getLearnableLevelMoves().length)) {
@ -1730,8 +1731,14 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => { new WeightedModifierType(modifierTypes.EVIOLITE, (party: Pokemon[]) => {
const { gameMode, gameData } = party[0].scene; const { gameMode, gameData } = party[0].scene;
if (gameMode.isDaily || (!gameMode.isFreshStartChallenge() && gameData.isUnlocked(Unlockables.EVIOLITE))) { 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))) return party.some(p => {
&& !p.getHeldItems().some(i => i instanceof EvolutionStatBoosterModifier) && !p.isMax()) ? 10 : 0; // 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; return 0;
}), }),
@ -1794,7 +1801,6 @@ const modifierPool: ModifierPool = {
new WeightedModifierType(modifierTypes.SOUL_DEW, 7), new WeightedModifierType(modifierTypes.SOUL_DEW, 7),
//new WeightedModifierType(modifierTypes.OVAL_CHARM, 6), //new WeightedModifierType(modifierTypes.OVAL_CHARM, 6),
new WeightedModifierType(modifierTypes.CATCHING_CHARM, (party: Pokemon[]) => !party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.getSpeciesCount(d => !!d.caughtAttr) > 100 ? 4 : 0, 4), new WeightedModifierType(modifierTypes.CATCHING_CHARM, (party: Pokemon[]) => !party[0].scene.gameMode.isFreshStartChallenge() && party[0].scene.gameData.getSpeciesCount(d => !!d.caughtAttr) > 100 ? 4 : 0, 4),
new WeightedModifierType(modifierTypes.SOOTHE_BELL, 4),
new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)), new WeightedModifierType(modifierTypes.ABILITY_CHARM, skipInClassicAfterWave(189, 6)),
new WeightedModifierType(modifierTypes.FOCUS_BAND, 5), new WeightedModifierType(modifierTypes.FOCUS_BAND, 5),
new WeightedModifierType(modifierTypes.KINGS_ROCK, 3), new WeightedModifierType(modifierTypes.KINGS_ROCK, 3),

View 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);
});
});

View File

@ -1,9 +1,8 @@
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { describe, expect, it, vi } from "vitest"; import { describe, expect, it, vi } from "vitest";
import Pokemon from "#app/field/pokemon"; 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 { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { BattlerTagType } from "#app/enums/battler-tag-type";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
vi.mock("#app/battle-scene.js"); vi.mock("#app/battle-scene.js");
@ -33,30 +32,4 @@ describe("BattlerTag - OctolockTag", () => {
it ("traps its target (extends TrappedTag)", async () => { it ("traps its target (extends TrappedTag)", async () => {
expect(new OctolockTag(1)).toBeInstanceOf(TrappedTag); 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);
});
}); });

View File

@ -1,11 +1,8 @@
import { Stat } from "#enums/stat";
import { TrappedTag } from "#app/data/battler-tags"; 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 { Abilities } from "#enums/abilities";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Stat } from "#enums/stat";
import GameManager from "#test/utils/gameManager"; import GameManager from "#test/utils/gameManager";
import Phaser from "phaser"; import Phaser from "phaser";
import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest";
@ -27,12 +24,13 @@ describe("Moves - Octolock", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
game.override.battleType("single") game.override
.enemySpecies(Species.RATTATA) .battleType("single")
.enemySpecies(Species.MAGIKARP)
.enemyMoveset(Moves.SPLASH) .enemyMoveset(Moves.SPLASH)
.enemyAbility(Abilities.BALL_FETCH) .enemyAbility(Abilities.BALL_FETCH)
.startingLevel(2000) .startingLevel(2000)
.moveset([ Moves.OCTOLOCK, Moves.SPLASH ]) .moveset([ Moves.OCTOLOCK, Moves.SPLASH, Moves.TRICK_OR_TREAT ])
.ability(Abilities.BALL_FETCH); .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 // use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase); await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-1);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1);
// take a second turn to make sure stat changes occur again // take a second turn to make sure stat changes occur again
await game.phaseInterceptor.to(CommandPhase);
game.move.select(Moves.SPLASH); game.move.select(Moves.SPLASH);
await game.toNextTurn();
await game.phaseInterceptor.to(TurnInitPhase);
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(-2);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).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 // use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase); await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(-1); 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 // use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase); await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).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 // use Octolock and advance to init phase of next turn to check for stat changes
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
await game.phaseInterceptor.to(TurnInitPhase); await game.toNextTurn();
expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.DEF)).toBe(0);
expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0); expect(enemyPokemon.getStatStage(Stat.SPDEF)).toBe(0);
@ -110,7 +107,44 @@ describe("Moves - Octolock", () => {
game.move.select(Moves.OCTOLOCK); game.move.select(Moves.OCTOLOCK);
// after Octolock - enemy should be trapped // after Octolock - enemy should be trapped
await game.phaseInterceptor.to(MoveEndPhase); await game.phaseInterceptor.to("MoveEndPhase");
expect(enemyPokemon.findTag(t => t instanceof TrappedTag)).toBeDefined(); 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);
});
}); });

View File

@ -266,6 +266,9 @@ describe("Clowning Around - Mystery Encounter", () => {
// 5 Lucky Egg on lead (ultra) // 5 Lucky Egg on lead (ultra)
itemType = generateModifierType(scene, modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType; itemType = generateModifierType(scene, modifierTypes.LUCKY_EGG) as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType);
// 3 Soothe Bell on lead (great tier, but counted as ultra by this ME)
itemType = generateModifierType(scene, modifierTypes.SOOTHE_BELL) as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getPlayerParty()[0], 3, itemType);
// 5 Soul Dew on lead (rogue) // 5 Soul Dew on lead (rogue)
itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType; itemType = generateModifierType(scene, modifierTypes.SOUL_DEW) as PokemonHeldItemModifierType;
await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType); await addItemToPokemon(scene, scene.getPlayerParty()[0], 5, itemType);
@ -286,7 +289,7 @@ describe("Clowning Around - Mystery Encounter", () => {
const rogueCountAfter = leadItemsAfter const rogueCountAfter = leadItemsAfter
.filter(m => m.type.tier === ModifierTier.ROGUE) .filter(m => m.type.tier === ModifierTier.ROGUE)
.reduce((a, b) => a + b.stackCount, 0); .reduce((a, b) => a + b.stackCount, 0);
expect(ultraCountAfter).toBe(10); expect(ultraCountAfter).toBe(13);
expect(rogueCountAfter).toBe(7); expect(rogueCountAfter).toBe(7);
const secondItemsAfter = scene.getPlayerParty()[1].getHeldItems(); const secondItemsAfter = scene.getPlayerParty()[1].getHeldItems();

View File

@ -18,6 +18,7 @@ import { TheExpertPokemonBreederEncounter } from "#app/data/mystery-encounters/e
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { EggTier } from "#enums/egg-type"; import { EggTier } from "#enums/egg-type";
import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
import { FRIENDSHIP_GAIN_FROM_BATTLE } from "#app/data/balance/starters";
const namespace = "mysteryEncounters/theExpertPokemonBreeder"; const namespace = "mysteryEncounters/theExpertPokemonBreeder";
const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ]; const defaultParty = [ Species.LAPRAS, Species.GENGAR, Species.ABRA ];
@ -182,7 +183,10 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
await game.phaseInterceptor.to(PostMysteryEncounterPhase); await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship; const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon1.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle // 20 from ME + extra from winning battle (that extra is not accurate to what happens in game.
// The Pokemon normally gets FRIENDSHIP_GAIN_FROM_BATTLE 3 times, once for each defeated Pokemon
// but due to how skipBattleRunMysteryEncounterRewardsPhase is implemented, it only receives it once)
expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE);
}); });
}); });
@ -261,7 +265,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
await game.phaseInterceptor.to(PostMysteryEncounterPhase); await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship; const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon2.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE); // 20 from ME + extra for friendship gained from winning battle
}); });
}); });
@ -340,7 +344,7 @@ describe("The Expert Pokémon Breeder - Mystery Encounter", () => {
await game.phaseInterceptor.to(PostMysteryEncounterPhase); await game.phaseInterceptor.to(PostMysteryEncounterPhase);
const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship; const friendshipAfter = scene.currentBattle.mysteryEncounter!.misc.pokemon3.friendship;
expect(friendshipAfter).toBe(friendshipBefore + 20 + 2); // +2 extra for friendship gained from winning battle expect(friendshipAfter).toBe(friendshipBefore + 20 + FRIENDSHIP_GAIN_FROM_BATTLE); // 20 + extra for friendship gained from winning battle
}); });
}); });
}); });

View File

@ -313,6 +313,11 @@ export default class PokemonInfoContainer extends Phaser.GameObjects.Container {
this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme)); this.pokemonShinyNewIcon.setShadowColor(getTextColor(TextStyle.SUMMARY_BLUE, true, this.scene.uiTheme));
const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0)); const newShinyOrVariant = ((newShiny & caughtAttr) === BigInt(0)) || ((newVariant & caughtAttr) === BigInt(0));
this.pokemonShinyNewIcon.setVisible(!!newShinyOrVariant); 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 { } else {
this.pokemonShinyNewIcon.setVisible(false); this.pokemonShinyNewIcon.setVisible(false);
} }

View File

@ -51,6 +51,7 @@ import { Abilities } from "#enums/abilities";
import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters"; import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCandyCounts } from "#app/data/balance/starters";
import { BooleanHolder, capitalizeString, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils"; import { BooleanHolder, capitalizeString, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils";
import type { Nature } from "#enums/nature"; import type { Nature } from "#enums/nature";
import { PLAYER_PARTY_MAX_SIZE } from "#app/constants";
export type StarterSelectCallback = (starters: Starter[]) => void; 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 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); 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 = [ options = [
{ {
label: i18next.t("starterSelectUiHandler:addToParty"), label: i18next.t("starterSelectUiHandler:addToParty"),