Merge branch 'beta' into bugfix-egg-hatch-crash-missing-sprite

This commit is contained in:
Moka 2024-12-01 18:34:24 +01:00 committed by GitHub
commit 52a22932bb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 186 additions and 260 deletions

View File

@ -5713,9 +5713,7 @@ export function initAbilities() {
.condition(getSheerForceHitDisableAbCondition()), .condition(getSheerForceHitDisableAbCondition()),
new Ability(Abilities.SHEER_FORCE, 5) new Ability(Abilities.SHEER_FORCE, 5)
.attr(MovePowerBoostAbAttr, (user, target, move) => move.chance >= 1, 5461 / 4096) .attr(MovePowerBoostAbAttr, (user, target, move) => move.chance >= 1, 5461 / 4096)
.attr(MoveEffectChanceMultiplierAbAttr, 0) .attr(MoveEffectChanceMultiplierAbAttr, 0), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented
.edgeCase() // Should disable shell bell and Meloetta's relic song transformation
.edgeCase(), // Should disable life orb, eject button, red card, kee/maranga berry if they get implemented
new Ability(Abilities.CONTRARY, 5) new Ability(Abilities.CONTRARY, 5)
.attr(StatStageChangeMultiplierAbAttr, -1) .attr(StatStageChangeMultiplierAbAttr, -1)
.ignorable(), .ignorable(),

View File

@ -16,9 +16,9 @@ interface PokemonSpeciesFormLevelMoves {
} }
/** Moves that can only be learned with a memory-mushroom */ /** Moves that can only be learned with a memory-mushroom */
const RELEARN_MOVE = -1; export const RELEARN_MOVE = -1;
/** Moves that can only be learned with an evolve */ /** Moves that can only be learned with an evolve */
const EVOLVE_MOVE = 0; export const EVOLVE_MOVE = 0;
export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = { export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = {
[Species.BULBASAUR]: [ [Species.BULBASAUR]: [

View File

@ -351,6 +351,10 @@ export class MeloettaFormChangePostMoveTrigger extends SpeciesFormChangePostMove
if (pokemon.scene.gameMode.hasChallenge(Challenges.SINGLE_TYPE)) { if (pokemon.scene.gameMode.hasChallenge(Challenges.SINGLE_TYPE)) {
return false; return false;
} else { } else {
// Meloetta will not transform if it has the ability Sheer Force when using Relic Song
if (pokemon.hasAbility(Abilities.SHEER_FORCE)) {
return false;
}
return super.canChange(pokemon); return super.canChange(pokemon);
} }
} }

View File

@ -29,7 +29,7 @@ import { BattlerIndex } from "#app/battle";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import PartyUiHandler, { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import { LevelMoves } from "#app/data/balance/pokemon-level-moves"; import { EVOLVE_MOVE, LevelMoves, RELEARN_MOVE } from "#app/data/balance/pokemon-level-moves";
import { DamageAchv, achvs } from "#app/system/achv"; import { DamageAchv, achvs } from "#app/system/achv";
import { DexAttr, StarterDataEntry, StarterMoveset } from "#app/system/game-data"; import { DexAttr, StarterDataEntry, StarterMoveset } from "#app/system/game-data";
import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities"; import { QuantizerCelebi, argbFromRgba, rgbaFromArgb } from "@material/material-color-utilities";
@ -71,6 +71,15 @@ import { Nature } from "#enums/nature";
import { StatusEffect } from "#enums/status-effect"; import { StatusEffect } from "#enums/status-effect";
import { doShinySparkleAnim } from "#app/field/anims"; import { doShinySparkleAnim } from "#app/field/anims";
export enum LearnMoveSituation {
MISC,
LEVEL_UP,
RELEARN,
EVOLUTION,
EVOLUTION_FUSED, // If fusionSpecies has Evolved
EVOLUTION_FUSED_BASE // If fusion's base species has Evolved
}
export enum FieldPosition { export enum FieldPosition {
CENTER, CENTER,
LEFT, LEFT,
@ -1817,40 +1826,44 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* @param {boolean} includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves * @param {boolean} includeRelearnerMoves Whether to include moves that would require a relearner. Note the move relearner inherently allows evolution moves
* @returns {LevelMoves} A list of moves and the levels they can be learned at * @returns {LevelMoves} A list of moves and the levels they can be learned at
*/ */
getLevelMoves(startingLevel?: integer, includeEvolutionMoves: boolean = false, simulateEvolutionChain: boolean = false, includeRelearnerMoves: boolean = false): LevelMoves { getLevelMoves(startingLevel?: integer, includeEvolutionMoves: boolean = false, simulateEvolutionChain: boolean = false, includeRelearnerMoves: boolean = false, learnSituation: LearnMoveSituation = LearnMoveSituation.MISC): LevelMoves {
const ret: LevelMoves = []; const ret: LevelMoves = [];
let levelMoves: LevelMoves = []; let levelMoves: LevelMoves = [];
if (!startingLevel) { if (!startingLevel) {
startingLevel = this.level; startingLevel = this.level;
} }
if (simulateEvolutionChain) { if (learnSituation === LearnMoveSituation.EVOLUTION_FUSED && this.fusionSpecies) { // For fusion evolutions, get ONLY the moves of the component mon that evolved
const evolutionChain = this.species.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer()); levelMoves = this.getFusionSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || (includeRelearnerMoves && lm[0] === RELEARN_MOVE) || lm[0] > 0);
for (let e = 0; e < evolutionChain.length; e++) {
// TODO: Might need to pass specific form index in simulated evolution chain
const speciesLevelMoves = getPokemonSpeciesForm(evolutionChain[e][0], this.formIndex).getLevelMoves();
if (includeRelearnerMoves) {
levelMoves.push(...speciesLevelMoves);
} else {
levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === evolutionChain.length - 1 || lm[0] <= evolutionChain[e + 1][1]))));
}
}
} else { } else {
levelMoves = this.getSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0);
}
if (this.fusionSpecies) {
if (simulateEvolutionChain) { if (simulateEvolutionChain) {
const fusionEvolutionChain = this.fusionSpecies.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer()); const evolutionChain = this.species.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer());
for (let e = 0; e < fusionEvolutionChain.length; e++) { for (let e = 0; e < evolutionChain.length; e++) {
// TODO: Might need to pass specific form index in simulated evolution chain // TODO: Might need to pass specific form index in simulated evolution chain
const speciesLevelMoves = getPokemonSpeciesForm(fusionEvolutionChain[e][0], this.fusionFormIndex).getLevelMoves(); const speciesLevelMoves = getPokemonSpeciesForm(evolutionChain[e][0], this.formIndex).getLevelMoves();
if (includeRelearnerMoves) { if (includeRelearnerMoves) {
levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || lm[0] !== 0)); levelMoves.push(...speciesLevelMoves);
} else { } else {
levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === 0) || ((!e || lm[0] > 1) && (e === fusionEvolutionChain.length - 1 || lm[0] <= fusionEvolutionChain[e + 1][1])))); levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || ((!e || lm[0] > 1) && (e === evolutionChain.length - 1 || lm[0] <= evolutionChain[e + 1][1]))));
} }
} }
} else { } else {
levelMoves.push(...this.getFusionSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === 0) || (includeRelearnerMoves && lm[0] === -1) || lm[0] > 0)); levelMoves = this.getSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || (includeRelearnerMoves && lm[0] === RELEARN_MOVE) || lm[0] > 0);
}
if (this.fusionSpecies && learnSituation !== LearnMoveSituation.EVOLUTION_FUSED_BASE) { // For fusion evolutions, get ONLY the moves of the component mon that evolved
if (simulateEvolutionChain) {
const fusionEvolutionChain = this.fusionSpecies.getSimulatedEvolutionChain(this.level, this.hasTrainer(), this.isBoss(), this.isPlayer());
for (let e = 0; e < fusionEvolutionChain.length; e++) {
// TODO: Might need to pass specific form index in simulated evolution chain
const speciesLevelMoves = getPokemonSpeciesForm(fusionEvolutionChain[e][0], this.fusionFormIndex).getLevelMoves();
if (includeRelearnerMoves) {
levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || lm[0] !== EVOLVE_MOVE));
} else {
levelMoves.push(...speciesLevelMoves.filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || ((!e || lm[0] > 1) && (e === fusionEvolutionChain.length - 1 || lm[0] <= fusionEvolutionChain[e + 1][1]))));
}
}
} else {
levelMoves.push(...this.getFusionSpeciesForm(true).getLevelMoves().filter(lm => (includeEvolutionMoves && lm[0] === EVOLVE_MOVE) || (includeRelearnerMoves && lm[0] === RELEARN_MOVE) || lm[0] > 0));
}
} }
} }
levelMoves.sort((lma: [integer, integer], lmb: [integer, integer]) => lma[0] > lmb[0] ? 1 : lma[0] < lmb[0] ? -1 : 0); levelMoves.sort((lma: [integer, integer], lmb: [integer, integer]) => lma[0] > lmb[0] ? 1 : lma[0] < lmb[0] ? -1 : 0);
@ -4279,28 +4292,29 @@ export class PlayerPokemon extends Pokemon {
}); });
} }
addFriendship(friendship: integer): void { addFriendship(friendship: number): void {
const starterSpeciesId = this.species.getRootSpeciesId(); if (friendship > 0) {
const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0; const starterSpeciesId = this.species.getRootSpeciesId();
const starterData = [ const fusionStarterSpeciesId = this.isFusion() && this.fusionSpecies ? this.fusionSpecies.getRootSpeciesId() : 0;
this.scene.gameData.starterData[starterSpeciesId], const starterData = [
fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null this.scene.gameData.starterData[starterSpeciesId],
].filter(d => !!d); fusionStarterSpeciesId ? this.scene.gameData.starterData[fusionStarterSpeciesId] : null
const amount = new Utils.IntegerHolder(friendship); ].filter(d => !!d);
let candyFriendshipMultiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER; const amount = new Utils.NumberHolder(friendship);
if (this.scene.eventManager.isEventActive()) {
candyFriendshipMultiplier *= this.scene.eventManager.getFriendshipMultiplier();
}
const starterAmount = new Utils.IntegerHolder(Math.floor(friendship * (this.scene.gameMode.isClassic && friendship > 0 ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1)));
if (amount.value > 0) {
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount); this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, amount);
this.scene.applyModifier(PokemonFriendshipBoosterModifier, true, this, starterAmount); let candyFriendshipMultiplier = CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER;
if (this.scene.eventManager.isEventActive()) {
candyFriendshipMultiplier *= this.scene.eventManager.getFriendshipMultiplier();
}
const starterAmount = new Utils.NumberHolder(Math.floor(amount.value * (this.scene.gameMode.isClassic ? candyFriendshipMultiplier : 1) / (fusionStarterSpeciesId ? 2 : 1)));
// Add friendship to this PlayerPokemon
this.friendship = Math.min(this.friendship + amount.value, 255); this.friendship = Math.min(this.friendship + amount.value, 255);
if (this.friendship === 255) { if (this.friendship === 255) {
this.scene.validateAchv(achvs.MAX_FRIENDSHIP); this.scene.validateAchv(achvs.MAX_FRIENDSHIP);
} }
starterData.forEach((sd: StarterDataEntry, i: integer) => { // Add to candy progress for this mon's starter species and its fused species (if it has one)
starterData.forEach((sd: StarterDataEntry, i: number) => {
const speciesId = !i ? starterSpeciesId : fusionStarterSpeciesId as Species; const speciesId = !i ? starterSpeciesId : fusionStarterSpeciesId as Species;
sd.friendship = (sd.friendship || 0) + starterAmount.value; sd.friendship = (sd.friendship || 0) + starterAmount.value;
if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[speciesId])) { if (sd.friendship >= getStarterValueFriendshipCap(speciesStarterCosts[speciesId])) {
@ -4309,10 +4323,8 @@ export class PlayerPokemon extends Pokemon {
} }
}); });
} else { } else {
this.friendship = Math.max(this.friendship + amount.value, 0); // Lose friendship upon fainting
for (const sd of starterData) { this.friendship = Math.max(this.friendship + friendship, 0);
sd.friendship = Math.max((sd.friendship || 0) + starterAmount.value, 0);
}
} }
} }
/** /**

View File

@ -18,7 +18,6 @@ import type { VoucherType } from "#app/system/voucher";
import { Command } from "#app/ui/command-ui-handler"; import { Command } from "#app/ui/command-ui-handler";
import { addTextObject, TextStyle } from "#app/ui/text"; import { addTextObject, TextStyle } from "#app/ui/text";
import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } from "#app/utils"; import { BooleanHolder, hslToHex, isNullOrUndefined, NumberHolder, toDmgValue } from "#app/utils";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
@ -726,22 +725,6 @@ export abstract class PokemonHeldItemModifier extends PersistentModifier {
return 1; return 1;
} }
//Applies to items with chance of activating secondary effects ie Kings Rock
getSecondaryChanceMultiplier(pokemon: Pokemon): number {
// Temporary quickfix to stop game from freezing when the opponet uses u-turn while holding on to king's rock
if (!pokemon.getLastXMoves()[0]) {
return 1;
}
const sheerForceAffected = allMoves[pokemon.getLastXMoves()[0].move].chance >= 0 && pokemon.hasAbility(Abilities.SHEER_FORCE);
if (sheerForceAffected) {
return 0;
} else if (pokemon.hasAbility(Abilities.SERENE_GRACE)) {
return 2;
}
return 1;
}
getMaxStackCount(scene: BattleScene, forThreshold?: boolean): number { getMaxStackCount(scene: BattleScene, forThreshold?: boolean): number {
const pokemon = this.getPokemon(scene); const pokemon = this.getPokemon(scene);
if (!pokemon) { if (!pokemon) {
@ -1614,9 +1597,16 @@ export class BypassSpeedChanceModifier extends PokemonHeldItemModifier {
} }
} }
/**
* Class for Pokemon held items like King's Rock
* Because King's Rock can be stacked in PokeRogue, unlike mainline, it does not receive a boost from Abilities.SERENE_GRACE
*/
export class FlinchChanceModifier extends PokemonHeldItemModifier { export class FlinchChanceModifier extends PokemonHeldItemModifier {
private chance: number;
constructor(type: ModifierType, pokemonId: number, stackCount?: number) { constructor(type: ModifierType, pokemonId: number, stackCount?: number) {
super(type, pokemonId, stackCount); super(type, pokemonId, stackCount);
this.chance = 10;
} }
matchType(modifier: Modifier) { matchType(modifier: Modifier) {
@ -1644,7 +1634,8 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier {
* @returns `true` if {@linkcode FlinchChanceModifier} has been applied * @returns `true` if {@linkcode FlinchChanceModifier} has been applied
*/ */
override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean { override apply(pokemon: Pokemon, flinched: BooleanHolder): boolean {
if (!flinched.value && pokemon.randSeedInt(10) < (this.getStackCount() * this.getSecondaryChanceMultiplier(pokemon))) { // The check for pokemon.battleSummonData is to ensure that a crash doesn't occur when a Pokemon with King's Rock procs a flinch
if (pokemon.battleSummonData && !flinched.value && pokemon.randSeedInt(100) < (this.getStackCount() * this.chance)) {
flinched.value = true; flinched.value = true;
return true; return true;
} }
@ -1652,7 +1643,7 @@ export class FlinchChanceModifier extends PokemonHeldItemModifier {
return false; return false;
} }
getMaxHeldItemCount(pokemon: Pokemon): number { getMaxHeldItemCount(_pokemon: Pokemon): number {
return 3; return 3;
} }
} }

View File

@ -1,17 +1,18 @@
import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import { Phase } from "#app/phase"; import { Phase } from "#app/phase";
import BattleScene, { AnySound } from "#app/battle-scene"; import BattleScene, { AnySound } from "#app/battle-scene";
import { SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions"; import { FusionSpeciesFormEvolution, SpeciesFormEvolution } from "#app/data/balance/pokemon-evolutions";
import EvolutionSceneHandler from "#app/ui/evolution-scene-handler"; import EvolutionSceneHandler from "#app/ui/evolution-scene-handler";
import * as Utils from "#app/utils"; import * as Utils from "#app/utils";
import { Mode } from "#app/ui/ui"; import { Mode } from "#app/ui/ui";
import { cos, sin } from "#app/field/anims"; import { cos, sin } from "#app/field/anims";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import Pokemon, { LearnMoveSituation, PlayerPokemon } from "#app/field/pokemon";
import { getTypeRgb } from "#app/data/type"; import { getTypeRgb } from "#app/data/type";
import i18next from "i18next"; import i18next from "i18next";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { LearnMovePhase } from "#app/phases/learn-move-phase"; import { LearnMovePhase } from "#app/phases/learn-move-phase";
import { EndEvolutionPhase } from "#app/phases/end-evolution-phase"; import { EndEvolutionPhase } from "#app/phases/end-evolution-phase";
import { EVOLVE_MOVE } from "#app/data/balance/pokemon-level-moves";
export class EvolutionPhase extends Phase { export class EvolutionPhase extends Phase {
protected pokemon: PlayerPokemon; protected pokemon: PlayerPokemon;
@ -20,6 +21,7 @@ export class EvolutionPhase extends Phase {
private preEvolvedPokemonName: string; private preEvolvedPokemonName: string;
private evolution: SpeciesFormEvolution | null; private evolution: SpeciesFormEvolution | null;
private fusionSpeciesEvolved: boolean; // Whether the evolution is of the fused species
private evolutionBgm: AnySound; private evolutionBgm: AnySound;
private evolutionHandler: EvolutionSceneHandler; private evolutionHandler: EvolutionSceneHandler;
@ -39,6 +41,7 @@ export class EvolutionPhase extends Phase {
this.pokemon = pokemon; this.pokemon = pokemon;
this.evolution = evolution; this.evolution = evolution;
this.lastLevel = lastLevel; this.lastLevel = lastLevel;
this.fusionSpeciesEvolved = evolution instanceof FusionSpeciesFormEvolution;
} }
validate(): boolean { validate(): boolean {
@ -273,7 +276,8 @@ export class EvolutionPhase extends Phase {
this.evolutionHandler.canCancel = false; this.evolutionHandler.canCancel = false;
this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => { this.pokemon.evolve(this.evolution, this.pokemon.species).then(() => {
const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true); const learnSituation: LearnMoveSituation = this.fusionSpeciesEvolved ? LearnMoveSituation.EVOLUTION_FUSED : this.pokemon.fusionSpecies ? LearnMoveSituation.EVOLUTION_FUSED_BASE : LearnMoveSituation.EVOLUTION;
const levelMoves = this.pokemon.getLevelMoves(this.lastLevel + 1, true, false, false, learnSituation).filter(lm => lm[0] === EVOLVE_MOVE);
for (const lm of levelMoves) { for (const lm of levelMoves) {
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1])); this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.scene.getPlayerParty().indexOf(this.pokemon), lm[1]));
} }

View File

@ -1,15 +1,12 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, MoveEffectChanceMultiplierAbAttr } from "#app/data/ability";
import { Stat } from "#enums/stat";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import * as Utils from "#app/utils";
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 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 { allMoves } from "#app/data/move";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { FlinchAttr } from "#app/data/move";
describe("Abilities - Serene Grace", () => { describe("Abilities - Serene Grace", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -27,66 +24,26 @@ describe("Abilities - Serene Grace", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
const movesToUse = [ Moves.AIR_SLASH, Moves.TACKLE ]; game.override
game.override.battleType("single"); .battleType("single")
game.override.enemySpecies(Species.ONIX); .ability(Abilities.SERENE_GRACE)
game.override.startingLevel(100); .moveset([ Moves.AIR_SLASH, Moves.TACKLE ])
game.override.moveset(movesToUse); .enemyLevel(10)
game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); .enemyMoveset([ Moves.SPLASH ]);
}); });
it("Move chance without Serene Grace", async () => { it("Serene Grace should double the secondary effect chance of a move", async () => {
const moveToUse = Moves.AIR_SLASH; await game.classicMode.startBattle([ Species.SHUCKLE ]);
await game.startBattle([
Species.PIDGEOT
]);
const airSlashMove = allMoves[Moves.AIR_SLASH];
const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0];
vi.spyOn(airSlashFlinchAttr, "getMoveChance");
game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000; game.move.select(Moves.AIR_SLASH);
expect(game.scene.getPlayerParty()[0].formIndex).toBe(0);
game.move.select(moveToUse);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase");
// Check chance of Air Slash without Serene Grace expect(airSlashFlinchAttr.getMoveChance).toHaveLastReturnedWith(60);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; });
const move = phase.move.getMove();
expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance);
console.log(move.chance + " Their ability is " + phase.getUserPokemon()!.getAbility().name);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(30);
}, 20000);
it("Move chance with Serene Grace", async () => {
const moveToUse = Moves.AIR_SLASH;
game.override.ability(Abilities.SERENE_GRACE);
await game.startBattle([
Species.TOGEKISS
]);
game.scene.getEnemyParty()[0].stats[Stat.SPDEF] = 10000;
expect(game.scene.getPlayerParty()[0].formIndex).toBe(0);
game.move.select(moveToUse);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
// Check chance of Air Slash with Serene Grace
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
expect(move.id).toBe(Moves.AIR_SLASH);
const chance = new Utils.IntegerHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
expect(chance.value).toBe(60);
}, 20000);
//TODO King's Rock Interaction Unit Test
}); });

View File

@ -1,15 +1,13 @@
import { BattlerIndex } from "#app/battle"; import { BattlerIndex } from "#app/battle";
import { applyAbAttrs, applyPostDefendAbAttrs, applyPreAttackAbAttrs, MoveEffectChanceMultiplierAbAttr, MovePowerBoostAbAttr, PostDefendTypeChangeAbAttr } from "#app/data/ability"; import { Type } from "#app/enums/type";
import { MoveEffectPhase } from "#app/phases/move-effect-phase";
import { NumberHolder } from "#app/utils";
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 { 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, vi } from "vitest";
import { allMoves } from "#app/data/move"; import { allMoves, FlinchAttr } from "#app/data/move";
describe("Abilities - Sheer Force", () => { describe("Abilities - Sheer Force", () => {
let phaserGame: Phaser.Game; let phaserGame: Phaser.Game;
@ -27,143 +25,91 @@ describe("Abilities - Sheer Force", () => {
beforeEach(() => { beforeEach(() => {
game = new GameManager(phaserGame); game = new GameManager(phaserGame);
const movesToUse = [ Moves.AIR_SLASH, Moves.BIND, Moves.CRUSH_CLAW, Moves.TACKLE ]; game.override
game.override.battleType("single"); .battleType("single")
game.override.enemySpecies(Species.ONIX); .ability(Abilities.SHEER_FORCE)
game.override.startingLevel(100); .enemySpecies(Species.ONIX)
game.override.moveset(movesToUse); .enemyAbility(Abilities.BALL_FETCH)
game.override.enemyMoveset([ Moves.TACKLE, Moves.TACKLE, Moves.TACKLE, Moves.TACKLE ]); .enemyMoveset([ Moves.SPLASH ])
.disableCrits();
}); });
it("Sheer Force", async () => { const SHEER_FORCE_MULT = 5461 / 4096;
const moveToUse = Moves.AIR_SLASH;
game.override.ability(Abilities.SHEER_FORCE); it("Sheer Force should boost the power of the move but disable secondary effects", async () => {
game.override.moveset([ Moves.AIR_SLASH ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const airSlashMove = allMoves[Moves.AIR_SLASH];
vi.spyOn(airSlashMove, "calculateBattlePower");
const airSlashFlinchAttr = airSlashMove.getAttrs(FlinchAttr)[0];
vi.spyOn(airSlashFlinchAttr, "getMoveChance");
game.move.select(Moves.AIR_SLASH);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase", false);
expect(airSlashMove.calculateBattlePower).toHaveLastReturnedWith(airSlashMove.power * SHEER_FORCE_MULT);
expect(airSlashFlinchAttr.getMoveChance).toHaveLastReturnedWith(0);
});
it("Sheer Force does not affect the base damage or secondary effects of binding moves", async () => {
game.override.moveset([ Moves.BIND ]);
await game.classicMode.startBattle([ Species.SHUCKLE ]);
const bindMove = allMoves[Moves.BIND];
vi.spyOn(bindMove, "calculateBattlePower");
game.move.select(Moves.BIND);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase", false);
expect(bindMove.calculateBattlePower).toHaveLastReturnedWith(bindMove.power);
}, 20000);
it("Sheer Force does not boost the base damage of moves with no secondary effect", async () => {
game.override.moveset([ Moves.TACKLE ]);
await game.classicMode.startBattle([ Species.PIDGEOT ]); await game.classicMode.startBattle([ Species.PIDGEOT ]);
game.scene.getEnemyPokemon()!.stats[Stat.SPDEF] = 10000; const tackleMove = allMoves[Moves.TACKLE];
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0); vi.spyOn(tackleMove, "calculateBattlePower");
game.move.select(moveToUse);
game.move.select(Moves.TACKLE);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase", false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(tackleMove.calculateBattlePower).toHaveLastReturnedWith(tackleMove.power);
const move = phase.move.getMove(); });
expect(move.id).toBe(Moves.AIR_SLASH);
//Verify the move is boosted and has no chance of secondary effects it("Sheer Force can disable the on-hit activation of specific abilities", async () => {
const power = new NumberHolder(move.power); game.override
const chance = new NumberHolder(move.chance); .moveset([ Moves.HEADBUTT ])
.enemySpecies(Species.SQUIRTLE)
.enemyLevel(10)
.enemyAbility(Abilities.COLOR_CHANGE);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(0);
expect(power.value).toBe(move.power * 5461 / 4096);
}, 20000);
it("Sheer Force with exceptions including binding moves", async () => {
const moveToUse = Moves.BIND;
game.override.ability(Abilities.SHEER_FORCE);
await game.classicMode.startBattle([ Species.PIDGEOT ]); await game.classicMode.startBattle([ Species.PIDGEOT ]);
const enemyPokemon = game.scene.getEnemyPokemon();
const headbuttMove = allMoves[Moves.HEADBUTT];
vi.spyOn(headbuttMove, "calculateBattlePower");
const headbuttFlinchAttr = headbuttMove.getAttrs(FlinchAttr)[0];
vi.spyOn(headbuttFlinchAttr, "getMoveChance");
game.move.select(Moves.HEADBUTT);
game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000;
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0);
game.move.select(moveToUse);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false); await game.move.forceHit();
await game.phaseInterceptor.to("BerryPhase", false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase; expect(enemyPokemon?.getTypes()[0]).toBe(Type.WATER);
const move = phase.move.getMove(); expect(headbuttMove.calculateBattlePower).toHaveLastReturnedWith(headbuttMove.power * SHEER_FORCE_MULT);
expect(move.id).toBe(Moves.BIND); expect(headbuttFlinchAttr.getMoveChance).toHaveLastReturnedWith(0);
});
//Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1
const power = new NumberHolder(move.power);
const chance = new NumberHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power);
}, 20000);
it("Sheer Force with moves with no secondary effect", async () => {
const moveToUse = Moves.TACKLE;
game.override.ability(Abilities.SHEER_FORCE);
await game.classicMode.startBattle([ Species.PIDGEOT ]);
game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000;
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0);
game.move.select(moveToUse);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
expect(move.id).toBe(Moves.TACKLE);
//Binding moves and other exceptions are not affected by Sheer Force and have a chance.value of -1
const power = new NumberHolder(move.power);
const chance = new NumberHolder(move.chance);
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, phase.getUserPokemon()!, null, false, chance, move, phase.getFirstTarget(), false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, phase.getUserPokemon()!, phase.getFirstTarget()!, move, false, power);
expect(chance.value).toBe(-1);
expect(power.value).toBe(move.power);
}, 20000);
it("Sheer Force Disabling Specific Abilities", async () => {
const moveToUse = Moves.CRUSH_CLAW;
game.override.enemyAbility(Abilities.COLOR_CHANGE);
game.override.startingHeldItems([{ name: "KINGS_ROCK", count: 1 }]);
game.override.ability(Abilities.SHEER_FORCE);
await game.startBattle([ Species.PIDGEOT ]);
game.scene.getEnemyPokemon()!.stats[Stat.DEF] = 10000;
expect(game.scene.getPlayerPokemon()!.formIndex).toBe(0);
game.move.select(moveToUse);
await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]);
await game.phaseInterceptor.to(MoveEffectPhase, false);
const phase = game.scene.getCurrentPhase() as MoveEffectPhase;
const move = phase.move.getMove();
expect(move.id).toBe(Moves.CRUSH_CLAW);
//Disable color change due to being hit by Sheer Force
const power = new NumberHolder(move.power);
const chance = new NumberHolder(move.chance);
const user = phase.getUserPokemon()!;
const target = phase.getFirstTarget()!;
const opponentType = target.getTypes()[0];
applyAbAttrs(MoveEffectChanceMultiplierAbAttr, user, null, false, chance, move, target, false);
applyPreAttackAbAttrs(MovePowerBoostAbAttr, user, target, move, false, power);
applyPostDefendAbAttrs(PostDefendTypeChangeAbAttr, target, user, move, target.apply(user, move));
expect(chance.value).toBe(0);
expect(power.value).toBe(move.power * 5461 / 4096);
expect(target.getTypes().length).toBe(2);
expect(target.getTypes()[0]).toBe(opponentType);
}, 20000);
it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => { it("Two Pokemon with abilities disabled by Sheer Force hitting each other should not cause a crash", async () => {
const moveToUse = Moves.CRUNCH; const moveToUse = Moves.CRUNCH;
@ -191,5 +137,19 @@ describe("Abilities - Sheer Force", () => {
expect(onix.getTypes()).toStrictEqual(expectedTypes); expect(onix.getTypes()).toStrictEqual(expectedTypes);
}); });
//TODO King's Rock Interaction Unit Test it("Sheer Force should disable Meloetta's transformation from Relic Song", async () => {
game.override
.ability(Abilities.SHEER_FORCE)
.moveset([ Moves.RELIC_SONG ])
.enemyMoveset([ Moves.SPLASH ])
.enemyLevel(100);
await game.classicMode.startBattle([ Species.MELOETTA ]);
const playerPokemon = game.scene.getPlayerPokemon();
const formKeyStart = playerPokemon?.getFormKey();
game.move.select(Moves.RELIC_SONG);
await game.phaseInterceptor.to("TurnEndPhase");
expect(formKeyStart).toBe(playerPokemon?.getFormKey());
});
}); });