From e45cb42f7ee86899e45d9aa40aa390b65251dd76 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:42:47 -0800 Subject: [PATCH 01/15] [Balance] Disable King's Rock for moves that can already flinch (#4860) --- src/phases/move-effect-phase.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/phases/move-effect-phase.ts b/src/phases/move-effect-phase.ts index 24a0b51da96..afc8dd0475d 100644 --- a/src/phases/move-effect-phase.ts +++ b/src/phases/move-effect-phase.ts @@ -26,6 +26,7 @@ import { applyMoveAttrs, AttackMove, DelayedAttackAttr, + FlinchAttr, HitsTagAttr, MissEffectAttr, MoveAttr, @@ -502,6 +503,10 @@ export class MoveEffectPhase extends PokemonPhase { */ protected applyHeldItemFlinchCheck(user: Pokemon, target: Pokemon, dealsDamage: boolean) : () => void { return () => { + if (this.move.getMove().hasAttr(FlinchAttr)) { + return; + } + if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr) && !this.move.getMove().hitsSubstitute(user, target)) { const flinched = new BooleanHolder(false); user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched); From 162eea500dcfaa5e39b06481339a60ebfb2d0c78 Mon Sep 17 00:00:00 2001 From: muscode Date: Wed, 13 Nov 2024 00:28:22 -0600 Subject: [PATCH 02/15] Fixed wild form changes messages, and form-changed Cramorant crashing the game when both sides faint at the same time (#4859) --- src/phases/quiet-form-change-phase.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/phases/quiet-form-change-phase.ts b/src/phases/quiet-form-change-phase.ts index c28cc28b592..6c84c0d1a8a 100644 --- a/src/phases/quiet-form-change-phase.ts +++ b/src/phases/quiet-form-change-phase.ts @@ -29,10 +29,14 @@ export class QuietFormChangePhase extends BattlePhase { const preName = getPokemonNameWithAffix(this.pokemon); - if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag)) { - this.pokemon.changeForm(this.formChange).then(() => { - this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500); - }); + if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag) || this.pokemon.isFainted()) { + if (this.pokemon.isPlayer() || this.pokemon.isActive()) { + this.pokemon.changeForm(this.formChange).then(() => { + this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500); + }); + } else { + this.end(); + } return; } From 0c521bbe0828746ace0dd7310340fb6340bee6e5 Mon Sep 17 00:00:00 2001 From: geeilhan <107366005+geeilhan@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:41:39 +0100 Subject: [PATCH 03/15] [Move] Implement Freeze Dry type-changed interactions (#4840) * Full implementation of freeze-dry including edge cases such as Normalize and Electrify plus tests * Update comments * renamed WaterSuperEffectTypeMultiplierAttr to FreezeDryAttr * Added test case for freeze dry during inverse battles * cleaned up code making it more general * Added some more documentation * implementing reviewed changes * used getMoveType() instead of move.type * added additional test cases to freeze dry * Revert "used getMoveType() instead of move.type" This reverts commit 03445dfab4db52b0dddbe7abf7d4b4dfa8b9c583. * added reviewed changes without changing public/locales --------- Co-authored-by: ga27lok --- src/data/move.ts | 40 ++++++-- src/test/moves/freeze_dry.test.ts | 163 +++++++++++++++++++++++++++++- 2 files changed, 192 insertions(+), 11 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index 089bb51bf5e..74ac61af884 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4971,16 +4971,42 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy } } -export class WaterSuperEffectTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { +/** + * This class forces Freeze-Dry to be super effective against Water Type. + * It considers if target is Mono or Dual Type and calculates the new Multiplier accordingly. + * @see {@linkcode apply} + */ +export class FreezeDryAttr extends VariableMoveTypeMultiplierAttr { + /** + * If the target is Mono Type (Water only) then a 2x Multiplier is always forced. + * If target is Dual Type (containing Water) then only a 2x Multiplier is forced for the Water Type. + * + * Additionally Freeze-Dry's effectiveness against water is always forced during {@linkcode InverseBattleChallenge}. + * The multiplier is recalculated for the non-Water Type in case of Dual Type targets containing Water Type. + * + * @param user The {@linkcode Pokemon} applying the move + * @param target The {@linkcode Pokemon} targeted by the move + * @param move The move used by the user + * @param args `[0]` a {@linkcode Utils.NumberHolder | NumberHolder} containing a type effectiveness multiplier + * @returns `true` if super effectiveness on water type is forced; `false` otherwise + */ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const multiplier = args[0] as Utils.NumberHolder; - if (target.isOfType(Type.WATER)) { - const effectivenessAgainstWater = new Utils.NumberHolder(getTypeDamageMultiplier(move.type, Type.WATER)); - applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstWater); - if (effectivenessAgainstWater.value !== 0) { - multiplier.value *= 2 / effectivenessAgainstWater.value; + if (target.isOfType(Type.WATER) && multiplier.value !== 0) { + const multipleTypes = (target.getTypes().length > 1); + + if (multipleTypes) { + const nonWaterType = target.getTypes().filter(type => type !== Type.WATER)[0]; + const effectivenessAgainstTarget = new Utils.NumberHolder(getTypeDamageMultiplier(user.getMoveType(move), nonWaterType)); + + applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstTarget); + + multiplier.value = effectivenessAgainstTarget.value * 2; return true; } + + multiplier.value = 2; + return true; } return false; @@ -9422,7 +9448,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6) .attr(StatusEffectAttr, StatusEffect.FREEZE) - .attr(WaterSuperEffectTypeMultiplierAttr) + .attr(FreezeDryAttr) .edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell. new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) .soundBased() diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index f766ed41a82..8bc6717f435 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -2,6 +2,7 @@ import { BattlerIndex } from "#app/battle"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; +import { Challenges } from "#enums/challenges"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; @@ -97,8 +98,7 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }); - // enable if this is ever fixed (lol) - it.todo("should deal 2x damage to water types under Normalize", async () => { + it("should deal 2x damage to water type under Normalize", async () => { game.override.ability(Abilities.NORMALIZE); await game.classicMode.startBattle(); @@ -112,8 +112,39 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); }); - // enable once Electrify is implemented (and the interaction is fixed, as above) - it.todo("should deal 2x damage to water types under Electrify", async () => { + it("should deal 0.25x damage to rock/steel type under Normalize", async () => { + game.override + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.SHIELDON); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25); + }); + + it("should deal 0x damage to water/ghost type under Normalize", async () => { + game.override + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.JELLICENT); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0); + }); + + it("should deal 2x damage to water type under Electrify", async () => { game.override.enemyMoveset([ Moves.ELECTRIFY ]); await game.classicMode.startBattle(); @@ -126,4 +157,128 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); }); + + it("should deal 4x damage to water/flying type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.GYARADOS); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(4); + }); + + it("should deal 0x damage to water/ground type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.BARBOACH); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0); + }); + + it("should deal 0.25x damage to Grass/Dragon type under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.FLAPPLE); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.25); + }); + + it("should deal 2x damage to Water type during inverse battle", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .enemySpecies(Species.MAGIKARP); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 2x damage to Water type during inverse battle under Normalize", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .ability(Abilities.NORMALIZE) + .enemySpecies(Species.MAGIKARP); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 2x damage to Water type during inverse battle under Electrify", async () => { + game.override + .moveset([ Moves.FREEZE_DRY ]) + .enemySpecies(Species.MAGIKARP) + .enemyMoveset([ Moves.ELECTRIFY ]); + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveLastReturnedWith(2); + }); + + it("should deal 1x damage to water/flying type during inverse battle under Electrify", async () => { + game.override + .enemyMoveset([ Moves.ELECTRIFY ]) + .enemySpecies(Species.GYARADOS); + + game.challengeMode.addChallenge(Challenges.INVERSE_BATTLE, 1, 1); + + await game.challengeMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.ENEMY, BattlerIndex.PLAYER ]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(1); + }); }); From d0d9eb78da7beadb980f32a6c466f521f9e600cf Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 14 Nov 2024 11:35:13 -0800 Subject: [PATCH 04/15] Set the IVs of default starters to 15. (#4861) Co-authored-by: frutescens --- src/data/challenge.ts | 2 +- src/system/game-data.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/challenge.ts b/src/data/challenge.ts index af6bbf5b00f..4301ea7b375 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -653,7 +653,7 @@ export class FreshStartChallenge extends Challenge { pokemon.shiny = false; // Not shiny pokemon.variant = 0; // Not shiny pokemon.formIndex = 0; // Froakie should be base form - pokemon.ivs = [ 10, 10, 10, 10, 10, 10 ]; // Default IVs of 10 for all stats + pokemon.ivs = [ 15, 15, 15, 15, 15, 15 ]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0) return true; } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 8f179ddb677..a9726ac0713 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -1540,7 +1540,7 @@ export class GameData { entry.caughtAttr = defaultStarterAttr; entry.natureAttr = 1 << (defaultStarterNatures[ds] + 1); for (const i in entry.ivs) { - entry.ivs[i] = 10; + entry.ivs[i] = 15; } } From 640ac237412dd82863f7ef7e704e8406c2d3c3fe Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Thu, 14 Nov 2024 14:40:01 -0500 Subject: [PATCH 05/15] [Dev] Add overrides for alternating between single and double battles (#4833) * [Dev] Add overrides for alternating between single and double battles * PR feedback * Add type `DoubleType` * Fixed Gastro Acid test using `game.override.battleType(null)` * Changed new type name to `SingleOrDoubleType` * `SingleOrDoubleType` is now `BattleStyle` * Update src/test/utils/helpers/overridesHelper.ts --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/battle-scene.ts | 34 ++++++++++++++---- src/overrides.ts | 15 +++++++- src/test/battle/double_battle.test.ts | 42 ++++++++++++++++++++++- src/test/moves/gastro_acid.test.ts | 2 +- src/test/utils/helpers/overridesHelper.ts | 9 ++--- 5 files changed, 89 insertions(+), 13 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index c30ab2e2912..e659b588208 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1233,12 +1233,34 @@ export default class BattleScene extends SceneBase { newDouble = !!double; } - if (Overrides.BATTLE_TYPE_OVERRIDE === "double") { - newDouble = true; - } - /* Override battles into single only if not fighting with trainers */ - if (newBattleType !== BattleType.TRAINER && Overrides.BATTLE_TYPE_OVERRIDE === "single") { - newDouble = false; + if (!isNullOrUndefined(Overrides.BATTLE_TYPE_OVERRIDE)) { + let doubleOverrideForWave: "single" | "double" | null = null; + + switch (Overrides.BATTLE_TYPE_OVERRIDE) { + case "double": + doubleOverrideForWave = "double"; + break; + case "single": + doubleOverrideForWave = "single"; + break; + case "even-doubles": + doubleOverrideForWave = (newWaveIndex % 2) ? "single" : "double"; + break; + case "odd-doubles": + doubleOverrideForWave = (newWaveIndex % 2) ? "double" : "single"; + break; + } + + if (doubleOverrideForWave === "double") { + newDouble = true; + } + /** + * Override battles into single only if not fighting with trainers. + * @see {@link https://github.com/pagefaultgames/pokerogue/issues/1948 | GitHub Issue #1948} + */ + if (newBattleType !== BattleType.TRAINER && doubleOverrideForWave === "single") { + newDouble = false; + } } const lastBattle = this.currentBattle; diff --git a/src/overrides.ts b/src/overrides.ts index d7a8ee18f15..7b73cd47b03 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -47,7 +47,18 @@ class DefaultOverrides { /** a specific seed (default: a random string of 24 characters) */ readonly SEED_OVERRIDE: string = ""; readonly WEATHER_OVERRIDE: WeatherType = WeatherType.NONE; - readonly BATTLE_TYPE_OVERRIDE: "double" | "single" | null = null; + /** + * If `null`, ignore this override. + * + * If `"single"`, set every non-trainer battle to be a single battle. + * + * If `"double"`, set every battle (including trainer battles) to be a double battle. + * + * If `"even-doubles"`, follow the `"double"` rule on even wave numbers, and follow the `"single"` rule on odd wave numbers. + * + * If `"odd-doubles"`, follow the `"double"` rule on odd wave numbers, and follow the `"single"` rule on even wave numbers. + */ + readonly BATTLE_TYPE_OVERRIDE: BattleStyle | null = null; readonly STARTING_WAVE_OVERRIDE: number = 0; readonly STARTING_BIOME_OVERRIDE: Biome = Biome.TOWN; readonly ARENA_TINT_OVERRIDE: TimeOfDay | null = null; @@ -229,3 +240,5 @@ export default { ...defaultOverrides, ...overrides } satisfies InstanceType; + +export type BattleStyle = "double" | "single" | "even-doubles" | "odd-doubles"; diff --git a/src/test/battle/double_battle.test.ts b/src/test/battle/double_battle.test.ts index 1fc24bfc027..b48f2a96a5b 100644 --- a/src/test/battle/double_battle.test.ts +++ b/src/test/battle/double_battle.test.ts @@ -1,4 +1,6 @@ import { Status } from "#app/data/status-effect"; +import { Abilities } from "#enums/abilities"; +import { GameModes, getGameMode } from "#app/game-mode"; import { BattleEndPhase } from "#app/phases/battle-end-phase"; import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { Moves } from "#enums/moves"; @@ -6,9 +8,11 @@ import { Species } from "#enums/species"; import { StatusEffect } from "#enums/status-effect"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; -import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; describe("Double Battles", () => { + const DOUBLE_CHANCE = 8; // Normal chance of double battle is 1/8 + let phaserGame: Phaser.Game; let game: GameManager; @@ -56,4 +60,40 @@ describe("Double Battles", () => { await game.phaseInterceptor.to(TurnInitPhase); expect(game.scene.getPlayerField().filter(p => !p.isFainted())).toHaveLength(2); }, 20000); + + it("randomly chooses between single and double battles if there is no battle type override", async () => { + let rngSweepProgress = 0; // Will simulate RNG rolls by slowly increasing from 0 to 1 + let doubleCount = 0; + let singleCount = 0; + + vi.spyOn(Phaser.Math.RND, "realInRange").mockImplementation((min: number, max: number) => { + return rngSweepProgress * (max - min) + min; + }); + + game.override.enemyMoveset(Moves.SPLASH) + .moveset(Moves.SPLASH) + .enemyAbility(Abilities.BALL_FETCH) + .ability(Abilities.BALL_FETCH); + + // Play through endless, waves 1 to 9, counting number of double battles from waves 2 to 9 + await game.classicMode.startBattle([ Species.BULBASAUR ]); + game.scene.gameMode = getGameMode(GameModes.ENDLESS); + + for (let i = 0; i < DOUBLE_CHANCE; i++) { + rngSweepProgress = (i + 0.5) / DOUBLE_CHANCE; + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + + if (game.scene.getEnemyParty().length === 1) { + singleCount++; + } else if (game.scene.getEnemyParty().length === 2) { + doubleCount++; + } + } + + expect(doubleCount).toBe(1); + expect(singleCount).toBe(DOUBLE_CHANCE - 1); + }); }); diff --git a/src/test/moves/gastro_acid.test.ts b/src/test/moves/gastro_acid.test.ts index fdd75b90b13..ec9246c855c 100644 --- a/src/test/moves/gastro_acid.test.ts +++ b/src/test/moves/gastro_acid.test.ts @@ -62,7 +62,7 @@ describe("Moves - Gastro Acid", () => { }); it("fails if used on an enemy with an already-suppressed ability", async () => { - game.override.battleType(null); + game.override.battleType("single"); await game.startBattle(); diff --git a/src/test/utils/helpers/overridesHelper.ts b/src/test/utils/helpers/overridesHelper.ts index 02950d497ee..1c05f92a334 100644 --- a/src/test/utils/helpers/overridesHelper.ts +++ b/src/test/utils/helpers/overridesHelper.ts @@ -4,7 +4,7 @@ import { Abilities } from "#app/enums/abilities"; import * as GameMode from "#app/game-mode"; import { GameModes, getGameMode } from "#app/game-mode"; import { ModifierOverride } from "#app/modifier/modifier-type"; -import Overrides from "#app/overrides"; +import Overrides, { BattleStyle } from "#app/overrides"; import { Unlockables } from "#app/system/unlockables"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; @@ -238,13 +238,14 @@ export class OverridesHelper extends GameManagerHelper { } /** - * Override the battle type (single or double) + * Override the battle type (e.g., single or double). + * @see {@linkcode Overrides.BATTLE_TYPE_OVERRIDE} * @param battleType battle type to set * @returns `this` */ - public battleType(battleType: "single" | "double" | null): this { + public battleType(battleType: BattleStyle | null): this { vi.spyOn(Overrides, "BATTLE_TYPE_OVERRIDE", "get").mockReturnValue(battleType); - this.log(`Battle type set to ${battleType} only!`); + this.log(battleType === null ? "Battle type override disabled!" : `Battle type set to ${battleType}!`); return this; } From 58912db8f1044a8ea5fa23a6f4989bfdc1cc426f Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 14 Nov 2024 13:16:05 -0800 Subject: [PATCH 06/15] [P2] Telekinesis now sets FloatingTag to 3 turns instead of 5 turns (#4869) Co-authored-by: frutescens --- src/data/battler-tags.ts | 6 +++--- src/data/move.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 25d65fbc372..5c6d9d66b7c 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -1809,8 +1809,8 @@ export class TypeImmuneTag extends BattlerTag { * @see {@link https://bulbapedia.bulbagarden.net/wiki/Telekinesis_(move) | Moves.TELEKINESIS} */ export class FloatingTag extends TypeImmuneTag { - constructor(tagType: BattlerTagType, sourceMove: Moves) { - super(tagType, sourceMove, Type.GROUND, 5); + constructor(tagType: BattlerTagType, sourceMove: Moves, turnCount: number) { + super(tagType, sourceMove, Type.GROUND, turnCount); } onAdd(pokemon: Pokemon): void { @@ -3053,7 +3053,7 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source case BattlerTagType.CHARGED: return new TypeBoostTag(tagType, sourceMove, Type.ELECTRIC, 2, true); case BattlerTagType.FLOATING: - return new FloatingTag(tagType, sourceMove); + return new FloatingTag(tagType, sourceMove, turnCount); case BattlerTagType.MINIMIZED: return new MinimizeTag(); case BattlerTagType.DESTINY_BOND: diff --git a/src/data/move.ts b/src/data/move.ts index 74ac61af884..a288c0e9618 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8897,7 +8897,7 @@ export function initMoves() { new SelfStatusMove(Moves.AQUA_RING, Type.WATER, -1, 20, -1, 0, 4) .attr(AddBattlerTagAttr, BattlerTagType.AQUA_RING, true, true), new SelfStatusMove(Moves.MAGNET_RISE, Type.ELECTRIC, -1, 10, -1, 0, 4) - .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true) + .attr(AddBattlerTagAttr, BattlerTagType.FLOATING, true, true, 5) .condition((user, target, move) => !user.scene.arena.getTag(ArenaTagType.GRAVITY) && [ BattlerTagType.FLOATING, BattlerTagType.IGNORE_FLYING, BattlerTagType.INGRAIN ].every((tag) => !user.getTag(tag))), new AttackMove(Moves.FLARE_BLITZ, Type.FIRE, MoveCategory.PHYSICAL, 120, 100, 15, 10, 0, 4) .attr(RecoilAttr, false, 0.33) From f778bd587700c5def856d038a51b1b25fc42da90 Mon Sep 17 00:00:00 2001 From: Moka <54149968+MokaStitcher@users.noreply.github.com> Date: Fri, 15 Nov 2024 00:01:04 +0100 Subject: [PATCH 07/15] Prevent crash from null dexData attributes (#4871) --- src/system/game-data.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/system/game-data.ts b/src/system/game-data.ts index a9726ac0713..4d01ae9998a 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -679,7 +679,7 @@ export class GameData { return ret; } - return k.endsWith("Attr") && ![ "natureAttr", "abilityAttr", "passiveAttr" ].includes(k) ? BigInt(v) : v; + return k.endsWith("Attr") && ![ "natureAttr", "abilityAttr", "passiveAttr" ].includes(k) ? BigInt(v ?? 0) : v; }) as SystemSaveData; } From b1138c1d70a9428e767539ff64c858bf5dc80f36 Mon Sep 17 00:00:00 2001 From: PigeonBar <56974298+PigeonBar@users.noreply.github.com> Date: Fri, 15 Nov 2024 00:07:19 -0500 Subject: [PATCH 08/15] [P2][Beta] Freeze-dry Re-implementation (#4874) --- src/data/ability.ts | 4 +- src/data/move.ts | 82 ++++++++++++++----------------- src/field/pokemon.ts | 10 ++-- src/test/moves/freeze_dry.test.ts | 68 ++++++++++++++++++++++++- 4 files changed, 113 insertions(+), 51 deletions(-) diff --git a/src/data/ability.ts b/src/data/ability.ts index d58c6c5c9b9..b77b18947be 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -516,7 +516,7 @@ export class NonSuperEffectiveImmunityAbAttr extends TypeImmunityAbAttr { applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { const modifierValue = args.length > 0 ? (args[0] as Utils.NumberHolder).value - : pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker); + : pokemon.getAttackTypeEffectiveness(attacker.getMoveType(move), attacker, undefined, undefined, move); if (move instanceof AttackMove && modifierValue < 2) { cancelled.value = true; // Suppresses "No Effect" message @@ -3180,7 +3180,7 @@ function getAnticipationCondition(): AbAttrCondition { continue; } // the move's base type (not accounting for variable type changes) is super effective - if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true) >= 2) { + if (move.getMove() instanceof AttackMove && pokemon.getAttackTypeEffectiveness(move.getMove().type, opponent, true, undefined, move.getMove()) >= 2) { return true; } // move is a OHKO diff --git a/src/data/move.ts b/src/data/move.ts index a288c0e9618..e7cd9d10615 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -897,7 +897,7 @@ export class AttackMove extends Move { let attackScore = 0; - const effectiveness = target.getAttackTypeEffectiveness(this.type, user); + const effectiveness = target.getAttackTypeEffectiveness(this.type, user, undefined, undefined, this); attackScore = Math.pow(effectiveness - 1, 2) * effectiveness < 1 ? -2 : 2; if (attackScore) { if (this.category === MoveCategory.PHYSICAL) { @@ -4971,48 +4971,6 @@ export class NeutralDamageAgainstFlyingTypeMultiplierAttr extends VariableMoveTy } } -/** - * This class forces Freeze-Dry to be super effective against Water Type. - * It considers if target is Mono or Dual Type and calculates the new Multiplier accordingly. - * @see {@linkcode apply} - */ -export class FreezeDryAttr extends VariableMoveTypeMultiplierAttr { - /** - * If the target is Mono Type (Water only) then a 2x Multiplier is always forced. - * If target is Dual Type (containing Water) then only a 2x Multiplier is forced for the Water Type. - * - * Additionally Freeze-Dry's effectiveness against water is always forced during {@linkcode InverseBattleChallenge}. - * The multiplier is recalculated for the non-Water Type in case of Dual Type targets containing Water Type. - * - * @param user The {@linkcode Pokemon} applying the move - * @param target The {@linkcode Pokemon} targeted by the move - * @param move The move used by the user - * @param args `[0]` a {@linkcode Utils.NumberHolder | NumberHolder} containing a type effectiveness multiplier - * @returns `true` if super effectiveness on water type is forced; `false` otherwise - */ - apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - const multiplier = args[0] as Utils.NumberHolder; - if (target.isOfType(Type.WATER) && multiplier.value !== 0) { - const multipleTypes = (target.getTypes().length > 1); - - if (multipleTypes) { - const nonWaterType = target.getTypes().filter(type => type !== Type.WATER)[0]; - const effectivenessAgainstTarget = new Utils.NumberHolder(getTypeDamageMultiplier(user.getMoveType(move), nonWaterType)); - - applyChallenges(user.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, effectivenessAgainstTarget); - - multiplier.value = effectivenessAgainstTarget.value * 2; - return true; - } - - multiplier.value = 2; - return true; - } - - return false; - } -} - export class IceNoEffectTypeAttr extends VariableMoveTypeMultiplierAttr { /** * Checks to see if the Target is Ice-Type or not. If so, the move will have no effect. @@ -5040,6 +4998,41 @@ export class FlyingTypeMultiplierAttr extends VariableMoveTypeMultiplierAttr { } } +/** + * Attribute for moves which have a custom type chart interaction. + */ +export class VariableMoveTypeChartAttr extends MoveAttr { + /** + * @param user {@linkcode Pokemon} using the move + * @param target {@linkcode Pokemon} target of the move + * @param move {@linkcode Move} with this attribute + * @param args [0] {@linkcode NumberHolder} holding the type effectiveness + * @param args [1] A single defensive type of the target + * + * @returns true if application of the attribute succeeds + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + return false; + } +} + +/** + * This class forces Freeze-Dry to be super effective against Water Type. + */ +export class FreezeDryAttr extends VariableMoveTypeChartAttr { + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + const multiplier = args[0] as Utils.NumberHolder; + const defType = args[1] as Type; + + if (defType === Type.WATER) { + multiplier.value = 2; + return true; + } else { + return false; + } + } +} + export class OneHitKOAccuracyAttr extends VariableAccuracyAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { const accuracy = args[0] as Utils.NumberHolder; @@ -9448,8 +9441,7 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.FREEZE_DRY, Type.ICE, MoveCategory.SPECIAL, 70, 100, 20, 10, 0, 6) .attr(StatusEffectAttr, StatusEffect.FREEZE) - .attr(FreezeDryAttr) - .edgeCase(), // This currently just multiplies the move's power instead of changing its effectiveness. It also doesn't account for abilities that modify type effectiveness such as tera shell. + .attr(FreezeDryAttr), new AttackMove(Moves.DISARMING_VOICE, Type.FAIRY, MoveCategory.SPECIAL, 40, -1, 15, -1, 0, 6) .soundBased() .target(MoveTarget.ALL_NEAR_ENEMIES), diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 5d912f7d6e6..bc7e844a290 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -3,7 +3,7 @@ import BattleScene, { AnySound } from "#app/battle-scene"; import { Variant, VariantSet, variantColorCache } from "#app/data/variant"; import { variantData } from "#app/data/variant"; import BattleInfo, { PlayerBattleInfo, EnemyBattleInfo } from "#app/ui/battle-info"; -import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr } from "#app/data/move"; +import Move, { HighCritAttr, HitsTagAttr, applyMoveAttrs, FixedDamageAttr, VariableAtkAttr, allMoves, MoveCategory, TypelessAttr, CritOnlyAttr, getMoveTargets, OneHitKOAttr, VariableMoveTypeAttr, VariableDefAttr, AttackMove, ModifiedDamageAttr, VariableMoveTypeMultiplierAttr, IgnoreOpponentStatStagesAttr, SacrificialAttr, VariableMoveCategoryAttr, CounterDamageAttr, StatStageChangeAttr, RechargeAttr, IgnoreWeatherTypeDebuffAttr, BypassBurnDamageReductionAttr, SacrificialAttrOnHit, OneHitKOAccuracyAttr, RespectAttackTypeImmunityAttr, MoveTarget, CombinedPledgeStabBoostAttr, VariableMoveTypeChartAttr } from "#app/data/move"; import { default as PokemonSpecies, PokemonSpeciesForm, getFusedSpeciesName, getPokemonSpecies, getPokemonSpeciesForm } from "#app/data/pokemon-species"; import { CLASSIC_CANDY_FRIENDSHIP_MULTIPLIER, getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; @@ -1632,7 +1632,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const moveType = source.getMoveType(move); const typeMultiplier = new Utils.NumberHolder((move.category !== MoveCategory.STATUS || move.hasAttr(RespectAttackTypeImmunityAttr)) - ? this.getAttackTypeEffectiveness(moveType, source, false, simulated) + ? this.getAttackTypeEffectiveness(moveType, source, false, simulated, move) : 1); applyMoveAttrs(VariableMoveTypeMultiplierAttr, source, this, move, typeMultiplier); @@ -1684,9 +1684,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * @param source {@linkcode Pokemon} the Pokemon using the move * @param ignoreStrongWinds whether or not this ignores strong winds (anticipation, forewarn, stealth rocks) * @param simulated tag to only apply the strong winds effect message when the move is used + * @param move (optional) the move whose type effectiveness is to be checked. Used for applying {@linkcode VariableMoveTypeChartAttr} * @returns a multiplier for the type effectiveness */ - getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true): TypeDamageMultiplier { + getAttackTypeEffectiveness(moveType: Type, source?: Pokemon, ignoreStrongWinds: boolean = false, simulated: boolean = true, move?: Move): TypeDamageMultiplier { if (moveType === Type.STELLAR) { return this.isTerastallized() ? 2 : 1; } @@ -1705,6 +1706,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { let multiplier = types.map(defType => { const multiplier = new Utils.NumberHolder(getTypeDamageMultiplier(moveType, defType)); applyChallenges(this.scene.gameMode, ChallengeType.TYPE_EFFECTIVENESS, multiplier); + if (move) { + applyMoveAttrs(VariableMoveTypeChartAttr, null, this, move, multiplier, defType); + } if (source) { const ignoreImmunity = new Utils.BooleanHolder(false); if (source.isActive(true) && source.hasAbilityWithAttr(IgnoreTypeImmunityAbAttr)) { diff --git a/src/test/moves/freeze_dry.test.ts b/src/test/moves/freeze_dry.test.ts index 8bc6717f435..9206a103a35 100644 --- a/src/test/moves/freeze_dry.test.ts +++ b/src/test/moves/freeze_dry.test.ts @@ -2,6 +2,7 @@ import { BattlerIndex } from "#app/battle"; import { Abilities } from "#app/enums/abilities"; import { Moves } from "#app/enums/moves"; import { Species } from "#app/enums/species"; +import { Type } from "#enums/type"; import { Challenges } from "#enums/challenges"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; @@ -29,7 +30,7 @@ describe("Moves - Freeze-Dry", () => { .enemyMoveset(Moves.SPLASH) .starterSpecies(Species.FEEBAS) .ability(Abilities.BALL_FETCH) - .moveset([ Moves.FREEZE_DRY ]); + .moveset([ Moves.FREEZE_DRY, Moves.FORESTS_CURSE, Moves.SOAK ]); }); it("should deal 2x damage to pure water types", async () => { @@ -98,6 +99,71 @@ describe("Moves - Freeze-Dry", () => { expect(enemy.hp).toBeLessThan(enemy.getMaxHp()); }); + it("should deal 8x damage to water/ground/grass type under Forest's Curse", async () => { + game.override.enemySpecies(Species.QUAGSIRE); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FORESTS_CURSE); + await game.toNextTurn(); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(8); + }); + + it("should deal 2x damage to steel type terastallized into water", async () => { + game.override.enemySpecies(Species.SKARMORY) + .enemyHeldItems([{ name: "TERA_SHARD", type: Type.WATER }]); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(2); + }); + + it("should deal 0.5x damage to water type terastallized into fire", async () => { + game.override.enemySpecies(Species.PELIPPER) + .enemyHeldItems([{ name: "TERA_SHARD", type: Type.FIRE }]); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5); + }); + + it("should deal 0.5x damage to water type Terapagos with Tera Shell", async () => { + game.override.enemySpecies(Species.TERAPAGOS) + .enemyAbility(Abilities.TERA_SHELL); + await game.classicMode.startBattle(); + + const enemy = game.scene.getEnemyPokemon()!; + vi.spyOn(enemy, "getMoveEffectiveness"); + + game.move.select(Moves.SOAK); + await game.toNextTurn(); + + game.move.select(Moves.FREEZE_DRY); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("MoveEffectPhase"); + + expect(enemy.getMoveEffectiveness).toHaveReturnedWith(0.5); + }); + it("should deal 2x damage to water type under Normalize", async () => { game.override.ability(Abilities.NORMALIZE); await game.classicMode.startBattle(); From 6dec84e39c0e1229160af14dddb0b9df6aeedfcb Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Thu, 14 Nov 2024 21:13:23 -0800 Subject: [PATCH 09/15] [Balance] No more double 50x bosses in Endless + cleaning up RNG (#4862) * Added double battle exclusion to Endless bosses. * Brought Endure token RNG in line with game RNG formatting. * Corrected incorrect modulo condition in isEndlessBoss * Moved new doubles conditional to be above overrides. * Fixed bad RNG calls for Covet and Thief too. * Updated unburden test. * Revert "Updated unburden test." This reverts commit 01788d40c2f5c32d89bd0cb899bfc67fc77f881e. * Revert "Fixed bad RNG calls for Covet and Thief too." This reverts commit c7fcfd195de7e98c1e582ec67c829e003663cad0. --------- Co-authored-by: frutescens --- src/battle-scene.ts | 5 +++++ src/game-mode.ts | 2 +- src/modifier/modifier.ts | 8 ++++---- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index e659b588208..061fc6e28f2 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1233,6 +1233,11 @@ export default class BattleScene extends SceneBase { newDouble = !!double; } + // Disable double battles on Endless/Endless Spliced Wave 50x boss battles (Introduced 1.2.0) + if (this.gameMode.isEndlessBoss(newWaveIndex)) { + newDouble = false; + } + if (!isNullOrUndefined(Overrides.BATTLE_TYPE_OVERRIDE)) { let doubleOverrideForWave: "single" | "double" | null = null; diff --git a/src/game-mode.ts b/src/game-mode.ts index 8f1bb9356e6..0f997bf651e 100644 --- a/src/game-mode.ts +++ b/src/game-mode.ts @@ -236,7 +236,7 @@ export class GameMode implements GameModeConfig { * @returns true if waveIndex is a multiple of 50 in Endless */ isEndlessBoss(waveIndex: integer): boolean { - return !!(waveIndex % 50) && + return waveIndex % 50 === 0 && (this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS); } diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 7aa4b9308d1..ac8dc556b98 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -3631,7 +3631,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { super(type, stackCount || 10); //Hardcode temporarily - this.chance = .02; + this.chance = 2; } match(modifier: Modifier) { @@ -3639,11 +3639,11 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { } clone() { - return new EnemyEndureChanceModifier(this.type, this.chance * 100, this.stackCount); + return new EnemyEndureChanceModifier(this.type, this.chance, this.stackCount); } getArgs(): any[] { - return [ this.chance * 100 ]; + return [ this.chance ]; } /** @@ -3652,7 +3652,7 @@ export class EnemyEndureChanceModifier extends EnemyPersistentModifier { * @returns `true` if {@linkcode Pokemon} endured */ override apply(target: Pokemon): boolean { - if (target.battleData.endured || Phaser.Math.RND.realInRange(0, 1) >= (this.chance * this.getStackCount())) { + if (target.battleData.endured || target.randSeedInt(100) >= (this.chance * this.getStackCount())) { return false; } From 8326e3556b90c95379bf7c5c95ed31c33485d2cc Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:29:52 -0800 Subject: [PATCH 10/15] Remove `.edgeCase()` from fully implemented moves (#4876) This includes Sunsteel Strike, Moongeist Beam and Photon Geyser --- src/data/move.ts | 9 ++-- src/test/moves/moongeist_beam.test.ts | 59 +++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 6 deletions(-) create mode 100644 src/test/moves/moongeist_beam.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index e7cd9d10615..ae2ba919b52 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -9876,11 +9876,9 @@ export function initMoves() { .ignoresSubstitute() .partial(), // Does not steal stats new AttackMove(Moves.SUNSTEEL_STRIKE, Type.STEEL, MoveCategory.PHYSICAL, 100, 100, 5, -1, 0, 7) - .ignoresAbilities() - .edgeCase(), // Should not ignore abilities when called virtually (metronome) + .ignoresAbilities(), new AttackMove(Moves.MOONGEIST_BEAM, Type.GHOST, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) - .ignoresAbilities() - .edgeCase(), // Should not ignore abilities when called virtually (metronome) + .ignoresAbilities(), new StatusMove(Moves.TEARFUL_LOOK, Type.NORMAL, -1, 20, -1, 0, 7) .attr(StatStageChangeAttr, [ Stat.ATK, Stat.SPATK ], -1), new AttackMove(Moves.ZING_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 80, 100, 10, 30, 0, 7) @@ -9903,8 +9901,7 @@ export function initMoves() { .punchingMove(), new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) - .ignoresAbilities() - .edgeCase(), // Should not ignore abilities when called virtually (metronome) + .ignoresAbilities(), /* Unused */ new AttackMove(Moves.LIGHT_THAT_BURNS_THE_SKY, Type.PSYCHIC, MoveCategory.SPECIAL, 200, -1, 1, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) diff --git a/src/test/moves/moongeist_beam.test.ts b/src/test/moves/moongeist_beam.test.ts new file mode 100644 index 00000000000..216eee482fb --- /dev/null +++ b/src/test/moves/moongeist_beam.test.ts @@ -0,0 +1,59 @@ +import { allMoves, RandomMoveAttr } from "#app/data/move"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - Moongeist Beam", () => { + 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.MOONGEIST_BEAM, Moves.METRONOME ]) + .ability(Abilities.BALL_FETCH) + .startingLevel(200) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.STURDY) + .enemyMoveset(Moves.SPLASH); + }); + + // Also covers Photon Geyser and Sunsteel Strike + it("should ignore enemy abilities", async () => { + await game.classicMode.startBattle([ Species.MILOTIC ]); + + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.MOONGEIST_BEAM); + await game.phaseInterceptor.to("BerryPhase"); + + expect(enemy.isFainted()).toBe(true); + }); + + // Also covers Photon Geyser and Sunsteel Strike + it("should not ignore enemy abilities when called by another move, such as metronome", async () => { + await game.classicMode.startBattle([ Species.MILOTIC ]); + vi.spyOn(allMoves[Moves.METRONOME].getAttrs(RandomMoveAttr)[0], "getMoveOverride").mockReturnValue(Moves.MOONGEIST_BEAM); + + game.move.select(Moves.METRONOME); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()!.isFainted()).toBe(false); + expect(game.scene.getPlayerPokemon()!.getLastXMoves()[0].move).toBe(Moves.MOONGEIST_BEAM); + }); +}); From 9273b4930d929cd9c8fb67466ea2501e8ffb8bf4 Mon Sep 17 00:00:00 2001 From: innerthunder <168692175+innerthunder@users.noreply.github.com> Date: Fri, 15 Nov 2024 08:56:05 -0800 Subject: [PATCH 11/15] [Beta][P1] Fix crash when resetting Commanded Dondozo before Trainer battles (#4873) * Add failsafe to Commander remove anim * Commanded tag saves Tatsu form on reload --- src/data/battler-tags.ts | 5 +++++ src/phases/pokemon-anim-phase.ts | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 5c6d9d66b7c..28ab5ff2a4f 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2158,6 +2158,11 @@ export class CommandedTag extends BattlerTag { pokemon.scene.triggerPokemonBattleAnim(pokemon, PokemonAnimType.COMMANDER_REMOVE); } } + + override loadTag(source: BattlerTag | any): void { + super.loadTag(source); + this._tatsugiriFormKey = source._tatsugiriFormKey; + } } /** diff --git a/src/phases/pokemon-anim-phase.ts b/src/phases/pokemon-anim-phase.ts index ad0be34af7d..eb5431cbc56 100644 --- a/src/phases/pokemon-anim-phase.ts +++ b/src/phases/pokemon-anim-phase.ts @@ -312,6 +312,10 @@ export class PokemonAnimPhase extends BattlePhase { // Note: unlike the other Commander animation, this is played through the // Dondozo instead of the Tatsugiri. const tatsugiri = this.pokemon.getAlly(); + if (isNullOrUndefined(tatsugiri)) { + console.warn("Aborting COMMANDER_REMOVE anim: Tatsugiri is undefined"); + return this.end(); + } const tatsuSprite = this.scene.addPokemonSprite( tatsugiri, From ef7d860166136e126973a69bd80450dfd92c1c25 Mon Sep 17 00:00:00 2001 From: AJ Fontaine <36677462+Fontbane@users.noreply.github.com> Date: Fri, 15 Nov 2024 11:57:02 -0500 Subject: [PATCH 12/15] [Balance] Remove from trainers: Pika/Eevee forms before 30, BB Greninja, Rival starter HA (#4863) * Remove Pika/Eevee forms from Trainers before wave 30, and BB Gren * Fix `egg` test * Ban hidden ability from Rival starter --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/battle-scene.ts | 9 +++++++++ src/data/trainer-config.ts | 18 +++++++++++++----- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 061fc6e28f2..2f062667808 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1424,10 +1424,19 @@ export default class BattleScene extends SceneBase { case Species.PALDEA_TAUROS: return Utils.randSeedInt(species.forms.length); case Species.PIKACHU: + if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) { + return 0; // Ban Cosplay and Partner Pika from Trainers before wave 30 + } return Utils.randSeedInt(8); case Species.EEVEE: + if (this.currentBattle?.battleType === BattleType.TRAINER && this.currentBattle?.waveIndex < 30) { + return 0; // No Partner Eevee for Wave 12 Preschoolers + } return Utils.randSeedInt(2); case Species.GRENINJA: + if (this.currentBattle?.battleType === BattleType.TRAINER) { + return 0; // Don't give trainers Battle Bond Greninja + } return Utils.randSeedInt(2); case Species.ZYGARDE: return Utils.randSeedInt(4); diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index d82d568ecc6..5e5f38bd00d 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -1841,21 +1841,25 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.RIVAL]: new TrainerConfig((t = TrainerType.RIVAL)).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL) .setModifierRewardFuncs(() => modifierTypes.SUPER_EXP_CHARM, () => modifierTypes.EXP_SHARE) .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM, () => modifierTypes.ABILITY_CHARM) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, Species.CHIKORITA, Species.CYNDAQUIL, Species.TOTODILE, Species.TREECKO, Species.TORCHIC, Species.MUDKIP, Species.TURTWIG, Species.CHIMCHAR, Species.PIPLUP, Species.SNIVY, Species.TEPIG, Species.OSHAWOTT, Species.CHESPIN, Species.FENNEKIN, Species.FROAKIE, Species.ROWLET, Species.LITTEN, Species.POPPLIO, Species.GROOKEY, Species.SCORBUNNY, Species.SOBBLE, Species.SPRIGATITO, Species.FUECOCO, Species.QUAXLY ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEY, Species.HOOTHOOT, Species.TAILLOW, Species.STARLY, Species.PIDOVE, Species.FLETCHLING, Species.PIKIPEK, Species.ROOKIDEE, Species.WATTREL ], TrainerSlot.TRAINER, true)), [TrainerType.RIVAL_2]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_2) .setModifierRewardFuncs(() => modifierTypes.EXP_SHARE) .setEventModifierRewardFuncs(() => modifierTypes.SHINY_CHARM) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.IVYSAUR, Species.CHARMELEON, Species.WARTORTLE, Species.BAYLEEF, Species.QUILAVA, Species.CROCONAW, Species.GROVYLE, Species.COMBUSKEN, Species.MARSHTOMP, Species.GROTLE, Species.MONFERNO, Species.PRINPLUP, Species.SERVINE, Species.PIGNITE, Species.DEWOTT, Species.QUILLADIN, Species.BRAIXEN, Species.FROGADIER, Species.DARTRIX, Species.TORRACAT, Species.BRIONNE, Species.THWACKEY, Species.RABOOT, Species.DRIZZILE, Species.FLORAGATO, Species.CROCALOR, Species.QUAXWELL ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOTTO, Species.HOOTHOOT, Species.TAILLOW, Species.STARAVIA, Species.TRANQUILL, Species.FLETCHINDER, Species.TRUMBEAK, Species.CORVISQUIRE, Species.WATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)), [TrainerType.RIVAL_3]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setStaticParty().setMoneyMultiplier(1.5).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival").setMixedBattleBgm("battle_rival").setPartyTemplates(trainerPartyTemplates.RIVAL_3) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540), [TrainerType.RIVAL_4]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(1.75).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_2").setMixedBattleBgm("battle_rival_2").setPartyTemplates(trainerPartyTemplates.RIVAL_4) - .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true)) + .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, + (p => p.abilityIndex = 0))) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540) @@ -1865,7 +1869,10 @@ export const trainerConfigs: TrainerConfigs = { }), [TrainerType.RIVAL_5]: new TrainerConfig(++t).setName("Finn").setHasGenders("Ivy").setHasCharSprite().setTitle("Rival").setBoss().setStaticParty().setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.RIVAL).setBattleBgm("battle_rival_3").setMixedBattleBgm("battle_rival_3").setPartyTemplates(trainerPartyTemplates.RIVAL_5) .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, - p => p.setBoss(true, 2))) + p => { + p.setBoss(true, 2); + p.abilityIndex = 0; + })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true)) .setPartyMemberFunc(2, getSpeciesFilterRandomPartyMemberFunc((species: PokemonSpecies) => !pokemonEvolutions.hasOwnProperty(species.speciesId) && !pokemonPrevolutions.hasOwnProperty(species.speciesId) && species.baseTotal >= 450)) .setSpeciesFilter(species => species.baseTotal >= 540) @@ -1883,6 +1890,7 @@ export const trainerConfigs: TrainerConfigs = { .setPartyMemberFunc(0, getRandomPartyMemberFunc([ Species.VENUSAUR, Species.CHARIZARD, Species.BLASTOISE, Species.MEGANIUM, Species.TYPHLOSION, Species.FERALIGATR, Species.SCEPTILE, Species.BLAZIKEN, Species.SWAMPERT, Species.TORTERRA, Species.INFERNAPE, Species.EMPOLEON, Species.SERPERIOR, Species.EMBOAR, Species.SAMUROTT, Species.CHESNAUGHT, Species.DELPHOX, Species.GRENINJA, Species.DECIDUEYE, Species.INCINEROAR, Species.PRIMARINA, Species.RILLABOOM, Species.CINDERACE, Species.INTELEON, Species.MEOWSCARADA, Species.SKELEDIRGE, Species.QUAQUAVAL ], TrainerSlot.TRAINER, true, p => { p.setBoss(true, 3); + p.abilityIndex = 0; p.generateAndPopulateMoveset(); })) .setPartyMemberFunc(1, getRandomPartyMemberFunc([ Species.PIDGEOT, Species.NOCTOWL, Species.SWELLOW, Species.STARAPTOR, Species.UNFEZANT, Species.TALONFLAME, Species.TOUCANNON, Species.CORVIKNIGHT, Species.KILOWATTREL ], TrainerSlot.TRAINER, true, From 5ca1fd5cfd8cc8f98c9cfc939a484cea6399a57d Mon Sep 17 00:00:00 2001 From: pom-eranian Date: Fri, 15 Nov 2024 11:58:50 -0500 Subject: [PATCH 13/15] [Sprite] Set default fps to 10 instead of 12 on pokemon animations (#4842) * Set default fps to 10 instead of 12 for pokemon sprites * [Sprite] Set pokemon animation framerate to 10 where assigned --- src/data/pokemon-species.ts | 4 ++-- src/field/mystery-encounter-intro.ts | 2 +- src/field/pokemon.ts | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index 203e545503a..ec104d4d4aa 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -505,11 +505,11 @@ export abstract class PokemonSpeciesForm { scene.anims.create({ key: this.getSpriteKey(female, formIndex, shiny, variant), frames: frameNames, - frameRate: 12, + frameRate: 10, repeat: -1 }); } else { - scene.anims.get(spriteKey).frameRate = 12; + scene.anims.get(spriteKey).frameRate = 10; } let spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, ""); const useExpSprite = scene.experimentalSprites && scene.hasExpSprite(spriteKey); diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 12bcace500c..1577d1157d7 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -212,7 +212,7 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con this.scene.anims.create({ key: config.spriteKey, frames: frameNames, - frameRate: 12, + frameRate: 10, repeat: -1 }); } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index bc7e844a290..30d1aceea4b 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -427,7 +427,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.scene.anims.create({ key: this.getBattleSpriteKey(), frames: battleFrameNames, - frameRate: 12, + frameRate: 10, repeat: -1 }); } @@ -3612,7 +3612,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.status = null; if (lastStatus === StatusEffect.SLEEP) { - this.setFrameRate(12); + this.setFrameRate(10); if (this.getTag(BattlerTagType.NIGHTMARE)) { this.lapseTag(BattlerTagType.NIGHTMARE); } From eb3c0d731a38178722636391f175002c9b467f5d Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:17:46 -0800 Subject: [PATCH 14/15] [P2] Lunar Blessing / Jungle Healing now heal Freeze (#4877) * Added Freeze to statuses healed by Jungle Healing / Lunar Blessing * Fixed up documentation. --------- Co-authored-by: frutescens --- src/data/move.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/data/move.ts b/src/data/move.ts index ae2ba919b52..2ac4d74b712 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -2589,12 +2589,11 @@ export class HealStatusEffectAttr extends MoveEffectAttr { /** * @param selfTarget - Whether this move targets the user - * @param ...effects - List of status effects to cure + * @param effects - status effect or list of status effects to cure */ - constructor(selfTarget: boolean, ...effects: StatusEffect[]) { + constructor(selfTarget: boolean, effects: StatusEffect | StatusEffect[]) { super(selfTarget, { lastHitOnly: true }); - - this.effects = effects; + this.effects = [ effects ].flat(1); } /** @@ -8583,7 +8582,7 @@ export function initMoves() { .attr(AddArenaTagAttr, ArenaTagType.IMPRISON, 1, true, false) .target(MoveTarget.ENEMY_SIDE), new SelfStatusMove(Moves.REFRESH, Type.NORMAL, -1, 20, -1, 0, 3) - .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN) + .attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN ]) .condition((user, target, move) => !!user.status && (user.status.effect === StatusEffect.PARALYSIS || user.status.effect === StatusEffect.POISON || user.status.effect === StatusEffect.TOXIC || user.status.effect === StatusEffect.BURN)), new SelfStatusMove(Moves.GRUDGE, Type.GHOST, -1, 5, -1, 0, 3) .attr(AddBattlerTagAttr, BattlerTagType.GRUDGE, true, undefined, 1), @@ -9792,7 +9791,7 @@ export function initMoves() { .condition( (user: Pokemon, target: Pokemon, move: Move) => isNonVolatileStatusEffect(target.status?.effect!)) // TODO: is this bang correct? .attr(HealAttr, 0.5) - .attr(HealStatusEffectAttr, false, ...getNonVolatileStatusEffects()) + .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .triageMove(), new AttackMove(Moves.REVELATION_DANCE, Type.NORMAL, MoveCategory.SPECIAL, 90, 100, 15, -1, 0, 7) .danceMove() @@ -10216,7 +10215,7 @@ export function initMoves() { .attr(StatusEffectAttr, StatusEffect.BURN), new StatusMove(Moves.JUNGLE_HEALING, Type.GRASS, -1, 10, -1, 0, 8) .attr(HealAttr, 0.25, true, false) - .attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP) + .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .target(MoveTarget.USER_AND_ALLIES), new AttackMove(Moves.WICKED_BLOW, Type.DARK, MoveCategory.PHYSICAL, 75, 100, 5, -1, 0, 8) .attr(CritOnlyAttr) @@ -10320,12 +10319,12 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES), new StatusMove(Moves.LUNAR_BLESSING, Type.PSYCHIC, -1, 5, -1, 0, 8) .attr(HealAttr, 0.25, true, false) - .attr(HealStatusEffectAttr, false, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP) + .attr(HealStatusEffectAttr, false, getNonVolatileStatusEffects()) .target(MoveTarget.USER_AND_ALLIES) .triageMove(), new SelfStatusMove(Moves.TAKE_HEART, Type.PSYCHIC, -1, 10, -1, 0, 8) .attr(StatStageChangeAttr, [ Stat.SPATK, Stat.SPDEF ], 1, true) - .attr(HealStatusEffectAttr, true, StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP), + .attr(HealStatusEffectAttr, true, [ StatusEffect.PARALYSIS, StatusEffect.POISON, StatusEffect.TOXIC, StatusEffect.BURN, StatusEffect.SLEEP ]), /* Unused new AttackMove(Moves.G_MAX_WILDFIRE, Type.FIRE, MoveCategory.PHYSICAL, 10, -1, 10, -1, 0, 8) .target(MoveTarget.ALL_NEAR_ENEMIES) From c535e928d84688b361c54e9bb8992a079f71013d Mon Sep 17 00:00:00 2001 From: Mumble <171087428+frutescens@users.noreply.github.com> Date: Fri, 15 Nov 2024 09:45:21 -0800 Subject: [PATCH 15/15] [Beta][QoL] Improved cursor memory for target selection in Doubles (#4849) * Added more intelligent cursor memory for target selection in Doubles * Added documentation * Fixed variable name. * Apply suggestions from code review Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --------- Co-authored-by: frutescens Co-authored-by: Moka <54149968+MokaStitcher@users.noreply.github.com> --- src/ui/target-select-ui-handler.ts | 40 +++++++++++++++++++++++++----- 1 file changed, 34 insertions(+), 6 deletions(-) diff --git a/src/ui/target-select-ui-handler.ts b/src/ui/target-select-ui-handler.ts index ecc15e5985e..249ae7b8b01 100644 --- a/src/ui/target-select-ui-handler.ts +++ b/src/ui/target-select-ui-handler.ts @@ -13,9 +13,11 @@ import { SubstituteTag } from "#app/data/battler-tags"; export type TargetSelectCallback = (targets: BattlerIndex[]) => void; export default class TargetSelectUiHandler extends UiHandler { - private fieldIndex: integer; + private fieldIndex: number; private move: Moves; private targetSelectCallback: TargetSelectCallback; + private cursor0: number; // associated with BattlerIndex.PLAYER + private cursor1: number; // associated with BattlerIndex.PLAYER_2 private isMultipleTargets: boolean = false; private targets: BattlerIndex[]; @@ -42,8 +44,9 @@ export default class TargetSelectUiHandler extends UiHandler { this.fieldIndex = args[0] as integer; this.move = args[1] as Moves; this.targetSelectCallback = args[2] as TargetSelectCallback; + const user = this.scene.getPlayerField()[this.fieldIndex]; - const moveTargets = getMoveTargets(this.scene.getPlayerField()[this.fieldIndex], this.move); + const moveTargets = getMoveTargets(user, this.move); this.targets = moveTargets.targets; this.isMultipleTargets = moveTargets.multiple ?? false; @@ -53,11 +56,29 @@ export default class TargetSelectUiHandler extends UiHandler { this.enemyModifiers = this.scene.getModifierBar(true); - this.setCursor(this.targets.includes(this.cursor) ? this.cursor : this.targets[0]); - + if (this.fieldIndex === BattlerIndex.PLAYER) { + this.resetCursor(this.cursor0, user); + } else if (this.fieldIndex === BattlerIndex.PLAYER_2) { + this.resetCursor(this.cursor1, user); + } return true; } + /** + * Determines what value to assign the main cursor based on the previous turn's target or the user's status + * @param cursorN the cursor associated with the user's field index + * @param user the Pokemon using the move + */ + resetCursor(cursorN: number, user: Pokemon): void { + if (!Utils.isNullOrUndefined(cursorN)) { + if ([ BattlerIndex.PLAYER, BattlerIndex.PLAYER_2 ].includes(cursorN) || user.battleSummonData.waveTurnCount === 1) { + // Reset cursor on the first turn of a fight or if an ally was targeted last turn + cursorN = -1; + } + } + this.setCursor(this.targets.includes(cursorN) ? cursorN : this.targets[0]); + } + processInput(button: Button): boolean { const ui = this.getUi(); @@ -67,6 +88,15 @@ export default class TargetSelectUiHandler extends UiHandler { const targetIndexes: BattlerIndex[] = this.isMultipleTargets ? this.targets : [ this.cursor ]; this.targetSelectCallback(button === Button.ACTION ? targetIndexes : []); success = true; + if (this.fieldIndex === BattlerIndex.PLAYER) { + if (Utils.isNullOrUndefined(this.cursor0) || this.cursor0 !== this.cursor) { + this.cursor0 = this.cursor; + } + } else if (this.fieldIndex === BattlerIndex.PLAYER_2) { + if (Utils.isNullOrUndefined(this.cursor1) || this.cursor1 !== this.cursor) { + this.cursor1 = this.cursor; + } + } } else if (this.isMultipleTargets) { success = false; } else { @@ -152,7 +182,6 @@ export default class TargetSelectUiHandler extends UiHandler { yoyo: true })); }); - return ret; } @@ -184,7 +213,6 @@ export default class TargetSelectUiHandler extends UiHandler { } clear() { - this.cursor = -1; super.clear(); this.eraseCursor(); }