diff --git a/locales b/locales index 67a0c026068..ddf9509e1c6 160000 --- a/locales +++ b/locales @@ -1 +1 @@ -Subproject commit 67a0c02606848cc6ca3f8998a7cbacb0d7dd5a9e +Subproject commit ddf9509e1c6abe8fc93b455d79bfaa0202e05ede diff --git a/package.json b/package.json index b23b9b9b75d..e08e5a393a4 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "pokemon-rogue-battle", "private": true, - "version": "1.11.0", + "version": "1.11.2", "type": "module", "scripts": { "start:prod": "vite --mode production", diff --git a/src/data/abilities/ability.ts b/src/data/abilities/ability.ts index 301016d899b..cd18bbcfb9c 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 { 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"; @@ -4142,6 +4143,25 @@ export class ChangeMovePriorityAbAttr extends AbAttr { } } +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: MovePriorityInBracket) { + 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, + ChangeMovePriorityInBracketAbAttr, 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(ChangeMovePriorityInBracketAbAttr, (_pokemon, _move: Move) => true, MovePriorityInBracket.LAST) .build(), new AbBuilder(AbilityId.TECHNICIAN, 4) .attr(MovePowerBoostAbAttr, (user, target, move) => { @@ -7440,17 +7461,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) + // // 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() + // .bypassFaint() + .unimplemented() // TODO reimplement Illusion properly .build(), new AbBuilder(AbilityId.IMPOSTER, 5) .attr(PostSummonTransformAbAttr) @@ -8185,7 +8207,7 @@ export function initAbilities() { .ignorable() .build(), new AbBuilder(AbilityId.MYCELIUM_MIGHT, 9) - .attr(ChangeMovePriorityAbAttr, (_pokemon, move) => move.category === MoveCategory.STATUS, -0.2) + .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/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/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 }, diff --git a/src/data/daily-run.ts b/src/data/daily-run.ts index ea2852976e8..215e23be3b9 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 != null, + variant, + ); const starter: Starter = { speciesId: starterSpecies.speciesId, shiny: pokemon.shiny, @@ -172,7 +180,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}))?/; /** @@ -215,14 +227,106 @@ 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(.*?)(?:\/|$)/; +/** + * 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 it did not match. + */ +// TODO: Rework this setup into JSON or similar - this is quite hard to maintain +function getDailyEventSeedStarters(seed: string): StarterTuple | null { + if (!isDailyEventSeed(seed)) { + return null; + } + + const seedAfterPrefix = seed.split(STARTER_SEED_PREFIX_REGEX)[1] as string | undefined; + if (!seedAfterPrefix) { + return null; + } + + const speciesConfigurations = [...seedAfterPrefix.matchAll(STARTER_SEED_MATCH_REGEX)]; + + 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) { + return legacyStarters; + } + console.error("Invalid starters used for custom daily run seed!", seed); + return null; + } + + const speciesIds = getEnumValues(SpeciesId); + const starters: Starter[] = []; + + for (const match of speciesConfigurations) { + 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); + // 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 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 getDailyEventSeedStartersLegacy(seed: string): StarterTuple | null { if (!isDailyEventSeed(seed)) { return null; } diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index b32ed6bbee4..8637c65966b 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 { MovePriorityInBracket } from "#enums/move-priority-in-bracket"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -1060,17 +1061,21 @@ 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) { + if (user.getTag(BattlerTagType.BYPASS_SPEED)) { + return MovePriorityInBracket.FIRST; + } + const modifierHolder = new NumberHolder(MovePriorityInBracket.NORMAL); + applyAbAttrs("ChangeMovePriorityInBracketAbAttr", { pokemon: user, simulated, move: this, priority: modifierHolder }); + 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/data/trainers/rival-party-config.ts b/src/data/trainers/rival-party-config.ts index b5a8cb532b3..67b3f50d379 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, @@ -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, diff --git a/src/enums/move-priority-in-bracket.ts b/src/enums/move-priority-in-bracket.ts new file mode 100644 index 00000000000..ea314fda0e4 --- /dev/null +++ b/src/enums/move-priority-in-bracket.ts @@ -0,0 +1,13 @@ +import type { ObjectValues } from "#types/type-helpers"; + +/** + * Enum representing modifiers for Move priorities. + */ +export const MovePriorityInBracket = Object.freeze({ + /** Used when moves go last in their priority bracket, but before moves of lower priority. */ + LAST: 0, + NORMAL: 1, + /** Used when moves go first in their priority bracket, but before moves of lower priority. */ + FIRST: 2, +}); +export type MovePriorityInBracket = ObjectValues; diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index be85e2ebe3a..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(); } @@ -6388,12 +6385,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; } 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", { 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); } 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/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/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/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; } 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; 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", 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/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/abilities/illusion.test.ts b/test/abilities/illusion.test.ts index 2343a11cb74..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; 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(); 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 }, + ]); + }); }); }); }); 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); + }); +});