From 87b78e6b706924d435f57924c991d9705b93972b Mon Sep 17 00:00:00 2001 From: damocleas Date: Sat, 22 Mar 2025 15:47:41 -0400 Subject: [PATCH 01/12] [Balance] [Move] Made more moves callable with Metronome, Assist, Sleep Talk, and Copycat (#5549) Update invalid-moves.ts --- src/data/moves/invalid-moves.ts | 104 -------------------------------- 1 file changed, 104 deletions(-) diff --git a/src/data/moves/invalid-moves.ts b/src/data/moves/invalid-moves.ts index 50f815b9e58..64052902d9d 100644 --- a/src/data/moves/invalid-moves.ts +++ b/src/data/moves/invalid-moves.ts @@ -3,144 +3,47 @@ import { Moves } from "#enums/moves"; /** Set of moves that cannot be called by {@linkcode Moves.METRONOME Metronome} */ export const invalidMetronomeMoves: ReadonlySet = new Set([ Moves.AFTER_YOU, - Moves.APPLE_ACID, - Moves.ARMOR_CANNON, Moves.ASSIST, - Moves.ASTRAL_BARRAGE, - Moves.AURA_WHEEL, Moves.BANEFUL_BUNKER, Moves.BEAK_BLAST, - Moves.BEHEMOTH_BASH, - Moves.BEHEMOTH_BLADE, Moves.BELCH, Moves.BESTOW, - Moves.BLAZING_TORQUE, - Moves.BODY_PRESS, - Moves.BRANCH_POKE, - Moves.BREAKING_SWIPE, - Moves.CELEBRATE, - Moves.CHATTER, - Moves.CHILLING_WATER, - Moves.CHILLY_RECEPTION, - Moves.CLANGOROUS_SOUL, - Moves.COLLISION_COURSE, - Moves.COMBAT_TORQUE, Moves.COMEUPPANCE, Moves.COPYCAT, Moves.COUNTER, - Moves.COVET, Moves.CRAFTY_SHIELD, - Moves.DECORATE, Moves.DESTINY_BOND, Moves.DETECT, - Moves.DIAMOND_STORM, - Moves.DOODLE, - Moves.DOUBLE_IRON_BASH, - Moves.DOUBLE_SHOCK, - Moves.DRAGON_ASCENT, - Moves.DRAGON_ENERGY, - Moves.DRUM_BEATING, - Moves.DYNAMAX_CANNON, - Moves.ELECTRO_DRIFT, Moves.ENDURE, - Moves.ETERNABEAM, - Moves.FALSE_SURRENDER, Moves.FEINT, - Moves.FIERY_WRATH, - Moves.FILLET_AWAY, Moves.FLEUR_CANNON, Moves.FOCUS_PUNCH, Moves.FOLLOW_ME, - Moves.FREEZE_SHOCK, - Moves.FREEZING_GLARE, - Moves.GLACIAL_LANCE, - Moves.GRAV_APPLE, Moves.HELPING_HAND, - Moves.HOLD_HANDS, - Moves.HYPER_DRILL, - Moves.HYPERSPACE_FURY, - Moves.HYPERSPACE_HOLE, - Moves.ICE_BURN, Moves.INSTRUCT, - Moves.JET_PUNCH, - Moves.JUNGLE_HEALING, Moves.KINGS_SHIELD, - Moves.LIFE_DEW, - Moves.LIGHT_OF_RUIN, - Moves.MAKE_IT_RAIN, - Moves.MAGICAL_TORQUE, Moves.MAT_BLOCK, Moves.ME_FIRST, - Moves.METEOR_ASSAULT, Moves.METRONOME, Moves.MIMIC, - Moves.MIND_BLOWN, Moves.MIRROR_COAT, Moves.MIRROR_MOVE, - Moves.MOONGEIST_BEAM, - Moves.NATURE_POWER, - Moves.NATURES_MADNESS, - Moves.NOXIOUS_TORQUE, Moves.OBSTRUCT, - Moves.ORDER_UP, - Moves.ORIGIN_PULSE, - Moves.OVERDRIVE, - Moves.PHOTON_GEYSER, - Moves.PLASMA_FISTS, - Moves.POPULATION_BOMB, - Moves.POUNCE, - Moves.POWER_SHIFT, - Moves.PRECIPICE_BLADES, Moves.PROTECT, - Moves.PYRO_BALL, Moves.QUASH, Moves.QUICK_GUARD, - Moves.RAGE_FIST, Moves.RAGE_POWDER, - Moves.RAGING_BULL, - Moves.RAGING_FURY, - Moves.RELIC_SONG, Moves.REVIVAL_BLESSING, - Moves.RUINATION, - Moves.SALT_CURE, - Moves.SECRET_SWORD, - Moves.SHED_TAIL, Moves.SHELL_TRAP, Moves.SILK_TRAP, Moves.SKETCH, Moves.SLEEP_TALK, - Moves.SNAP_TRAP, - Moves.SNARL, Moves.SNATCH, Moves.SNORE, - Moves.SNOWSCAPE, - Moves.SPECTRAL_THIEF, - Moves.SPICY_EXTRACT, Moves.SPIKY_SHIELD, - Moves.SPIRIT_BREAK, Moves.SPOTLIGHT, - Moves.STEAM_ERUPTION, - Moves.STEEL_BEAM, - Moves.STRANGE_STEAM, Moves.STRUGGLE, - Moves.SUNSTEEL_STRIKE, - Moves.SURGING_STRIKES, - Moves.SWITCHEROO, - Moves.TECHNO_BLAST, - Moves.TERA_STARSTORM, - Moves.THIEF, - Moves.THOUSAND_ARROWS, - Moves.THOUSAND_WAVES, - Moves.THUNDER_CAGE, - Moves.THUNDEROUS_KICK, - Moves.TIDY_UP, - Moves.TRAILBLAZE, Moves.TRANSFORM, - Moves.TRICK, - Moves.TWIN_BEAM, - Moves.V_CREATE, - Moves.WICKED_BLOW, - Moves.WICKED_TORQUE, Moves.WIDE_GUARD, ]); @@ -157,7 +60,6 @@ export const invalidAssistMoves: ReadonlySet = new Set([ Moves.CIRCLE_THROW, Moves.COPYCAT, Moves.COUNTER, - Moves.COVET, Moves.DESTINY_BOND, Moves.DETECT, Moves.DIG, @@ -192,7 +94,6 @@ export const invalidAssistMoves: ReadonlySet = new Set([ Moves.SPOTLIGHT, Moves.STRUGGLE, Moves.SWITCHEROO, - Moves.THIEF, Moves.TRANSFORM, Moves.TRICK, Moves.WHIRLWIND, @@ -208,7 +109,6 @@ export const invalidSleepTalkMoves: ReadonlySet = new Set([ Moves.COPYCAT, Moves.DIG, Moves.DIVE, - Moves.DYNAMAX_CANNON, Moves.FREEZE_SHOCK, Moves.FLY, Moves.FOCUS_PUNCH, @@ -238,15 +138,12 @@ export const invalidCopycatMoves: ReadonlySet = new Set([ Moves.ASSIST, Moves.BANEFUL_BUNKER, Moves.BEAK_BLAST, - Moves.BEHEMOTH_BASH, - Moves.BEHEMOTH_BLADE, Moves.BESTOW, Moves.CELEBRATE, Moves.CHATTER, Moves.CIRCLE_THROW, Moves.COPYCAT, Moves.COUNTER, - Moves.COVET, Moves.DESTINY_BOND, Moves.DETECT, Moves.DRAGON_TAIL, @@ -274,7 +171,6 @@ export const invalidCopycatMoves: ReadonlySet = new Set([ Moves.SPOTLIGHT, Moves.STRUGGLE, Moves.SWITCHEROO, - Moves.THIEF, Moves.TRANSFORM, Moves.TRICK, Moves.WHIRLWIND, From d0a9672e91de1c7fcf87d1f81eede4faa7dc6a86 Mon Sep 17 00:00:00 2001 From: damocleas Date: Sat, 22 Mar 2025 16:18:08 -0400 Subject: [PATCH 02/12] [Balance] [Move] Add Fleur Cannon as a Metronome callableove (#5551) * Update invalid-moves.ts * forgot fleur cannon --- src/data/moves/invalid-moves.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/data/moves/invalid-moves.ts b/src/data/moves/invalid-moves.ts index 64052902d9d..7284831528a 100644 --- a/src/data/moves/invalid-moves.ts +++ b/src/data/moves/invalid-moves.ts @@ -16,7 +16,6 @@ export const invalidMetronomeMoves: ReadonlySet = new Set([ Moves.DETECT, Moves.ENDURE, Moves.FEINT, - Moves.FLEUR_CANNON, Moves.FOCUS_PUNCH, Moves.FOLLOW_ME, Moves.HELPING_HAND, From 4b8f1df8cd4bc98b985f05931befa11183e0443d Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Sat, 22 Mar 2025 14:46:58 -0700 Subject: [PATCH 03/12] [Bug] Add list of uncallable moves for Mirror Move (#5552) * Add list of uncallable moves for Mirror Move * Mirror Move now respects the uncallable moves list --------- Co-authored-by: damocleas --- src/data/moves/invalid-moves.ts | 66 +++++++++++++++++++++++++++++++++ src/data/moves/move.ts | 7 ++-- 2 files changed, 70 insertions(+), 3 deletions(-) diff --git a/src/data/moves/invalid-moves.ts b/src/data/moves/invalid-moves.ts index 7284831528a..5cd45de7939 100644 --- a/src/data/moves/invalid-moves.ts +++ b/src/data/moves/invalid-moves.ts @@ -174,3 +174,69 @@ export const invalidCopycatMoves: ReadonlySet = new Set([ Moves.TRICK, Moves.WHIRLWIND, ]); + +export const invalidMirrorMoveMoves: ReadonlySet = new Set([ + Moves.ACUPRESSURE, + Moves.AFTER_YOU, + Moves.AROMATIC_MIST, + Moves.BEAK_BLAST, + Moves.BELCH, + Moves.CHILLY_RECEPTION, + Moves.COACHING, + Moves.CONVERSION_2, + Moves.COUNTER, + Moves.CRAFTY_SHIELD, + Moves.CURSE, + Moves.DECORATE, + Moves.DOODLE, + Moves.DOOM_DESIRE, + Moves.DRAGON_CHEER, + Moves.ELECTRIC_TERRAIN, + Moves.FINAL_GAMBIT, + Moves.FLORAL_HEALING, + Moves.FLOWER_SHIELD, + Moves.FOCUS_PUNCH, + Moves.FUTURE_SIGHT, + Moves.GEAR_UP, + Moves.GRASSY_TERRAIN, + Moves.GRAVITY, + Moves.GUARD_SPLIT, + Moves.HAIL, + Moves.HAZE, + Moves.HEAL_PULSE, + Moves.HELPING_HAND, + Moves.HOLD_HANDS, + Moves.INSTRUCT, + Moves.ION_DELUGE, + Moves.MAGNETIC_FLUX, + Moves.MAT_BLOCK, + Moves.ME_FIRST, + Moves.MIMIC, + Moves.MIRROR_COAT, + Moves.MIRROR_MOVE, + Moves.MIST, + Moves.MISTY_TERRAIN, + Moves.MUD_SPORT, + Moves.PERISH_SONG, + Moves.POWER_SPLIT, + Moves.PSYCH_UP, + Moves.PSYCHIC_TERRAIN, + Moves.PURIFY, + Moves.QUICK_GUARD, + Moves.RAIN_DANCE, + Moves.REFLECT_TYPE, + Moves.ROLE_PLAY, + Moves.ROTOTILLER, + Moves.SANDSTORM, + Moves.SHELL_TRAP, + Moves.SKETCH, + Moves.SNOWSCAPE, + Moves.SPIT_UP, + Moves.SPOTLIGHT, + Moves.STRUGGLE, + Moves.SUNNY_DAY, + Moves.TEATIME, + Moves.TRANSFORM, + Moves.WATER_SPORT, + Moves.WIDE_GUARD, +]); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 3d80b6b3f4c..c7b5b4aec6b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -125,7 +125,7 @@ import { MoveTarget } from "#enums/MoveTarget"; import { MoveFlags } from "#enums/MoveFlags"; import { MoveEffectTrigger } from "#enums/MoveEffectTrigger"; import { MultiHitType } from "#enums/MultiHitType"; -import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidSleepTalkMoves } from "./invalid-moves"; +import { invalidAssistMoves, invalidCopycatMoves, invalidMetronomeMoves, invalidMirrorMoveMoves, invalidSleepTalkMoves } from "./invalid-moves"; type MoveConditionFunc = (user: Pokemon, target: Pokemon, move: Move) => boolean; type UserMoveConditionFunc = (user: Pokemon, move: Move) => boolean; @@ -6966,7 +6966,8 @@ export class CopyMoveAttr extends CallMoveAttr { getCondition(): MoveConditionFunc { return (user, target, move) => { if (this.mirrorMove) { - return target.getMoveHistory().length !== 0; + const lastMove = target.getLastXMoves()[0]?.move; + return !!lastMove && !this.invalidMoves.has(lastMove); } else { const lastMove = globalScene.currentBattle.lastMove; return lastMove !== undefined && !this.invalidMoves.has(lastMove); @@ -8562,7 +8563,7 @@ export function initMoves() { new SelfStatusMove(Moves.METRONOME, PokemonType.NORMAL, -1, 10, -1, 0, 1) .attr(RandomMoveAttr, invalidMetronomeMoves), new StatusMove(Moves.MIRROR_MOVE, PokemonType.FLYING, -1, 20, -1, 0, 1) - .attr(CopyMoveAttr, true), + .attr(CopyMoveAttr, true, invalidMirrorMoveMoves), new AttackMove(Moves.SELF_DESTRUCT, PokemonType.NORMAL, MoveCategory.PHYSICAL, 200, 100, 5, -1, 0, 1) .attr(SacrificialAttr) .makesContact(false) From 1e876ec595195d4094d3cccc076c8bd7d98a5099 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Serrado=20Marques?= Date: Sun, 23 Mar 2025 19:07:14 +0000 Subject: [PATCH 04/12] [BUG] fixes #5472 - transform on reload (#5508) * [BUG] fixes #5472 - transform on reload * Fix the bug where transformed pokemon failed to load sprite on reload if it was not the base form * Now properly loads the transformed sprite assets during summon phase --- src/field/pokemon.ts | 2 +- src/phases/summon-phase.ts | 4 ++ src/system/pokemon-data.ts | 29 +++++++++++++- test/abilities/imposter.test.ts | 59 +++++++++++++++++++++++++++ test/moves/transform.test.ts | 70 ++++++++++++++++++++++++++++++++- 5 files changed, 160 insertions(+), 4 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 890c6bab0d6..42fe1dc1010 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -5575,7 +5575,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.resetBattleSummonData(); if (this.summonDataPrimer) { - for (const k of Object.keys(this.summonData)) { + for (const k of Object.keys(this.summonDataPrimer)) { if (this.summonDataPrimer[k]) { this.summonData[k] = this.summonDataPrimer[k]; } diff --git a/src/phases/summon-phase.ts b/src/phases/summon-phase.ts index 31cd2645e68..42463e7edb0 100644 --- a/src/phases/summon-phase.ts +++ b/src/phases/summon-phase.ts @@ -195,6 +195,10 @@ export class SummonPhase extends PartyMemberPokemonPhase { pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 }); pokemon.getSprite().clearTint(); pokemon.resetSummonData(); + // necessary to stay transformed during wild waves + if (pokemon.summonData?.speciesForm) { + pokemon.loadAssets(false); + } globalScene.time.delayedCall(1000, () => this.end()); }, }); diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 4e694bc1189..76f5526e407 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -3,7 +3,7 @@ import { globalScene } from "#app/global-scene"; import type { Gender } from "../data/gender"; import type { Nature } from "#enums/nature"; import type { PokeballType } from "#enums/pokeball"; -import { getPokemonSpecies } from "../data/pokemon-species"; +import { getPokemonSpecies, getPokemonSpeciesForm } from "../data/pokemon-species"; import { Status } from "../data/status-effect"; import Pokemon, { EnemyPokemon, PokemonMove, PokemonSummonData } from "../field/pokemon"; import { TrainerSlot } from "../data/trainer-config"; @@ -14,6 +14,7 @@ import { Moves } from "#enums/moves"; import type { Species } from "#enums/species"; import { CustomPokemonData } from "#app/data/custom-pokemon-data"; import type { PokemonType } from "#enums/pokemon-type"; +import { getSpeciesFormChangeMessage } from "#app/data/pokemon-forms"; export default class PokemonData { public id: number; @@ -63,6 +64,7 @@ export default class PokemonData { public bossSegments?: number; public summonData: PokemonSummonData; + public summonDataSpeciesFormIndex: number; /** Data that can customize a Pokemon in non-standard ways from its Species */ public customPokemonData: CustomPokemonData; @@ -145,8 +147,9 @@ export default class PokemonData { this.moveset = sourcePokemon.moveset; if (!forHistory) { this.status = sourcePokemon.status; - if (this.player) { + if (this.player && sourcePokemon.summonData) { this.summonData = sourcePokemon.summonData; + this.summonDataSpeciesFormIndex = this.getSummonDataSpeciesFormIndex(); } } } else { @@ -170,6 +173,8 @@ export default class PokemonData { this.summonData.ability = source.summonData.ability; this.summonData.moveset = source.summonData.moveset?.map(m => PokemonMove.loadMove(m)); this.summonData.types = source.summonData.types; + this.summonData.speciesForm = source.summonData.speciesForm; + this.summonDataSpeciesFormIndex = source.summonDataSpeciesFormIndex; if (source.summonData.tags) { this.summonData.tags = source.summonData.tags?.map(t => loadBattlerTag(t)); @@ -213,8 +218,28 @@ export default class PokemonData { this, ); if (this.summonData) { + // when loading from saved session, recover summonData.speciesFrom and form index species object + // used to stay transformed on reload session + if (this.summonData.speciesForm) { + this.summonData.speciesForm = getPokemonSpeciesForm( + this.summonData.speciesForm.speciesId, + this.summonDataSpeciesFormIndex, + ); + } ret.primeSummonData(this.summonData); } return ret; } + + /** + * Method to save summon data species form index + * Necessary in case the pokemon is transformed + * to reload the correct form + */ + getSummonDataSpeciesFormIndex(): number { + if (this.summonData.speciesForm) { + return this.summonData.speciesForm.formIndex; + } + return 0; + } } diff --git a/test/abilities/imposter.test.ts b/test/abilities/imposter.test.ts index b4469cd9042..2c7302d04b7 100644 --- a/test/abilities/imposter.test.ts +++ b/test/abilities/imposter.test.ts @@ -127,4 +127,63 @@ describe("Abilities - Imposter", () => { expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); }); + + it("should persist transformed attributes across reloads", async () => { + game.override.moveset([Moves.ABSORB]); + + await game.classicMode.startBattle([Species.DITTO]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + + expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase"); + expect(game.scene.currentBattle.waveIndex).toBe(2); + + await game.reload.reloadSession(); + + const playerReloaded = game.scene.getPlayerPokemon()!; + const playerMoveset = player.getMoveset(); + + expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); + expect(playerReloaded.getAbility()).toBe(enemy.getAbility()); + expect(playerReloaded.getGender()).toBe(enemy.getGender()); + + expect(playerReloaded.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP)); + for (const s of EFFECTIVE_STATS) { + expect(playerReloaded.getStat(s, false)).toBe(enemy.getStat(s, false)); + } + + expect(playerMoveset.length).toEqual(1); + expect(playerMoveset[0]?.moveId).toEqual(Moves.SPLASH); + }); + + it("should stay transformed with the correct form after reload", async () => { + game.override.moveset([Moves.ABSORB]); + game.override.enemySpecies(Species.UNOWN); + await game.classicMode.startBattle([Species.DITTO]); + + const enemy = game.scene.getEnemyPokemon()!; + + // change form + enemy.species.forms[5]; + enemy.species.formIndex = 5; + + game.move.select(Moves.SPLASH); + await game.doKillOpponents(); + await game.toNextWave(); + + expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase"); + expect(game.scene.currentBattle.waveIndex).toBe(2); + + await game.reload.reloadSession(); + + const playerReloaded = game.scene.getPlayerPokemon()!; + + expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); + expect(playerReloaded.getSpeciesForm().formIndex).toBe(enemy.getSpeciesForm().formIndex); + }); }); diff --git a/test/moves/transform.test.ts b/test/moves/transform.test.ts index 5140202a383..d37decf28f4 100644 --- a/test/moves/transform.test.ts +++ b/test/moves/transform.test.ts @@ -6,6 +6,7 @@ import { TurnEndPhase } from "#app/phases/turn-end-phase"; import { Moves } from "#enums/moves"; import { Stat, BATTLE_STATS, EFFECTIVE_STATS } from "#enums/stat"; import { Abilities } from "#enums/abilities"; +import { BattlerIndex } from "#app/battle"; // TODO: Add more tests once Transform is fully implemented describe("Moves - Transform", () => { @@ -58,7 +59,7 @@ describe("Moves - Transform", () => { } const playerMoveset = player.getMoveset(); - const enemyMoveset = player.getMoveset(); + const enemyMoveset = enemy.getMoveset(); expect(playerMoveset.length).toBe(enemyMoveset.length); for (let i = 0; i < playerMoveset.length && i < enemyMoveset.length; i++) { @@ -127,4 +128,71 @@ describe("Moves - Transform", () => { expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); }); + + it("should persist transformed attributes across reloads", async () => { + game.override.enemyMoveset([]).moveset([]); + + await game.classicMode.startBattle([Species.DITTO]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + game.move.changeMoveset(player, Moves.TRANSFORM); + game.move.changeMoveset(enemy, Moves.MEMENTO); + + game.move.select(Moves.TRANSFORM); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextWave(); + + expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase"); + expect(game.scene.currentBattle.waveIndex).toBe(2); + + await game.reload.reloadSession(); + + const playerReloaded = game.scene.getPlayerPokemon()!; + const playerMoveset = player.getMoveset(); + + expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); + expect(playerReloaded.getAbility()).toBe(enemy.getAbility()); + expect(playerReloaded.getGender()).toBe(enemy.getGender()); + + expect(playerReloaded.getStat(Stat.HP, false)).not.toBe(enemy.getStat(Stat.HP)); + for (const s of EFFECTIVE_STATS) { + expect(playerReloaded.getStat(s, false)).toBe(enemy.getStat(s, false)); + } + + expect(playerMoveset.length).toEqual(1); + expect(playerMoveset[0]?.moveId).toEqual(Moves.MEMENTO); + }); + + it("should stay transformed with the correct form after reload", async () => { + game.override.enemyMoveset([]).moveset([]); + game.override.enemySpecies(Species.DARMANITAN); + + await game.classicMode.startBattle([Species.DITTO]); + + const player = game.scene.getPlayerPokemon()!; + const enemy = game.scene.getEnemyPokemon()!; + + // change form + enemy.species.forms[1]; + enemy.species.formIndex = 1; + + game.move.changeMoveset(player, Moves.TRANSFORM); + game.move.changeMoveset(enemy, Moves.MEMENTO); + + game.move.select(Moves.TRANSFORM); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.toNextWave(); + + expect(game.scene.getCurrentPhase()?.constructor.name).toBe("CommandPhase"); + expect(game.scene.currentBattle.waveIndex).toBe(2); + + await game.reload.reloadSession(); + + const playerReloaded = game.scene.getPlayerPokemon()!; + + expect(playerReloaded.getSpeciesForm().speciesId).toBe(enemy.getSpeciesForm().speciesId); + expect(playerReloaded.getSpeciesForm().formIndex).toBe(enemy.getSpeciesForm().formIndex); + }); }); From 817095d8950d75c062c7c41ecf24c2f3c1bfe2d3 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Sun, 23 Mar 2025 13:47:51 -0700 Subject: [PATCH 05/12] [Bug] Fix #2769 Revival Blessing Softlock in doubles (#5141) * Properly handle cases where enemy switches in due to revival * Fix user ally using move when revived * Move revival blessing function to move.ts * Properly filter for the right switch phase to remove * Re-add bug fix * Add test --- src/data/moves/move.ts | 14 +++++++++++--- test/moves/revival_blessing.test.ts | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index c7b5b4aec6b..9d470b86186 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -16,6 +16,7 @@ import type { AttackMoveResult, TurnMove } from "../../field/pokemon"; import type Pokemon from "../../field/pokemon"; import { EnemyPokemon, + FieldPosition, HitResult, MoveResult, PlayerPokemon, @@ -6158,9 +6159,16 @@ export class RevivalBlessingAttr extends MoveEffectAttr { if (globalScene.currentBattle.double && globalScene.getEnemyParty().length > 1) { const allyPokemon = user.getAlly(); - if (slotIndex <= 1) { - globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, pokemon.getFieldIndex(), slotIndex, false, false)); - } else if (allyPokemon.isFainted()) { + // Handle cases where revived pokemon needs to get switched in on same turn + if (allyPokemon.isFainted() || allyPokemon === pokemon) { + // Enemy switch phase should be removed and replaced with the revived pkmn switching in + globalScene.tryRemovePhase((phase: SwitchSummonPhase) => phase instanceof SwitchSummonPhase && phase.getPokemon() === pokemon); + // If the pokemon being revived was alive earlier in the turn, cancel its move + // (revived pokemon can't move in the turn they're brought back) + globalScene.findPhase((phase: MovePhase) => phase.pokemon === pokemon)?.cancel(); + if (user.fieldPosition === FieldPosition.CENTER) { + user.setFieldPosition(FieldPosition.LEFT); + } globalScene.unshiftPhase(new SwitchSummonPhase(SwitchType.SWITCH, allyPokemon.getFieldIndex(), slotIndex, false, false)); } } diff --git a/test/moves/revival_blessing.test.ts b/test/moves/revival_blessing.test.ts index 187b5e62e76..1ceb850edea 100644 --- a/test/moves/revival_blessing.test.ts +++ b/test/moves/revival_blessing.test.ts @@ -114,4 +114,25 @@ describe("Moves - Revival Blessing", () => { expect(feebas.hp).toBe(toDmgValue(0.5 * feebas.getMaxHp())); expect(game.scene.getPlayerField()[0]).toBe(feebas); }); + + it("should not summon multiple pokemon to the same slot when reviving the enemy ally in doubles", async () => { + game.override + .battleType("double") + .enemyMoveset([ Moves.REVIVAL_BLESSING ]) + .moveset([ Moves.SPLASH ]) + .startingWave(25); // 2nd rival battle - must have 3+ pokemon + await game.classicMode.startBattle([ Species.ARCEUS, Species.GIRATINA ]); + + const enemyFainting = game.scene.getEnemyField()[0]; + + game.move.select(Moves.SPLASH, 0); + game.move.select(Moves.SPLASH, 1); + await game.killPokemon(enemyFainting); + + await game.phaseInterceptor.to("BerryPhase"); + await game.toNextTurn(); + // If there are incorrectly two switch phases into this slot, the fainted pokemon will end up in slot 3 + // Make sure it's still in slot 1 + expect(game.scene.getEnemyParty()[0]).toBe(enemyFainting); + }); }); From 40e1e7fd4eb3a852c92433683fbe7e11279a6055 Mon Sep 17 00:00:00 2001 From: schmidtc1 <62030095+schmidtc1@users.noreply.github.com> Date: Sun, 23 Mar 2025 18:59:19 -0400 Subject: [PATCH 06/12] [Bug] Fix Reviver Seed and endure triggering on indirect damage (#5182) * Create new turnData field for tracking damageResults, check for HitResult in Reviver Seed modifier * Optional chaining for cases like stealth rock * Adds HitResult.SELF for confusion to distinguish from indirect damage * Adds HitResult.SELF to damage sound effect switch * Cover edge case of salt cure, insert HitResult for ALL damage regardless of optional variable * Change Liquid Ooze HitResult to OTHER from HEAL * Adjust OHKO moves to not bypass endure or RSeed * Add tests for reviver seed * Fixes endure to no longer block indirect damage, updates weather damage to be HitResult.OTHER, adds/fixes unit test * Change destiny bond to HitResult.OTHER so it doesn't trigger rseed * Adds destiny bond unit test * Creates additional unit tests for endure * Rename SELF hitresult to CONFUSION * Update CONFUSION enum * Refactors implementation per Wlowscha's suggestions: removes damageSources array and preventEndure variable * Rename HitResult.OTHER to INDIRECT, create INDIRECT_KO for PSong/DBond, add functionality for INDIRECT_KO to damageanim/number handler * Fixes hit result for stealth rock * Removes unnecessary check, makes DamageResult default to EFFECTIVE, updates remaining damageAndUpdate calls to use INDIRECT * Refactors damageAndUpdate to replace optional parameters with object parameter * Fixes based on Kev's suggestions * Updates tsdocs for damageAndUpdate * Fix merge conflict --------- Co-authored-by: Wlowscha <54003515+Wlowscha@users.noreply.github.com> --- public/locales | 2 +- src/data/ability.ts | 12 +-- src/data/arena-tag.ts | 6 +- src/data/battler-tags.ts | 22 ++-- src/data/moves/move.ts | 19 ++-- src/field/damage-number-handler.ts | 1 + src/field/pokemon.ts | 79 +++++++------- src/modifier/modifier.ts | 3 +- src/phases/damage-anim-phase.ts | 6 +- src/phases/faint-phase.ts | 2 +- src/phases/pokemon-heal-phase.ts | 3 +- src/phases/weather-effect-phase.ts | 2 +- test/items/reviver_seed.test.ts | 159 +++++++++++++++++++++++++++++ test/moves/endure.test.ts | 39 +++++-- 14 files changed, 272 insertions(+), 83 deletions(-) create mode 100644 test/items/reviver_seed.test.ts diff --git a/public/locales b/public/locales index 6b3f37cb351..0e5c6096ba2 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 6b3f37cb351552721232f4dabefa17bddb5b9004 +Subproject commit 0e5c6096ba26f6b87aed1aab3fe9b0b23f6cbb7b diff --git a/src/data/ability.ts b/src/data/ability.ts index d9e29c5506c..3a14ad1456d 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1064,7 +1064,7 @@ export class PostDefendContactDamageAbAttr extends PostDefendAbAttr { } override applyPostDefend(pokemon: Pokemon, _passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, _hitResult: HitResult, _args: any[]): void { - attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); + attacker.damageAndUpdate(Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT }); attacker.turnData.damageTaken += Utils.toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)); } @@ -3792,7 +3792,7 @@ export class PostWeatherLapseDamageAbAttr extends PostWeatherLapseAbAttr { if (!simulated) { const abilityName = (!passive ? pokemon.getAbility() : pokemon.getPassiveAbility()).name; globalScene.queueMessage(i18next.t("abilityTriggers:postWeatherLapseDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName })); - pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)), HitResult.OTHER); + pokemon.damageAndUpdate(Utils.toDmgValue(pokemon.getMaxHp() / (16 / this.damageFactor)), { result: HitResult.INDIRECT }); } } } @@ -4084,7 +4084,7 @@ export class PostTurnHurtIfSleepingAbAttr extends PostTurnAbAttr { for (const opp of pokemon.getOpponents()) { if ((opp.status?.effect === StatusEffect.SLEEP || opp.hasAbility(Abilities.COMATOSE)) && !opp.hasAbilityWithAttr(BlockNonDirectDamageAbAttr) && !opp.switchOutStatus) { if (!simulated) { - opp.damageAndUpdate(Utils.toDmgValue(opp.getMaxHp() / 8), HitResult.OTHER); + opp.damageAndUpdate(Utils.toDmgValue(opp.getMaxHp() / 8), { result: HitResult.INDIRECT }); globalScene.queueMessage(i18next.t("abilityTriggers:badDreams", { pokemonName: getPokemonNameWithAffix(opp) })); } } @@ -4567,7 +4567,7 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { override applyPostFaint(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker?: Pokemon, move?: Move, hitResult?: HitResult, ...args: any[]): void { if (!simulated) { - attacker!.damageAndUpdate(Utils.toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); + attacker!.damageAndUpdate(Utils.toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT }); attacker!.turnData.damageTaken += Utils.toDmgValue(attacker!.getMaxHp() * (1 / this.damageRatio)); } } @@ -4588,7 +4588,7 @@ export class PostFaintHPDamageAbAttr extends PostFaintAbAttr { override applyPostFaint(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker?: Pokemon, move?: Move, hitResult?: HitResult, ...args: any[]): void { if (move !== undefined && attacker !== undefined && !simulated) { //If the mon didn't die to indirect damage const damage = pokemon.turnData.attacksReceived[0].damage; - attacker.damageAndUpdate((damage), HitResult.OTHER); + attacker.damageAndUpdate((damage), { result: HitResult.INDIRECT }); attacker.turnData.damageTaken += damage; } } @@ -4989,7 +4989,7 @@ export class FormBlockDamageAbAttr extends ReceivedMoveDamageMultiplierAbAttr { (args[0] as Utils.NumberHolder).value = this.multiplier; pokemon.removeTag(this.tagType); if (this.recoilDamageFunc) { - pokemon.damageAndUpdate(this.recoilDamageFunc(pokemon), HitResult.OTHER, false, false, true, true); + pokemon.damageAndUpdate(this.recoilDamageFunc(pokemon), { result: HitResult.INDIRECT, ignoreSegments: true, ignoreFaintPhase: true }); } } } diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index 6e8339dd54c..8f1d6b09a73 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -788,7 +788,7 @@ class SpikesTag extends ArenaTrapTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - pokemon.damageAndUpdate(damage, HitResult.OTHER); + pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT }); if (pokemon.turnData) { pokemon.turnData.damageTaken += damage; } @@ -982,7 +982,7 @@ class StealthRockTag extends ArenaTrapTag { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), }), ); - pokemon.damageAndUpdate(damage, HitResult.OTHER); + pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT }); if (pokemon.turnData) { pokemon.turnData.damageTaken += damage; } @@ -1327,7 +1327,7 @@ class FireGrassPledgeTag extends ArenaTag { globalScene.unshiftPhase( new CommonAnimPhase(pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.MAGMA_STORM), ); - pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); }); return super.lapse(arena); diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 7ff74d893b6..4952488b48e 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -757,7 +757,7 @@ export class ConfusedTag extends BattlerTag { ((((2 * pokemon.level) / 5 + 2) * 40 * atk) / def / 50 + 2) * (pokemon.randSeedIntRange(85, 100) / 100), ); globalScene.queueMessage(i18next.t("battlerTags:confusedLapseHurtItself")); - pokemon.damageAndUpdate(damage); + pokemon.damageAndUpdate(damage, { result: HitResult.CONFUSION }); pokemon.battleData.hitCount++; (globalScene.getCurrentPhase() as MovePhase).cancel(); } @@ -818,7 +818,7 @@ export class DestinyBondTag extends BattlerTag { pokemonNameWithAffix2: getPokemonNameWithAffix(pokemon), }), ); - pokemon.damageAndUpdate(pokemon.hp, HitResult.ONE_HIT_KO, false, false, true); + pokemon.damageAndUpdate(pokemon.hp, { result: HitResult.INDIRECT_KO, ignoreSegments: true }); return false; } } @@ -952,7 +952,7 @@ export class SeedTag extends BattlerTag { new CommonAnimPhase(source.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.LEECH_SEED), ); - const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8)); + const damage = pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); const reverseDrain = pokemon.hasAbilityWithAttr(ReverseDrainAbAttr, false); globalScene.unshiftPhase( new PokemonHealPhase( @@ -1029,7 +1029,7 @@ export class PowderTag extends BattlerTag { const cancelDamage = new BooleanHolder(false); applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelDamage); if (!cancelDamage.value) { - pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), HitResult.OTHER); + pokemon.damageAndUpdate(Math.floor(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); } // "When the flame touched the powder\non the Pokémon, it exploded!" @@ -1082,7 +1082,7 @@ export class NightmareTag extends BattlerTag { applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); } } @@ -1440,7 +1440,7 @@ export abstract class DamagingTrapTag extends TrappedTag { applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); } } @@ -1644,7 +1644,7 @@ export class ContactDamageProtectedTag extends ProtectedTag { if (effectPhase instanceof MoveEffectPhase && effectPhase.move.getMove().hasFlag(MoveFlags.MAKES_CONTACT)) { const attacker = effectPhase.getPokemon(); if (!attacker.hasAbilityWithAttr(BlockNonDirectDamageAbAttr)) { - attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); + attacker.damageAndUpdate(toDmgValue(attacker.getMaxHp() * (1 / this.damageRatio)), { result: HitResult.INDIRECT }); } } } @@ -1810,7 +1810,7 @@ export class PerishSongTag extends BattlerTag { }), ); } else { - pokemon.damageAndUpdate(pokemon.hp, HitResult.ONE_HIT_KO, false, true, true); + pokemon.damageAndUpdate(pokemon.hp, { result: HitResult.INDIRECT_KO, ignoreSegments: true }); } return ret; @@ -2240,7 +2240,7 @@ export class SaltCuredTag extends BattlerTag { if (!cancelled.value) { const pokemonSteelOrWater = pokemon.isOfType(PokemonType.STEEL) || pokemon.isOfType(PokemonType.WATER); - pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8)); + pokemon.damageAndUpdate(toDmgValue(pokemonSteelOrWater ? pokemon.getMaxHp() / 4 : pokemon.getMaxHp() / 8), { result: HitResult.INDIRECT }); globalScene.queueMessage( i18next.t("battlerTags:saltCuredLapse", { @@ -2288,7 +2288,7 @@ export class CursedTag extends BattlerTag { applyAbAttrs(BlockNonDirectDamageAbAttr, pokemon, cancelled); if (!cancelled.value) { - pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4)); + pokemon.damageAndUpdate(toDmgValue(pokemon.getMaxHp() / 4), { result: HitResult.INDIRECT }); globalScene.queueMessage( i18next.t("battlerTags:cursedLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), @@ -2611,7 +2611,7 @@ export class GulpMissileTag extends BattlerTag { applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled); if (!cancelled.value) { - attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER); + attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), { result: HitResult.INDIRECT }); } if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) { diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 9d470b86186..1555967789c 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1647,7 +1647,7 @@ export class RecoilAttr extends MoveEffectAttr { return false; } - user.damageAndUpdate(recoilDamage, HitResult.OTHER, false, true, true); + user.damageAndUpdate(recoilDamage, { result: HitResult.INDIRECT, ignoreSegments: true }); globalScene.queueMessage(i18next.t("moveTriggers:hitWithRecoil", { pokemonName: getPokemonNameWithAffix(user) })); user.turnData.damageTaken += recoilDamage; @@ -1679,7 +1679,7 @@ export class SacrificialAttr extends MoveEffectAttr { * @returns true if the function succeeds **/ apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - user.damageAndUpdate(user.hp, HitResult.OTHER, false, true, true); + user.damageAndUpdate(user.hp, { result: HitResult.INDIRECT, ignoreSegments: true }); user.turnData.damageTaken += user.hp; return true; @@ -1717,7 +1717,7 @@ export class SacrificialAttrOnHit extends MoveEffectAttr { return false; } - user.damageAndUpdate(user.hp, HitResult.OTHER, false, true, true); + user.damageAndUpdate(user.hp, { result: HitResult.INDIRECT, ignoreSegments: true }); user.turnData.damageTaken += user.hp; return true; @@ -1759,7 +1759,7 @@ export class HalfSacrificialAttr extends MoveEffectAttr { // Check to see if the Pokemon has an ability that blocks non-direct damage applyAbAttrs(BlockNonDirectDamageAbAttr, user, cancelled); if (!cancelled.value) { - user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), HitResult.OTHER, false, true, true); + user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT, ignoreSegments: true }); globalScene.queueMessage(i18next.t("moveTriggers:cutHpPowerUpMove", { pokemonName: getPokemonNameWithAffix(user) })); // Queue recoil message } return true; @@ -1806,7 +1806,7 @@ export class AddSubstituteAttr extends MoveEffectAttr { } const damageTaken = this.roundUp ? Math.ceil(user.getMaxHp() * this.hpCost) : Math.floor(user.getMaxHp() * this.hpCost); - user.damageAndUpdate(damageTaken, HitResult.OTHER, false, true, true); + user.damageAndUpdate(damageTaken, { result: HitResult.INDIRECT, ignoreSegments: true, ignoreFaintPhase: true }); user.addTag(BattlerTagType.SUBSTITUTE, 0, move.id, user.id); return true; } @@ -1956,7 +1956,7 @@ export class FlameBurstAttr extends MoveEffectAttr { return false; } - targetAlly.damageAndUpdate(Math.max(1, Math.floor(1 / 16 * targetAlly.getMaxHp())), HitResult.OTHER); + targetAlly.damageAndUpdate(Math.max(1, Math.floor(1 / 16 * targetAlly.getMaxHp())), { result: HitResult.INDIRECT }); return true; } @@ -3435,9 +3435,8 @@ export class CutHpStatStageBoostAttr extends StatStageChangeAttr { this.cutRatio = cutRatio; this.messageCallback = messageCallback; } - override apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { - user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), HitResult.OTHER, false, true); + user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / this.cutRatio), { result: HitResult.INDIRECT }); user.updateInfo(); const ret = super.apply(user, target, move, args); if (this.messageCallback) { @@ -5329,7 +5328,7 @@ const crashDamageFunc = (user: Pokemon, move: Move) => { return false; } - user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), HitResult.OTHER, false, true); + user.damageAndUpdate(Utils.toDmgValue(user.getMaxHp() / 2), { result: HitResult.INDIRECT }); globalScene.queueMessage(i18next.t("moveTriggers:keptGoingAndCrashed", { pokemonName: getPokemonNameWithAffix(user) })); user.turnData.damageTaken += Utils.toDmgValue(user.getMaxHp() / 2); @@ -5650,7 +5649,7 @@ export class CurseAttr extends MoveEffectAttr { return false; } const curseRecoilDamage = Math.max(1, Math.floor(user.getMaxHp() / 2)); - user.damageAndUpdate(curseRecoilDamage, HitResult.OTHER, false, true, true); + user.damageAndUpdate(curseRecoilDamage, { result: HitResult.INDIRECT, ignoreSegments: true }); globalScene.queueMessage( i18next.t("battlerTags:cursedOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(user), diff --git a/src/field/damage-number-handler.ts b/src/field/damage-number-handler.ts index 63da641a114..9e0010a0c10 100644 --- a/src/field/damage-number-handler.ts +++ b/src/field/damage-number-handler.ts @@ -46,6 +46,7 @@ export default class DamageNumberHandler { case HitResult.NOT_VERY_EFFECTIVE: [textColor, shadowColor] = ["#f08030", "#c03028"]; break; + case HitResult.INDIRECT_KO: case HitResult.ONE_HIT_KO: [textColor, shadowColor] = ["#a040a0", "#483850"]; break; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 42fe1dc1010..43bab0f049d 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -4465,11 +4465,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return result; } - // In case of fatal damage, this tag would have gotten cleared before we could lapse it. - const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); - const grudgeTag = this.getTag(BattlerTagType.GRUDGE); - - const isOneHitKo = result === HitResult.ONE_HIT_KO; + // In case of fatal damage, this tag would have gotten cleared before we could lapse it. + const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); + const grudgeTag = this.getTag(BattlerTagType.GRUDGE); if (dmg) { this.lapseTags(BattlerTagLapseType.HIT); @@ -4484,19 +4482,17 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { globalScene.applyModifiers(EnemyEndureChanceModifier, false, this); } - /** - * We explicitly require to ignore the faint phase here, as we want to show the messages - * about the critical hit and the super effective/not very effective messages before the faint phase. - */ - const damage = this.damageAndUpdate( - isBlockedBySubstitute ? 0 : dmg, - result as DamageResult, - isCritical, - isOneHitKo, - isOneHitKo, - true, - source, - ); + /** + * We explicitly require to ignore the faint phase here, as we want to show the messages + * about the critical hit and the super effective/not very effective messages before the faint phase. + */ + const damage = this.damageAndUpdate(isBlockedBySubstitute ? 0 : dmg, + { + result: result as DamageResult, + isCritical, + ignoreFaintPhase: true, + source + }); if (damage > 0) { if (source.isPlayer()) { @@ -4557,7 +4553,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { globalScene.unshiftPhase( new FaintPhase( this.getBattlerIndex(), - isOneHitKo, + false, destinyTag, grudgeTag, source, @@ -4635,28 +4631,37 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { /** * Called by apply(), given the damage, adds a new DamagePhase and actually updates HP values, etc. + * Checks for 'Indirect' HitResults to account for Endure/Reviver Seed applying correctly * @param damage integer - passed to damage() * @param result an enum if it's super effective, not very, etc. - * @param critical boolean if move is a critical hit + * @param isCritical boolean if move is a critical hit * @param ignoreSegments boolean, passed to damage() and not used currently * @param preventEndure boolean, ignore endure properties of pokemon, passed to damage() * @param ignoreFaintPhase boolean to ignore adding a FaintPhase, passsed to damage() * @returns integer of damage done */ - damageAndUpdate( - damage: number, - result?: DamageResult, - critical = false, - ignoreSegments = false, - preventEndure = false, - ignoreFaintPhase = false, - source?: Pokemon, + damageAndUpdate(damage: number, + { + result = HitResult.EFFECTIVE, + isCritical = false, + ignoreSegments = false, + ignoreFaintPhase = false, + source = undefined, + }: + { + result?: DamageResult, + isCritical?: boolean, + ignoreSegments?: boolean, + ignoreFaintPhase?: boolean, + source?: Pokemon, + } = {} ): number { + const isIndirectDamage = [ HitResult.INDIRECT, HitResult.INDIRECT_KO ].includes(result); const damagePhase = new DamageAnimPhase( - this.getBattlerIndex(), - damage, - result as DamageResult, - critical, + this.getBattlerIndex(), + damage, + result as DamageResult, + isCritical ); globalScene.unshiftPhase(damagePhase); if (this.switchOutStatus && source) { @@ -4665,7 +4670,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { damage = this.damage( damage, ignoreSegments, - preventEndure, + isIndirectDamage, ignoreFaintPhase, ); // Damage amount may have changed, but needed to be queued before calling damage function @@ -7711,8 +7716,10 @@ export enum HitResult { HEAL, FAIL, MISS, - OTHER, + INDIRECT, IMMUNE, + CONFUSION, + INDIRECT_KO, } export type DamageResult = @@ -7720,7 +7727,9 @@ export type DamageResult = | HitResult.SUPER_EFFECTIVE | HitResult.NOT_VERY_EFFECTIVE | HitResult.ONE_HIT_KO - | HitResult.OTHER; + | HitResult.CONFUSION + | HitResult.INDIRECT_KO + | HitResult.INDIRECT; /** Interface containing the results of a damage calculation for a given move */ export interface DamageCalculationResult { diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index a084474ddac..4c59bfe1ef1 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -5,8 +5,7 @@ import { allMoves } from "#app/data/moves/move"; import { MAX_PER_TYPE_POKEBALLS } from "#app/data/pokeball"; import { type FormChangeItem, SpeciesFormChangeItemTrigger } from "#app/data/pokemon-forms"; import { getStatusEffectHealText } from "#app/data/status-effect"; -import type { PlayerPokemon } from "#app/field/pokemon"; -import Pokemon from "#app/field/pokemon"; +import Pokemon, { type PlayerPokemon } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import Overrides from "#app/overrides"; import { EvolutionPhase } from "#app/phases/evolution-phase"; diff --git a/src/phases/damage-anim-phase.ts b/src/phases/damage-anim-phase.ts index e31fe8c9475..703cd3d160e 100644 --- a/src/phases/damage-anim-phase.ts +++ b/src/phases/damage-anim-phase.ts @@ -21,7 +21,7 @@ export class DamageAnimPhase extends PokemonPhase { start() { super.start(); - if (this.damageResult === HitResult.ONE_HIT_KO) { + if (this.damageResult === HitResult.ONE_HIT_KO || this.damageResult === HitResult.INDIRECT_KO) { if (globalScene.moveAnimations) { globalScene.toggleInvert(true); } @@ -42,9 +42,11 @@ export class DamageAnimPhase extends PokemonPhase { applyDamage() { switch (this.damageResult) { case HitResult.EFFECTIVE: + case HitResult.CONFUSION: globalScene.playSound("se/hit"); break; case HitResult.SUPER_EFFECTIVE: + case HitResult.INDIRECT_KO: case HitResult.ONE_HIT_KO: globalScene.playSound("se/hit_strong"); break; @@ -57,7 +59,7 @@ export class DamageAnimPhase extends PokemonPhase { globalScene.damageNumberHandler.add(this.getPokemon(), this.amount, this.damageResult, this.critical); } - if (this.damageResult !== HitResult.OTHER && this.amount > 0) { + if (this.damageResult !== HitResult.INDIRECT && this.amount > 0) { const flashTimer = globalScene.time.addEvent({ delay: 100, repeat: 5, diff --git a/src/phases/faint-phase.ts b/src/phases/faint-phase.ts index 7fc7a517853..dfc0e0653a5 100644 --- a/src/phases/faint-phase.ts +++ b/src/phases/faint-phase.ts @@ -258,7 +258,7 @@ export class FaintPhase extends PokemonPhase { } else { // Final boss' HP threshold has been bypassed; cancel faint and force check for 2nd phase enemy.hp++; - globalScene.unshiftPhase(new DamageAnimPhase(enemy.getBattlerIndex(), 0, HitResult.OTHER)); + globalScene.unshiftPhase(new DamageAnimPhase(enemy.getBattlerIndex(), 0, HitResult.INDIRECT)); this.end(); } return true; diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index ab49def5a3d..ecfe99389eb 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -3,7 +3,6 @@ import type { BattlerIndex } from "#app/battle"; import { CommonAnim } from "#app/data/battle-anims"; import { getStatusEffectHealText } from "#app/data/status-effect"; import { StatusEffect } from "#app/enums/status-effect"; -import type { DamageResult } from "#app/field/pokemon"; import { HitResult } from "#app/field/pokemon"; import { getPokemonNameWithAffix } from "#app/messages"; import { HealingBoosterModifier } from "#app/modifier/modifier"; @@ -79,7 +78,7 @@ export class PokemonHealPhase extends CommonAnimPhase { } const healAmount = new Utils.NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value)); if (healAmount.value < 0) { - pokemon.damageAndUpdate(healAmount.value * -1, HitResult.HEAL as DamageResult); + pokemon.damageAndUpdate(healAmount.value * -1, { result: HitResult.INDIRECT }); healAmount.value = 0; } // Prevent healing to full if specified (in case of healing tokens so Sturdy doesn't cause a softlock) diff --git a/src/phases/weather-effect-phase.ts b/src/phases/weather-effect-phase.ts index 9199b7996bc..d7a1f193029 100644 --- a/src/phases/weather-effect-phase.ts +++ b/src/phases/weather-effect-phase.ts @@ -66,7 +66,7 @@ export class WeatherEffectPhase extends CommonAnimPhase { const damage = Utils.toDmgValue(pokemon.getMaxHp() / 16); globalScene.queueMessage(getWeatherDamageMessage(this.weather?.weatherType!, pokemon)!); // TODO: are those bangs correct? - pokemon.damageAndUpdate(damage, HitResult.EFFECTIVE, false, false, true); + pokemon.damageAndUpdate(damage, { result: HitResult.INDIRECT, ignoreSegments: true }); }; this.executeForAll((pokemon: Pokemon) => { diff --git a/test/items/reviver_seed.test.ts b/test/items/reviver_seed.test.ts new file mode 100644 index 00000000000..ab249d48a23 --- /dev/null +++ b/test/items/reviver_seed.test.ts @@ -0,0 +1,159 @@ +import { BattlerIndex } from "#app/battle"; +import { allMoves } from "#app/data/moves/move"; +import { BattlerTagType } from "#app/enums/battler-tag-type"; +import type { PokemonInstantReviveModifier } from "#app/modifier/modifier"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/testUtils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Items - Reviver Seed", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.TACKLE, Moves.ENDURE ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .startingHeldItems([{ name: "REVIVER_SEED" }]) + .enemyHeldItems([{ name: "REVIVER_SEED" }]) + .enemyMoveset(Moves.SPLASH); + vi.spyOn(allMoves[Moves.SHEER_COLD], "accuracy", "get").mockReturnValue(100); + vi.spyOn(allMoves[Moves.LEECH_SEED], "accuracy", "get").mockReturnValue(100); + vi.spyOn(allMoves[Moves.WHIRLPOOL], "accuracy", "get").mockReturnValue(100); + vi.spyOn(allMoves[Moves.WILL_O_WISP], "accuracy", "get").mockReturnValue(100); + }); + + it.each([ + { moveType: "Special Move", move: Moves.WATER_GUN }, + { moveType: "Physical Move", move: Moves.TACKLE }, + { moveType: "Fixed Damage Move", move: Moves.SEISMIC_TOSS }, + { moveType: "Final Gambit", move: Moves.FINAL_GAMBIT }, + { moveType: "Counter", move: Moves.COUNTER }, + { moveType: "OHKO", move: Moves.SHEER_COLD } + ])("should activate the holder's reviver seed from a $moveType", async ({ move }) => { + game.override + .enemyLevel(100) + .startingLevel(1) + .enemyMoveset(move); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const player = game.scene.getPlayerPokemon()!; + player.damageAndUpdate(player.hp - 1); + + const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier; + vi.spyOn(reviverSeed, "apply"); + + game.move.select(Moves.TACKLE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(player.isFainted()).toBeFalsy(); + }); + + it("should activate the holder's reviver seed from confusion self-hit", async () => { + game.override + .enemyLevel(1) + .startingLevel(100) + .enemyMoveset(Moves.SPLASH); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const player = game.scene.getPlayerPokemon()!; + player.damageAndUpdate(player.hp - 1); + player.addTag(BattlerTagType.CONFUSED, 3); + + const reviverSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier; + vi.spyOn(reviverSeed, "apply"); + + vi.spyOn(player, "randSeedInt").mockReturnValue(0); // Force confusion self-hit + game.move.select(Moves.TACKLE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(player.isFainted()).toBeFalsy(); + }); + + // Damaging opponents tests + it.each([ + { moveType: "Damaging Move Chip Damage", move: Moves.SALT_CURE }, + { moveType: "Chip Damage", move: Moves.LEECH_SEED }, + { moveType: "Trapping Chip Damage", move: Moves.WHIRLPOOL }, + { moveType: "Status Effect Damage", move: Moves.WILL_O_WISP }, + { moveType: "Weather", move: Moves.SANDSTORM }, + ])("should not activate the holder's reviver seed from $moveType", async ({ move }) => { + game.override + .enemyLevel(1) + .startingLevel(100) + .enemySpecies(Species.MAGIKARP) + .moveset(move) + .enemyMoveset(Moves.ENDURE); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon()!; + enemy.damageAndUpdate(enemy.hp - 1); + + game.move.select(move); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy.isFainted()).toBeTruthy(); + }); + + // Self-damage tests + it.each([ + { moveType: "Recoil", move: Moves.DOUBLE_EDGE }, + { moveType: "Self-KO", move: Moves.EXPLOSION }, + { moveType: "Self-Deduction", move: Moves.CURSE }, + { moveType: "Liquid Ooze", move: Moves.GIGA_DRAIN }, + ])("should not activate the holder's reviver seed from $moveType", async ({ move }) => { + game.override + .enemyLevel(100) + .startingLevel(1) + .enemySpecies(Species.MAGIKARP) + .moveset(move) + .enemyAbility(Abilities.LIQUID_OOZE) + .enemyMoveset(Moves.SPLASH); + await game.classicMode.startBattle([ Species.GASTLY, Species.FEEBAS ]); + const player = game.scene.getPlayerPokemon()!; + player.damageAndUpdate(player.hp - 1); + + const playerSeed = player.getHeldItems()[0] as PokemonInstantReviveModifier; + vi.spyOn(playerSeed, "apply"); + + game.move.select(move); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(player.isFainted()).toBeTruthy(); + }); + + it("should not activate the holder's reviver seed from Destiny Bond fainting", async () => { + game.override + .enemyLevel(100) + .startingLevel(1) + .enemySpecies(Species.MAGIKARP) + .moveset(Moves.DESTINY_BOND) + .startingHeldItems([]) // reset held items to nothing so user doesn't revive and not trigger Destiny Bond + .enemyMoveset(Moves.TACKLE); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const player = game.scene.getPlayerPokemon()!; + player.damageAndUpdate(player.hp - 1); + const enemy = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.DESTINY_BOND); + await game.setTurnOrder([ BattlerIndex.PLAYER, BattlerIndex.ENEMY ]); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy.isFainted()).toBeTruthy(); + }); +}); diff --git a/test/moves/endure.test.ts b/test/moves/endure.test.ts index 8151128479d..d706d5d9581 100644 --- a/test/moves/endure.test.ts +++ b/test/moves/endure.test.ts @@ -22,7 +22,7 @@ describe("Moves - Endure", () => { beforeEach(() => { game = new GameManager(phaserGame); game.override - .moveset([Moves.THUNDER, Moves.BULLET_SEED, Moves.TOXIC]) + .moveset([ Moves.THUNDER, Moves.BULLET_SEED, Moves.TOXIC, Moves.SHEER_COLD ]) .ability(Abilities.SKILL_LINK) .startingLevel(100) .battleType("single") @@ -50,16 +50,37 @@ describe("Moves - Endure", () => { expect(game.scene.getEnemyPokemon()!.hp).toBe(1); }); - it("shouldn't prevent fainting from indirect damage", async () => { - game.override.enemyLevel(100); - await game.classicMode.startBattle([Species.ARCEUS]); - + it("should let the pokemon survive against OHKO moves", async () => { + await game.classicMode.startBattle([ Species.MAGIKARP ]); const enemy = game.scene.getEnemyPokemon()!; - enemy.hp = 2; - game.move.select(Moves.TOXIC); - await game.phaseInterceptor.to("VictoryPhase"); + game.move.select(Moves.SHEER_COLD); + await game.phaseInterceptor.to("TurnEndPhase"); - expect(enemy.isFainted()).toBe(true); + expect(enemy.isFainted()).toBeFalsy(); + }); + + // comprehensive indirect damage test copied from Reviver Seed test + it.each([ + { moveType: "Damaging Move Chip Damage", move: Moves.SALT_CURE }, + { moveType: "Chip Damage", move: Moves.LEECH_SEED }, + { moveType: "Trapping Chip Damage", move: Moves.WHIRLPOOL }, + { moveType: "Status Effect Damage", move: Moves.TOXIC }, + { moveType: "Weather", move: Moves.SANDSTORM }, + ])("should not prevent fainting from $moveType", async ({ move }) => { + game.override + .enemyLevel(1) + .startingLevel(100) + .enemySpecies(Species.MAGIKARP) + .moveset(move) + .enemyMoveset(Moves.ENDURE); + await game.classicMode.startBattle([ Species.MAGIKARP, Species.FEEBAS ]); + const enemy = game.scene.getEnemyPokemon()!; + enemy.damageAndUpdate(enemy.hp - 1); + + game.move.select(move); + await game.phaseInterceptor.to("TurnEndPhase"); + + expect(enemy.isFainted()).toBeTruthy(); }); }); From dbc8ac26af131ae09caec5a2872d820eb38dc43d Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Sun, 23 Mar 2025 16:27:20 -0700 Subject: [PATCH 07/12] [Bug] Fix #5422 Neut. Gas and Primal Weather Persist After Flee (#5496) * Add applyPreLeaveFieldAbAttrs call to AttemptRunPhase * Update tests --- src/phases/attempt-run-phase.ts | 4 +++- test/abilities/desolate-land.test.ts | 18 ++++++++++++++++++ test/abilities/neutralizing_gas.test.ts | 18 ++++++++++++++++++ 3 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/phases/attempt-run-phase.ts b/src/phases/attempt-run-phase.ts index c9c0e23dabb..dab5b8789da 100644 --- a/src/phases/attempt-run-phase.ts +++ b/src/phases/attempt-run-phase.ts @@ -1,4 +1,4 @@ -import { applyAbAttrs, RunSuccessAbAttr } from "#app/data/ability"; +import { applyAbAttrs, applyPreLeaveFieldAbAttrs, PreLeaveFieldAbAttr, RunSuccessAbAttr } from "#app/data/ability"; import { Stat } from "#app/enums/stat"; import { StatusEffect } from "#app/enums/status-effect"; import type { PlayerPokemon, EnemyPokemon } from "#app/field/pokemon"; @@ -29,6 +29,8 @@ export class AttemptRunPhase extends PokemonPhase { applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, false, escapeChance); if (playerPokemon.randSeedInt(100) < escapeChance.value && !this.forceFailEscape) { + enemyField.forEach(enemyPokemon => applyPreLeaveFieldAbAttrs(PreLeaveFieldAbAttr, enemyPokemon)); + globalScene.playSound("se/flee"); globalScene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500); diff --git a/test/abilities/desolate-land.test.ts b/test/abilities/desolate-land.test.ts index 67d9ac1cdf5..405aab873aa 100644 --- a/test/abilities/desolate-land.test.ts +++ b/test/abilities/desolate-land.test.ts @@ -1,5 +1,7 @@ import { PokeballType } from "#app/enums/pokeball"; import { WeatherType } from "#app/enums/weather-type"; +import type { CommandPhase } from "#app/phases/command-phase"; +import { Command } from "#app/ui/command-ui-handler"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -131,4 +133,20 @@ describe("Abilities - Desolate Land", () => { expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN); }); + + it("should lift after fleeing from a wild pokemon", async () => { + game.override + .enemyAbility(Abilities.DESOLATE_LAND) + .ability(Abilities.BALL_FETCH); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.HARSH_SUN); + + vi.spyOn(game.scene.getPlayerPokemon()!, "randSeedInt").mockReturnValue(0); + + const commandPhase = game.scene.getCurrentPhase() as CommandPhase; + commandPhase.handleCommand(Command.RUN, 0); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.weather?.weatherType).not.toBe(WeatherType.HARSH_SUN); + }); }); diff --git a/test/abilities/neutralizing_gas.test.ts b/test/abilities/neutralizing_gas.test.ts index 08ab884d806..0003ee56598 100644 --- a/test/abilities/neutralizing_gas.test.ts +++ b/test/abilities/neutralizing_gas.test.ts @@ -1,4 +1,6 @@ import { BattlerIndex } from "#app/battle"; +import type { CommandPhase } from "#app/phases/command-phase"; +import { Command } from "#app/ui/command-ui-handler"; import { PostSummonWeatherChangeAbAttr } from "#app/data/ability"; import { Abilities } from "#enums/abilities"; import { ArenaTagType } from "#enums/arena-tag-type"; @@ -157,6 +159,22 @@ describe("Abilities - Neutralizing Gas", () => { expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined(); }); + it("should deactivate after fleeing from a wild pokemon", async () => { + game.override + .enemyAbility(Abilities.NEUTRALIZING_GAS) + .ability(Abilities.BALL_FETCH); + await game.classicMode.startBattle([ Species.MAGIKARP ]); + expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeDefined(); + + vi.spyOn(game.scene.getPlayerPokemon()!, "randSeedInt").mockReturnValue(0); + + const commandPhase = game.scene.getCurrentPhase() as CommandPhase; + commandPhase.handleCommand(Command.RUN, 0); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.getTag(ArenaTagType.NEUTRALIZING_GAS)).toBeUndefined(); + }); + it("should not activate abilities of pokemon no longer on the field", async () => { game.override .battleType("single") From 37e51e965740dd986310ae436c8940db8db82781 Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Mon, 24 Mar 2025 03:40:17 +0100 Subject: [PATCH 08/12] [Bug][Challenge][UI/UX] Exclude invalid starters when combining challenges (#5509) * Filtering correctly when combining gen and monotype challenges * Clean-up * Apply suggestions from code review Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Always check requested form first * Fixing Basculin * Only check forms which are starter selectable * Exclude form changes that are not triggered by an item --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/challenge.ts | 153 +++++++++++++++++----------- src/ui/pokedex-page-ui-handler.ts | 19 ---- src/ui/starter-select-ui-handler.ts | 85 +++++----------- 3 files changed, 115 insertions(+), 142 deletions(-) diff --git a/src/data/challenge.ts b/src/data/challenge.ts index b9d817836c3..a54f72aa7cc 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -18,9 +18,10 @@ import { TrainerType } from "#enums/trainer-type"; import { Nature } from "#enums/nature"; import type { Moves } from "#enums/moves"; import { TypeColor, TypeShadow } from "#enums/color"; -import { pokemonEvolutions } from "#app/data/balance/pokemon-evolutions"; -import { pokemonFormChanges } from "#app/data/pokemon-forms"; import { ModifierTier } from "#app/modifier/modifier-tier"; +import { globalScene } from "#app/global-scene"; +import { pokemonFormChanges } from "./pokemon-forms"; +import { pokemonEvolutions } from "./balance/pokemon-evolutions"; /** A constant for the default max cost of the starting party before a run */ const DEFAULT_PARTY_MAX_COST = 10; @@ -285,15 +286,9 @@ export abstract class Challenge { * @param _pokemon {@link PokemonSpecies} The pokemon to check the validity of. * @param _valid {@link Utils.BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. * @param _dexAttr {@link DexAttrProps} The dex attributes of the pokemon. - * @param _soft {@link boolean} If true, allow it if it could become a valid pokemon. * @returns {@link boolean} Whether this function did anything. */ - applyStarterChoice( - _pokemon: PokemonSpecies, - _valid: Utils.BooleanHolder, - _dexAttr: DexAttrProps, - _soft = false, - ): boolean { + applyStarterChoice(_pokemon: PokemonSpecies, _valid: Utils.BooleanHolder, _dexAttr: DexAttrProps): boolean { return false; } @@ -445,27 +440,8 @@ export class SingleGenerationChallenge extends Challenge { super(Challenges.SINGLE_GENERATION, 9); } - applyStarterChoice( - pokemon: PokemonSpecies, - valid: Utils.BooleanHolder, - _dexAttr: DexAttrProps, - soft = false, - ): boolean { - const generations = [pokemon.generation]; - if (soft) { - const speciesToCheck = [pokemon.speciesId]; - while (speciesToCheck.length) { - const checking = speciesToCheck.pop(); - if (checking && pokemonEvolutions.hasOwnProperty(checking)) { - pokemonEvolutions[checking].forEach(e => { - speciesToCheck.push(e.speciesId); - generations.push(getPokemonSpecies(e.speciesId).generation); - }); - } - } - } - - if (!generations.includes(this.value)) { + applyStarterChoice(pokemon: PokemonSpecies, valid: Utils.BooleanHolder): boolean { + if (pokemon.generation !== this.value) { valid.value = false; return true; } @@ -739,41 +715,14 @@ export class SingleTypeChallenge extends Challenge { { species: Species.CASTFORM, type: PokemonType.NORMAL, fusion: false }, ]; // TODO: Find a solution for all Pokemon with this ssui issue, including Basculin and Burmy - private static SPECIES_OVERRIDES: Species[] = [Species.MELOETTA]; constructor() { super(Challenges.SINGLE_TYPE, 18); } - override applyStarterChoice( - pokemon: PokemonSpecies, - valid: Utils.BooleanHolder, - dexAttr: DexAttrProps, - soft = false, - ): boolean { + override applyStarterChoice(pokemon: PokemonSpecies, valid: Utils.BooleanHolder, dexAttr: DexAttrProps): boolean { const speciesForm = getPokemonSpeciesForm(pokemon.speciesId, dexAttr.formIndex); const types = [speciesForm.type1, speciesForm.type2]; - if (soft && !SingleTypeChallenge.SPECIES_OVERRIDES.includes(pokemon.speciesId)) { - const speciesToCheck = [pokemon.speciesId]; - while (speciesToCheck.length) { - const checking = speciesToCheck.pop(); - if (checking && pokemonEvolutions.hasOwnProperty(checking)) { - pokemonEvolutions[checking].forEach(e => { - speciesToCheck.push(e.speciesId); - types.push(getPokemonSpecies(e.speciesId).type1, getPokemonSpecies(e.speciesId).type2); - }); - } - if (checking && pokemonFormChanges.hasOwnProperty(checking)) { - pokemonFormChanges[checking].forEach(f1 => { - getPokemonSpecies(checking).forms.forEach(f2 => { - if (f1.formKey === f2.formKey) { - types.push(f2.type1, f2.type2); - } - }); - }); - } - } - } if (!types.includes(this.value - 1)) { valid.value = false; return true; @@ -1030,7 +979,6 @@ export class LowerStarterPointsChallenge extends Challenge { * @param pokemon {@link PokemonSpecies} The pokemon to check the validity of. * @param valid {@link Utils.BooleanHolder} A BooleanHolder, the value gets set to false if the pokemon isn't allowed. * @param dexAttr {@link DexAttrProps} The dex attributes of the pokemon. - * @param soft {@link boolean} If true, allow it if it could become a valid pokemon. * @returns True if any challenge was successfully applied. */ export function applyChallenges( @@ -1039,7 +987,6 @@ export function applyChallenges( pokemon: PokemonSpecies, valid: Utils.BooleanHolder, dexAttr: DexAttrProps, - soft: boolean, ): boolean; /** * Apply all challenges that modify available total starter points. @@ -1222,7 +1169,7 @@ export function applyChallenges(gameMode: GameMode, challengeType: ChallengeType if (c.value !== 0) { switch (challengeType) { case ChallengeType.STARTER_CHOICE: - ret ||= c.applyStarterChoice(args[0], args[1], args[2], args[3]); + ret ||= c.applyStarterChoice(args[0], args[1], args[2]); break; case ChallengeType.STARTER_POINTS: ret ||= c.applyStarterPoints(args[0]); @@ -1305,3 +1252,87 @@ export function initChallenges() { new FlipStatChallenge(), ); } + +/** + * Apply all challenges to the given starter (and form) to check its validity. + * Differs from {@linkcode checkSpeciesValidForChallenge} which only checks form changes. + * @param species - The {@linkcode PokemonSpecies} to check the validity of. + * @param dexAttr - The {@linkcode DexAttrProps | dex attributes} of the species, including its form index. + * @param soft - If `true`, allow it if it could become valid through evolution or form change. + * @returns `true` if the species is considered valid. + */ +export function checkStarterValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { + if (!soft) { + const isValidForChallenge = new Utils.BooleanHolder(true); + applyChallenges(globalScene.gameMode, ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props); + return isValidForChallenge.value; + } + // We check the validity of every evolution and form change, and require that at least one is valid + const speciesToCheck = [species.speciesId]; + while (speciesToCheck.length) { + const checking = speciesToCheck.pop(); + // Linter complains if we don't handle this + if (!checking) { + return false; + } + const checkingSpecies = getPokemonSpecies(checking); + if (checkSpeciesValidForChallenge(checkingSpecies, props, true)) { + return true; + } + if (checking && pokemonEvolutions.hasOwnProperty(checking)) { + pokemonEvolutions[checking].forEach(e => { + // Form check to deal with cases such as Basculin -> Basculegion + // TODO: does this miss anything if checking forms of a stage 2 Pokémon? + if (!e?.preFormKey || e.preFormKey === species.forms[props.formIndex].formKey) { + speciesToCheck.push(e.speciesId); + } + }); + } + } + return false; +} + +/** + * Apply all challenges to the given species (and form) to check its validity. + * Differs from {@linkcode checkStarterValidForChallenge} which also checks evolutions. + * @param species - The {@linkcode PokemonSpecies} to check the validity of. + * @param dexAttr - The {@linkcode DexAttrProps | dex attributes} of the species, including its form index. + * @param soft - If `true`, allow it if it could become valid through a form change. + * @returns `true` if the species is considered valid. + */ +function checkSpeciesValidForChallenge(species: PokemonSpecies, props: DexAttrProps, soft: boolean) { + const isValidForChallenge = new Utils.BooleanHolder(true); + applyChallenges(globalScene.gameMode, ChallengeType.STARTER_CHOICE, species, isValidForChallenge, props); + if (!soft || !pokemonFormChanges.hasOwnProperty(species.speciesId)) { + return isValidForChallenge.value; + } + // If the form in props is valid, return true before checking other form changes + if (soft && isValidForChallenge.value) { + return true; + } + pokemonFormChanges[species.speciesId].forEach(f1 => { + // Exclude form changes that require the mon to be on the field to begin with, + // such as Castform + if (!("item" in f1)) { + return; + } + species.forms.forEach((f2, formIndex) => { + if (f1.formKey === f2.formKey) { + const formProps = { ...props }; + formProps.formIndex = formIndex; + const isFormValidForChallenge = new Utils.BooleanHolder(true); + applyChallenges( + globalScene.gameMode, + ChallengeType.STARTER_CHOICE, + species, + isFormValidForChallenge, + formProps, + ); + if (isFormValidForChallenge.value) { + return true; + } + } + }); + }); + return false; +} diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index 74921f13683..fc6e96d427f 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -20,7 +20,6 @@ import { allSpecies, getPokemonSpecies, getPokemonSpeciesForm, normalForm } from import { getStarterValueFriendshipCap, speciesStarterCosts } from "#app/data/balance/starters"; import { starterPassiveAbilities } from "#app/data/balance/passives"; import { PokemonType } from "#enums/pokemon-type"; -import { GameModes } from "#app/game-mode"; import type { DexEntry, StarterAttributes } from "#app/system/game-data"; import { AbilityAttr, DexAttr } from "#app/system/game-data"; import type { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; @@ -33,7 +32,6 @@ import { Egg } from "#app/data/egg"; import Overrides from "#app/overrides"; import { SettingKeyboard } from "#app/system/settings/settings-keyboard"; import { Passive as PassiveAttr } from "#enums/passive"; -import * as Challenge from "#app/data/challenge"; import MoveInfoOverlay from "#app/ui/move-info-overlay"; import PokedexInfoOverlay from "#app/ui/pokedex-info-overlay"; import { getEggTierForSpecies } from "#app/data/egg"; @@ -51,7 +49,6 @@ import { BooleanHolder, getLocalizedSpriteKey, isNullOrUndefined, - NumberHolder, padInt, rgbHexToRgba, toReadableString, @@ -2128,22 +2125,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { } } - getValueLimit(): number { - const valueLimit = new NumberHolder(0); - switch (globalScene.gameMode.modeId) { - case GameModes.ENDLESS: - case GameModes.SPLICED_ENDLESS: - valueLimit.value = 15; - break; - default: - valueLimit.value = 10; - } - - Challenge.applyChallenges(globalScene.gameMode, Challenge.ChallengeType.STARTER_POINTS, valueLimit); - - return valueLimit.value; - } - setCursor(cursor: number): boolean { const ret = super.setCursor(cursor); diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index ccc56f38368..ab8ea0adf7c 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -1,6 +1,6 @@ import type { CandyUpgradeNotificationChangedEvent } from "#app/events/battle-scene"; import { BattleSceneEventType } from "#app/events/battle-scene"; -import { pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; +import { pokemonEvolutions, pokemonPrevolutions } from "#app/data/balance/pokemon-evolutions"; import type { Variant } from "#app/data/variant"; import { getVariantTint, getVariantIcon } from "#app/data/variant"; import { argbFromRgba } from "@material/material-color-utilities"; @@ -19,7 +19,7 @@ import { pokemonFormChanges } from "#app/data/pokemon-forms"; import type { LevelMoves } from "#app/data/balance/pokemon-level-moves"; import { pokemonFormLevelMoves, pokemonSpeciesLevelMoves } from "#app/data/balance/pokemon-level-moves"; import type PokemonSpecies from "#app/data/pokemon-species"; -import { allSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data/pokemon-species"; +import { allSpecies, getPokemonSpecies, getPokemonSpeciesForm, getPokerusStarters } from "#app/data/pokemon-species"; import { getStarterValueFriendshipCap, speciesStarterCosts, POKERUS_STARTER_COUNT } from "#app/data/balance/starters"; import { PokemonType } from "#enums/pokemon-type"; import { GameModes } from "#app/game-mode"; @@ -80,6 +80,7 @@ import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { achvs } from "#app/system/achv"; import * as Utils from "../utils"; import type { GameObjects } from "phaser"; +import { checkStarterValidForChallenge } from "#app/data/challenge"; export type StarterSelectCallback = (starters: Starter[]) => void; @@ -1760,21 +1761,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const species = starter.species; const [isDupe] = this.isInParty(species); const starterCost = globalScene.gameData.getSpeciesStarterValue(species.speciesId); - const isValidForChallenge = new BooleanHolder(true); - Challenge.applyChallenges( - globalScene.gameMode, - Challenge.ChallengeType.STARTER_CHOICE, + const isValidForChallenge = checkStarterValidForChallenge( species, - isValidForChallenge, globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), this.isPartyValid(), ); const isCaught = globalScene.gameData.dexData[species.speciesId].caughtAttr; return ( - !isDupe && - isValidForChallenge.value && - currentPartyValue + starterCost <= this.getValueLimit() && - isCaught + !isDupe && isValidForChallenge && currentPartyValue + starterCost <= this.getValueLimit() && isCaught ); }); if (validStarters.length === 0) { @@ -1861,16 +1855,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const ui = this.getUi(); let options: any[] = []; // TODO: add proper type - const [isDupe, removeIndex]: [boolean, number] = this.isInParty(this.lastSpecies); // checks to see if the pokemon is a duplicate; if it is, returns the index that will be removed + const [isDupe, removeIndex]: [boolean, number] = this.isInParty(this.lastSpecies); const isPartyValid = this.isPartyValid(); - const isValidForChallenge = new BooleanHolder(true); - - Challenge.applyChallenges( - globalScene.gameMode, - Challenge.ChallengeType.STARTER_CHOICE, + const isValidForChallenge = checkStarterValidForChallenge( this.lastSpecies, - isValidForChallenge, globalScene.gameData.getSpeciesDexAttrProps( this.lastSpecies, this.getCurrentDexProps(this.lastSpecies.speciesId), @@ -1888,11 +1877,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const newCost = globalScene.gameData.getSpeciesStarterValue(this.lastSpecies.speciesId); if ( !isDupe && - isValidForChallenge.value && + isValidForChallenge && currentPartyValue + newCost <= this.getValueLimit() && this.starterSpecies.length < PLAYER_PARTY_MAX_SIZE ) { - // this checks to make sure the pokemon doesn't exist in your party, it's valid for the challenge and that it won't go over the cost limit; if it meets all these criteria it will add it to your party options = [ { label: i18next.t("starterSelectUiHandler:addToParty"), @@ -1902,7 +1890,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { globalScene.gameData.getSpeciesStarterValue(this.lastSpecies.speciesId), true, ); - if (!isDupe && isValidForChallenge.value && isOverValueLimit) { + if (!isDupe && isValidForChallenge && isOverValueLimit) { const cursorObj = this.starterCursorObjs[this.starterSpecies.length]; cursorObj.setVisible(true); cursorObj.setPosition(this.cursorObj.x, this.cursorObj.y); @@ -2993,32 +2981,27 @@ export default class StarterSelectUiHandler extends MessageUiHandler { /* Here we are making a fake form index dex props for challenges * Since some pokemon rely on forms to be valid (i.e. blaze tauros for fire challenges), we make a fake form and dex props to use in the challenge */ + if (!species.forms[i].isStarterSelectable) { + continue; + } const tempFormProps = BigInt(Math.pow(2, i)) * DexAttr.DEFAULT_FORM; - const isValidForChallenge = new BooleanHolder(true); - Challenge.applyChallenges( - globalScene.gameMode, - Challenge.ChallengeType.STARTER_CHOICE, + const isValidForChallenge = checkStarterValidForChallenge( container.species, - isValidForChallenge, globalScene.gameData.getSpeciesDexAttrProps(species, tempFormProps), true, ); - allFormsValid = allFormsValid || isValidForChallenge.value; + allFormsValid = allFormsValid || isValidForChallenge; } } else { - const isValidForChallenge = new BooleanHolder(true); - Challenge.applyChallenges( - globalScene.gameMode, - Challenge.ChallengeType.STARTER_CHOICE, + const isValidForChallenge = checkStarterValidForChallenge( container.species, - isValidForChallenge, globalScene.gameData.getSpeciesDexAttrProps( species, globalScene.gameData.getSpeciesDefaultDexAttr(container.species, false, true), ), true, ); - allFormsValid = isValidForChallenge.value; + allFormsValid = isValidForChallenge; } if (allFormsValid) { this.validStarterContainers.push(container); @@ -3851,15 +3834,6 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonSprite.setVisible(!this.statsMode); } - const isValidForChallenge = new BooleanHolder(true); - Challenge.applyChallenges( - globalScene.gameMode, - Challenge.ChallengeType.STARTER_CHOICE, - species, - isValidForChallenge, - globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor), - !!this.starterSpecies.length, - ); const currentFilteredContainer = this.filteredStarterContainers.find( p => p.species.speciesId === species.speciesId, ); @@ -4233,20 +4207,15 @@ export default class StarterSelectUiHandler extends MessageUiHandler { globalScene.time.delayedCall(fixedInt(500), () => this.tryUpdateValue()); return false; } - let isPartyValid: boolean = this.isPartyValid(); // this checks to see if the party is valid + let isPartyValid: boolean = this.isPartyValid(); if (addingToParty) { - // this does a check to see if the pokemon being added is valid; if so, it will update the isPartyValid boolean - const isNewPokemonValid = new BooleanHolder(true); const species = this.filteredStarterContainers[this.cursor].species; - Challenge.applyChallenges( - globalScene.gameMode, - Challenge.ChallengeType.STARTER_CHOICE, + const isNewPokemonValid = checkStarterValidForChallenge( species, - isNewPokemonValid, globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), false, ); - isPartyValid = isPartyValid || isNewPokemonValid.value; + isPartyValid = isPartyValid || isNewPokemonValid; } /** @@ -4270,12 +4239,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { * If speciesStarterDexEntry?.caughtAttr is true, this species registered in stater. * we change to can AddParty value to true since the user has enough cost to choose this pokemon and this pokemon registered too. */ - const isValidForChallenge = new BooleanHolder(true); - Challenge.applyChallenges( - globalScene.gameMode, - Challenge.ChallengeType.STARTER_CHOICE, + const isValidForChallenge = checkStarterValidForChallenge( this.allSpecies[s], - isValidForChallenge, globalScene.gameData.getSpeciesDexAttrProps( this.allSpecies[s], this.getCurrentDexProps(this.allSpecies[s].speciesId), @@ -4283,7 +4248,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { isPartyValid, ); - const canBeChosen = remainValue >= speciesStarterValue && isValidForChallenge.value; + const canBeChosen = remainValue >= speciesStarterValue && isValidForChallenge; const isPokemonInParty = this.isInParty(this.allSpecies[s])[0]; // this will get the valud of isDupe from isInParty. This will let us see if the pokemon in question is in our party already so we don't grey out the sprites if they're invalid @@ -4417,17 +4382,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler { isPartyValid(): boolean { let canStart = false; for (let s = 0; s < this.starterSpecies.length; s++) { - const isValidForChallenge = new BooleanHolder(true); const species = this.starterSpecies[s]; - Challenge.applyChallenges( - globalScene.gameMode, - Challenge.ChallengeType.STARTER_CHOICE, + const isValidForChallenge = checkStarterValidForChallenge( species, - isValidForChallenge, globalScene.gameData.getSpeciesDexAttrProps(species, this.getCurrentDexProps(species.speciesId)), false, ); - canStart = canStart || isValidForChallenge.value; + canStart = canStart || isValidForChallenge; } return canStart; } From 7f72794d2343825a0b8148cfa970663389e18a6b Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Mon, 24 Mar 2025 06:03:11 +0100 Subject: [PATCH 09/12] =?UTF-8?q?[UI/UX]=20Cancel=20button=20on=20Pok?= =?UTF-8?q?=C3=A9dex=20page=20to=20previously=20selected=20Pok=C3=A9mon=20?= =?UTF-8?q?(#5528)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Removed redundant form index argument in show() of pokedex page * Storing previous pokémon for cancel button --- src/ui/party-ui-handler.ts | 4 +-- src/ui/pokedex-page-ui-handler.ts | 48 +++++++++++++++++++++++------ src/ui/pokedex-ui-handler.ts | 4 +-- src/ui/starter-select-ui-handler.ts | 2 +- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/ui/party-ui-handler.ts b/src/ui/party-ui-handler.ts index 21e5f9077f4..6a9b565989d 100644 --- a/src/ui/party-ui-handler.ts +++ b/src/ui/party-ui-handler.ts @@ -574,9 +574,7 @@ export default class PartyUiHandler extends MessageUiHandler { form: pokemon.formIndex, female: pokemon.gender === Gender.FEMALE, }; - ui.setOverlayMode(Mode.POKEDEX_PAGE, pokemon.species, pokemon.formIndex, attributes).then(() => - this.clearOptions(), - ); + ui.setOverlayMode(Mode.POKEDEX_PAGE, pokemon.species, attributes).then(() => this.clearOptions()); return true; } else if (option === PartyOption.UNPAUSE_EVOLUTION) { this.clearOptions(); diff --git a/src/ui/pokedex-page-ui-handler.ts b/src/ui/pokedex-page-ui-handler.ts index fc6e96d427f..86460a24fdc 100644 --- a/src/ui/pokedex-page-ui-handler.ts +++ b/src/ui/pokedex-page-ui-handler.ts @@ -239,6 +239,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler { private starterAttributes: StarterAttributes; private savedStarterAttributes: StarterAttributes; + private previousSpecies: PokemonSpecies[]; + private previousStarterAttributes: StarterAttributes[]; + protected blockInput = false; protected blockInputOverlay = false; @@ -653,6 +656,9 @@ export default class PokedexPageUiHandler extends MessageUiHandler { // Filter bar sits above everything, except the message box this.starterSelectContainer.bringToTop(this.starterSelectMessageBoxContainer); + + this.previousSpecies = []; + this.previousStarterAttributes = []; } show(args: any[]): boolean { @@ -665,14 +671,14 @@ export default class PokedexPageUiHandler extends MessageUiHandler { return false; } this.species = args[0]; - this.formIndex = args[1] ?? 0; - this.savedStarterAttributes = args[2] ?? { + this.savedStarterAttributes = args[1] ?? { shiny: false, female: true, variant: 0, form: 0, }; - this.filteredIndices = args[3] ?? null; + this.formIndex = this.savedStarterAttributes.form ?? 0; + this.filteredIndices = args[2] ?? null; this.starterSetup(); this.moveInfoOverlay.clear(); // clear this when removing a menu; the cancel button doesn't seem to trigger this automatically on controllers @@ -1088,8 +1094,19 @@ export default class PokedexPageUiHandler extends MessageUiHandler { if (this.statsMode) { this.toggleStatsMode(false); success = true; + } else if (this.previousSpecies.length > 0) { + this.blockInput = true; + ui.setModeWithoutClear(Mode.OPTION_SELECT).then(() => { + const species = this.previousSpecies.pop(); + const starterAttributes = this.previousStarterAttributes.pop(); + this.moveInfoOverlay.clear(); + this.clearText(); + ui.setModeForceTransition(Mode.POKEDEX_PAGE, species, starterAttributes); + success = true; + }); + this.blockInput = false; } else { - this.getUi().revertMode(); + ui.revertMode(); success = true; } } else { @@ -1504,6 +1521,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler { ? (preSpecies ?? this.species).getFormNameToDisplay(preFormIndex, true) : (preSpecies ?? this.species).getExpandedSpeciesName(), handler: () => { + this.previousSpecies.push(this.species); + this.previousStarterAttributes.push({ ...this.savedStarterAttributes }); const newSpecies = allSpecies.find( species => species.speciesId === pokemonPrevolutions[pre.speciesId], ); @@ -1519,7 +1538,7 @@ export default class PokedexPageUiHandler extends MessageUiHandler { this.savedStarterAttributes.form = newFormIndex; this.moveInfoOverlay.clear(); this.clearText(); - ui.setMode(Mode.POKEDEX_PAGE, newSpecies, newFormIndex, this.savedStarterAttributes); + ui.setMode(Mode.POKEDEX_PAGE, newSpecies, this.savedStarterAttributes); return true; }, onHover: () => this.showText(conditionText), @@ -1555,11 +1574,13 @@ export default class PokedexPageUiHandler extends MessageUiHandler { : (evoSpecies ?? this.species).getExpandedSpeciesName(), style: isCaughtEvo && isFormCaughtEvo ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, handler: () => { + this.previousSpecies.push(this.species); + this.previousStarterAttributes.push({ ...this.savedStarterAttributes }); this.starterAttributes.form = newFormIndex; this.savedStarterAttributes.form = newFormIndex; this.moveInfoOverlay.clear(); this.clearText(); - ui.setMode(Mode.POKEDEX_PAGE, evoSpecies, newFormIndex, this.savedStarterAttributes); + ui.setMode(Mode.POKEDEX_PAGE, evoSpecies, this.savedStarterAttributes); return true; }, onHover: () => this.showText(conditionText), @@ -1595,6 +1616,8 @@ export default class PokedexPageUiHandler extends MessageUiHandler { label: label, style: isFormCaught ? TextStyle.WINDOW : TextStyle.SHADOW_TEXT, handler: () => { + this.previousSpecies.push(this.species); + this.previousStarterAttributes.push({ ...this.savedStarterAttributes }); const newSpecies = this.species; const newFormIndex = this.species.forms.find(f => f.formKey === bf.formKey)?.formIndex; this.starterAttributes.form = newFormIndex; @@ -1604,7 +1627,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { ui.setMode( Mode.POKEDEX_PAGE, newSpecies, - newFormIndex, this.savedStarterAttributes, this.filteredIndices, ); @@ -1955,6 +1977,11 @@ export default class PokedexPageUiHandler extends MessageUiHandler { case Button.LEFT: this.blockInput = true; ui.setModeWithoutClear(Mode.OPTION_SELECT).then(() => { + // Always go back to first selection after scrolling around + if (this.previousSpecies.length === 0) { + this.previousSpecies.push(this.species); + this.previousStarterAttributes.push({ ...this.savedStarterAttributes }); + } let newSpecies: PokemonSpecies; if (this.filteredIndices) { const index = this.filteredIndices.findIndex(id => id === this.species.speciesId); @@ -1976,7 +2003,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { ui.setModeForceTransition( Mode.POKEDEX_PAGE, newSpecies, - newFormIndex, this.savedStarterAttributes, this.filteredIndices, ); @@ -1985,6 +2011,11 @@ export default class PokedexPageUiHandler extends MessageUiHandler { break; case Button.RIGHT: ui.setModeWithoutClear(Mode.OPTION_SELECT).then(() => { + // Always go back to first selection after scrolling around + if (this.previousSpecies.length === 0) { + this.previousSpecies.push(this.species); + this.previousStarterAttributes.push({ ...this.savedStarterAttributes }); + } let newSpecies: PokemonSpecies; if (this.filteredIndices) { const index = this.filteredIndices.findIndex(id => id === this.species.speciesId); @@ -2006,7 +2037,6 @@ export default class PokedexPageUiHandler extends MessageUiHandler { ui.setModeForceTransition( Mode.POKEDEX_PAGE, newSpecies, - newFormIndex, this.savedStarterAttributes, this.filteredIndices, ); diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index f3625f64476..1f79d7006b0 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -1125,7 +1125,7 @@ export default class PokedexUiHandler extends MessageUiHandler { } else if (this.showingTray) { if (button === Button.ACTION) { const formIndex = this.trayForms[this.trayCursor].formIndex; - ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, formIndex, { form: formIndex }, this.filteredIndices); + ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, { form: formIndex }, this.filteredIndices); success = true; } else { const numberOfForms = this.trayContainers.length; @@ -1174,7 +1174,7 @@ export default class PokedexUiHandler extends MessageUiHandler { } } else { if (button === Button.ACTION) { - ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, 0, null, this.filteredIndices); + ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, null, this.filteredIndices); success = true; } else { switch (button) { diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index ab8ea0adf7c..8265ad827bc 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -2324,7 +2324,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { form: starterAttributes.form, female: starterAttributes.female, }; - ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, starterAttributes.form, attributes); + ui.setOverlayMode(Mode.POKEDEX_PAGE, this.lastSpecies, attributes); }); return true; }, From f3141280c91174cd7e7fef0cf4e1979cec58057d Mon Sep 17 00:00:00 2001 From: Madmadness65 <59298170+Madmadness65@users.noreply.github.com> Date: Tue, 25 Mar 2025 00:51:59 -0500 Subject: [PATCH 10/12] [Balance] Implement more trainer types & add to biomes (#5520) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add more trainer types to biomes This makes the Hooligans, Musician, Pilot, Poké Fan, Rich, and Rich Kid trainer types able to be battled. * Adjusted Rich and Rich Kid $$$ multipliers * Add basic dialogue for trainer types Also added the Clerk's dialogue entries, as that trainer type has localized text, but no entry in the code. * Fix Musician to Pokefan encounter music * Add dialogue entries for new dialogue --------- Co-authored-by: damocleas --- src/data/balance/biomes.ts | 94 ++++++++++++++++++++------------ src/data/dialogue.ts | 107 +++++++++++++++++++++++++++++++++++++ src/data/trainer-config.ts | 48 ++++++++++++++--- 3 files changed, 208 insertions(+), 41 deletions(-) diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index a4e051d80c9..3dff1722af6 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -1659,7 +1659,7 @@ export const biomeTrainerPools: BiomeTrainerPools = { }, [Biome.GRASS]: { [BiomePoolTier.COMMON]: [ TrainerType.BREEDER, TrainerType.SCHOOL_KID ], - [BiomePoolTier.UNCOMMON]: [ TrainerType.ACE_TRAINER ], + [BiomePoolTier.UNCOMMON]: [ TrainerType.ACE_TRAINER, TrainerType.POKEFAN ], [BiomePoolTier.RARE]: [ TrainerType.BLACK_BELT ], [BiomePoolTier.SUPER_RARE]: [], [BiomePoolTier.ULTRA_RARE]: [], @@ -1680,9 +1680,9 @@ export const biomeTrainerPools: BiomeTrainerPools = { [BiomePoolTier.BOSS_ULTRA_RARE]: [] }, [Biome.METROPOLIS]: { - [BiomePoolTier.COMMON]: [ TrainerType.CLERK, TrainerType.CYCLIST, TrainerType.OFFICER, TrainerType.WAITER, TrainerType.BEAUTY ], + [BiomePoolTier.COMMON]: [ TrainerType.BEAUTY, TrainerType.CLERK, TrainerType.CYCLIST, TrainerType.OFFICER, TrainerType.WAITER ], [BiomePoolTier.UNCOMMON]: [ TrainerType.BREEDER, TrainerType.DEPOT_AGENT, TrainerType.GUITARIST ], - [BiomePoolTier.RARE]: [ TrainerType.ARTIST ], + [BiomePoolTier.RARE]: [ TrainerType.ARTIST, TrainerType.RICH_KID ], [BiomePoolTier.SUPER_RARE]: [], [BiomePoolTier.ULTRA_RARE]: [], [BiomePoolTier.BOSS]: [ TrainerType.WHITNEY, TrainerType.NORMAN, TrainerType.IONO, TrainerType.LARRY ], @@ -1702,7 +1702,7 @@ export const biomeTrainerPools: BiomeTrainerPools = { [BiomePoolTier.BOSS_ULTRA_RARE]: [] }, [Biome.SEA]: { - [BiomePoolTier.COMMON]: [ TrainerType.SWIMMER, TrainerType.SAILOR ], + [BiomePoolTier.COMMON]: [ TrainerType.SAILOR, TrainerType.SWIMMER ], [BiomePoolTier.UNCOMMON]: [], [BiomePoolTier.RARE]: [], [BiomePoolTier.SUPER_RARE]: [], @@ -1758,7 +1758,7 @@ export const biomeTrainerPools: BiomeTrainerPools = { }, [Biome.MOUNTAIN]: { [BiomePoolTier.COMMON]: [ TrainerType.BACKPACKER, TrainerType.BLACK_BELT, TrainerType.HIKER ], - [BiomePoolTier.UNCOMMON]: [ TrainerType.ACE_TRAINER ], + [BiomePoolTier.UNCOMMON]: [ TrainerType.ACE_TRAINER, TrainerType.PILOT ], [BiomePoolTier.RARE]: [], [BiomePoolTier.SUPER_RARE]: [], [BiomePoolTier.ULTRA_RARE]: [], @@ -1790,7 +1790,7 @@ export const biomeTrainerPools: BiomeTrainerPools = { [BiomePoolTier.BOSS_ULTRA_RARE]: [] }, [Biome.DESERT]: { - [BiomePoolTier.COMMON]: [ TrainerType.SCIENTIST, TrainerType.BACKPACKER ], + [BiomePoolTier.COMMON]: [ TrainerType.BACKPACKER, TrainerType.SCIENTIST ], [BiomePoolTier.UNCOMMON]: [], [BiomePoolTier.RARE]: [], [BiomePoolTier.SUPER_RARE]: [], @@ -1812,8 +1812,8 @@ export const biomeTrainerPools: BiomeTrainerPools = { [BiomePoolTier.BOSS_ULTRA_RARE]: [] }, [Biome.MEADOW]: { - [BiomePoolTier.COMMON]: [ TrainerType.PARASOL_LADY, TrainerType.BEAUTY ], - [BiomePoolTier.UNCOMMON]: [ TrainerType.ACE_TRAINER, TrainerType.BREEDER, TrainerType.BAKER ], + [BiomePoolTier.COMMON]: [ TrainerType.BEAUTY, TrainerType.MUSICIAN, TrainerType.PARASOL_LADY ], + [BiomePoolTier.UNCOMMON]: [ TrainerType.ACE_TRAINER, TrainerType.BAKER, TrainerType.BREEDER, TrainerType.POKEFAN ], [BiomePoolTier.RARE]: [], [BiomePoolTier.SUPER_RARE]: [], [BiomePoolTier.ULTRA_RARE]: [], @@ -1967,7 +1967,7 @@ export const biomeTrainerPools: BiomeTrainerPools = { }, [Biome.SLUM]: { [BiomePoolTier.COMMON]: [ TrainerType.BIKER, TrainerType.OFFICER, TrainerType.ROUGHNECK ], - [BiomePoolTier.UNCOMMON]: [ TrainerType.BAKER ], + [BiomePoolTier.UNCOMMON]: [ TrainerType.BAKER, TrainerType.HOOLIGANS ], [BiomePoolTier.RARE]: [], [BiomePoolTier.SUPER_RARE]: [], [BiomePoolTier.ULTRA_RARE]: [], @@ -1988,8 +1988,8 @@ export const biomeTrainerPools: BiomeTrainerPools = { [BiomePoolTier.BOSS_ULTRA_RARE]: [] }, [Biome.ISLAND]: { - [BiomePoolTier.COMMON]: [], - [BiomePoolTier.UNCOMMON]: [], + [BiomePoolTier.COMMON]: [ TrainerType.RICH_KID ], + [BiomePoolTier.UNCOMMON]: [ TrainerType.RICH ], [BiomePoolTier.RARE]: [], [BiomePoolTier.SUPER_RARE]: [], [BiomePoolTier.ULTRA_RARE]: [], @@ -7178,7 +7178,8 @@ export function initBiomes() { [ Biome.METROPOLIS, BiomePoolTier.COMMON ], [ Biome.MEADOW, BiomePoolTier.COMMON ], [ Biome.FAIRY_CAVE, BiomePoolTier.COMMON ] - ]], + ] + ], [ TrainerType.BIKER, [ [ Biome.SLUM, BiomePoolTier.COMMON ] ] @@ -7208,7 +7209,8 @@ export function initBiomes() { ], [ TrainerType.CLERK, [ [ Biome.METROPOLIS, BiomePoolTier.COMMON ] - ]], + ] + ], [ TrainerType.CYCLIST, [ [ Biome.PLAINS, BiomePoolTier.UNCOMMON ], [ Biome.METROPOLIS, BiomePoolTier.COMMON ] @@ -7217,18 +7219,23 @@ export function initBiomes() { [ TrainerType.DANCER, []], [ TrainerType.DEPOT_AGENT, [ [ Biome.METROPOLIS, BiomePoolTier.UNCOMMON ] - ]], + ] + ], [ TrainerType.DOCTOR, []], + [ TrainerType.FIREBREATHER, [ + [ Biome.VOLCANO, BiomePoolTier.COMMON ] + ] + ], [ TrainerType.FISHERMAN, [ [ Biome.LAKE, BiomePoolTier.COMMON ], [ Biome.BEACH, BiomePoolTier.COMMON ] ] ], - [ TrainerType.RICH, []], [ TrainerType.GUITARIST, [ [ Biome.METROPOLIS, BiomePoolTier.UNCOMMON ], [ Biome.POWER_PLANT, BiomePoolTier.COMMON ] - ]], + ] + ], [ TrainerType.HARLEQUIN, []], [ TrainerType.HIKER, [ [ Biome.MOUNTAIN, BiomePoolTier.COMMON ], @@ -7236,13 +7243,24 @@ export function initBiomes() { [ Biome.BADLANDS, BiomePoolTier.COMMON ] ] ], - [ TrainerType.HOOLIGANS, []], + [ TrainerType.HOOLIGANS, [ + [ Biome.SLUM, BiomePoolTier.UNCOMMON ] + ] + ], [ TrainerType.HOOPSTER, []], [ TrainerType.INFIELDER, []], [ TrainerType.JANITOR, []], [ TrainerType.LINEBACKER, []], [ TrainerType.MAID, []], - [ TrainerType.MUSICIAN, []], + [ TrainerType.MUSICIAN, [ + [ Biome.MEADOW, BiomePoolTier.COMMON ] + ] + ], + [ TrainerType.HEX_MANIAC, [ + [ Biome.RUINS, BiomePoolTier.UNCOMMON ], + [ Biome.GRAVEYARD, BiomePoolTier.UNCOMMON ] + ] + ], [ TrainerType.NURSERY_AIDE, []], [ TrainerType.OFFICER, [ [ Biome.METROPOLIS, BiomePoolTier.COMMON ], @@ -7256,8 +7274,15 @@ export function initBiomes() { [ Biome.MEADOW, BiomePoolTier.COMMON ] ] ], - [ TrainerType.PILOT, []], - [ TrainerType.POKEFAN, []], + [ TrainerType.PILOT, [ + [ Biome.MOUNTAIN, BiomePoolTier.UNCOMMON ] + ] + ], + [ TrainerType.POKEFAN, [ + [ Biome.GRASS, BiomePoolTier.UNCOMMON ], + [ Biome.MEADOW, BiomePoolTier.UNCOMMON ] + ] + ], [ TrainerType.PRESCHOOLER, []], [ TrainerType.PSYCHIC, [ [ Biome.GRAVEYARD, BiomePoolTier.COMMON ], @@ -7270,11 +7295,24 @@ export function initBiomes() { [ Biome.JUNGLE, BiomePoolTier.COMMON ] ] ], - [ TrainerType.RICH_KID, []], + [ TrainerType.RICH, [ + [ Biome.ISLAND, BiomePoolTier.UNCOMMON ] + ] + ], + [ TrainerType.RICH_KID, [ + [ Biome.METROPOLIS, BiomePoolTier.RARE ], + [ Biome.ISLAND, BiomePoolTier.COMMON ] + ] + ], [ TrainerType.ROUGHNECK, [ [ Biome.SLUM, BiomePoolTier.COMMON ] ] ], + [ TrainerType.SAILOR, [ + [ Biome.SEA, BiomePoolTier.COMMON ], + [ Biome.BEACH, BiomePoolTier.COMMON ] + ] + ], [ TrainerType.SCIENTIST, [ [ Biome.DESERT, BiomePoolTier.COMMON ], [ Biome.RUINS, BiomePoolTier.COMMON ] @@ -7317,20 +7355,6 @@ export function initBiomes() { [ Biome.TOWN, BiomePoolTier.COMMON ] ] ], - [ TrainerType.HEX_MANIAC, [ - [ Biome.RUINS, BiomePoolTier.UNCOMMON ], - [ Biome.GRAVEYARD, BiomePoolTier.UNCOMMON ] - ] - ], - [ TrainerType.FIREBREATHER, [ - [ Biome.VOLCANO, BiomePoolTier.COMMON ] - ] - ], - [ TrainerType.SAILOR, [ - [ Biome.SEA, BiomePoolTier.COMMON ], - [ Biome.BEACH, BiomePoolTier.COMMON ] - ] - ], [ TrainerType.BROCK, [ [ Biome.CAVE, BiomePoolTier.BOSS ] ] diff --git a/src/data/dialogue.ts b/src/data/dialogue.ts index 208e269bd9c..0e755d54e15 100644 --- a/src/data/dialogue.ts +++ b/src/data/dialogue.ts @@ -312,6 +312,113 @@ export const trainerTypeDialogue: TrainerTypeDialogue = { victory: ["dialogue:sailor.victory.1", "dialogue:sailor.victory.2", "dialogue:sailor.victory.3"], }, ], + [TrainerType.CLERK]: [ + { + encounter: ["dialogue:clerk.encounter.1", "dialogue:clerk.encounter.2", "dialogue:clerk.encounter.3"], + victory: ["dialogue:clerk.victory.1", "dialogue:clerk.victory.2", "dialogue:clerk.victory.3"], + }, + { + encounter: [ + "dialogue:clerk_female.encounter.1", + "dialogue:clerk_female.encounter.2", + "dialogue:clerk_female.encounter.3", + ], + victory: [ + "dialogue:clerk_female.victory.1", + "dialogue:clerk_female.victory.2", + "dialogue:clerk_female.victory.3", + ], + }, + ], + [TrainerType.HOOLIGANS]: [ + { + encounter: ["dialogue:hooligans.encounter.1", "dialogue:hooligans.encounter.2"], + victory: ["dialogue:hooligans.victory.1", "dialogue:hooligans.victory.2"], + }, + ], + [TrainerType.MUSICIAN]: [ + { + encounter: [ + "dialogue:musician.encounter.1", + "dialogue:musician.encounter.2", + "dialogue:musician.encounter.3", + "dialogue:musician.encounter.4", + ], + victory: ["dialogue:musician.victory.1", "dialogue:musician.victory.2", "dialogue:musician.victory.3"], + }, + ], + [TrainerType.PILOT]: [ + { + encounter: [ + "dialogue:pilot.encounter.1", + "dialogue:pilot.encounter.2", + "dialogue:pilot.encounter.3", + "dialogue:pilot.encounter.4", + ], + victory: [ + "dialogue:pilot.victory.1", + "dialogue:pilot.victory.2", + "dialogue:pilot.victory.3", + "dialogue:pilot.victory.4", + ], + }, + ], + [TrainerType.POKEFAN]: [ + { + encounter: ["dialogue:pokefan.encounter.1", "dialogue:pokefan.encounter.2", "dialogue:pokefan.encounter.3"], + victory: ["dialogue:pokefan.victory.1", "dialogue:pokefan.victory.2", "dialogue:pokefan.victory.3"], + }, + { + encounter: [ + "dialogue:pokefan_female.encounter.1", + "dialogue:pokefan_female.encounter.2", + "dialogue:pokefan_female.encounter.3", + ], + victory: [ + "dialogue:pokefan_female.victory.1", + "dialogue:pokefan_female.victory.2", + "dialogue:pokefan_female.victory.3", + ], + }, + ], + [TrainerType.RICH]: [ + { + encounter: ["dialogue:rich.encounter.1", "dialogue:rich.encounter.2", "dialogue:rich.encounter.3"], + victory: ["dialogue:rich.victory.1", "dialogue:rich.victory.2", "dialogue:rich.victory.3"], + }, + { + encounter: [ + "dialogue:rich_female.encounter.1", + "dialogue:rich_female.encounter.2", + "dialogue:rich_female.encounter.3", + ], + victory: ["dialogue:rich_female.victory.1", "dialogue:rich_female.victory.2", "dialogue:rich_female.victory.3"], + }, + ], + [TrainerType.RICH_KID]: [ + { + encounter: ["dialogue:rich_kid.encounter.1", "dialogue:rich_kid.encounter.2", "dialogue:rich_kid.encounter.3"], + victory: [ + "dialogue:rich_kid.victory.1", + "dialogue:rich_kid.victory.2", + "dialogue:rich_kid.victory.3", + "dialogue:rich_kid.victory.4", + ], + }, + { + encounter: [ + "dialogue:rich_kid_female.encounter.1", + "dialogue:rich_kid_female.encounter.2", + "dialogue:rich_kid_female.encounter.3", + ], + victory: [ + "dialogue:rich_kid_female.victory.1", + "dialogue:rich_kid_female.victory.2", + "dialogue:rich_kid_female.victory.3", + "dialogue:rich_kid_female.victory.4", + ], + }, + ], [TrainerType.ROCKET_GRUNT]: [ { encounter: [ diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index ffe5cdfe04c..0417e7abc32 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -2141,7 +2141,15 @@ export const trainerConfigs: TrainerConfigs = { }), [TrainerType.HOOLIGANS]: new TrainerConfig(++t) .setDoubleOnly() + .setMoneyMultiplier(1.5) .setEncounterBgm(TrainerType.ROUGHNECK) + .setPartyTemplateFunc(() => + getWavePartyTemplate( + trainerPartyTemplates.TWO_WEAK, + trainerPartyTemplates.TWO_AVG, + trainerPartyTemplates.ONE_AVG_ONE_STRONG, + ), + ) .setSpeciesFilter(s => s.isOfType(PokemonType.POISON) || s.isOfType(PokemonType.DARK)), [TrainerType.HOOPSTER]: new TrainerConfig(++t).setMoneyMultiplier(1.2).setEncounterBgm(TrainerType.CYCLIST), [TrainerType.INFIELDER]: new TrainerConfig(++t).setMoneyMultiplier(1.2).setEncounterBgm(TrainerType.CYCLIST), @@ -2149,7 +2157,14 @@ export const trainerConfigs: TrainerConfigs = { [TrainerType.LINEBACKER]: new TrainerConfig(++t).setMoneyMultiplier(1.2).setEncounterBgm(TrainerType.CYCLIST), [TrainerType.MAID]: new TrainerConfig(++t).setMoneyMultiplier(1.6).setEncounterBgm(TrainerType.RICH), [TrainerType.MUSICIAN]: new TrainerConfig(++t) - .setEncounterBgm(TrainerType.ROUGHNECK) + .setMoneyMultiplier(1.1) + .setEncounterBgm(TrainerType.POKEFAN) + .setPartyTemplates( + trainerPartyTemplates.FOUR_WEAKER, + trainerPartyTemplates.THREE_WEAK, + trainerPartyTemplates.TWO_WEAK_ONE_AVG, + trainerPartyTemplates.TWO_AVG, + ) .setSpeciesFilter(s => !!s.getLevelMoves().find(plm => plm[1] === Moves.SING)), [TrainerType.HEX_MANIAC]: new TrainerConfig(++t) .setMoneyMultiplier(1.5) @@ -2214,7 +2229,14 @@ export const trainerConfigs: TrainerConfigs = { ) || s.getLevelMoves().some(plm => plm[1] === Moves.RAIN_DANCE), ), // Mons with rain abilities or who learn Rain Dance by level [TrainerType.PILOT]: new TrainerConfig(++t) + .setMoneyMultiplier(1.75) .setEncounterBgm(TrainerType.CLERK) + .setPartyTemplates( + trainerPartyTemplates.THREE_WEAK, + trainerPartyTemplates.TWO_WEAK_ONE_AVG, + trainerPartyTemplates.TWO_AVG, + trainerPartyTemplates.THREE_AVG, + ) .setSpeciesFilter(s => tmSpecies[Moves.FLY].indexOf(s.speciesId) > -1), [TrainerType.POKEFAN]: new TrainerConfig(++t) .setMoneyMultiplier(1.4) @@ -2230,7 +2252,8 @@ export const trainerConfigs: TrainerConfigs = { trainerPartyTemplates.FOUR_WEAK_SAME, trainerPartyTemplates.FIVE_WEAK, trainerPartyTemplates.SIX_WEAKER_SAME, - ), + ) + .setSpeciesFilter(s => tmSpecies[Moves.HELPING_HAND].indexOf(s.speciesId) > -1), [TrainerType.PRESCHOOLER]: new TrainerConfig(++t) .setMoneyMultiplier(0.2) .setEncounterBgm(TrainerType.YOUNGSTER) @@ -2352,16 +2375,29 @@ export const trainerConfigs: TrainerConfigs = { [TrainerPoolTier.SUPER_RARE]: [Species.LARVESTA], }), [TrainerType.RICH]: new TrainerConfig(++t) - .setMoneyMultiplier(5) + .setMoneyMultiplier(3.25) .setName("Gentleman") .setHasGenders("Madame") - .setHasDouble("Rich Couple"), + .setHasDouble("Rich Couple") + .setPartyTemplates( + trainerPartyTemplates.THREE_WEAK, + trainerPartyTemplates.FOUR_WEAK, + trainerPartyTemplates.TWO_WEAK_ONE_AVG, + trainerPartyTemplates.THREE_AVG, + ) + .setSpeciesFilter(s => s.isOfType(PokemonType.NORMAL) || s.isOfType(PokemonType.ELECTRIC)), [TrainerType.RICH_KID]: new TrainerConfig(++t) - .setMoneyMultiplier(3.75) + .setMoneyMultiplier(2.5) .setName("Rich Boy") .setHasGenders("Lady") .setHasDouble("Rich Kids") - .setEncounterBgm(TrainerType.RICH), + .setEncounterBgm(TrainerType.RICH) + .setPartyTemplates( + trainerPartyTemplates.FOUR_WEAKER, + trainerPartyTemplates.THREE_WEAK_SAME, + trainerPartyTemplates.TWO_WEAK_SAME_ONE_AVG, + ) + .setSpeciesFilter(s => s.baseTotal <= 460), [TrainerType.ROUGHNECK]: new TrainerConfig(++t) .setMoneyMultiplier(1.4) .setEncounterBgm(TrainerType.ROUGHNECK) From 443264a3ea377da4ef4f25554aa156395dc1cf6f Mon Sep 17 00:00:00 2001 From: damocleas Date: Tue, 25 Mar 2025 01:55:26 -0400 Subject: [PATCH 11/12] [i18n] Update locales submodule --- public/locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/locales b/public/locales index 0e5c6096ba2..cd4057af258 160000 --- a/public/locales +++ b/public/locales @@ -1 +1 @@ -Subproject commit 0e5c6096ba26f6b87aed1aab3fe9b0b23f6cbb7b +Subproject commit cd4057af258b659ba2c1ed2778bb2793fa1f6141 From f78015d75730d205d4bf08a4fd09547d8b8a8b37 Mon Sep 17 00:00:00 2001 From: Lugiad <2070109+Adri1@users.noreply.github.com> Date: Tue, 25 Mar 2025 21:06:24 +0100 Subject: [PATCH 12/12] [Localization] Catalan reactivation (#5555) * Add files via upload * Update settings.ts * Add files via upload * Add files via upload * Update starter-select-ui-handler.ts * Update utils.ts with Catalan * Update loading-scene.ts --- public/images/statuses_ca-ES.png | Bin 441 -> 2244 bytes public/images/types_ca-ES.json | 440 ++++++++++++++++++++++++++++ public/images/types_ca-ES.png | Bin 0 -> 6383 bytes src/loading-scene.ts | 2 +- src/system/settings/settings.ts | 8 +- src/ui/starter-select-ui-handler.ts | 2 +- src/utils.ts | 1 + 7 files changed, 447 insertions(+), 6 deletions(-) create mode 100644 public/images/types_ca-ES.json create mode 100644 public/images/types_ca-ES.png diff --git a/public/images/statuses_ca-ES.png b/public/images/statuses_ca-ES.png index d372b989be966007e5fccac5f54fe341457e598b..fe05e243f7a5cd6de4d3b9a340707ebf06cc7e58 100644 GIT binary patch literal 2244 zcmbVO4Nw$i7+$d-vQWaTbkep33$wlbxgYKpP6dtx^U%{%FlENQ-92vOZg<;VjypzY z(EJ~Nl7dMAr=gU>$)rY7F^g=lI#J0&MXpR#RL~-uL4>UD__?&HoZOjjzi)S+?|I+n zec$h$T{}B-den$6eD^xG%Gqdjn5+iGz2A0^+`07$EmP`%jE?#@^js12+Xr)Buj6_tx_uI z;?oOdZf;?wohi&?Oe`{W3Y_Gl0D*^7Y1rp+3ku~kBmKM-SchI?2;2`*^UO$c$RM0; zoeigoG6(B529zOmB%EN<5C+^t>Ry0JoJhd%1dPz4I6>hCN^61#F9cYVStm8e^5UQ^ z@MK0@swzgb^A%B&i?BGGUyQFT4AVvkb<$IS(hO3cwO!tmG0^ zQE`d?LLL5mhXD{;t2J!nAARw7!X^|o%?o4<2IL>nirp`9*c?s~^JRuh^8z^qS6AxWmwDzP^{s3mNIfQj?)Py)L=A{sE*WWQJQejsE%Ru9Em#| zq|-38-y$;kp}Y?5XaDE^SuzjK745!f9#H>(DI8)+28tArIA!p3q|mv8D>pwSBs1hD zVbl|x!>C2I2~GypYjrGYB5@8Sw6vZv7zl%b!IQAiQ2?9Zfz<#J_TLu!3zSOdLmv1C z?at+14SN9h*Ca(Vp|fU2m{5^%EHe0-zvqk$&=$BjK>8o$Hvm&ar&>VETyieR-d&0t z8Q^=96tpQn{Kd^O4+Ej9JMnfKZb*Rz?s+P@tAL=};;g`cZ)t!s$x zG9R1Ov!dfebIQW`?2(F`v)(0RHa8xBuq<+|A{5tH)7PDv9~*e0f$wZ=&Ju5xKNnp^ zM0_3;4m2whkxSm;MYpnBTWWL8UrH&CThm=tbZz*gtxrvcrq%s8u3K~T>=s{K+CFVg zuQ3)nA$wOgZjLq0f8&Vh*`=K^o9iz|Bx+0dc)Aygt72@$ar+Ie{)|b!#fyFZ6^XTH zZY4mi`vY9b?r%sa@?6`~(4oMI%IfMp^e4fY#&aHT@Ipihm#}``8g|E`*@+we_}Du$ z&sJw!l~lB$Ej#Xjj@fwH-#5?ld|-XcjnYQ{&YaR=Pd0P}TTtWoLAW))V?{|s(c7ou zq0|jgO+jUOP;nSDj(W|nw5}Po-Q^c-+ZJA3-1e?_5F|51mtWbF1&%BJZN4 zH@7tBuiKxw=ekp=kPL~?*_XS^gyPo&7hB6KvQ9ZK?L{u4k zRzAISi@v^*#u_SN-kY_VUJE9fk2@I(~mO zK5b{UWnRhXaScoTlHtM_nOSM0{KZO-TQ>G}+I zIn7o0!+Dp;xZJZk++_QIf*BQ_vO9cL>_x7%KzeP1y+B2_I7G_xHE-s8-)5{aFy1?z zvW;$NS#fE(5J*QH>;TO#qj-;m3y@V^jDKUiXB4T~am+que5?h|bD2QvJlYc8%_G~a zG}z~@!Q2dSmt873PQXLUg+O|T!QM`p^A|K=@Suc&?b@)04>kGLv3=K17yh?|Mhx#G)+hhQwMYUUT0RT>1-XbQdPI;dWdHyG07*qoM6N<$g7t5` A=>Px# diff --git a/public/images/types_ca-ES.json b/public/images/types_ca-ES.json new file mode 100644 index 00000000000..fa3abaaf259 --- /dev/null +++ b/public/images/types_ca-ES.json @@ -0,0 +1,440 @@ +{ + "textures": [ + { + "image": "types_ca-ES.png", + "format": "RGBA8888", + "size": { + "w": 32, + "h": 280 + }, + "scale": 1, + "frames": [ + { + "filename": "unknown", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + } + }, + { + "filename": "bug", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 14, + "w": 32, + "h": 14 + } + }, + { + "filename": "dark", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 28, + "w": 32, + "h": 14 + } + }, + { + "filename": "dragon", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 42, + "w": 32, + "h": 14 + } + }, + { + "filename": "electric", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 56, + "w": 32, + "h": 14 + } + }, + { + "filename": "fairy", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 70, + "w": 32, + "h": 14 + } + }, + { + "filename": "fighting", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 84, + "w": 32, + "h": 14 + } + }, + { + "filename": "fire", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 98, + "w": 32, + "h": 14 + } + }, + { + "filename": "flying", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 112, + "w": 32, + "h": 14 + } + }, + { + "filename": "ghost", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 126, + "w": 32, + "h": 14 + } + }, + { + "filename": "grass", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 140, + "w": 32, + "h": 14 + } + }, + { + "filename": "ground", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 154, + "w": 32, + "h": 14 + } + }, + { + "filename": "ice", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 168, + "w": 32, + "h": 14 + } + }, + { + "filename": "normal", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 182, + "w": 32, + "h": 14 + } + }, + { + "filename": "poison", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 196, + "w": 32, + "h": 14 + } + }, + { + "filename": "psychic", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 210, + "w": 32, + "h": 14 + } + }, + { + "filename": "rock", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 224, + "w": 32, + "h": 14 + } + }, + { + "filename": "steel", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 238, + "w": 32, + "h": 14 + } + }, + { + "filename": "water", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 252, + "w": 32, + "h": 14 + } + }, + { + "filename": "stellar", + "rotated": false, + "trimmed": false, + "sourceSize": { + "w": 32, + "h": 14 + }, + "spriteSourceSize": { + "x": 0, + "y": 0, + "w": 32, + "h": 14 + }, + "frame": { + "x": 0, + "y": 266, + "w": 32, + "h": 14 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:f14cf47d9a8f1d40c8e03aa6ba00fff3:6fc4227b57a95d429a1faad4280f7ec8:5961efbfbf4c56b8745347e7a663a32f$" + } +} diff --git a/public/images/types_ca-ES.png b/public/images/types_ca-ES.png new file mode 100644 index 0000000000000000000000000000000000000000..e85c84ed9c987049fd9965771a4a64a12dcc6b0b GIT binary patch literal 6383 zcmb_h2{=@J+dn8mlr==gkR^uM%$UhYcJ){aMYPOv1~H47VGvOoB_&#@tPe%rvX?Dc z3xzCYX+fgYqoPRGd`H{6e9yal@Ab`G=Q{WKpZkB`zu&!_`^+^l&W^TH5-Jh^07%)} zS?z?*FaQuzS|tieB(14P=p@dz^WXx2^xfq{=z_lVdH@jX^>uaUxjXzuq_P->WEzVC z8U{1ikTw9AZV6_SseT|2MghHjnI!o5?HV}Dmqvo`Hg-Tduq{C!U%OBa=o0GaN)7d+ z5@_%(W-!xWB4mI8^2o4YhCh=_3?{+9*(E~q^0E;e_DzN7M}lu%b_jEKaE4j3I3Uc} z5Qm_muxJ>bV2HvY323YV42?wLjgWXF6b6Ar5s^3|h5-BVfkWOnG&*somCX-d&=m>p z!{f1uMn-%--;j?nWO2NWPy_f*Zo*k%JLTuI5h*Rv?$k@n!RTSxnfn zMKXmI$Roiaq~9qp*uTs&xj(`L10OU>9GSj{@-OI{fI{ow@XN=aVqq|Tx#04wgCH6|0`jNYT-OjbXtWdL zvI03&&^ic$skzLCO|;~IWFCv-%3}HdOqBCa$S_MwnC@<0CXL1C>ivy6Xhr6MBsdz0 zHbx*Z2sGLijUl24L}Q%(a`KShRUKF~UwX(tsA65wNFowXL=*l_6)G4SnMeM&$}}pG z&f+l05M5se*&8%sGri%kUqK{Vviw;b$T5Tt^9_ZA1JRzz<&l|G(B6s!hZr0B`qGF< z0*J<72owaFVvIu2(O3$Cf1qmP-M15xg?YCl41DEsqb3YB*PaqRaFa#6| zK_Jsn2r31OM-Z@RGIR-2Ci@3{e_GiJG2%jrL*PM>N~TaLWE`FTFLvWipqOGo zgb4-;GtC%FK$u{WCX1Z zKdG$$tTMiv<@SkoSZ!1~Hx#+F5R zO;_JJ#2@ef5_My6^<+@Zp@(TDqbaxNKZJiFVF45N*A(fM<->k1*zqHiqf>EN?biy= zh>W(sefpQCGQCTrBfD*UJU?y%Y8PHUL+NbUqGd=CD^)0{hc1NxqYsOkuh$JSt1AQ0 zj=@)+uPBeY&ygXR(-ow3M0({rW;fc>KoB{UC7)`1#1K>vTW0xDT`xe1G#>Awz3x# zHTIm*`U8?aCnU&B78q>l3ESB823KJ8GK6!?k9780vOd{5z*J?rI_l)h;75e6_EhgF z?&#-BO>e0;ZtL|$ICSE9NO-oU>F9HMOO&z@+Qc(>DuXA{N4W~x)fI8# zqzjMo(hapPk1Ax@IgXtABxsiFLtSYNKG3xGX7Pf%`4Iie+se3X+l6hax{C?>Cvl{g zy-RbH0*QHGVa~00|0JOXJ{J6C!FqmMxi5b`0GrQNZcZU}zmvX0Yj`sNyQ&u>1d}X$*5W!}RUCpOPGPtUId%){=xwx(Mu_M+Dbevt|Sz%%d z@1lDlnNR$Hm9Djl4F2u7q3SVoT%IUoDsbvzWKt%(G2k~L<<*fMiz}vH+*r+0)f4M` zIjo@Bzs?QEQcb`-t$al|$ZudvB2?a(eN@Em?M=93>R>Nl*S6el1UEs-+p%(0#8{(X1lS=OnU=qYMsp);9%QJ7 z1P)s{mhZbbA#XW!LKA+B=PRm&5x(*JM%6WrLt+-?4z}?LZeVo9;w^+?-0{<&1G2qb zRe=!~m`Ia+$B^_&Eemv|g_yf_X@*^LQ&F*F=Z@;NAt@5Hs?M)Z6{+}?3jsA}byMEW z-U?u>>gI3j#OQ0~b$XDlwM_p0G=1%**;D5#bzFx5=cLhwxmw`hLR12w10&gS`w{QX z=C+`wM>VwJ&tsd;$<=*i%=UX6PIfu=Y~WQB zB>5j7d>HtoqK&I>$p?VlC|N@-u~Po5P%T=dzGCYx!z=O~Dak?}71yqs@50l>Xy!Sa zTvw^FCARd_U*X#GMjpgu9M<4?>Nd(kY5SG=+te5CCAs#A0l`ZF z!LeU+Y&|b1Eh%RBIVsS*pu0)Ne9FDY>z(5dtI-HuhyTdKw+>gCC0w0fLGIs3Eu#D( z6<2U~$4f8IacdX+CVdT(X65@JgM-q|k&cq5uU#=0)mr&L70}vQyK5XR-D~4U*h{lY z&*R`PN>9``3`=wsluY=i=jb04iG(`OGW}dS?rErz%IzV3&1)A~K~DP`rQ1V~5c+P* z06%_;nrklT4r_qFKK$nCE8$ysU9TicUPV(^!ROgR4NTpLeR|^6iW6!ukzz-g{DTL8 zTW0CA+qh^)JC;pVot5es`>wRB)M_ApO`iXZ(EYY+J)@GoyY_I`-~wCv7f$M~>@;_0 z+1Yg_l%9w^DNgbcgYisGo)JH`{HKum)+jA@24!xK6 z8Q`~X-4aZ(5`-IApUO8b-M1?u=cd+f%ZzZjU`8iO#gvdZd|ya<%-lXdy1u*7auwpU zo37X1*t1^q4XfaUM9l(hQc_7>xKVSQ6N{U^6+syUHtjR!`kulbqw-Z+TKnS_#IT3wZL; zK>FIzCf{Adr_JZJsrag_uq^0t?GpwOIkP2)j!!9Bb@|H-w?m!byPK?jJwD4%fA@x+ z^WhB_^w+$F#A1>fX6&_sV%moihB8UUJG^uOHx_<$Zcd=x8$QFQz856oIwd(_q&XAc z90yP4;Hj=`M45F=Q%qd!Q>?Aiz~5o|!skw<)S$S}qUnA8mB7(do_; z|98n*p_ll+@-Xb5y$Boucv?CLV0?xnBQ86@$jiLtSjD zwFE93m&{PHO4PA^I={i}p4yoYBCUa2J4wFTpRP-Vd!309+7;KWSE)lj7dAM0&RUGfq>iVH?qj^c1T`$U26P_4+V38-=3tTKO=;ycF|J{3FeX~o_4-CCOu(p;n)syHH3@RI|#K1-UDpG z6(EvO;&Wsy@}07I8Awokunyi{y~WF!9yX|MsC1kBu~iQO_~in(kr`~!IT~G(V?LQ? zA{IVY^Ehn&tJ&SLr=QN9teM)RU%uH``nJn3J9JEOd;06Q!^QD?P6XvNYTS7~Q+~Cq zsK@7A?<$$Dy~WC3@+?C`6zI>(9@bkQjW;WOma1}mpm7hT_0H*AWC6)vtS>$%X}5Js zbi50!K_Hy_Xkw}d@rHpl8_SAKuz|KFakih257xk~BH)O@k_<@Gfg z?W~si=aMJNm@qV?qbPC%b*U&4nU_PtJ)-njj|p!&U{a$?pip%;*p zEvK;=Cd@Lgmx*%9E?>XqnNXzq&dYMQvR(q>guPy-j-tbf!X7$TR9~cSW9r2VvP7=b zdkp(`?>=zbA&DeePn6mo#(#1?wce?$|1aqWyw<^$gVR)J^3cOh(hEI(QZ*srg0s2{ zv*S{YF4%~#ui(q?4Ex?Ox{pV2;k0`(YTXk#=QF#Kg@IItF`blZosTyB;dLt7X(%p1SmK%2-^oeX#df%l3nCek5+S z(rn&&rHfW4+X_~z7Q`*;UA@@1gQ2JrAQ5?0Hzt56_*$)POA_X)!L-&>_LM``uXE}< z3yMP(#a!z&iL5PB%SeJ6#PW??Wd-{e@7ycLEv#tQ4XBM8_Ze`SI5p6{a8qKwd5`qo z&7U5B(jU`PIU3Rul>GkW)wku7Vi@Z?&9?-u88oi|4D<@jcR4C2X@(!&_)6i8kALA*4AV~9 zRgp%q4_OIhWEIt^UVP<4=#kmuIC5-kTeC~+1HBSuh5HqCokGO(j!|-i&}`E5xc3hWV+@H6{ycew!_}Qtm@Qs18wfB8p z!xaY&!Z+A{v1U}Z*OyB35>LT%3#EaM!Qu_}0%JRg_@Gp0(T!sja^zRqqi(ltfk^9) z+*DV0@AZ#;4HTbWD69cVDNBOrBM;B+$golsx>QsyW4GxB#&UFphq4YKSKbOftgUlv zeNdv31}#>0m7>Q^a_Z@GN7{t;T-uNly(7#xv07ud=6M;y)x#RHh_&gW5TaB4>`!XO z+cMch=C`ud7F+g3PC8+kGdCvBTn(S4ejQ&U0*LBdZacFyI}6+5KcC*$7{LpDdn=N0 zDhU_~JM8YG$+VgP9_PB35W1WetzHPMl6_iJ1(k^XQ2&oEX4f>Qif98Gyu(y{HcLtb&)E((6fDW_@0@wMcfzi&`Ai`EcQ@I74TX#L(Uou3LSmC zbFAig_?L6r?GULaN3PnJ_h7{FhHI)vYsQ+W?dsKdp_+wOi!t-vHD``@IH}uNhUyTp}dY%WXZK!;LteotA zF%FA(aYvBq-62$o@v}z(FKA8sew!OqZAI+!<9HM2P~l8v=7X<#&MU$uc5M1Xte3Jb z@?NeaDq@dfP<5zGtjoHW9soFqoSIh3saTuX&*u3EpRFnC^dH}p>^)>q+`A&;tZhwjgmmdFC_r`dCy#s&CF*5Kdw)bN3 zh)L(XaOA1Sv$bxbNeRS3Y;)AEiF0z#!y{XH!JC$Br6_~NOLM|%ZFfuRKErJa|DtpE zZasJUa}=(D*=U{dh>JtH%W0y*W-TK&iy{~SQrg;H#a2U{1^L}nxj}-CX;29J?31+V nfw37K!%K_pVKkZUuWL*XN7yT_Qi@ytOTymT(JJ5Kz|sE!_KvZK literal 0 HcmV?d00001 diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 295dc318db4..7f05634db48 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -250,7 +250,7 @@ export class LoadingScene extends SceneBase { this.loadAtlas("statuses", ""); this.loadAtlas("types", ""); } - const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN"]; + const availableLangs = ["en", "de", "it", "fr", "ja", "ko", "es-ES", "pt-BR", "zh-CN", "zh-TW", "ca-ES"]; if (lang && availableLangs.includes(lang)) { this.loadImage(`pkmnday2025event-${lang}`, "events"); } else { diff --git a/src/system/settings/settings.ts b/src/system/settings/settings.ts index 1a7279d371c..b2b1d3eb298 100644 --- a/src/system/settings/settings.ts +++ b/src/system/settings/settings.ts @@ -948,10 +948,10 @@ export function setSetting(setting: string, value: number): boolean { label: "日本語", handler: () => changeLocaleHandler("ja"), }, - // { - // label: "Català", - // handler: () => changeLocaleHandler("ca-ES") - // }, + { + label: "Català", + handler: () => changeLocaleHandler("ca-ES") + }, { label: i18next.t("settings:back"), handler: () => cancelHandler(), diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index 8265ad827bc..91940d3af76 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -150,7 +150,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { instructionTextSize: "38px", }, "ca-ES": { - starterInfoTextSize: "56px", + starterInfoTextSize: "52px", instructionTextSize: "38px", }, }; diff --git a/src/utils.ts b/src/utils.ts index 7d3dea0247e..fbca8410feb 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -464,6 +464,7 @@ export function hasAllLocalizedSprites(lang?: string): boolean { case "pt-BR": case "ko": case "ja": + case "ca-ES": return true; default: return false;