From c38fb267cedacd05e25c5b4e8087c423acc616bb Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 20 Sep 2025 11:47:03 -0500 Subject: [PATCH 1/6] Make type boost items consider moves that have a variable, but static type --- src/data/moves/move.ts | 65 +++++++++++++++++++++++++++++++++-- src/modifier/modifier-type.ts | 51 +++++++++++++-------------- 2 files changed, 85 insertions(+), 31 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 075876d8ddd..2a1bfd7df82 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -54,7 +54,7 @@ import { MoveEffectTrigger } from "#enums/move-effect-trigger"; import { MoveFlags } from "#enums/move-flags"; import { MoveTarget } from "#enums/move-target"; import { MultiHitType } from "#enums/multi-hit-type"; -import { PokemonType } from "#enums/pokemon-type"; +import { MAX_POKEMON_TYPE, PokemonType } from "#enums/pokemon-type"; import { PositionalTagType } from "#enums/positional-tag-type"; import { SpeciesId } from "#enums/species-id"; import { @@ -4977,6 +4977,16 @@ export class VariableMoveTypeAttr extends MoveAttr { apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { return false; } + + /** + * Determine the type of the move for the purpose of determining the type-boosting item to spawn + * @param user - The Pokémon using the move + * @param move - The move being used + * @returns An array of types to add to the pool of type-boosting items + */ + getTypesForItemSpawn(user: Pokemon, move: Move): PokemonType[] { + return [move.type]; + } } export class FormChangeItemTypeAttr extends VariableMoveTypeAttr { @@ -4988,8 +4998,10 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr { if ([ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(SpeciesId.ARCEUS) || [ user.species.speciesId, user.fusionSpecies?.speciesId ].includes(SpeciesId.SILVALLY)) { const form = user.species.speciesId === SpeciesId.ARCEUS || user.species.speciesId === SpeciesId.SILVALLY ? user.formIndex : user.fusionSpecies?.formIndex!; - - moveType.value = PokemonType[PokemonType[form]]; + if (form >= 0 && form <= MAX_POKEMON_TYPE && form !== PokemonType.STELLAR) { + moveType.value = form as PokemonType; + return true; + } return true; } @@ -5000,6 +5012,14 @@ export class FormChangeItemTypeAttr extends VariableMoveTypeAttr { moveType.value = move.type return true; } + + override getTypesForItemSpawn(user: Pokemon, move: Move): PokemonType[] { + // Get the type + const typeHolder = new NumberHolder(move.type); + // Passing user in for target is fine; the parameter is unused anyway + this.apply(user, user, move, [ typeHolder ]); + return [typeHolder.value]; + } } export class TechnoBlastTypeAttr extends VariableMoveTypeAttr { @@ -5034,6 +5054,12 @@ export class TechnoBlastTypeAttr extends VariableMoveTypeAttr { return false; } + + override getTypesForItemSpawn(user: Pokemon, move: Move): PokemonType[] { + const typeHolder = new NumberHolder(move.type); + this.apply(user, user, move, [ typeHolder ]); + return [typeHolder.value]; + } } export class AuraWheelTypeAttr extends VariableMoveTypeAttr { @@ -5059,6 +5085,15 @@ export class AuraWheelTypeAttr extends VariableMoveTypeAttr { return false; } + + override getTypesForItemSpawn(user: Pokemon, move: Move): PokemonType[] { + // On Morpeko only, allow this to count for both blackglasses and magnet + if (this.apply(user, user, move, [new NumberHolder(move.type)])) { + return [PokemonType.DARK, PokemonType.ELECTRIC]; + } + + return [move.type]; + } } export class RagingBullTypeAttr extends VariableMoveTypeAttr { @@ -5087,6 +5122,12 @@ export class RagingBullTypeAttr extends VariableMoveTypeAttr { return false; } + + override getTypesForItemSpawn(user: Pokemon, move: Move): PokemonType[] { + const typeHolder = new NumberHolder(move.type); + this.apply(user, user, move, [ typeHolder ]); + return [ typeHolder.value ]; + } } export class IvyCudgelTypeAttr extends VariableMoveTypeAttr { @@ -5122,6 +5163,12 @@ export class IvyCudgelTypeAttr extends VariableMoveTypeAttr { return false; } + + override getTypesForItemSpawn(user: Pokemon, move: Move): PokemonType[] { + const typeHolder = new NumberHolder(move.type); + this.apply(user, user, move, [ typeHolder ]); + return [ typeHolder.value ]; + } } export class WeatherBallTypeAttr extends VariableMoveTypeAttr { @@ -5235,6 +5282,12 @@ export class HiddenPowerTypeAttr extends VariableMoveTypeAttr { return true; } + + override getTypesForItemSpawn(user: Pokemon, move: Move): PokemonType[] { + const typeHolder = new NumberHolder(move.type); + this.apply(user, user, move, [ typeHolder ]); + return [typeHolder.value]; + } } /** @@ -5304,7 +5357,13 @@ export class MatchUserTypeAttr extends VariableMoveTypeAttr { } else { return false; } + } + override getTypesForItemSpawn(user: Pokemon, move: Move): PokemonType[] { + // Instead of calling apply, just return the user's primary type + // this avoids inconsistencies when the user's type is temporarily changed + // from tera + return [user.getTypes(false, true, true, false)[0] ?? move.type]; } } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index d67011bc145..e993850ec3f 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1288,52 +1288,47 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { return new AttackTypeBoosterModifierType(pregenArgs[0] as PokemonType, TYPE_BOOST_ITEM_BOOST_PERCENT); } - const attackMoveTypes = party.flatMap(p => - p - .getMoveset() - .map(m => m.getMove()) - .filter(m => m.is("AttackMove")) - .map(m => m.type), - ); - if (attackMoveTypes.length === 0) { - return null; - } - const attackMoveTypeWeights = new Map(); let totalWeight = 0; - for (const t of attackMoveTypes) { - if (attackMoveTypeWeights.has(t)) { - if (attackMoveTypeWeights.get(t)! < 3) { - // attackMoveTypeWeights.has(t) was checked before - attackMoveTypeWeights.set(t, attackMoveTypeWeights.get(t)! + 1); - } else { + for (const p of party) { + if (!p.isAllowedInChallenge()) { + continue; + } + for (const pokemonMove of p.getMoveset()) { + const move = pokemonMove.getMove(); + if (!move.is("AttackMove")) { continue; } - } else { - attackMoveTypeWeights.set(t, 1); + // Account for variable type changing moves + // Get a variable type attribute of the move + const variableTypeAttr = move.getAttrs("VariableMoveTypeAttr")[0]; + if (variableTypeAttr != null) { + for (const type of variableTypeAttr.getTypesForItemSpawn(p, move)) { + const currentWeight = attackMoveTypeWeights.get(type) ?? 0; + if (currentWeight < 3) { + attackMoveTypeWeights.set(type, currentWeight + 1); + totalWeight++; + } + } + } } - totalWeight++; } - if (!totalWeight) { + if (attackMoveTypeWeights.size === 0) { return null; } - let type: PokemonType; - const randInt = randSeedInt(totalWeight); let weight = 0; - for (const t of attackMoveTypeWeights.keys()) { - const typeWeight = attackMoveTypeWeights.get(t)!; // guranteed to be defined + for (const [type, typeWeight] of attackMoveTypeWeights.entries()) { if (randInt <= weight + typeWeight) { - type = t; - break; + return new AttackTypeBoosterModifierType(type, TYPE_BOOST_ITEM_BOOST_PERCENT); } weight += typeWeight; } - return new AttackTypeBoosterModifierType(type!, TYPE_BOOST_ITEM_BOOST_PERCENT); + return null; }); } } From 22dcbe6aa46f22cd12670fbc49981043dd4c01c9 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 20 Sep 2025 12:36:26 -0500 Subject: [PATCH 2/6] fix: type boost item not spawning for moves without variable type --- src/modifier/modifier-type.ts | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index e993850ec3f..e66e484baad 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1302,13 +1302,12 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { // Account for variable type changing moves // Get a variable type attribute of the move const variableTypeAttr = move.getAttrs("VariableMoveTypeAttr")[0]; - if (variableTypeAttr != null) { - for (const type of variableTypeAttr.getTypesForItemSpawn(p, move)) { - const currentWeight = attackMoveTypeWeights.get(type) ?? 0; - if (currentWeight < 3) { - attackMoveTypeWeights.set(type, currentWeight + 1); - totalWeight++; - } + const types = variableTypeAttr != null ? variableTypeAttr.getTypesForItemSpawn(p, move) : [move.type]; + for (const type of types) { + const currentWeight = attackMoveTypeWeights.get(type) ?? 0; + if (currentWeight < 3) { + attackMoveTypeWeights.set(type, currentWeight + 1); + totalWeight++; } } } From 6bc927a2071bd5f45c0af728c81684ff5b97593f Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 20 Sep 2025 14:19:55 -0500 Subject: [PATCH 3/6] make tera blast consider a tera-capable user's tera type --- src/ai/ai-moveset-gen.ts | 8 ++---- src/data/moves/move.ts | 23 ++++++++++++++++ src/modifier/modifier-type.ts | 7 +++-- src/ui/handlers/command-ui-handler.ts | 13 +++------ src/utils/pokemon-utils.ts | 39 ++++++++++++++++++++++++++- 5 files changed, 72 insertions(+), 18 deletions(-) diff --git a/src/ai/ai-moveset-gen.ts b/src/ai/ai-moveset-gen.ts index f392ca46d3f..c996fd4d885 100644 --- a/src/ai/ai-moveset-gen.ts +++ b/src/ai/ai-moveset-gen.ts @@ -31,6 +31,7 @@ import { Stat } from "#enums/stat"; import type { EnemyPokemon, Pokemon } from "#field/pokemon"; import { PokemonMove } from "#moves/pokemon-move"; import { NumberHolder, randSeedInt } from "#utils/common"; +import { willTerastallize } from "#utils/pokemon-utils"; import { isBeta } from "#utils/utility-vars"; /** @@ -676,12 +677,7 @@ export function generateMoveset(pokemon: Pokemon): void { } /** Determine whether this pokemon will instantly tera */ - const willTera = - hasTrainer - && globalScene.currentBattle?.trainer?.config.trainerAI.instantTeras.includes( - // The cast to EnemyPokemon is safe; includes will just return false if the property doesn't exist - (pokemon as EnemyPokemon).initialTeamIndex, - ); + const willTera = hasTrainer && willTerastallize(pokemon as EnemyPokemon); adjustDamageMoveWeights(movePool, pokemon, willTera); diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 2a1bfd7df82..e9e48754b8c 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -94,6 +94,7 @@ import i18next from "i18next"; import { applyChallenges } from "#utils/challenge-utils"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { AbstractConstructor } from "#types/type-helpers"; +import { willTerastallize } from "#utils/pokemon-utils"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -5314,6 +5315,28 @@ export class TeraBlastTypeAttr extends VariableMoveTypeAttr { return false; } + + override getTypesForItemSpawn(user: Pokemon, move: Move): PokemonType[] { + const coreType = move.type; + const teraType = user.getTeraType(); + /** Whether the user is allowed to tera. In the case of an enemy Pokémon, whether it *will* tera. */ + const hasTeraAccess = user.isPlayer() ? globalScene.findModifier(m => m.is("TerastallizeAccessModifier")) != null : willTerastallize(user); + if ( + // tera type matches the move's type; no change + teraType === coreType + || teraType === PokemonType.STELLAR + || teraType === PokemonType.UNKNOWN + || user.isMega() + || user.isMax() + || user.hasSpecies(SpeciesId.NECROZMA, "ultra") + || (!hasTeraAccess) + ) { + return [coreType]; + } else { + + } + return [coreType, teraType]; + } } /** diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index e66e484baad..a4529fc54a5 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1302,8 +1302,9 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { // Account for variable type changing moves // Get a variable type attribute of the move const variableTypeAttr = move.getAttrs("VariableMoveTypeAttr")[0]; - const types = variableTypeAttr != null ? variableTypeAttr.getTypesForItemSpawn(p, move) : [move.type]; + const types = variableTypeAttr?.getTypesForItemSpawn(p, move) ?? [move.type]; for (const type of types) { + console.info("%cConsidering type " + PokemonType[type], "color: orange"); const currentWeight = attackMoveTypeWeights.get(type) ?? 0; if (currentWeight < 3) { attackMoveTypeWeights.set(type, currentWeight + 1); @@ -1318,10 +1319,12 @@ class AttackTypeBoosterModifierTypeGenerator extends ModifierTypeGenerator { } const randInt = randSeedInt(totalWeight); + console.log("%cTotal weight " + totalWeight + ", rolled " + randInt, "color: orange"); let weight = 0; for (const [type, typeWeight] of attackMoveTypeWeights.entries()) { - if (randInt <= weight + typeWeight) { + console.log("%cWeighted type " + PokemonType[type] + " with weight " + typeWeight, "color: orange"); + if (randInt < weight + typeWeight) { return new AttackTypeBoosterModifierType(type, TYPE_BOOST_ITEM_BOOST_PERCENT); } weight += typeWeight; diff --git a/src/ui/handlers/command-ui-handler.ts b/src/ui/handlers/command-ui-handler.ts index 693fe0eefef..c1a977e5a72 100644 --- a/src/ui/handlers/command-ui-handler.ts +++ b/src/ui/handlers/command-ui-handler.ts @@ -4,14 +4,13 @@ import { getTypeRgb } from "#data/type"; import { Button } from "#enums/buttons"; import { Command } from "#enums/command"; import { PokemonType } from "#enums/pokemon-type"; -import { SpeciesId } from "#enums/species-id"; import { TextStyle } from "#enums/text-style"; import { UiMode } from "#enums/ui-mode"; -import { TerastallizeAccessModifier } from "#modifiers/modifier"; import type { CommandPhase } from "#phases/command-phase"; import { PartyUiHandler, PartyUiMode } from "#ui/party-ui-handler"; import { addTextObject } from "#ui/text"; import { UiHandler } from "#ui/ui-handler"; +import { canTerastallize } from "#utils/pokemon-utils"; import i18next from "i18next"; export class CommandUiHandler extends UiHandler { @@ -198,14 +197,10 @@ export class CommandUiHandler extends UiHandler { } canTera(): boolean { - const hasTeraMod = globalScene.getModifiers(TerastallizeAccessModifier).length > 0; const activePokemon = globalScene.getField()[this.fieldIndex]; - const isBlockedForm = - activePokemon.isMega() || activePokemon.isMax() || activePokemon.hasSpecies(SpeciesId.NECROZMA, "ultra"); - const currentTeras = globalScene.arena.playerTerasUsed; - const plannedTera = - globalScene.currentBattle.preTurnCommands[0]?.command === Command.TERA && this.fieldIndex > 0 ? 1 : 0; - return hasTeraMod && !isBlockedForm && currentTeras + plannedTera < 1; + const canTera = activePokemon.isPlayer() && canTerastallize(activePokemon); + const plannedTera = globalScene.currentBattle.preTurnCommands[0]?.command === Command.TERA && this.fieldIndex > 0; + return canTera && !plannedTera; } toggleTeraButton() { diff --git a/src/utils/pokemon-utils.ts b/src/utils/pokemon-utils.ts index 60a4d9e0ef7..f8d0308d468 100644 --- a/src/utils/pokemon-utils.ts +++ b/src/utils/pokemon-utils.ts @@ -2,7 +2,8 @@ import { globalScene } from "#app/global-scene"; import { POKERUS_STARTER_COUNT, speciesStarterCosts } from "#balance/starters"; import { allSpecies } from "#data/data-lists"; import type { PokemonSpecies, PokemonSpeciesForm } from "#data/pokemon-species"; -import type { SpeciesId } from "#enums/species-id"; +import { SpeciesId } from "#enums/species-id"; +import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon"; import { randSeedItem } from "./common"; /** @@ -123,3 +124,39 @@ export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): Po } return retSpecies; } + +/** + * Determine whether an enemy Pokémon will Terastallize the user + * + * Does not check if the Pokémon is allowed to Terastallize (e.g., if it's a mega) + * @param pokemon - The Pokémon to check + * @returns Whether the Pokémon will Terastallize + * + * @remarks + * Should really only be called with an enemy Pokémon, but will technically work with any Pokémon. + * + * @privateRemarks + * Assumes that Pokémon with no trainer ever tera, so this must be changed if + * a wild Pokémon is allowed to tera, e.g. for a Mystery Encounter. + */ +export function willTerastallize(pokemon: Pokemon): boolean { + // cast is safe, as if it is just a Pokémon, initialTeamIndex will be undefined triggering the null check + const initialTeamIndex = (pokemon as EnemyPokemon).initialTeamIndex; + return ( + initialTeamIndex != null + && pokemon.hasTrainer() + && (globalScene.currentBattle?.trainer?.config.trainerAI.instantTeras.includes(initialTeamIndex) ?? false) + ); +} + +/** + * Determine whether a player Pokémon can Terastallize + * @param pokemon - The Pokémon to check + * @returns Whether the Pokémon can Terastallize + */ +export function canTerastallize(pokemon: PlayerPokemon): boolean { + const hasTeraMod = globalScene.findModifier(modifier => modifier.is("TerastallizeAccessModifier")) != null; + const isBlockedForm = pokemon.isMega() || pokemon.isMax() || pokemon.hasSpecies(SpeciesId.NECROZMA, "ultra"); + const currentTeras = globalScene.arena.playerTerasUsed === 0; + return hasTeraMod && !isBlockedForm && currentTeras; +} From 002397bee6b4a2ab41fe1a9831d19a7a1f9424d2 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Sat, 20 Sep 2025 22:59:48 -0500 Subject: [PATCH 4/6] Adjust tera blast's getTypesForItemSpawn method --- src/data/moves/move.ts | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index e9e48754b8c..27a1052465e 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -94,7 +94,7 @@ import i18next from "i18next"; import { applyChallenges } from "#utils/challenge-utils"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { AbstractConstructor } from "#types/type-helpers"; -import { willTerastallize } from "#utils/pokemon-utils"; +import { canTerastallize, willTerastallize } from "#utils/pokemon-utils"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -5320,20 +5320,15 @@ export class TeraBlastTypeAttr extends VariableMoveTypeAttr { const coreType = move.type; const teraType = user.getTeraType(); /** Whether the user is allowed to tera. In the case of an enemy Pokémon, whether it *will* tera. */ - const hasTeraAccess = user.isPlayer() ? globalScene.findModifier(m => m.is("TerastallizeAccessModifier")) != null : willTerastallize(user); + const hasTeraAccess = user.isPlayer() ? canTerastallize(user) : willTerastallize(user); if ( // tera type matches the move's type; no change - teraType === coreType + !hasTeraAccess + || teraType === coreType || teraType === PokemonType.STELLAR || teraType === PokemonType.UNKNOWN - || user.isMega() - || user.isMax() - || user.hasSpecies(SpeciesId.NECROZMA, "ultra") - || (!hasTeraAccess) ) { return [coreType]; - } else { - } return [coreType, teraType]; } From e9e827341e3b234d11326305077b03defcdf3cb9 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 19:43:14 -0500 Subject: [PATCH 5/6] Address comments from code review --- src/constants.ts | 5 +++++ src/data/moves/move.ts | 4 ++-- src/ui/handlers/command-ui-handler.ts | 8 ++++++-- src/utils/pokemon-utils.ts | 22 ++++++++++++++++------ 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 17cf08aa7e2..d5dc8803255 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -107,3 +107,8 @@ export const FAKE_TITLE_LOGO_CHANCE = 10000; * Using rare candies will never increase friendship beyond this value. */ export const RARE_CANDY_FRIENDSHIP_CAP = 200; + +/** + * The maximum number of times a player can Terastallize in a single arena run + */ +export const MAX_TERAS_PER_ARENA = 1; diff --git a/src/data/moves/move.ts b/src/data/moves/move.ts index 27a1052465e..8152a3c88e8 100644 --- a/src/data/moves/move.ts +++ b/src/data/moves/move.ts @@ -94,7 +94,7 @@ import i18next from "i18next"; import { applyChallenges } from "#utils/challenge-utils"; import { MovePhaseTimingModifier } from "#enums/move-phase-timing-modifier"; import type { AbstractConstructor } from "#types/type-helpers"; -import { canTerastallize, willTerastallize } from "#utils/pokemon-utils"; +import { canSpeciesTera, willTerastallize } from "#utils/pokemon-utils"; /** * A function used to conditionally determine execution of a given {@linkcode MoveAttr}. @@ -5320,7 +5320,7 @@ export class TeraBlastTypeAttr extends VariableMoveTypeAttr { const coreType = move.type; const teraType = user.getTeraType(); /** Whether the user is allowed to tera. In the case of an enemy Pokémon, whether it *will* tera. */ - const hasTeraAccess = user.isPlayer() ? canTerastallize(user) : willTerastallize(user); + const hasTeraAccess = user.isPlayer() ? canSpeciesTera(user) : willTerastallize(user); if ( // tera type matches the move's type; no change !hasTeraAccess diff --git a/src/ui/handlers/command-ui-handler.ts b/src/ui/handlers/command-ui-handler.ts index c1a977e5a72..c5af2aa4434 100644 --- a/src/ui/handlers/command-ui-handler.ts +++ b/src/ui/handlers/command-ui-handler.ts @@ -1,3 +1,4 @@ +import { MAX_TERAS_PER_ARENA } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { getPokemonNameWithAffix } from "#app/messages"; import { getTypeRgb } from "#data/type"; @@ -198,9 +199,12 @@ export class CommandUiHandler extends UiHandler { canTera(): boolean { const activePokemon = globalScene.getField()[this.fieldIndex]; + const currentTeras = globalScene.arena.playerTerasUsed; const canTera = activePokemon.isPlayer() && canTerastallize(activePokemon); - const plannedTera = globalScene.currentBattle.preTurnCommands[0]?.command === Command.TERA && this.fieldIndex > 0; - return canTera && !plannedTera; + const plannedTera = +( + globalScene.currentBattle.preTurnCommands[0]?.command === Command.TERA && this.fieldIndex > 0 + ); + return canTera && currentTeras + plannedTera < MAX_TERAS_PER_ARENA; } toggleTeraButton() { diff --git a/src/utils/pokemon-utils.ts b/src/utils/pokemon-utils.ts index f8d0308d468..53e2da0aac3 100644 --- a/src/utils/pokemon-utils.ts +++ b/src/utils/pokemon-utils.ts @@ -1,3 +1,4 @@ +import { MAX_TERAS_PER_ARENA } from "#app/constants"; import { globalScene } from "#app/global-scene"; import { POKERUS_STARTER_COUNT, speciesStarterCosts } from "#balance/starters"; import { allSpecies } from "#data/data-lists"; @@ -136,7 +137,7 @@ export function getPokemonSpeciesForm(species: SpeciesId, formIndex: number): Po * Should really only be called with an enemy Pokémon, but will technically work with any Pokémon. * * @privateRemarks - * Assumes that Pokémon with no trainer ever tera, so this must be changed if + * Assumes that Pokémon without a trainer will never tera, so this must be changed if * a wild Pokémon is allowed to tera, e.g. for a Mystery Encounter. */ export function willTerastallize(pokemon: Pokemon): boolean { @@ -150,13 +151,22 @@ export function willTerastallize(pokemon: Pokemon): boolean { } /** - * Determine whether a player Pokémon can Terastallize + * Determine whether the Pokémon's species is tera capable, and that the player has acquired the tera orb. + * @param pokemon - The Pokémon to check + * @returns Whether + */ +export function canSpeciesTera(pokemon: Pokemon): boolean { + const hasTeraMod = globalScene.findModifier(modifier => modifier.is("TerastallizeAccessModifier")) != null; + const isBlockedForm = pokemon.isMega() || pokemon.isMax() || pokemon.hasSpecies(SpeciesId.NECROZMA, "ultra"); + return hasTeraMod && !isBlockedForm; +} + +/** + * Same as {@linkcode canSpeciesTera}, but also checks that the player has not already used their tera in the arena * @param pokemon - The Pokémon to check * @returns Whether the Pokémon can Terastallize */ export function canTerastallize(pokemon: PlayerPokemon): boolean { - const hasTeraMod = globalScene.findModifier(modifier => modifier.is("TerastallizeAccessModifier")) != null; - const isBlockedForm = pokemon.isMega() || pokemon.isMax() || pokemon.hasSpecies(SpeciesId.NECROZMA, "ultra"); - const currentTeras = globalScene.arena.playerTerasUsed === 0; - return hasTeraMod && !isBlockedForm && currentTeras; + const hasAvailableTeras = globalScene.arena.playerTerasUsed < MAX_TERAS_PER_ARENA; + return hasAvailableTeras && canSpeciesTera(pokemon); } From b057314f90a0e1c7b28767bbd4e998bfa1e98b73 Mon Sep 17 00:00:00 2001 From: Sirz Benjie <142067137+SirzBenjie@users.noreply.github.com> Date: Mon, 22 Sep 2025 20:03:40 -0500 Subject: [PATCH 6/6] docs: clarify `canTerastallize` --- src/utils/pokemon-utils.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/utils/pokemon-utils.ts b/src/utils/pokemon-utils.ts index 53e2da0aac3..9de9106169c 100644 --- a/src/utils/pokemon-utils.ts +++ b/src/utils/pokemon-utils.ts @@ -162,7 +162,10 @@ export function canSpeciesTera(pokemon: Pokemon): boolean { } /** - * Same as {@linkcode canSpeciesTera}, but also checks that the player has not already used their tera in the arena + * Same as {@linkcode canSpeciesTera}, but also checks that the player has not already used their tera in the arena. + * + * @remarks + * ⚠️ This does not account for tera commands that may be pending, so this should not be used during command selection! * @param pokemon - The Pokémon to check * @returns Whether the Pokémon can Terastallize */