From a48952e354e69a341f12982cbf46dd6ca05debbf Mon Sep 17 00:00:00 2001 From: damocleas Date: Thu, 30 Oct 2025 21:08:00 -0400 Subject: [PATCH 01/27] [Balance] [Bug] Fix Rhydon not being a Rhyhorn in Rival 2 --- src/data/trainers/rival-party-config.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/trainers/rival-party-config.ts b/src/data/trainers/rival-party-config.ts index b5a8cb532b3..fa040ff66de 100644 --- a/src/data/trainers/rival-party-config.ts +++ b/src/data/trainers/rival-party-config.ts @@ -253,7 +253,7 @@ const SLOT_3_FIGHT_2 = [ SpeciesId.MACHOP, SpeciesId.GASTLY, SpeciesId.MAGNEMITE, - SpeciesId.RHYDON, + SpeciesId.RHYHORN, SpeciesId.TANGELA, SpeciesId.PORYGON, SpeciesId.ELEKID, From 9a4381c7766482b3996d29ced5dfe79dc8fed2f4 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Thu, 30 Oct 2025 18:58:06 -0700 Subject: [PATCH 02/27] Re-Add MovePriorityModifier --- src/data/abilities/ability.ts | 25 +++++++++++++++++++++++-- src/data/moves/move.ts | 13 ++++++++----- src/data/terrain.ts | 2 +- src/enums/move-priority-modifier.ts | 13 +++++++++++++ src/queues/move-phase-priority-queue.ts | 7 +++++++ test/arena/psychic-terrain.test.ts | 4 ++-- 6 files changed, 54 insertions(+), 10 deletions(-) create mode 100644 src/enums/move-priority-modifier.ts diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 301016d899b..180ba0f3e90 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -34,6 +34,7 @@ import { MoveCategory } from "#enums/move-category"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; +import { MovePriorityModifier } from "#enums/move-priority-modifier"; import { MoveResult } from "#enums/move-result"; import { MoveTarget } from "#enums/move-target"; import { MoveUseMode } from "#enums/move-use-mode"; @@ -4142,6 +4143,25 @@ export class ChangeMovePriorityAbAttr extends AbAttr { } } +export class ChangeMovePriorityModifierAbAttr extends AbAttr { + private readonly newModifier: MovePriorityModifier; + private readonly moveFunc: (pokemon: Pokemon, move: Move) => boolean; + + constructor(moveFunc: (pokemon: Pokemon, move: Move) => boolean, newModifier: MovePriorityModifier) { + super(false); + this.newModifier = newModifier; + this.moveFunc = moveFunc; + } + + override canApply({ pokemon, move }: ChangeMovePriorityAbAttrParams): boolean { + return this.moveFunc(pokemon, move); + } + + override apply({ priority }: ChangeMovePriorityAbAttrParams): void { + priority.value = this.newModifier; + } +} + export class IgnoreContactAbAttr extends AbAttr { private declare readonly _: never; } @@ -6721,6 +6741,7 @@ const AbilityAttrs = Object.freeze({ BlockStatusDamageAbAttr, BlockOneHitKOAbAttr, ChangeMovePriorityAbAttr, + ChangeMovePriorityModifierAbAttr, IgnoreContactAbAttr, PreWeatherEffectAbAttr, PreWeatherDamageAbAttr, @@ -7238,7 +7259,7 @@ export function initAbilities() { .attr(DoubleBattleChanceAbAttr) .build(), new AbBuilder(AbilityId.STALL, 4) - .attr(ChangeMovePriorityAbAttr, (_pokemon, _move: Move) => true, -0.2) + .attr(ChangeMovePriorityModifierAbAttr, (_pokemon, _move: Move) => true, MovePriorityModifier.LAST_IN_BRACKET) .build(), new AbBuilder(AbilityId.TECHNICIAN, 4) .attr(MovePowerBoostAbAttr, (user, target, move) => { @@ -8185,7 +8206,7 @@ export function initAbilities() { .ignorable() .build(), new AbBuilder(AbilityId.MYCELIUM_MIGHT, 9) - .attr(ChangeMovePriorityAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, -0.2) + .attr(ChangeMovePriorityModifierAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, MovePriorityModifier.LAST_IN_BRACKET) .attr(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS) .attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS) .build(), diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index b32ed6bbee4..44240d7e358 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -101,6 +101,7 @@ import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { inSpeedOrder } from "#utils/speed-order-generator"; import { canSpeciesTera, willTerastallize } from "#utils/pokemon-utils"; import type { ReadonlyGenericUint8Array } from "#types/typed-arrays"; +import { MovePriorityModifier } from "#enums/move-priority-modifier"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -1060,17 +1061,19 @@ export abstract class Move implements Localizable { getPriority(user: Pokemon, simulated: boolean = true) { const priority = new NumberHolder(this.priority); - applyMoveAttrs("IncrementMovePriorityAttr", user, null, this, priority); applyAbAttrs("ChangeMovePriorityAbAttr", {pokemon: user, simulated, move: this, priority}); - if (user.getTag(BattlerTagType.BYPASS_SPEED)) { - priority.value += 0.2; - } - return priority.value; } + public getPriorityModifier(user: Pokemon, simulated = true) { + const modifierHolder = new NumberHolder(MovePriorityModifier.NORMAL); + applyAbAttrs("ChangeMovePriorityModifierAbAttr", {pokemon: user, simulated: simulated, move: this, priority: modifierHolder}); + modifierHolder.value = user.getTag(BattlerTagType.BYPASS_SPEED) ? MovePriorityModifier.FIRST_IN_BRACKET : modifierHolder.value; + return modifierHolder.value; + } + /** * Calculate the [Expected Power](https://en.wikipedia.org/wiki/Expected_value) per turn * of this move, taking into account multi hit moves, accuracy, and the number of turns it diff --git a/src/data/terrain.ts b/src/data/terrain.ts index bd90f3985b4..54b5aeffb6a 100644 --- a/src/data/terrain.ts +++ b/src/data/terrain.ts @@ -67,7 +67,7 @@ export class Terrain { return ( !isFieldTargeted(move) && !isSpreadMove(move) - && move.getPriority(user) > 0.2 // fractional priority is used by quick claw etc and is not blocked by terrain + && move.getPriority(user) > 0 && user.getOpponents(true).some(o => targets.includes(o.getBattlerIndex()) && o.isGrounded()) ); } diff --git a/src/enums/move-priority-modifier.ts b/src/enums/move-priority-modifier.ts new file mode 100644 index 00000000000..3e51aaec0a3 --- /dev/null +++ b/src/enums/move-priority-modifier.ts @@ -0,0 +1,13 @@ +import type { ObjectValues } from "#types/type-helpers"; + +/** + * Enum representing modifiers for Move priorities. + */ +export const MovePriorityModifier = Object.freeze({ + /** Used when moves go last in their priority bracket, but before moves of lower priority. */ + LAST_IN_BRACKET: 0, + NORMAL: 1, + /** Used when moves go first in their priority bracket, but before moves of lower priority. */ + FIRST_IN_BRACKET: 2, +}); +export type MovePriorityModifier = ObjectValues; diff --git a/src/queues/move-phase-priority-queue.ts b/src/queues/move-phase-priority-queue.ts index 5f0b20c3c2e..a30162158f3 100644 --- a/src/queues/move-phase-priority-queue.ts +++ b/src/queues/move-phase-priority-queue.ts @@ -92,11 +92,18 @@ export class MovePhasePriorityQueue extends PokemonPhasePriorityQueue }); const timingModifiers = [a, b].map(movePhase => movePhase.timingModifier); + const priorityModifiers = [a, b].map(movePhase => + movePhase.move.getMove().getPriorityModifier(movePhase.pokemon), + ); if (timingModifiers[0] !== timingModifiers[1]) { return timingModifiers[1] - timingModifiers[0]; } + if (priority[0] === priority[1] && priorityModifiers[0] !== priorityModifiers[1]) { + return priorityModifiers[1] - priorityModifiers[0]; + } + return priority[1] - priority[0]; }); } diff --git a/test/arena/psychic-terrain.test.ts b/test/arena/psychic-terrain.test.ts index 6112db41cd8..9e1e5fc9d8e 100644 --- a/test/arena/psychic-terrain.test.ts +++ b/test/arena/psychic-terrain.test.ts @@ -72,7 +72,7 @@ describe("Arena - Psychic Terrain", () => { await game.phaseInterceptor.to("MovePhase", false); const feebas = game.field.getPlayerPokemon(); - expect(allMoves[MoveId.POUND].getPriority(feebas)).toBe(0.2); + expect(allMoves[MoveId.POUND].getPriority(feebas)).toBe(0); await game.toEndOfTurn(); @@ -93,7 +93,7 @@ describe("Arena - Psychic Terrain", () => { await game.phaseInterceptor.to("MovePhase", false); const feebas = game.field.getPlayerPokemon(); - expect(allMoves[MoveId.QUICK_ATTACK].getPriority(feebas)).toBe(1.2); + expect(allMoves[MoveId.QUICK_ATTACK].getPriority(feebas)).toBe(1); await game.toEndOfTurn(); From 9ea5a014a1d0f700fffcd7cc007a436b39d50762 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:14:31 -0700 Subject: [PATCH 03/27] [Bug] Allow fainted Pokemon to be released post-battle in hardcore https://github.com/pagefaultgames/pokerogue/pull/6723 --- src/ui/handlers/party-ui-handler.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/ui/handlers/party-ui-handler.ts b/src/ui/handlers/party-ui-handler.ts index 7806a6111c1..6fbb4052aeb 100644 --- a/src/ui/handlers/party-ui-handler.ts +++ b/src/ui/handlers/party-ui-handler.ts @@ -1586,9 +1586,8 @@ export class PartyUiHandler extends MessageUiHandler { this.updateOptionsWithModifierTransferMode(pokemon); break; case PartyUiMode.SWITCH: - this.options.push(PartyOption.RELEASE); - break; case PartyUiMode.RELEASE: + case PartyUiMode.CHECK: this.options.push(PartyOption.RELEASE); break; } From 1d4a9259b83441c358a6ca591afb282a2e7c88ca Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:14:48 -0700 Subject: [PATCH 04/27] [Hotfix] Fix Queenly Majesty/Dazzling affecting user's moves instead of enemy's (#6722) Fix QM/Dazzling --- src/phases/move-phase.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 6350791e9bb..5115e3da595 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -286,7 +286,7 @@ export class MovePhase extends PokemonPhase { // Apply queenly majesty / dazzling if (!failed) { - const defendingSidePlayField = user.isPlayer() ? globalScene.getPlayerField() : globalScene.getEnemyField(); + const defendingSidePlayField = user.isPlayer() ? globalScene.getEnemyField() : globalScene.getPlayerField(); const cancelled = new BooleanHolder(false); defendingSidePlayField.forEach((pokemon: Pokemon) => { applyAbAttrs("FieldPriorityMoveImmunityAbAttr", { From d7cfb2087cbcea8aeaaa003e8b074bf7dbfb1215 Mon Sep 17 00:00:00 2001 From: Wlowscha <54003515+Wlowscha@users.noreply.github.com> Date: Fri, 31 Oct 2025 04:21:26 +0100 Subject: [PATCH 05/27] [Bug][UI/UX] Make ribbon option in dex selectable with classic wins https://github.com/pagefaultgames/pokerogue/pull/6720 --- src/ui/handlers/pokedex-page-ui-handler.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/ui/handlers/pokedex-page-ui-handler.ts b/src/ui/handlers/pokedex-page-ui-handler.ts index fa10c88952c..c3be9f87d21 100644 --- a/src/ui/handlers/pokedex-page-ui-handler.ts +++ b/src/ui/handlers/pokedex-page-ui-handler.ts @@ -776,7 +776,8 @@ export class PokedexPageUiHandler extends MessageUiHandler { || (this.tmMoves.length === 0 && o === MenuOptions.TM_MOVES) || (!globalScene.gameData.dexData[this.species.speciesId].ribbons.getRibbons() && o === MenuOptions.RIBBONS - && !globalScene.showMissingRibbons); + && !globalScene.showMissingRibbons + && !globalScene.gameData.starterData[this.species.speciesId]?.classicWinCount); const color = getTextColor(isDark ? TextStyle.SHADOW_TEXT : TextStyle.SETTINGS_VALUE, false); const shadow = getTextColor(isDark ? TextStyle.SHADOW_TEXT : TextStyle.SETTINGS_VALUE, true); return `[shadow=${shadow}][color=${color}]${label}[/color][/shadow]`; @@ -1778,6 +1779,7 @@ export class PokedexPageUiHandler extends MessageUiHandler { } else if ( !globalScene.gameData.dexData[this.species.speciesId].ribbons.getRibbons() && !globalScene.showMissingRibbons + && !globalScene.gameData.starterData[this.species.speciesId]?.classicWinCount ) { ui.showText(i18next.t("pokedexUiHandler:noRibbons")); error = true; From 893333ff3bb0699ac3843dc41169c31e3dc08681 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Thu, 30 Oct 2025 20:24:39 -0700 Subject: [PATCH 06/27] Update version to 1.11.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b23b9b9b75d..102c76830b1 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.11.0", + "version": "1.11.1", "type": "module", "scripts": { "start:prod": "vite --mode production", From df98e506ad4fdc5fac1f70dec875072081e11f21 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Thu, 30 Oct 2025 21:38:49 -0700 Subject: [PATCH 07/27] [Bug] Prevent self speed ties (#6719) * Prevent self speed ties * Remove outdated parameter doc --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- .../post-summon-phase-priority-queue.ts | 2 +- src/utils/speed-order.ts | 44 +++++++++----- test/utils/speed-order.test.ts | 58 +++++++++++++++++++ 3 files changed, 89 insertions(+), 15 deletions(-) create mode 100644 test/utils/speed-order.test.ts diff --git a/src/queues/post-summon-phase-priority-queue.ts b/src/queues/post-summon-phase-priority-queue.ts index 28e727de01b..fe08713e501 100644 --- a/src/queues/post-summon-phase-priority-queue.ts +++ b/src/queues/post-summon-phase-priority-queue.ts @@ -11,7 +11,7 @@ import { sortInSpeedOrder } from "#app/utils/speed-order"; */ export class PostSummonPhasePriorityQueue extends PokemonPhasePriorityQueue { protected override reorder(): void { - this.queue = sortInSpeedOrder(this.queue, false); + this.queue = sortInSpeedOrder(this.queue); this.queue.sort((phaseA, phaseB) => phaseB.getPriority() - phaseA.getPriority()); } diff --git a/src/utils/speed-order.ts b/src/utils/speed-order.ts index f2733a74998..aaf3a4526c7 100644 --- a/src/utils/speed-order.ts +++ b/src/utils/speed-order.ts @@ -12,15 +12,13 @@ interface hasPokemon { /** * Sorts an array of {@linkcode Pokemon} by speed, taking Trick Room into account. * @param pokemonList - The list of Pokemon or objects containing Pokemon - * @param shuffleFirst - Whether to shuffle the list before sorting (to handle speed ties). Default `true`. * @returns The sorted array of {@linkcode Pokemon} */ -export function sortInSpeedOrder(pokemonList: T[], shuffleFirst = true): T[] { - if (shuffleFirst) { - shufflePokemonList(pokemonList); - } - sortBySpeed(pokemonList); - return pokemonList; +export function sortInSpeedOrder(pokemonList: T[]): T[] { + const grouped = groupPokemon(pokemonList); + shufflePokemonList(grouped); + sortBySpeed(grouped); + return grouped.flat(); } /** @@ -28,7 +26,7 @@ export function sortInSpeedOrder(pokemonList: T[ * @param pokemonList - The array of Pokemon or objects containing Pokemon * @returns The same array instance that was passed in, shuffled. */ -function shufflePokemonList(pokemonList: T[]): T[] { +function shufflePokemonList(pokemonList: T[][]): void { // This is seeded with the current turn to prevent an inconsistency where it // was varying based on how long since you last reloaded globalScene.executeWithSeedOffset( @@ -36,7 +34,6 @@ function shufflePokemonList(pokemonList: T[]): T globalScene.currentBattle.turn * 1000 + pokemonList.length, globalScene.waveSeed, ); - return pokemonList; } /** Type guard for {@linkcode sortBySpeed} to avoid importing {@linkcode Pokemon} */ @@ -44,11 +41,15 @@ function isPokemon(p: Pokemon | hasPokemon): p is Pokemon { return typeof (p as hasPokemon).getPokemon !== "function"; } +function getPokemon(p: Pokemon | hasPokemon): Pokemon { + return isPokemon(p) ? p : p.getPokemon(); +} + /** Sorts an array of {@linkcode Pokemon} by speed (without shuffling) */ -function sortBySpeed(pokemonList: T[]): void { - pokemonList.sort((a, b) => { - const aSpeed = (isPokemon(a) ? a : a.getPokemon()).getEffectiveStat(Stat.SPD); - const bSpeed = (isPokemon(b) ? b : b.getPokemon()).getEffectiveStat(Stat.SPD); +function sortBySpeed(groupedPokemonList: T[][]): void { + groupedPokemonList.sort((a, b) => { + const aSpeed = getPokemon(a[0]).getEffectiveStat(Stat.SPD); + const bSpeed = getPokemon(b[0]).getEffectiveStat(Stat.SPD); return bSpeed - aSpeed; }); @@ -57,6 +58,21 @@ function sortBySpeed(pokemonList: T[]): void { const speedReversed = new BooleanHolder(false); globalScene.arena.applyTags(ArenaTagType.TRICK_ROOM, speedReversed); if (speedReversed.value) { - pokemonList.reverse(); + groupedPokemonList.reverse(); } } + +function groupPokemon(pokemonList: T[]): T[][] { + const runs: T[][] = []; + for (const pkmn of pokemonList) { + const pokemon = getPokemon(pkmn); + const lastGroup = runs.at(-1); + if (lastGroup != null && lastGroup.length > 0 && getPokemon(lastGroup[0]) === pokemon) { + lastGroup.push(pkmn); + } else { + runs.push([pkmn]); + } + } + + return runs; +} diff --git a/test/utils/speed-order.test.ts b/test/utils/speed-order.test.ts new file mode 100644 index 00000000000..e408c5823c4 --- /dev/null +++ b/test/utils/speed-order.test.ts @@ -0,0 +1,58 @@ +import { AbilityId } from "#enums/ability-id"; +import { MoveId } from "#enums/move-id"; +import { SpeciesId } from "#enums/species-id"; +import { Stat } from "#enums/stat"; +import { GameManager } from "#test/test-utils/game-manager"; +import { sortInSpeedOrder } from "#utils/speed-order"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Utils - Speed Order", () => { + 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 + .battleStyle("single") + .startingLevel(100) + .enemyLevel(100) + .enemyMoveset(MoveId.SPLASH) + .enemyAbility(AbilityId.BALL_FETCH) + .ability(AbilityId.BALL_FETCH) + .enemySpecies(SpeciesId.REGIELEKI); + }); + + it("Sorts correctly in the basic case", async () => { + await game.classicMode.startBattle([SpeciesId.SLOWPOKE, SpeciesId.MEW]); + const [slowpoke, mew] = game.field.getPlayerParty(); + const regieleki = game.field.getEnemyPokemon(); + const pkmnList = [slowpoke, regieleki, mew]; + + expect(sortInSpeedOrder(pkmnList)).toEqual([regieleki, mew, slowpoke]); + }); + + it("Correctly sorts grouped pokemon", async () => { + await game.classicMode.startBattle([SpeciesId.SLOWPOKE, SpeciesId.MEW, SpeciesId.DITTO]); + const [slowpoke, mew, ditto] = game.field.getPlayerParty(); + const regieleki = game.field.getEnemyPokemon(); + ditto.stats[Stat.SPD] = slowpoke.getStat(Stat.SPD); + + const pkmnList = [slowpoke, slowpoke, ditto, ditto, mew, regieleki, regieleki]; + const sorted = sortInSpeedOrder(pkmnList); + + expect([ + [regieleki, regieleki, mew, slowpoke, slowpoke, ditto, ditto], + [regieleki, regieleki, mew, ditto, ditto, slowpoke, slowpoke], + ]).toContainEqual(sorted); + }); +}); From 76453a31d1ee6bd42f4ea37b93a88e7a008e5240 Mon Sep 17 00:00:00 2001 From: damocleas Date: Fri, 31 Oct 2025 00:47:07 -0400 Subject: [PATCH 08/27] [Balance][Bug] Rival Fight 3 Fix, Move Noibat in Biomes Electivire corrected to be Electabuzz in Slot 3 on Fight 3 (55) Noibat has been moved from Uncommon -> Rare in Grassy Field --- src/data/balance/biomes.ts | 8 ++++---- src/data/balance/init-biomes.ts | 2 +- src/data/trainers/rival-party-config.ts | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/data/balance/biomes.ts b/src/data/balance/biomes.ts index 562c07396e0..f81f76058ec 100644 --- a/src/data/balance/biomes.ts +++ b/src/data/balance/biomes.ts @@ -163,15 +163,15 @@ export const biomePokemonPools: BiomePokemonPools = { [BiomePoolTier.UNCOMMON]: { [TimeOfDay.DAWN]: [ SpeciesId.SUNKERN, SpeciesId.COMBEE ], [TimeOfDay.DAY]: [ SpeciesId.SUNKERN, SpeciesId.COMBEE ], - [TimeOfDay.DUSK]: [ SpeciesId.SEEDOT, SpeciesId.NOIBAT ], - [TimeOfDay.NIGHT]: [ SpeciesId.SEEDOT, SpeciesId.NOIBAT ], + [TimeOfDay.DUSK]: [ SpeciesId.SEEDOT ], + [TimeOfDay.NIGHT]: [ SpeciesId.SEEDOT ], [TimeOfDay.ALL]: [ SpeciesId.MILTANK, SpeciesId.CHERUBI, SpeciesId.FOONGUS, ] }, [BiomePoolTier.RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], - [TimeOfDay.DUSK]: [], - [TimeOfDay.NIGHT]: [], + [TimeOfDay.DUSK]: [ SpeciesId.NOIBAT ], + [TimeOfDay.NIGHT]: [ SpeciesId.NOIBAT ], [TimeOfDay.ALL]: [ SpeciesId.BULBASAUR, SpeciesId.GROWLITHE, SpeciesId.TURTWIG, SpeciesId.BONSLY ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] }, diff --git a/src/data/balance/init-biomes.ts b/src/data/balance/init-biomes.ts index 1b410e637e8..eb2d4151fbf 100644 --- a/src/data/balance/init-biomes.ts +++ b/src/data/balance/init-biomes.ts @@ -3547,7 +3547,7 @@ export function initBiomes() { ], [SpeciesId.NOIBAT, PokemonType.FLYING, PokemonType.DRAGON, [ [BiomeId.CAVE, BiomePoolTier.UNCOMMON], - [BiomeId.GRASS, BiomePoolTier.UNCOMMON, [TimeOfDay.DUSK, TimeOfDay.NIGHT]] + [BiomeId.GRASS, BiomePoolTier.RARE, [TimeOfDay.DUSK, TimeOfDay.NIGHT]] ] ], [SpeciesId.NOIVERN, PokemonType.FLYING, PokemonType.DRAGON, [ diff --git a/src/data/trainers/rival-party-config.ts b/src/data/trainers/rival-party-config.ts index fa040ff66de..67b3f50d379 100644 --- a/src/data/trainers/rival-party-config.ts +++ b/src/data/trainers/rival-party-config.ts @@ -298,7 +298,7 @@ const SLOT_3_FIGHT_3 = [ SpeciesId.RHYDON, SpeciesId.TANGROWTH, SpeciesId.PORYGON2, - SpeciesId.ELECTIVIRE, + SpeciesId.ELECTABUZZ, SpeciesId.MAGMAR, SpeciesId.AZUMARILL, SpeciesId.URSARING, From babeb32edb9a1e6906d04a2d1e2bd19151d737a8 Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Thu, 30 Oct 2025 21:49:37 -0700 Subject: [PATCH 09/27] [Bug] Shinies won't be forced to match event boss shiny if set (#6724) --- src/field/pokemon.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index be85e2ebe3a..6ce5c1ffc2c 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -6388,12 +6388,14 @@ export class EnemyPokemon extends Pokemon { } const eventBossVariant = getDailyEventSeedBossVariant(globalScene.seed); - if (eventBossVariant != null && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex)) { + const eventBossVariantEnabled = + eventBossVariant != null && globalScene.gameMode.isWaveFinal(globalScene.currentBattle.waveIndex); + if (eventBossVariantEnabled) { this.shiny = true; } if (this.shiny) { - this.variant = eventBossVariant ?? this.generateShinyVariant(); + this.variant = eventBossVariantEnabled ? eventBossVariant : this.generateShinyVariant(); if (Overrides.ENEMY_VARIANT_OVERRIDE !== null) { this.variant = Overrides.ENEMY_VARIANT_OVERRIDE; } From 2d955165f9ef63b8db02db0173280170ae925cee Mon Sep 17 00:00:00 2001 From: NightKev <34855794+DayKev@users.noreply.github.com> Date: Thu, 30 Oct 2025 22:03:31 -0700 Subject: [PATCH 10/27] Update version to 1.11.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 102c76830b1..e08e5a393a4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.11.1", + "version": "1.11.2", "type": "module", "scripts": { "start:prod": "vite --mode production", From 2a1e0c4373b8e14f98badb297a67ccd4cebab133 Mon Sep 17 00:00:00 2001 From: fabske0 <192151969+fabske0@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:12:51 +0100 Subject: [PATCH 11/27] - Add option for shiny starter - change how the starter part of the seed is parsed --- src/data/daily-run.ts | 70 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 66 insertions(+), 4 deletions(-) diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index ea2852976e8..c3aec54c4e4 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -58,11 +58,19 @@ export function getDailyRunStarters(seed: string): StarterTuple { } // TODO: Refactor this unmaintainable mess -function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLevel: number): Starter { +function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLevel: number, variant?: Variant): Starter { const starterSpecies = starterSpeciesForm instanceof PokemonSpecies ? starterSpeciesForm : getPokemonSpecies(starterSpeciesForm.speciesId); const formIndex = starterSpeciesForm instanceof PokemonSpecies ? undefined : starterSpeciesForm.formIndex; - const pokemon = globalScene.addPlayerPokemon(starterSpecies, startingLevel, undefined, formIndex); + const pokemon = globalScene.addPlayerPokemon( + starterSpecies, + startingLevel, + undefined, + formIndex, + undefined, + variant !== undefined ? true : undefined, + variant, + ); const starter: Starter = { speciesId: starterSpecies.speciesId, shiny: pokemon.shiny, @@ -215,14 +223,68 @@ function setDailyRunEventStarterMovesets(seed: string, starters: StarterTuple): } } +/** + * Expects the seed to contain `starters` followed by 3 `s{\d{4}}` for the starters. The 4 digits are the species ID. \ + * Each starter can optionally be followed by `f{\d{2}}` for the form index and `v{\d{2}}` for the variant. \ + * The order of `f` and `v` does not matter. + * @example `/starterss0003f01s0025v01s0150f02v02` + * @param seed - The daily run seed + * @returns An array of {@linkcode Starter}s, or `null` if no valid match. + */ +// TODO: Rework this setup into JSON or similar - this is quite hard to maintain +function getDailyEventSeedStarters(seed: string): StarterTuple | null { + const speciesCongigurations = + /starters(?s\d{4})(?:(?f\d{2})(?v\d{2})?|(?v\d{2})(?f\d{2})?)?(?s\d{4})(?:(?f\d{2})(?v\d{2})?|(?v\d{2})(?f\d{2})?)?(?s\d{4})(?:(?f\d{2})(?v\d{2})?|(?v\d{2})(?f\d{2})?)?/.exec( + seed, + )?.groups; + + if (!speciesCongigurations) { + const legacyStarters = getDailyEventSeedStartersLegay(seed); + if (legacyStarters != null) { + console.log("Using lecacy starter parsing for daily run seed."); + return legacyStarters; + } + console.error("Invalid starters used for custom daily run seed!"); + return null; + } + console.log(speciesCongigurations); + + const speciesIds = getEnumValues(SpeciesId); + + const starters: Starter[] = []; + for (let i = 0; i < 3; i++) { + const speciesId = Number.parseInt(speciesCongigurations[`species${i + 1}`].slice(1)) as SpeciesId; + const formIndex = Number.parseInt(speciesCongigurations[`form${i + 1}`]?.slice(1) ?? "00"); + let variant: Variant | undefined = Number.parseInt(speciesCongigurations[`variant${i + 1}`]?.slice(1)) as Variant; + + if (!speciesIds.includes(speciesId)) { + console.error("Invalid species ID used for custom daily run seed starter:", speciesId); + return null; + } + + const starterSpecies = getPokemonSpecies(speciesId); + if (Number.isNaN(variant) || variant > 2 || (!starterSpecies.hasVariants() && variant !== 0)) { + console.error("Invalid variant used for custom daily run seed starter:", variant); + variant = undefined; + } + + const starterForm = getPokemonSpeciesForm(speciesId, formIndex); + const startingLevel = globalScene.gameMode.getStartingLevel(); + const starter = getDailyRunStarter(starterForm, startingLevel, variant); + starters.push(starter); + } + + return starters as StarterTuple; +} + /** * Expects the seed to contain `/starters\d{18}/` * where the digits alternate between 4 digits for the species ID and 2 digits for the form index * (left padded with `0`s as necessary). * @returns An array of {@linkcode Starter}s, or `null` if no valid match. */ -// TODO: Rework this setup into JSON or similar - this is quite hard to maintain -export function getDailyEventSeedStarters(seed: string): StarterTuple | null { +// TODO: Can be removed after october 31st 2025 +function getDailyEventSeedStartersLegay(seed: string): StarterTuple | null { if (!isDailyEventSeed(seed)) { return null; } From 8e01876bd0ddf19e3ec3103553b991953a3ee6d7 Mon Sep 17 00:00:00 2001 From: fabske0 <192151969+fabske0@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:16:33 +0100 Subject: [PATCH 12/27] fix typo --- src/data/daily-run.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index c3aec54c4e4..c79bc9a3efd 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -233,12 +233,12 @@ function setDailyRunEventStarterMovesets(seed: string, starters: StarterTuple): */ // TODO: Rework this setup into JSON or similar - this is quite hard to maintain function getDailyEventSeedStarters(seed: string): StarterTuple | null { - const speciesCongigurations = + const speciesConfigurations = /starters(?s\d{4})(?:(?f\d{2})(?v\d{2})?|(?v\d{2})(?f\d{2})?)?(?s\d{4})(?:(?f\d{2})(?v\d{2})?|(?v\d{2})(?f\d{2})?)?(?s\d{4})(?:(?f\d{2})(?v\d{2})?|(?v\d{2})(?f\d{2})?)?/.exec( seed, )?.groups; - if (!speciesCongigurations) { + if (!speciesConfigurations) { const legacyStarters = getDailyEventSeedStartersLegay(seed); if (legacyStarters != null) { console.log("Using lecacy starter parsing for daily run seed."); @@ -247,15 +247,15 @@ function getDailyEventSeedStarters(seed: string): StarterTuple | null { console.error("Invalid starters used for custom daily run seed!"); return null; } - console.log(speciesCongigurations); + console.log(speciesConfigurations); const speciesIds = getEnumValues(SpeciesId); const starters: Starter[] = []; for (let i = 0; i < 3; i++) { - const speciesId = Number.parseInt(speciesCongigurations[`species${i + 1}`].slice(1)) as SpeciesId; - const formIndex = Number.parseInt(speciesCongigurations[`form${i + 1}`]?.slice(1) ?? "00"); - let variant: Variant | undefined = Number.parseInt(speciesCongigurations[`variant${i + 1}`]?.slice(1)) as Variant; + const speciesId = Number.parseInt(speciesConfigurations[`species${i + 1}`].slice(1)) as SpeciesId; + const formIndex = Number.parseInt(speciesConfigurations[`form${i + 1}`]?.slice(1) ?? "00"); + let variant: Variant | undefined = Number.parseInt(speciesConfigurations[`variant${i + 1}`]?.slice(1)) as Variant; if (!speciesIds.includes(speciesId)) { console.error("Invalid species ID used for custom daily run seed starter:", speciesId); From 6049038537de3350448090e84e4934ad15291baa Mon Sep 17 00:00:00 2001 From: fabske0 <192151969+fabske0@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:17:14 +0100 Subject: [PATCH 13/27] remove console.log and another typo --- src/data/daily-run.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index c79bc9a3efd..8cd9ff8a115 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -241,13 +241,12 @@ function getDailyEventSeedStarters(seed: string): StarterTuple | null { if (!speciesConfigurations) { const legacyStarters = getDailyEventSeedStartersLegay(seed); if (legacyStarters != null) { - console.log("Using lecacy starter parsing for daily run seed."); + console.log("Using legacy starter parsing for daily run seed."); return legacyStarters; } console.error("Invalid starters used for custom daily run seed!"); return null; } - console.log(speciesConfigurations); const speciesIds = getEnumValues(SpeciesId); From b150b5208c1544beb343a51f7c59b8736a1050c2 Mon Sep 17 00:00:00 2001 From: fabske0 <192151969+fabske0@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:48:01 +0100 Subject: [PATCH 14/27] split up regex --- src/data/daily-run.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index 8cd9ff8a115..a5635bf464e 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -233,10 +233,12 @@ function setDailyRunEventStarterMovesets(seed: string, starters: StarterTuple): */ // TODO: Rework this setup into JSON or similar - this is quite hard to maintain function getDailyEventSeedStarters(seed: string): StarterTuple | null { - const speciesConfigurations = - /starters(?s\d{4})(?:(?f\d{2})(?v\d{2})?|(?v\d{2})(?f\d{2})?)?(?s\d{4})(?:(?f\d{2})(?v\d{2})?|(?v\d{2})(?f\d{2})?)?(?s\d{4})(?:(?f\d{2})(?v\d{2})?|(?v\d{2})(?f\d{2})?)?/.exec( - seed, - )?.groups; + const speciesRegex = i => + `(?s\\d{4})(?:(?f\\d{2})(?v\\d{2})?|(?v\\d{2})(?f\\d{2})?)?`; + + const matcher = new RegExp(`starters${speciesRegex(1)}${speciesRegex(2)}${speciesRegex(3)}`); + + const speciesConfigurations = matcher.exec(seed)?.groups; if (!speciesConfigurations) { const legacyStarters = getDailyEventSeedStartersLegay(seed); From ac79e82013160f00c9045f06ae5357a96c35ebf3 Mon Sep 17 00:00:00 2001 From: fabske0 <192151969+fabske0@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:11:10 +0100 Subject: [PATCH 15/27] another typo --- src/data/daily-run.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index a5635bf464e..c52bb164f4b 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -241,7 +241,7 @@ function getDailyEventSeedStarters(seed: string): StarterTuple | null { const speciesConfigurations = matcher.exec(seed)?.groups; if (!speciesConfigurations) { - const legacyStarters = getDailyEventSeedStartersLegay(seed); + const legacyStarters = getDailyEventSeedStartersLegacy(seed); if (legacyStarters != null) { console.log("Using legacy starter parsing for daily run seed."); return legacyStarters; @@ -285,7 +285,7 @@ function getDailyEventSeedStarters(seed: string): StarterTuple | null { * @returns An array of {@linkcode Starter}s, or `null` if no valid match. */ // TODO: Can be removed after october 31st 2025 -function getDailyEventSeedStartersLegay(seed: string): StarterTuple | null { +function getDailyEventSeedStartersLegacy(seed: string): StarterTuple | null { if (!isDailyEventSeed(seed)) { return null; } From 6bda024b7eb07bd2207502ed1895e19ffa81e989 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:10:11 -0700 Subject: [PATCH 16/27] Only run speed bypass code for fight commands --- src/phases/turn-start-phase.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index 1f1b78af0ad..c9774c5873a 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -69,6 +69,10 @@ export class TurnStartPhase extends FieldPhase { const phaseManager = globalScene.phaseManager; for (const pokemon of inSpeedOrder(ArenaTagSide.BOTH)) { + if (globalScene.currentBattle.turnCommands[pokemon.getBattlerIndex()]?.command !== Command.FIGHT) { + continue; + } + applyAbAttrs("BypassSpeedChanceAbAttr", { pokemon }); globalScene.applyModifiers(BypassSpeedChanceModifier, pokemon.isPlayer(), pokemon); } From e78796077b19e1c11538eb337a8f164ba9401096 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Fri, 31 Oct 2025 13:22:40 -0700 Subject: [PATCH 17/27] Disable Illusion --- src/data/abilities/ability.ts | 23 +++++------ test/abilities/illusion.test.ts | 69 +++++++++++++++++---------------- 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 301016d899b..fd1eb8ea5ef 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -7440,17 +7440,18 @@ export function initAbilities() { 1.3) .build(), new AbBuilder(AbilityId.ILLUSION, 5) - // The Pokemon generate an illusion if it's available - .attr(IllusionPreSummonAbAttr, false) - .attr(IllusionBreakAbAttr) - // The Pokemon loses its illusion when damaged by a move - .attr(PostDefendIllusionBreakAbAttr, true) - // Disable Illusion in fusions - .attr(NoFusionAbilityAbAttr) - // Illusion is available again after a battle - .conditionalAttr((pokemon) => pokemon.isAllowedInBattle(), IllusionPostBattleAbAttr, false) - .uncopiable() - .bypassFaint() + // // The Pokemon generate an illusion if it's available + // .attr(IllusionPreSummonAbAttr, false) + // .attr(IllusionBreakAbAttr) + // // The Pokemon loses its illusion when damaged by a move + // .attr(PostDefendIllusionBreakAbAttr, true) + // // Disable Illusion in fusions + // .attr(NoFusionAbilityAbAttr) + // // Illusion is available again after a battle + // .conditionalAttr((pokemon) => pokemon.isAllowedInBattle(), IllusionPostBattleAbAttr, false) + // .uncopiable() + // .bypassFaint() + .unimplemented() // TODO reimplement Illusion properly .build(), new AbBuilder(AbilityId.IMPOSTER, 5) .attr(PostSummonTransformAbAttr) diff --git a/test/abilities/illusion.test.ts b/test/abilities/illusion.test.ts index 2343a11cb74..1264b458234 100644 --- a/test/abilities/illusion.test.ts +++ b/test/abilities/illusion.test.ts @@ -33,7 +33,7 @@ describe("Abilities - Illusion", () => { .startingHeldItems([{ name: "WIDE_LENS", count: 3 }]); }); - it("creates illusion at the start", async () => { + it.todo("creates illusion at the start", async () => { await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]); const zoroark = game.field.getPlayerPokemon(); const zorua = game.field.getEnemyPokemon(); @@ -42,7 +42,7 @@ describe("Abilities - Illusion", () => { expect(!!zorua.summonData.illusion).equals(true); }); - it("break after receiving damaging move", async () => { + it.todo("break after receiving damaging move", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.TACKLE); @@ -54,7 +54,7 @@ describe("Abilities - Illusion", () => { expect(zorua.name).equals("Zorua"); }); - it("break after getting ability changed", async () => { + it.todo("break after getting ability changed", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.WORRY_SEED); @@ -65,7 +65,7 @@ describe("Abilities - Illusion", () => { expect(!!zorua.summonData.illusion).equals(false); }); - it("breaks with neutralizing gas", async () => { + it.todo("breaks with neutralizing gas", async () => { game.override.enemyAbility(AbilityId.NEUTRALIZING_GAS); await game.classicMode.startBattle([SpeciesId.KOFFING]); @@ -74,7 +74,7 @@ describe("Abilities - Illusion", () => { expect(!!zorua.summonData.illusion).equals(false); }); - it("does not activate if neutralizing gas is active", async () => { + it.todo("does not activate if neutralizing gas is active", async () => { game.override .enemyAbility(AbilityId.NEUTRALIZING_GAS) .ability(AbilityId.ILLUSION) @@ -88,35 +88,38 @@ describe("Abilities - Illusion", () => { expect(game.field.getPlayerPokemon().summonData.illusion).toBeFalsy(); }); - it("causes enemy AI to consider the illusion's type instead of the actual type when considering move effectiveness", async () => { - game.override.enemyMoveset([MoveId.FLAMETHROWER, MoveId.PSYCHIC, MoveId.TACKLE]); - await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]); + it.todo( + "causes enemy AI to consider the illusion's type instead of the actual type when considering move effectiveness", + async () => { + game.override.enemyMoveset([MoveId.FLAMETHROWER, MoveId.PSYCHIC, MoveId.TACKLE]); + await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]); - const enemy = game.field.getEnemyPokemon(); - const zoroark = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); + const zoroark = game.field.getPlayerPokemon(); - const flameThrower = enemy.getMoveset()[0]!.getMove(); - const psychic = enemy.getMoveset()[1]!.getMove(); - const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness( - flameThrower.type, - enemy, - undefined, - undefined, - flameThrower, - true, - ); - const psychicEffectiveness = zoroark.getAttackTypeEffectiveness( - psychic.type, - enemy, - undefined, - undefined, - psychic, - true, - ); - expect(psychicEffectiveness).above(flameThrowerEffectiveness); - }); + const flameThrower = enemy.getMoveset()[0]!.getMove(); + const psychic = enemy.getMoveset()[1]!.getMove(); + const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness( + flameThrower.type, + enemy, + undefined, + undefined, + flameThrower, + true, + ); + const psychicEffectiveness = zoroark.getAttackTypeEffectiveness( + psychic.type, + enemy, + undefined, + undefined, + psychic, + true, + ); + expect(psychicEffectiveness).above(flameThrowerEffectiveness); + }, + ); - it("should not break from indirect damage from status, weather or recoil", async () => { + it.todo("should not break from indirect damage from status, weather or recoil", async () => { game.override.enemySpecies(SpeciesId.GIGALITH).enemyAbility(AbilityId.SAND_STREAM); await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.AZUMARILL]); @@ -129,7 +132,7 @@ describe("Abilities - Illusion", () => { expect(!!zoroark.summonData.illusion).equals(true); }); - it("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => { + it.todo("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => { game.override.enemyMoveset(MoveId.SPLASH); await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.ZOROARK, SpeciesId.AXEW]); @@ -152,7 +155,7 @@ describe("Abilities - Illusion", () => { expect(zoroark.getPokeball(true)).equals(PokeballType.GREAT_BALL); }); - it("breaks when suppressed", async () => { + it.todo("breaks when suppressed", async () => { game.override.moveset(MoveId.GASTRO_ACID); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const zorua = game.field.getEnemyPokemon(); From bfbd71ddde8d51413981a626708370f35c9048d2 Mon Sep 17 00:00:00 2001 From: Lugiad Date: Fri, 31 Oct 2025 22:48:30 +0100 Subject: [PATCH 18/27] [UI/UX] [Localization] Texts position adjustments for Turkish (#6731) * Adjustments for Turkish * Remove now-unnecessary comment Co-authored-by: Fabi <192151969+fabske0@users.noreply.github.com> --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> Co-authored-by: Fabi <192151969+fabske0@users.noreply.github.com> --- src/ui/handlers/egg-gacha-ui-handler.ts | 11 +++++------ src/ui/handlers/game-stats-ui-handler.ts | 2 +- src/ui/handlers/starter-select-ui-handler.ts | 1 + 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/ui/handlers/egg-gacha-ui-handler.ts b/src/ui/handlers/egg-gacha-ui-handler.ts index c90d4a12139..fcb4cb09538 100644 --- a/src/ui/handlers/egg-gacha-ui-handler.ts +++ b/src/ui/handlers/egg-gacha-ui-handler.ts @@ -81,7 +81,7 @@ export class EggGachaUiHandler extends MessageUiHandler { let pokemonIconX = -20; let pokemonIconY = 6; - if (["de", "es-ES", "es-419", "fr", "ko", "pt-BR", "ja", "ru"].includes(currentLanguage)) { + if (["de", "es-ES", "es-419", "fr", "ko", "pt-BR", "ja", "ru", "tr"].includes(currentLanguage)) { gachaTextStyle = TextStyle.SMALLER_WINDOW_ALT; gachaX = 2; gachaY = 2; @@ -89,7 +89,7 @@ export class EggGachaUiHandler extends MessageUiHandler { let legendaryLabelX = gachaX; let legendaryLabelY = gachaY; - if (["de", "es-ES", "es-419"].includes(currentLanguage)) { + if (["de", "es-ES", "es-419", "tr"].includes(currentLanguage)) { pokemonIconX = -25; pokemonIconY = 10; legendaryLabelX = -6; @@ -108,8 +108,7 @@ export class EggGachaUiHandler extends MessageUiHandler { let xOffset = 0; const pokemonIcon = globalScene.add.sprite(pokemonIconX, pokemonIconY, "pokemon_icons_0"); - // Intentionally left as "array includes" instead of an equality check to allow for future languages to reuse - if (["pt-BR"].includes(currentLanguage)) { + if (["pt-BR", "tr"].includes(currentLanguage)) { xOffset = 2; pokemonIcon.setX(pokemonIconX - 2); } @@ -120,14 +119,14 @@ export class EggGachaUiHandler extends MessageUiHandler { } break; case GachaType.MOVE: - if (["de", "es-ES", "fr", "pt-BR", "ru"].includes(currentLanguage)) { + if (["de", "es-ES", "fr", "pt-BR", "ru", "tr"].includes(currentLanguage)) { gachaUpLabel.setAlign("center").setY(0); } gachaUpLabel.setText(i18next.t("egg:moveUpGacha")).setX(0).setOrigin(0.5, 0); break; case GachaType.SHINY: - if (["de", "fr", "ko", "ru"].includes(currentLanguage)) { + if (["de", "fr", "ko", "ru", "tr"].includes(currentLanguage)) { gachaUpLabel.setAlign("center").setY(0); } diff --git a/src/ui/handlers/game-stats-ui-handler.ts b/src/ui/handlers/game-stats-ui-handler.ts index 53b23781584..30243008626 100644 --- a/src/ui/handlers/game-stats-ui-handler.ts +++ b/src/ui/handlers/game-stats-ui-handler.ts @@ -251,7 +251,7 @@ export class GameStatsUiHandler extends UiHandler { const resolvedLang = i18next.resolvedLanguage ?? "en"; // NOTE TO TRANSLATION TEAM: Add more languages that want to display // in a single-column inside of the `[]` (e.g. `["ru", "fr"]`) - return ["fr", "es-ES", "es-419", "it", "ja", "pt-BR", "ru"].includes(resolvedLang); + return ["fr", "es-ES", "es-419", "it", "ja", "pt-BR", "ru", "tr"].includes(resolvedLang); } /** The number of columns used by this menu in the resolved language */ private get columnCount(): 1 | 2 { diff --git a/src/ui/handlers/starter-select-ui-handler.ts b/src/ui/handlers/starter-select-ui-handler.ts index 2623016eb6b..c527b40dbff 100644 --- a/src/ui/handlers/starter-select-ui-handler.ts +++ b/src/ui/handlers/starter-select-ui-handler.ts @@ -155,6 +155,7 @@ const languageSettings: { [key: string]: LanguageSetting } = { tr: { starterInfoTextSize: "56px", instructionTextSize: "38px", + starterInfoXPos: 34, }, ro: { starterInfoTextSize: "56px", From 525ba57461f2128c1558265c03682cf8e87224f3 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:46:43 -0700 Subject: [PATCH 19/27] Review changes --- src/data/abilities/ability.ts | 2 +- test/abilities/illusion.test.ts | 71 ++++++++++++++++----------------- 2 files changed, 35 insertions(+), 38 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index fd1eb8ea5ef..4bc4a9573ee 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -7449,7 +7449,7 @@ export function initAbilities() { // .attr(NoFusionAbilityAbAttr) // // Illusion is available again after a battle // .conditionalAttr((pokemon) => pokemon.isAllowedInBattle(), IllusionPostBattleAbAttr, false) - // .uncopiable() + .uncopiable() // .bypassFaint() .unimplemented() // TODO reimplement Illusion properly .build(), diff --git a/test/abilities/illusion.test.ts b/test/abilities/illusion.test.ts index 1264b458234..9746ecca6d5 100644 --- a/test/abilities/illusion.test.ts +++ b/test/abilities/illusion.test.ts @@ -7,7 +7,7 @@ import { GameManager } from "#test/test-utils/game-manager"; import Phaser from "phaser"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; -describe("Abilities - Illusion", () => { +describe.todo("Abilities - Illusion", () => { let phaserGame: Phaser.Game; let game: GameManager; @@ -33,7 +33,7 @@ describe("Abilities - Illusion", () => { .startingHeldItems([{ name: "WIDE_LENS", count: 3 }]); }); - it.todo("creates illusion at the start", async () => { + it("creates illusion at the start", async () => { await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]); const zoroark = game.field.getPlayerPokemon(); const zorua = game.field.getEnemyPokemon(); @@ -42,7 +42,7 @@ describe("Abilities - Illusion", () => { expect(!!zorua.summonData.illusion).equals(true); }); - it.todo("break after receiving damaging move", async () => { + it("break after receiving damaging move", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.TACKLE); @@ -54,7 +54,7 @@ describe("Abilities - Illusion", () => { expect(zorua.name).equals("Zorua"); }); - it.todo("break after getting ability changed", async () => { + it("break after getting ability changed", async () => { await game.classicMode.startBattle([SpeciesId.FEEBAS]); game.move.select(MoveId.WORRY_SEED); @@ -65,7 +65,7 @@ describe("Abilities - Illusion", () => { expect(!!zorua.summonData.illusion).equals(false); }); - it.todo("breaks with neutralizing gas", async () => { + it("breaks with neutralizing gas", async () => { game.override.enemyAbility(AbilityId.NEUTRALIZING_GAS); await game.classicMode.startBattle([SpeciesId.KOFFING]); @@ -74,7 +74,7 @@ describe("Abilities - Illusion", () => { expect(!!zorua.summonData.illusion).equals(false); }); - it.todo("does not activate if neutralizing gas is active", async () => { + it("does not activate if neutralizing gas is active", async () => { game.override .enemyAbility(AbilityId.NEUTRALIZING_GAS) .ability(AbilityId.ILLUSION) @@ -88,38 +88,35 @@ describe("Abilities - Illusion", () => { expect(game.field.getPlayerPokemon().summonData.illusion).toBeFalsy(); }); - it.todo( - "causes enemy AI to consider the illusion's type instead of the actual type when considering move effectiveness", - async () => { - game.override.enemyMoveset([MoveId.FLAMETHROWER, MoveId.PSYCHIC, MoveId.TACKLE]); - await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]); + it("causes enemy AI to consider the illusion's type instead of the actual type when considering move effectiveness", async () => { + game.override.enemyMoveset([MoveId.FLAMETHROWER, MoveId.PSYCHIC, MoveId.TACKLE]); + await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.FEEBAS]); - const enemy = game.field.getEnemyPokemon(); - const zoroark = game.field.getPlayerPokemon(); + const enemy = game.field.getEnemyPokemon(); + const zoroark = game.field.getPlayerPokemon(); - const flameThrower = enemy.getMoveset()[0]!.getMove(); - const psychic = enemy.getMoveset()[1]!.getMove(); - const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness( - flameThrower.type, - enemy, - undefined, - undefined, - flameThrower, - true, - ); - const psychicEffectiveness = zoroark.getAttackTypeEffectiveness( - psychic.type, - enemy, - undefined, - undefined, - psychic, - true, - ); - expect(psychicEffectiveness).above(flameThrowerEffectiveness); - }, - ); + const flameThrower = enemy.getMoveset()[0]!.getMove(); + const psychic = enemy.getMoveset()[1]!.getMove(); + const flameThrowerEffectiveness = zoroark.getAttackTypeEffectiveness( + flameThrower.type, + enemy, + undefined, + undefined, + flameThrower, + true, + ); + const psychicEffectiveness = zoroark.getAttackTypeEffectiveness( + psychic.type, + enemy, + undefined, + undefined, + psychic, + true, + ); + expect(psychicEffectiveness).above(flameThrowerEffectiveness); + }); - it.todo("should not break from indirect damage from status, weather or recoil", async () => { + it("should not break from indirect damage from status, weather or recoil", async () => { game.override.enemySpecies(SpeciesId.GIGALITH).enemyAbility(AbilityId.SAND_STREAM); await game.classicMode.startBattle([SpeciesId.ZOROARK, SpeciesId.AZUMARILL]); @@ -132,7 +129,7 @@ describe("Abilities - Illusion", () => { expect(!!zoroark.summonData.illusion).equals(true); }); - it.todo("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => { + it("copies the the name, nickname, gender, shininess, and pokeball from the illusion source", async () => { game.override.enemyMoveset(MoveId.SPLASH); await game.classicMode.startBattle([SpeciesId.ABRA, SpeciesId.ZOROARK, SpeciesId.AXEW]); @@ -155,7 +152,7 @@ describe("Abilities - Illusion", () => { expect(zoroark.getPokeball(true)).equals(PokeballType.GREAT_BALL); }); - it.todo("breaks when suppressed", async () => { + it("breaks when suppressed", async () => { game.override.moveset(MoveId.GASTRO_ACID); await game.classicMode.startBattle([SpeciesId.MAGIKARP]); const zorua = game.field.getEnemyPokemon(); From 5c8c2151a834077f3bb4c8bf4cd6653c056c992f Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Fri, 31 Oct 2025 15:58:35 -0700 Subject: [PATCH 20/27] Update src/data/moves/move.ts Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/moves/move.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 44240d7e358..43baf361d5e 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -1068,9 +1068,11 @@ export abstract class Move implements Localizable { } public getPriorityModifier(user: Pokemon, simulated = true) { + if (user.getTag(BattlerTagType.BYPASS_SPEED)) { + return MovePriorityModifier.FIRST_IN_BRACKET; + } const modifierHolder = new NumberHolder(MovePriorityModifier.NORMAL); - applyAbAttrs("ChangeMovePriorityModifierAbAttr", {pokemon: user, simulated: simulated, move: this, priority: modifierHolder}); - modifierHolder.value = user.getTag(BattlerTagType.BYPASS_SPEED) ? MovePriorityModifier.FIRST_IN_BRACKET : modifierHolder.value; + applyAbAttrs("ChangeMovePriorityModifierAbAttr", { pokemon: user, simulated, move: this, priority: modifierHolder }); return modifierHolder.value; } From fb274077c233a30a3d24679684cb6e04fe319e45 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 31 Oct 2025 19:01:33 -0400 Subject: [PATCH 21/27] Reworked regex to use `matchAll`, added error handling + tests --- src/data/daily-run.ts | 92 +++++++++++++++++++++-------- src/utils/pokemon-utils.ts | 8 +-- test/daily-mode.test.ts | 115 ++++++++++++++++++++++++------------- 3 files changed, 145 insertions(+), 70 deletions(-) diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index c52bb164f4b..9daabc01529 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -68,9 +68,10 @@ function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLeve undefined, formIndex, undefined, - variant !== undefined ? true : undefined, + variant != null, variant, ); + console.log(`%c${pokemon.shiny} ${variant} ${variant != null}`, "color:blue"); const starter: Starter = { speciesId: starterSpecies.speciesId, shiny: pokemon.shiny, @@ -180,7 +181,11 @@ export function isDailyEventSeed(seed: string): boolean { * Must be updated whenever the `MoveId` enum gets a new digit! */ const MOVE_ID_STRING_LENGTH = 4; - +/** + * The regex literal used to parse daily run custom movesets. + * @privateRemarks + * Intentionally does not use the `g` flag to avoid altering `lastIndex` after each match. + */ const MOVE_ID_SEED_REGEX = /(?<=\/moves)((?:\d{4}){0,4})(?:,((?:\d{4}){0,4}))?(?:,((?:\d{4}){0,4}))?/; /** @@ -223,53 +228,90 @@ function setDailyRunEventStarterMovesets(seed: string, starters: StarterTuple): } } +/** The regex literal string used to extract the content of the "starters" block of Daily Run custom seeds. */ +const STARTER_SEED_PREFIX_REGEX = /\/starters(.*?)(?:\/|$)/; /** - * Expects the seed to contain `starters` followed by 3 `s{\d{4}}` for the starters. The 4 digits are the species ID. \ - * Each starter can optionally be followed by `f{\d{2}}` for the form index and `v{\d{2}}` for the variant. \ - * The order of `f` and `v` does not matter. - * @example `/starterss0003f01s0025v01s0150f02v02` + * The regex literal used to parse daily run custom starter information for a single starter. \ + * Contains a 4-digit species ID, as well as an optional 2-digit form index and 1-digit variant. + * + * If either of form index or variant are omitted, the starter will default to its species' base form/ + * not be shiny, respectively. + */ +const STARTER_SEED_MATCH_REGEX = /(?:s(?\d{4}))(?:f(?
\d{2}))?(?:v(?\d))?/g; + +/** + * Parse a custom daily run seed into a set of pre-defined starters. + * @see {@linkcode STARTER_SEED_MATCH_REGEX} * @param seed - The daily run seed - * @returns An array of {@linkcode Starter}s, or `null` if no valid match. + * @returns An array of {@linkcode Starter}s, or `null` if it did not match. */ // TODO: Rework this setup into JSON or similar - this is quite hard to maintain function getDailyEventSeedStarters(seed: string): StarterTuple | null { - const speciesRegex = i => - `(?s\\d{4})(?:(?f\\d{2})(?v\\d{2})?|(?v\\d{2})(?f\\d{2})?)?`; + if (!isDailyEventSeed(seed)) { + return null; + } - const matcher = new RegExp(`starters${speciesRegex(1)}${speciesRegex(2)}${speciesRegex(3)}`); + const seedAfterPrefix = seed.split(STARTER_SEED_PREFIX_REGEX)[1] as string | undefined; + if (!seedAfterPrefix) { + return null; + } - const speciesConfigurations = matcher.exec(seed)?.groups; + const speciesConfigurations = [...seedAfterPrefix.matchAll(STARTER_SEED_MATCH_REGEX)]; - if (!speciesConfigurations) { + if (speciesConfigurations.length !== 3) { + // TODO: Remove legacy fallback code after next hotfix version - this is needed for Oct 31's daily to function const legacyStarters = getDailyEventSeedStartersLegacy(seed); - if (legacyStarters != null) { - console.log("Using legacy starter parsing for daily run seed."); + if (legacyStarters == null) { return legacyStarters; } - console.error("Invalid starters used for custom daily run seed!"); + console.error("Invalid starters used for custom daily run seed!", seed); return null; } const speciesIds = getEnumValues(SpeciesId); - const starters: Starter[] = []; - for (let i = 0; i < 3; i++) { - const speciesId = Number.parseInt(speciesConfigurations[`species${i + 1}`].slice(1)) as SpeciesId; - const formIndex = Number.parseInt(speciesConfigurations[`form${i + 1}`]?.slice(1) ?? "00"); - let variant: Variant | undefined = Number.parseInt(speciesConfigurations[`variant${i + 1}`]?.slice(1)) as Variant; - if (!speciesIds.includes(speciesId)) { - console.error("Invalid species ID used for custom daily run seed starter:", speciesId); + for (const [i, match] of speciesConfigurations.entries()) { + const { groups } = match; + if (!groups) { + console.error("Invalid seed used for custom daily run starter:", match); + return null; + } + + const { species: speciesStr, form: formStr, variant: variantStr } = groups; + + const speciesId = Number.parseInt(speciesStr) as SpeciesId; + + // NB: We check the parsed integer here to exclude SpeciesID.NONE as well as invalid values; + // other fields only check the string to permit 0 as valid inputs + if (!speciesId || !speciesIds.includes(speciesId)) { + console.error("Invalid species ID used for custom daily run starter:", speciesStr); return null; } const starterSpecies = getPokemonSpecies(speciesId); - if (Number.isNaN(variant) || variant > 2 || (!starterSpecies.hasVariants() && variant !== 0)) { - console.error("Invalid variant used for custom daily run seed starter:", variant); + // Omitted form index = use base form + const starterForm = formStr ? starterSpecies.forms[Number.parseInt(formStr)] : starterSpecies; + + if (!starterForm) { + console.log(starterSpecies.name); + console.error("Invalid form index used for custom daily run starter:", formStr); + return null; + } + + // Get and validate variant + let variant = (variantStr ? Number.parseInt(variantStr) : undefined) as Variant | undefined; + if (!isBetween(variant ?? 0, 0, 2)) { + console.error("Variant used for custom daily run seed starter out of bounds:", variantStr); + return null; + } + + // Fall back to default variant if none exists + if (!starterSpecies.hasVariants() && !!variant) { + console.warn("Variant for custom daily run seed starter does not exist, using base variant...", variant); variant = undefined; } - const starterForm = getPokemonSpeciesForm(speciesId, formIndex); const startingLevel = globalScene.gameMode.getStartingLevel(); const starter = getDailyRunStarter(starterForm, startingLevel, variant); starters.push(starter); diff --git a/src/utils/pokemon-utils.ts b/src/utils/pokemon-utils.ts index e3c8d8eab68..f1716487b34 100644 --- a/src/utils/pokemon-utils.ts +++ b/src/utils/pokemon-utils.ts @@ -118,11 +118,9 @@ export function getFusedSpeciesName(speciesAName: string, speciesBName: string): } export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): PokemonSpeciesForm { - const retSpecies: PokemonSpecies = - species >= 2000 - ? allSpecies.find(s => s.speciesId === species)! // TODO: is the bang correct? - : allSpecies[species - 1]; - if (formIndex < retSpecies.forms?.length) { + const retSpecies: PokemonSpecies = getPokemonSpecies(species); + + if (formIndex < retSpecies.forms.length) { return retSpecies.forms[formIndex]; } return retSpecies; diff --git a/test/daily-mode.test.ts b/test/daily-mode.test.ts index e5284906318..6ab61cc0ed0 100644 --- a/test/daily-mode.test.ts +++ b/test/daily-mode.test.ts @@ -21,6 +21,8 @@ describe("Daily Mode", () => { beforeEach(() => { game = new GameManager(phaserGame); + + game.override.disableShinies = false; }); afterEach(() => { @@ -41,52 +43,85 @@ describe("Daily Mode", () => { }); describe("Custom Seeds", () => { - it("should support custom moves", async () => { - vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("/moves0001000200030004,03320006,01300919"); - await game.dailyMode.startBattle(); + describe("Moves", () => { + it("should support custom moves", async () => { + vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("/moves0001000200030004,03320006,01300919"); + await game.dailyMode.startBattle(); - const [moves1, moves2, moves3] = game.scene.getPlayerParty().map(p => p.moveset.map(pm => pm.moveId)); - expect(moves1, stringifyEnumArray(MoveId, moves1)).toEqual([ - MoveId.POUND, - MoveId.KARATE_CHOP, - MoveId.DOUBLE_SLAP, - MoveId.COMET_PUNCH, - ]); - expect(moves2, stringifyEnumArray(MoveId, moves2)).toEqual([ - MoveId.AERIAL_ACE, - MoveId.PAY_DAY, - expect.anything(), // make sure it doesn't replace normal moveset gen - expect.anything(), - ]); - expect(moves3, stringifyEnumArray(MoveId, moves3)).toEqual([ - MoveId.SKULL_BASH, - MoveId.MALIGNANT_CHAIN, - expect.anything(), - expect.anything(), - ]); + const [moves1, moves2, moves3] = game.scene.getPlayerParty().map(p => p.moveset.map(pm => pm.moveId)); + expect(moves1, stringifyEnumArray(MoveId, moves1)).toEqual([ + MoveId.POUND, + MoveId.KARATE_CHOP, + MoveId.DOUBLE_SLAP, + MoveId.COMET_PUNCH, + ]); + expect(moves2, stringifyEnumArray(MoveId, moves2)).toEqual([ + MoveId.AERIAL_ACE, + MoveId.PAY_DAY, + expect.anything(), // make sure it doesn't replace normal moveset gen + expect.anything(), + ]); + expect(moves3, stringifyEnumArray(MoveId, moves3)).toEqual([ + MoveId.SKULL_BASH, + MoveId.MALIGNANT_CHAIN, + expect.anything(), + expect.anything(), + ]); + }); + + it("should allow omitting movesets for some starters", async () => { + vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("/moves0001000200030004"); + await game.dailyMode.startBattle(); + + const [moves1, moves2, moves3] = game.scene.getPlayerParty().map(p => p.moveset.map(pm => pm.moveId)); + expect(moves1, stringifyEnumArray(MoveId, moves1)).toEqual([ + MoveId.POUND, + MoveId.KARATE_CHOP, + MoveId.DOUBLE_SLAP, + MoveId.COMET_PUNCH, + ]); + expect(moves2, "was not a random moveset").toHaveLength(4); + expect(moves3, "was not a random moveset").toHaveLength(4); + }); + + it("should skip invalid move IDs", async () => { + vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("/moves9999,,0919"); + await game.dailyMode.startBattle(); + + const moves = game.field.getPlayerPokemon().moveset.map(pm => pm.moveId); + expect(moves, "invalid move was in moveset").not.toContain(MoveId[9999]); + }); }); - it("should allow omitting movesets for some starters", async () => { - vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("/moves0001000200030004"); - await game.dailyMode.startBattle(); + describe("Starters", () => { + it("should support custom species IDs", async () => { + vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("foo/starterss0001s0113s1024"); + await game.dailyMode.startBattle(); - const [moves1, moves2, moves3] = game.scene.getPlayerParty().map(p => p.moveset.map(pm => pm.moveId)); - expect(moves1, stringifyEnumArray(MoveId, moves1)).toEqual([ - MoveId.POUND, - MoveId.KARATE_CHOP, - MoveId.DOUBLE_SLAP, - MoveId.COMET_PUNCH, - ]); - expect(moves2, "was not a random moveset").toHaveLength(4); - expect(moves3, "was not a random moveset").toHaveLength(4); - }); + const party = game.scene.getPlayerParty().map(p => p.species.speciesId); + expect(party, stringifyEnumArray(SpeciesId, party)).toEqual([ + SpeciesId.BULBASAUR, + SpeciesId.CHANSEY, + SpeciesId.TERAPAGOS, + ]); + }); - it("should skip invalid move IDs", async () => { - vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("/moves9999,,0919"); - await game.dailyMode.startBattle(); + it("should support custom forms and variants", async () => { + vi.spyOn(pokerogueApi.daily, "getSeed").mockResolvedValue("/starterss0006f01v2s0113v0s1024f02"); + await game.dailyMode.startBattle(); - const moves = game.field.getPlayerPokemon().moveset.map(pm => pm.moveId); - expect(moves, "invalid move was in moveset").not.toContain(MoveId[9999]); + const party = game.scene.getPlayerParty().map(p => ({ + speciesId: p.species.speciesId, + variant: p.getVariant(), + form: p.formIndex, + shiny: p.isShiny(), + })); + expect(party).toEqual([ + { speciesId: SpeciesId.CHARIZARD, variant: 2, form: 1, shiny: true }, + { speciesId: SpeciesId.CHANSEY, variant: 0, form: 0, shiny: true }, + { speciesId: SpeciesId.TERAPAGOS, variant: expect.anything(), form: 2, shiny: false }, + ]); + }); }); }); }); From 617e12f6342e650355cbdb01b04abaf502b0f2c6 Mon Sep 17 00:00:00 2001 From: Bertie690 Date: Fri, 31 Oct 2025 19:14:19 -0400 Subject: [PATCH 22/27] Removed console log --- src/data/daily-run.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index 9daabc01529..215e23be3b9 100644 --- a/src/data/daily-run.ts +++ b/src/data/daily-run.ts @@ -71,7 +71,6 @@ function getDailyRunStarter(starterSpeciesForm: PokemonSpeciesForm, startingLeve variant != null, variant, ); - console.log(`%c${pokemon.shiny} ${variant} ${variant != null}`, "color:blue"); const starter: Starter = { speciesId: starterSpecies.speciesId, shiny: pokemon.shiny, @@ -271,7 +270,7 @@ function getDailyEventSeedStarters(seed: string): StarterTuple | null { const speciesIds = getEnumValues(SpeciesId); const starters: Starter[] = []; - for (const [i, match] of speciesConfigurations.entries()) { + for (const match of speciesConfigurations) { const { groups } = match; if (!groups) { console.error("Invalid seed used for custom daily run starter:", match); From c33f9723f543ccb0693b7fc6966ea406da2c1f86 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:21:38 -0700 Subject: [PATCH 23/27] Update enum name --- src/data/abilities/ability.ts | 14 +++++++------- src/data/moves/move.ts | 8 ++++---- src/enums/move-priority-modifier.ts | 8 ++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 180ba0f3e90..d3c646b9321 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -34,7 +34,7 @@ import { MoveCategory } from "#enums/move-category"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; -import { MovePriorityModifier } from "#enums/move-priority-modifier"; +import { MovePriorityInBracket } from "#enums/move-priority-modifier"; import { MoveResult } from "#enums/move-result"; import { MoveTarget } from "#enums/move-target"; import { MoveUseMode } from "#enums/move-use-mode"; @@ -4143,11 +4143,11 @@ export class ChangeMovePriorityAbAttr extends AbAttr { } } -export class ChangeMovePriorityModifierAbAttr extends AbAttr { - private readonly newModifier: MovePriorityModifier; +export class ChangeMovePriorityInBracketAbAttr extends AbAttr { + private readonly newModifier: MovePriorityInBracket; private readonly moveFunc: (pokemon: Pokemon, move: Move) => boolean; - constructor(moveFunc: (pokemon: Pokemon, move: Move) => boolean, newModifier: MovePriorityModifier) { + constructor(moveFunc: (pokemon: Pokemon, move: Move) => boolean, newModifier: MovePriorityInBracket) { super(false); this.newModifier = newModifier; this.moveFunc = moveFunc; @@ -6741,7 +6741,7 @@ const AbilityAttrs = Object.freeze({ BlockStatusDamageAbAttr, BlockOneHitKOAbAttr, ChangeMovePriorityAbAttr, - ChangeMovePriorityModifierAbAttr, + ChangeMovePriorityInBracketAbAttr, IgnoreContactAbAttr, PreWeatherEffectAbAttr, PreWeatherDamageAbAttr, @@ -7259,7 +7259,7 @@ export function initAbilities() { .attr(DoubleBattleChanceAbAttr) .build(), new AbBuilder(AbilityId.STALL, 4) - .attr(ChangeMovePriorityModifierAbAttr, (_pokemon, _move: Move) => true, MovePriorityModifier.LAST_IN_BRACKET) + .attr(ChangeMovePriorityInBracketAbAttr, (_pokemon, _move: Move) => true, MovePriorityInBracket.LAST) .build(), new AbBuilder(AbilityId.TECHNICIAN, 4) .attr(MovePowerBoostAbAttr, (user, target, move) => { @@ -8206,7 +8206,7 @@ export function initAbilities() { .ignorable() .build(), new AbBuilder(AbilityId.MYCELIUM_MIGHT, 9) - .attr(ChangeMovePriorityModifierAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, MovePriorityModifier.LAST_IN_BRACKET) + .attr(ChangeMovePriorityInBracketAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, MovePriorityInBracket.LAST) .attr(PreventBypassSpeedChanceAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS) .attr(MoveAbilityBypassAbAttr, (_pokemon, move: Move) => move.category === MoveCategory.STATUS) .build(), diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 43baf361d5e..417ae7c3b93 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -101,7 +101,7 @@ import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { inSpeedOrder } from "#utils/speed-order-generator"; import { canSpeciesTera, willTerastallize } from "#utils/pokemon-utils"; import type { ReadonlyGenericUint8Array } from "#types/typed-arrays"; -import { MovePriorityModifier } from "#enums/move-priority-modifier"; +import { MovePriorityInBracket } from "#enums/move-priority-modifier"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -1069,10 +1069,10 @@ export abstract class Move implements Localizable { public getPriorityModifier(user: Pokemon, simulated = true) { if (user.getTag(BattlerTagType.BYPASS_SPEED)) { - return MovePriorityModifier.FIRST_IN_BRACKET; + return MovePriorityInBracket.FIRST; } - const modifierHolder = new NumberHolder(MovePriorityModifier.NORMAL); - applyAbAttrs("ChangeMovePriorityModifierAbAttr", { pokemon: user, simulated, move: this, priority: modifierHolder }); + const modifierHolder = new NumberHolder(MovePriorityInBracket.NORMAL); + applyAbAttrs("ChangeMovePriorityInBracketAbAttr", { pokemon: user, simulated, move: this, priority: modifierHolder }); return modifierHolder.value; } diff --git a/src/enums/move-priority-modifier.ts b/src/enums/move-priority-modifier.ts index 3e51aaec0a3..ea314fda0e4 100644 --- a/src/enums/move-priority-modifier.ts +++ b/src/enums/move-priority-modifier.ts @@ -3,11 +3,11 @@ import type { ObjectValues } from "#types/type-helpers"; /** * Enum representing modifiers for Move priorities. */ -export const MovePriorityModifier = Object.freeze({ +export const MovePriorityInBracket = Object.freeze({ /** Used when moves go last in their priority bracket, but before moves of lower priority. */ - LAST_IN_BRACKET: 0, + LAST: 0, NORMAL: 1, /** Used when moves go first in their priority bracket, but before moves of lower priority. */ - FIRST_IN_BRACKET: 2, + FIRST: 2, }); -export type MovePriorityModifier = ObjectValues; +export type MovePriorityInBracket = ObjectValues; From f928ca9bdf1d37c7e842df9bc2e9d74d6d02abe6 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:23:57 -0700 Subject: [PATCH 24/27] Update enum filename --- src/data/abilities/ability.ts | 2 +- src/data/moves/move.ts | 2 +- .../{move-priority-modifier.ts => move-priority-in-bracket.ts} | 0 3 files changed, 2 insertions(+), 2 deletions(-) rename src/enums/{move-priority-modifier.ts => move-priority-in-bracket.ts} (100%) diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index d3c646b9321..90adcd86b5d 100644 --- a/src/data/abilities/ability.ts +++ b/src/data/abilities/ability.ts @@ -34,7 +34,7 @@ import { MoveCategory } from "#enums/move-category"; import { MoveFlags } from "#enums/move-flags"; import { MoveId } from "#enums/move-id"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; -import { MovePriorityInBracket } from "#enums/move-priority-modifier"; +import { MovePriorityInBracket } from "#enums/move-priority-in-bracket"; import { MoveResult } from "#enums/move-result"; import { MoveTarget } from "#enums/move-target"; import { MoveUseMode } from "#enums/move-use-mode"; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 417ae7c3b93..8637c65966b 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -101,7 +101,7 @@ import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import { inSpeedOrder } from "#utils/speed-order-generator"; import { canSpeciesTera, willTerastallize } from "#utils/pokemon-utils"; import type { ReadonlyGenericUint8Array } from "#types/typed-arrays"; -import { MovePriorityInBracket } from "#enums/move-priority-modifier"; +import { MovePriorityInBracket } from "#enums/move-priority-in-bracket"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. diff --git a/src/enums/move-priority-modifier.ts b/src/enums/move-priority-in-bracket.ts similarity index 100% rename from src/enums/move-priority-modifier.ts rename to src/enums/move-priority-in-bracket.ts From c1c12e479223ce3e08e07dab736b87f69a8bed99 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:51:22 -0700 Subject: [PATCH 25/27] Revert uxie passive --- src/data/balance/passives.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/data/balance/passives.ts b/src/data/balance/passives.ts index 223f50471e8..bc7f8377b9e 100644 --- a/src/data/balance/passives.ts +++ b/src/data/balance/passives.ts @@ -506,7 +506,7 @@ export const starterPassiveAbilities: StarterPassiveAbilities = { [SpeciesId.SNOVER]: { 0: AbilityId.SLUSH_RUSH }, [SpeciesId.ABOMASNOW]: { 0: AbilityId.SLUSH_RUSH, 1: AbilityId.SEED_SOWER }, [SpeciesId.ROTOM]: { 0: AbilityId.HADRON_ENGINE, 1: AbilityId.HADRON_ENGINE, 2: AbilityId.HADRON_ENGINE, 3: AbilityId.HADRON_ENGINE, 4: AbilityId.HADRON_ENGINE, 5: AbilityId.HADRON_ENGINE }, - [SpeciesId.UXIE]: { 0: AbilityId.ILLUSION }, + [SpeciesId.UXIE]: { 0: AbilityId.MAGIC_BOUNCE }, [SpeciesId.MESPRIT]: { 0: AbilityId.MOODY }, [SpeciesId.AZELF]: { 0: AbilityId.NEUROFORCE }, [SpeciesId.DIALGA]: { 0: AbilityId.BERSERK, 1: AbilityId.BERSERK }, From 7a56989a3c1761287dcfb8ab55a3b1cc9fd97569 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Fri, 31 Oct 2025 16:51:49 -0700 Subject: [PATCH 26/27] Remove illusion bypassing summondata reset --- src/field/pokemon.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 6ce5c1ffc2c..1d58f7de883 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -143,7 +143,6 @@ import type { AbAttrMap, AbAttrString, TypeMultiplierAbAttrParams } from "#types import type { Constructor } from "#types/common"; import type { getAttackDamageParams, getBaseDamageParams } from "#types/damage-params"; import type { DamageCalculationResult, DamageResult } from "#types/damage-result"; -import type { IllusionData } from "#types/illusion-data"; import type { LevelMoves } from "#types/pokemon-level-moves"; import type { StarterDataEntry, StarterMoveset } from "#types/save-data"; import type { TurnMove } from "#types/turn-move"; @@ -5119,14 +5118,12 @@ export abstract class Pokemon extends Phaser.GameObjects.Container { * in preparation for switching pokemon, as well as removing any relevant on-switch tags. */ public resetSummonData(): void { - const illusion: IllusionData | null = this.summonData.illusion; if (this.summonData.speciesForm) { this.summonData.speciesForm = null; this.updateFusionPalette(); } this.summonData = new PokemonSummonData(); this.tempSummonData = new PokemonTempSummonData(); - this.summonData.illusion = illusion; this.updateInfo(); } From 3b704913e920d628efb321d5134c628702d09e1f Mon Sep 17 00:00:00 2001 From: fabske0 <192151969+fabske0@users.noreply.github.com> Date: Sat, 1 Nov 2025 00:52:51 +0100 Subject: [PATCH 27/27] update locales --- locales | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/locales b/locales index 67a0c026068..ddf9509e1c6 160000 --- a/locales +++ b/locales @@ -1 +1 @@ -Subproject commit 67a0c02606848cc6ca3f8998a7cbacb0d7dd5a9e +Subproject commit ddf9509e1c6abe8fc93b455d79bfaa0202e05ede