From 9cc1b177454baf60738885061793486dde111bec Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Wed, 19 Feb 2025 00:02:39 -0800 Subject: [PATCH 1/6] [Bug] Fix moves/abilities which modify abilities (#5146) * Add setAbility method to pokemon.ts * Edit SwitchAbilitiesAttr to use setAbility * Change AbilityGiveAttr to use setAbility * Rename setAbility to be more accurate * Fix AbilityCopyAttr * Fix AbilityChangeAttr * Fix Transform * Fix imposter * Fix PostDefendAbilityGiveAbAttr * Actually fix imposter * Actually fix transform * Fix CopyFaintedAllyAbilityAbAttr * Fix Trace * Fix PostDefendAbilitySwapAbAttr * Add tests for skill swap * Add tests for doodle * Add tests for entrainment * Add tests for role play * Add test for simple beam * Add test for transform * Add test for imposter * Add tests for mummy * Add tests for trace * Add tests for wandering spirit * Consider legendary weather when changing ability * Ensure that passives are not (re)applied when main abilities change * Add general ability swap test cases * Fix test name * Add NoMidTurnActivationAttr * Remove NoMidTurnActivationAttr from illusion * Remove extraneous call to triggerWeatherBasedFormChanges * Fix primal weather clearing * Change "MidTurn" to "OnGain" * Change NoOnGainActivationAttr to a field in PostSummonAbAttr * Add passive support * Remove redundant parentheses Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --------- Co-authored-by: damocleas Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/ability.ts | 142 +++++++++++++------- src/data/move.ts | 28 ++-- src/field/pokemon.ts | 18 ++- src/test/abilities/imposter.test.ts | 11 ++ src/test/abilities/mummy.test.ts | 52 +++++++ src/test/abilities/trace.test.ts | 53 ++++++++ src/test/abilities/wandering_spirit.test.ts | 65 +++++++++ src/test/battle/ability_swap.test.ts | 67 +++++++++ src/test/moves/doodle.test.ts | 70 ++++++++++ src/test/moves/entrainment.test.ts | 53 ++++++++ src/test/moves/role_play.test.ts | 53 ++++++++ src/test/moves/simple_beam.test.ts | 42 ++++++ src/test/moves/skill_swap.test.ts | 56 ++++++++ src/test/moves/transform.test.ts | 12 ++ 14 files changed, 661 insertions(+), 61 deletions(-) create mode 100644 src/test/abilities/mummy.test.ts create mode 100644 src/test/abilities/trace.test.ts create mode 100644 src/test/abilities/wandering_spirit.test.ts create mode 100644 src/test/battle/ability_swap.test.ts create mode 100644 src/test/moves/doodle.test.ts create mode 100644 src/test/moves/entrainment.test.ts create mode 100644 src/test/moves/role_play.test.ts create mode 100644 src/test/moves/simple_beam.test.ts create mode 100644 src/test/moves/skill_swap.test.ts diff --git a/src/data/ability.ts b/src/data/ability.ts index bf3b04e1f63..940b5f0c7d7 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -1044,9 +1044,9 @@ export class PostDefendAbilitySwapAbAttr extends PostDefendAbAttr { if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnswappableAbilityAbAttr) && !move.hitsSubstitute(attacker, pokemon)) { if (!simulated) { - const tempAbilityId = attacker.getAbility().id; - attacker.summonData.ability = pokemon.getAbility().id; - pokemon.summonData.ability = tempAbilityId; + const tempAbility = attacker.getAbility(); + attacker.setTempAbility(pokemon.getAbility()); + pokemon.setTempAbility(tempAbility); } return true; } @@ -1071,7 +1071,7 @@ export class PostDefendAbilityGiveAbAttr extends PostDefendAbAttr { if (move.checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon) && !attacker.getAbility().hasAttr(UnsuppressableAbilityAbAttr) && !attacker.getAbility().hasAttr(PostDefendAbilityGiveAbAttr) && !move.hitsSubstitute(attacker, pokemon)) { if (!simulated) { - attacker.summonData.ability = this.ability; + attacker.setTempAbility(allAbilities[this.ability]); } return true; @@ -1908,8 +1908,8 @@ export class CopyFaintedAllyAbilityAbAttr extends PostKnockOutAbAttr { applyPostKnockOut(pokemon: Pokemon, passive: boolean, simulated: boolean, knockedOut: Pokemon, args: any[]): boolean | Promise { if (pokemon.isPlayer() === knockedOut.isPlayer() && !knockedOut.getAbility().hasAttr(UncopiableAbilityAbAttr)) { if (!simulated) { - pokemon.summonData.ability = knockedOut.getAbility().id; globalScene.queueMessage(i18next.t("abilityTriggers:copyFaintedAllyAbility", { pokemonNameWithAffix: getPokemonNameWithAffix(knockedOut), abilityName: allAbilities[knockedOut.getAbility().id].name })); + pokemon.setTempAbility(knockedOut.getAbility()); } return true; } @@ -1993,6 +1993,21 @@ export class PostIntimidateStatStageChangeAbAttr extends AbAttr { * @see {@linkcode applyPostSummon()} */ export class PostSummonAbAttr extends AbAttr { + /** Should the ability activate when gained in battle? This will almost always be true */ + private activateOnGain: boolean; + + constructor(showAbility: boolean = true, activateOnGain: boolean = true) { + super(showAbility); + this.activateOnGain = activateOnGain; + } + + /** + * @returns Whether the ability should activate when gained in battle + */ + shouldActivateOnGain(): boolean { + return this.activateOnGain; + } + /** * Applies ability post summon (after switching in) * @param pokemon {@linkcode Pokemon} with this ability @@ -2330,7 +2345,7 @@ export class PostSummonCopyAbilityAbAttr extends PostSummonAbAttr { if (!simulated) { this.target = target!; this.targetAbilityName = allAbilities[target!.getAbility().id].name; - pokemon.summonData.ability = target!.getAbility().id; + pokemon.setTempAbility(target!.getAbility()); setAbilityRevealed(target!); pokemon.updateInfo(); } @@ -2427,7 +2442,7 @@ export class PostSummonCopyAllyStatsAbAttr extends PostSummonAbAttr { */ export class PostSummonTransformAbAttr extends PostSummonAbAttr { constructor() { - super(true); + super(true, false); } async applyPostSummon(pokemon: Pokemon, _passive: boolean, simulated: boolean, _args: any[]): Promise { @@ -2462,7 +2477,6 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { } pokemon.summonData.speciesForm = target.getSpeciesForm(); - pokemon.summonData.ability = target.getAbility().id; pokemon.summonData.gender = target.getGender(); // Copy all stats (except HP) @@ -2492,6 +2506,8 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { promises.push(pokemon.loadAssets(false).then(() => { pokemon.playAnim(); pokemon.updateInfo(); + // If the new ability activates immediately, it needs to happen after all the transform animations + pokemon.setTempAbility(target.getAbility()); })); await Promise.all(promises); @@ -4852,53 +4868,72 @@ async function applyAbAttrsInternal( showAbilityInstant: boolean = false, simulated: boolean = false, messages: string[] = [], + gainedMidTurn: boolean = false ) { for (const passive of [ false, true ]) { - if (!pokemon?.canApplyAbility(passive) || (passive && pokemon.getPassiveAbility().id === pokemon.getAbility().id)) { + if (!pokemon?.canApplyAbility(passive) || (passive && (pokemon.getPassiveAbility().id === pokemon.getAbility().id))) { continue; } - const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); - for (const attr of ability.getAttrs(attrType)) { - const condition = attr.getCondition(); - if (condition && !condition(pokemon)) { - continue; - } - - globalScene.setPhaseQueueSplice(); - - let result = applyFunc(attr, passive); - // TODO Remove this when promises get reworked - if (result instanceof Promise) { - result = await result; - } - if (result) { - if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) { - pokemon.summonData.abilitiesApplied.push(ability.id); - } - if (pokemon.battleData && !simulated && !pokemon.battleData.abilitiesApplied.includes(ability.id)) { - pokemon.battleData.abilitiesApplied.push(ability.id); - } - if (attr.showAbility && !simulated) { - if (showAbilityInstant) { - globalScene.abilityBar.showAbility(pokemon, passive); - } else { - queueShowAbility(pokemon, passive); - } - } - const message = attr.getTriggerMessage(pokemon, ability.name, args); - if (message) { - if (!simulated) { - globalScene.queueMessage(message); - } - } - messages.push(message!); - } - } + applySingleAbAttrs(pokemon, passive, attrType, applyFunc, args, gainedMidTurn, simulated, showAbilityInstant, messages); globalScene.clearPhaseQueueSplice(); } } +async function applySingleAbAttrs( + pokemon: Pokemon, + passive: boolean, + attrType: Constructor, + applyFunc: AbAttrApplyFunc, + args: any[], + gainedMidTurn: boolean = false, + simulated: boolean = false, + showAbilityInstant: boolean = false, + messages: string[] = [] +) { + const ability = passive ? pokemon.getPassiveAbility() : pokemon.getAbility(); + if (gainedMidTurn && ability.getAttrs(attrType).some(attr => attr instanceof PostSummonAbAttr && !attr.shouldActivateOnGain())) { + return; + } + + for (const attr of ability.getAttrs(attrType)) { + const condition = attr.getCondition(); + if (condition && !condition(pokemon)) { + continue; + } + + globalScene.setPhaseQueueSplice(); + + let result = applyFunc(attr, passive); + // TODO Remove this when promises get reworked + if (result instanceof Promise) { + result = await result; + } + if (result) { + if (pokemon.summonData && !pokemon.summonData.abilitiesApplied.includes(ability.id)) { + pokemon.summonData.abilitiesApplied.push(ability.id); + } + if (pokemon.battleData && !simulated && !pokemon.battleData.abilitiesApplied.includes(ability.id)) { + pokemon.battleData.abilitiesApplied.push(ability.id); + } + if (attr.showAbility && !simulated) { + if (showAbilityInstant) { + globalScene.abilityBar.showAbility(pokemon, passive); + } else { + queueShowAbility(pokemon, passive); + } + } + const message = attr.getTriggerMessage(pokemon, ability.name, args); + if (message) { + if (!simulated) { + globalScene.queueMessage(message); + } + } + messages.push(message!); + } + } +} + class ForceSwitchOutHelper { constructor(private switchType: SwitchType) {} @@ -5285,6 +5320,21 @@ export function applyPostItemLostAbAttrs(attrType: Constructor(attrType, pokemon, (attr, passive) => attr.applyPostItemLost(pokemon, simulated, args), args); } +/** + * Applies abilities when they become active mid-turn (ability switch) + * + * Ignores passives as they don't change and shouldn't be reapplied when main abilities change + */ +export function applyOnGainAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void { + applySingleAbAttrs(pokemon, passive, PostSummonAbAttr, (attr, passive) => attr.applyPostSummon(pokemon, passive, simulated, args), args, true, simulated); +} + +/** + * Clears primal weather during the turn if {@linkcode pokemon}'s ability corresponds to one + */ +export function applyOnLoseClearWeatherAbAttrs(pokemon: Pokemon, passive: boolean = false, simulated: boolean = false, ...args: any[]): void { + applySingleAbAttrs(pokemon, passive, PreLeaveFieldClearWeatherAbAttr, (attr, passive) => attr.applyPreLeaveField(pokemon, passive, simulated, [ ...args, true ]), args, true, simulated); +} function queueShowAbility(pokemon: Pokemon, passive: boolean): void { globalScene.unshiftPhase(new ShowAbilityPhase(pokemon.id, passive)); globalScene.clearPhaseQueueSplice(); diff --git a/src/data/move.ts b/src/data/move.ts index 016dae6ab0d..a81bbd4940e 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -7521,11 +7521,11 @@ export class AbilityChangeAttr extends MoveEffectAttr { const moveTarget = this.selfTarget ? user : target; - moveTarget.summonData.ability = this.ability; - globalScene.triggerPokemonFormChange(moveTarget, SpeciesFormChangeRevertWeatherFormTrigger); - globalScene.queueMessage(i18next.t("moveTriggers:acquiredAbility", { pokemonName: getPokemonNameWithAffix((this.selfTarget ? user : target)), abilityName: allAbilities[this.ability].name })); + moveTarget.setTempAbility(allAbilities[this.ability]); + globalScene.triggerPokemonFormChange(moveTarget, SpeciesFormChangeRevertWeatherFormTrigger); + return true; } @@ -7548,13 +7548,13 @@ export class AbilityCopyAttr extends MoveEffectAttr { return false; } - user.summonData.ability = target.getAbility().id; - globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(user), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name })); + user.setTempAbility(target.getAbility()); + if (this.copyToPartner && globalScene.currentBattle?.double && user.getAlly().hp) { - user.getAlly().summonData.ability = target.getAbility().id; globalScene.queueMessage(i18next.t("moveTriggers:copiedTargetAbility", { pokemonName: getPokemonNameWithAffix(user.getAlly()), targetName: getPokemonNameWithAffix(target), abilityName: allAbilities[target.getAbility().id].name })); + user.getAlly().setTempAbility(target.getAbility()); } return true; @@ -7585,10 +7585,10 @@ export class AbilityGiveAttr extends MoveEffectAttr { return false; } - target.summonData.ability = user.getAbility().id; - globalScene.queueMessage(i18next.t("moveTriggers:acquiredAbility", { pokemonName: getPokemonNameWithAffix(target), abilityName: allAbilities[user.getAbility().id].name })); + target.setTempAbility(user.getAbility()); + return true; } @@ -7603,15 +7603,14 @@ export class SwitchAbilitiesAttr extends MoveEffectAttr { return false; } - const tempAbilityId = user.getAbility().id; - user.summonData.ability = target.getAbility().id; - target.summonData.ability = tempAbilityId; + const tempAbility = user.getAbility(); globalScene.queueMessage(i18next.t("moveTriggers:swappedAbilitiesWithTarget", { pokemonName: getPokemonNameWithAffix(user) })); + + user.setTempAbility(target.getAbility()); + target.setTempAbility(tempAbility); // Swaps Forecast/Flower Gift from Castform/Cherrim globalScene.arena.triggerWeatherBasedFormChangesToNormal(); - // Swaps Forecast/Flower Gift to Castform/Cherrim (edge case) - globalScene.arena.triggerWeatherBasedFormChanges(); return true; } @@ -7690,7 +7689,6 @@ export class TransformAttr extends MoveEffectAttr { const promises: Promise[] = []; user.summonData.speciesForm = target.getSpeciesForm(); - user.summonData.ability = target.getAbility().id; user.summonData.gender = target.getGender(); // Power Trick's effect will not preserved after using Transform @@ -7723,6 +7721,8 @@ export class TransformAttr extends MoveEffectAttr { promises.push(user.loadAssets(false).then(() => { user.playAnim(); user.updateInfo(); + // If the new ability activates immediately, it needs to happen after all the transform animations + user.setTempAbility(target.getAbility()); })); await Promise.all(promises); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 1377f11187b..714f1ec7026 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -64,7 +64,7 @@ import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoo import { WeatherType } from "#enums/weather-type"; import { ArenaTagSide, NoCritTag, WeakenMoveScreenTag } from "#app/data/arena-tag"; import type { Ability, AbAttr } from "#app/data/ability"; -import { StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs } from "#app/data/ability"; +import { StatMultiplierAbAttr, BlockCritAbAttr, BonusCritAbAttr, BypassBurnDamageReductionAbAttr, FieldPriorityMoveImmunityAbAttr, IgnoreOpponentStatStagesAbAttr, MoveImmunityAbAttr, PreDefendFullHpEndureAbAttr, ReceivedMoveDamageMultiplierAbAttr, StabBoostAbAttr, StatusEffectImmunityAbAttr, TypeImmunityAbAttr, WeightMultiplierAbAttr, allAbilities, applyAbAttrs, applyStatMultiplierAbAttrs, applyPreApplyBattlerTagAbAttrs, applyPreAttackAbAttrs, applyPreDefendAbAttrs, applyPreSetStatusAbAttrs, UnsuppressableAbilityAbAttr, SuppressFieldAbilitiesAbAttr, NoFusionAbilityAbAttr, MultCritAbAttr, IgnoreTypeImmunityAbAttr, DamageBoostAbAttr, IgnoreTypeStatusEffectImmunityAbAttr, ConditionalCritAbAttr, applyFieldStatMultiplierAbAttrs, FieldMultiplyStatAbAttr, AddSecondStrikeAbAttr, UserFieldStatusEffectImmunityAbAttr, UserFieldBattlerTagImmunityAbAttr, BattlerTagImmunityAbAttr, MoveTypeChangeAbAttr, FullHpResistTypeAbAttr, applyCheckTrappedAbAttrs, CheckTrappedAbAttr, PostSetStatusAbAttr, applyPostSetStatusAbAttrs, InfiltratorAbAttr, AlliedFieldDamageReductionAbAttr, PostDamageAbAttr, applyPostDamageAbAttrs, CommanderAbAttr, applyPostItemLostAbAttrs, PostItemLostAbAttr, applyOnGainAbAttrs, PreLeaveFieldAbAttr, applyPreLeaveFieldAbAttrs, applyOnLoseClearWeatherAbAttrs } from "#app/data/ability"; import type PokemonData from "#app/system/pokemon-data"; import { BattlerIndex } from "#app/battle"; import { Mode } from "#app/ui/ui"; @@ -1481,6 +1481,22 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { return abilityAttrs; } + /** + * Sets the {@linkcode Pokemon}'s ability and activates it if it normally activates on summon + * + * Also clears primal weather if it is from the ability being changed + * @param ability New Ability + */ + public setTempAbility(ability: Ability, passive: boolean = false): void { + applyOnLoseClearWeatherAbAttrs(this, passive); + if (passive) { + this.summonData.passiveAbility = ability.id; + } else { + this.summonData.ability = ability.id; + } + applyOnGainAbAttrs(this, passive); + } + /** * Checks if a pokemon has a passive either from: * - bought with starter candy diff --git a/src/test/abilities/imposter.test.ts b/src/test/abilities/imposter.test.ts index 3445b3b322c..98f30c60505 100644 --- a/src/test/abilities/imposter.test.ts +++ b/src/test/abilities/imposter.test.ts @@ -116,4 +116,15 @@ describe("Abilities - Imposter", () => { } }); }); + + it("should activate its ability if it copies one that activates on summon", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE); + + await game.classicMode.startBattle([ Species.DITTO ]); + + game.move.select(Moves.TACKLE); + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); }); diff --git a/src/test/abilities/mummy.test.ts b/src/test/abilities/mummy.test.ts new file mode 100644 index 00000000000..bf93cdcf61d --- /dev/null +++ b/src/test/abilities/mummy.test.ts @@ -0,0 +1,52 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Mummy", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.MUMMY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + }); + + it("should set the enemy's ability to mummy when hit by a contact move", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.MUMMY); + }); + + it("should not change the enemy's ability hit by a non-contact move", async () => { + game.override.enemyMoveset(Moves.EARTHQUAKE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); +}); diff --git a/src/test/abilities/trace.test.ts b/src/test/abilities/trace.test.ts new file mode 100644 index 00000000000..ffa76f59769 --- /dev/null +++ b/src/test/abilities/trace.test.ts @@ -0,0 +1,53 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Trace", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.TRACE) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should copy the opponent's ability", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should activate a copied post-summon ability", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); diff --git a/src/test/abilities/wandering_spirit.test.ts b/src/test/abilities/wandering_spirit.test.ts new file mode 100644 index 00000000000..4bc9298353f --- /dev/null +++ b/src/test/abilities/wandering_spirit.test.ts @@ -0,0 +1,65 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Abilities - Wandering Spirit", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.WANDERING_SPIRIT) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE); + }); + + it("should exchange abilities when hit with a contact move", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.WANDERING_SPIRIT); + }); + + it("should not exchange abilities when hit with a non-contact move", async () => { + game.override.enemyMoveset(Moves.EARTHQUAKE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.WANDERING_SPIRIT); + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should activate post-summon abilities", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); diff --git a/src/test/battle/ability_swap.test.ts b/src/test/battle/ability_swap.test.ts new file mode 100644 index 00000000000..9ce7a36c16e --- /dev/null +++ b/src/test/battle/ability_swap.test.ts @@ -0,0 +1,67 @@ +import { allAbilities } from "#app/data/ability"; +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Test Ability Swapping", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should activate post-summon abilities", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + game.scene.getPlayerPokemon()?.setTempAbility(allAbilities[Abilities.INTIMIDATE]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); + + it("should remove primal weather when the setter's ability is removed", async () => { + game.override.ability(Abilities.DESOLATE_LAND); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + game.scene.getPlayerPokemon()?.setTempAbility(allAbilities[Abilities.BALL_FETCH]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.arena.weather?.weatherType).toBeUndefined(); + }); + + it("should not activate passive abilities", async () => { + game.override.passiveAbility(Abilities.INTREPID_SWORD); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SPLASH); + game.scene.getPlayerPokemon()?.setTempAbility(allAbilities[Abilities.BALL_FETCH]); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(1); // would be 2 if passive activated again + }); +}); diff --git a/src/test/moves/doodle.test.ts b/src/test/moves/doodle.test.ts new file mode 100644 index 00000000000..258abda392a --- /dev/null +++ b/src/test/moves/doodle.test.ts @@ -0,0 +1,70 @@ +import { BattlerIndex } from "#app/battle"; +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Doodle", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.DOODLE ]) + .ability(Abilities.ADAPTABILITY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should copy the opponent's ability in singles", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.DOODLE); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should copy the opponent's ability to itself and its ally in doubles", async () => { + game.override.battleType("double"); + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + game.move.select(Moves.DOODLE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerField()[0].getAbility().id).toBe(Abilities.BALL_FETCH); + expect(game.scene.getPlayerField()[1].getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should activate post-summon abilities", async () => { + game.override.battleType("double") + .enemyAbility(Abilities.INTIMIDATE); + + await game.classicMode.startBattle([ Species.FEEBAS, Species.MAGIKARP ]); + + game.move.select(Moves.DOODLE, 0, BattlerIndex.ENEMY); + game.move.select(Moves.SPLASH, 1); + await game.phaseInterceptor.to("BerryPhase"); + + // Enemies should have been intimidated twice + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-2); + }); +}); diff --git a/src/test/moves/entrainment.test.ts b/src/test/moves/entrainment.test.ts new file mode 100644 index 00000000000..5d0991c8dfd --- /dev/null +++ b/src/test/moves/entrainment.test.ts @@ -0,0 +1,53 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Entrainment", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.ENTRAINMENT ]) + .ability(Abilities.ADAPTABILITY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("gives its ability to the target", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ENTRAINMENT); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.ADAPTABILITY); + }); + + it("should activate post-summon abilities", async () => { + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ENTRAINMENT); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); diff --git a/src/test/moves/role_play.test.ts b/src/test/moves/role_play.test.ts new file mode 100644 index 00000000000..a37f4faac9b --- /dev/null +++ b/src/test/moves/role_play.test.ts @@ -0,0 +1,53 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Role Play", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.ROLE_PLAY ]) + .ability(Abilities.ADAPTABILITY) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should set the user's ability to the target's ability", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ROLE_PLAY); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + }); + + it("should activate post-summon abilities", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.ROLE_PLAY); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); diff --git a/src/test/moves/simple_beam.test.ts b/src/test/moves/simple_beam.test.ts new file mode 100644 index 00000000000..b4566669e8d --- /dev/null +++ b/src/test/moves/simple_beam.test.ts @@ -0,0 +1,42 @@ +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Simple Beam", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.SIMPLE_BEAM ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("sets the target's ability to simple", async () => { + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SIMPLE_BEAM); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.SIMPLE); + }); +}); diff --git a/src/test/moves/skill_swap.test.ts b/src/test/moves/skill_swap.test.ts new file mode 100644 index 00000000000..9c0f0b75ade --- /dev/null +++ b/src/test/moves/skill_swap.test.ts @@ -0,0 +1,56 @@ +import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; + +describe("Moves - Skill Swap", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + + beforeAll(() => { + phaserGame = new Phaser.Game({ + type: Phaser.HEADLESS, + }); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + }); + + beforeEach(() => { + game = new GameManager(phaserGame); + game.override + .moveset([ Moves.SPLASH, Moves.SKILL_SWAP ]) + .ability(Abilities.BALL_FETCH) + .battleType("single") + .disableCrits() + .enemySpecies(Species.MAGIKARP) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.SPLASH); + }); + + it("should swap the two abilities", async () => { + game.override.ability(Abilities.ADAPTABILITY); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getPlayerPokemon()?.getAbility().id).toBe(Abilities.BALL_FETCH); + expect(game.scene.getEnemyPokemon()?.getAbility().id).toBe(Abilities.ADAPTABILITY); + }); + + it("should activate post-summon abilities", async () => { + game.override.ability(Abilities.INTIMIDATE); + await game.classicMode.startBattle([ Species.FEEBAS ]); + + game.move.select(Moves.SKILL_SWAP); + await game.phaseInterceptor.to("BerryPhase"); + + // player atk should be -1 after opponent gains intimidate and it activates + expect(game.scene.getPlayerPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); +}); diff --git a/src/test/moves/transform.test.ts b/src/test/moves/transform.test.ts index adb97b42af7..ffe935aa61b 100644 --- a/src/test/moves/transform.test.ts +++ b/src/test/moves/transform.test.ts @@ -116,4 +116,16 @@ describe("Moves - Transform", () => { } }); }); + + it("should activate its ability if it copies one that activates on summon", async () => { + game.override.enemyAbility(Abilities.INTIMIDATE) + .ability(Abilities.BALL_FETCH); + + await game.classicMode.startBattle([ Species.DITTO ]); + game.move.select(Moves.TRANSFORM); + + await game.phaseInterceptor.to("BerryPhase"); + + expect(game.scene.getEnemyPokemon()?.getStatStage(Stat.ATK)).toBe(-1); + }); }); From 7ec0dba74b82060852c6e17df955884382990966 Mon Sep 17 00:00:00 2001 From: "Amani H." <109637146+xsn34kzx@users.noreply.github.com> Date: Wed, 19 Feb 2025 17:19:02 -0500 Subject: [PATCH 2/6] [Bug/Item] Fix Flame Orb Weight Function (#5380) --- src/modifier/modifier-type.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 336cbe67ccc..112a3c9aa35 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1822,7 +1822,7 @@ const modifierPool: ModifierPool = { if (!isHoldingOrb) { const moveset = p.getMoveset(true).filter(m => !isNullOrUndefined(m)).map(m => m.moveId); - const canSetStatus = p.canSetStatus(StatusEffect.TOXIC, true, true, null, true); + const canSetStatus = p.canSetStatus(StatusEffect.BURN, true, true, null, true); // Moves that take advantage of obtaining the actual status effect const hasStatusMoves = [ Moves.FACADE, Moves.PSYCHO_SHIFT ] From a346318f9dd8f9fa3f6d43790fb7122b278afdb5 Mon Sep 17 00:00:00 2001 From: Dean <69436131+emdeann@users.noreply.github.com> Date: Wed, 19 Feb 2025 16:59:54 -0800 Subject: [PATCH 3/6] [Move] Implement Quash (#5049) * Add quash logic for single targets * Multi-squash power * Update MovePhase constructor * Start searching from front of phaseQueue instead of weather * Use findPhase instead of looping to search * Basic test case * Test for failure on a already moved target * Speed order test * Fix speed test comment * Fix ForceLastAttr to properly respect speed order * Respect trick room in quash turn order * Test for respecting TR * Add comments, fix var name * Allow for quashed speed ties * Avoid reapplying if a move is already forced last * Spacing * Quash does fail in a single battle despite this not being documented anywhere * Add move text * Update move.ts (readability) Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> * Use globalScene --------- Co-authored-by: NightKev <34855794+DayKev@users.noreply.github.com> --- src/data/move.ts | 53 ++++++++++++++++++- src/phases/move-phase.ts | 14 ++++- src/test/moves/quash.test.ts | 99 ++++++++++++++++++++++++++++++++++++ 3 files changed, 164 insertions(+), 2 deletions(-) create mode 100644 src/test/moves/quash.test.ts diff --git a/src/data/move.ts b/src/data/move.ts index a81bbd4940e..acc68eb6a26 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -8035,6 +8035,56 @@ export class AfterYouAttr extends MoveEffectAttr { } } +/** + * Move effect to force the target to move last, ignoring priority. + * If applied to multiple targets, they move in speed order after all other moves. + * @extends MoveEffectAttr + */ +export class ForceLastAttr extends MoveEffectAttr { + /** + * Forces the target of this move to move last. + * + * @param user {@linkcode Pokemon} that is using the move. + * @param target {@linkcode Pokemon} that will be forced to move last. + * @param move {@linkcode Move} {@linkcode Moves.QUASH} + * @param _args N/A + * @returns true + */ + override apply(user: Pokemon, target: Pokemon, _move: Move, _args: any[]): boolean { + globalScene.queueMessage(i18next.t("moveTriggers:forceLast", { targetPokemonName: getPokemonNameWithAffix(target) })); + + const targetMovePhase = globalScene.findPhase((phase) => phase.pokemon === target); + if (targetMovePhase && !targetMovePhase.isForcedLast() && globalScene.tryRemovePhase((phase: MovePhase) => phase.pokemon === target)) { + // Finding the phase to insert the move in front of - + // Either the end of the turn or in front of another, slower move which has also been forced last + const prependPhase = globalScene.findPhase((phase) => + [ MovePhase, MoveEndPhase ].every(cls => !(phase instanceof cls)) + || (phase instanceof MovePhase) && phaseForcedSlower(phase, target, !!globalScene.arena.getTag(ArenaTagType.TRICK_ROOM)) + ); + if (prependPhase) { + globalScene.phaseQueue.splice( + globalScene.phaseQueue.indexOf(prependPhase), + 0, + new MovePhase(target, [ ...targetMovePhase.targets ], targetMovePhase.move, false, false, false, true) + ); + } + } + return true; + } +} + +/** Returns whether a {@linkcode MovePhase} has been forced last and the corresponding pokemon is slower than {@linkcode target} */ +const phaseForcedSlower = (phase: MovePhase, target: Pokemon, trickRoom: boolean): boolean => { + let slower: boolean; + // quashed pokemon still have speed ties + if (phase.pokemon.getEffectiveStat(Stat.SPD) === target.getEffectiveStat(Stat.SPD)) { + slower = !!target.randSeedInt(2); + } else { + slower = !trickRoom ? phase.pokemon.getEffectiveStat(Stat.SPD) < target.getEffectiveStat(Stat.SPD) : phase.pokemon.getEffectiveStat(Stat.SPD) > target.getEffectiveStat(Stat.SPD); + } + return phase.isForcedLast() && slower; +}; + const failOnGravityCondition: MoveConditionFunc = (user, target, move) => !globalScene.arena.getTag(ArenaTagType.GRAVITY); const failOnBossCondition: MoveConditionFunc = (user, target, move) => !target.isBossImmune(); @@ -9914,7 +9964,8 @@ export function initMoves() { .attr(RemoveHeldItemAttr, true), new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) .condition(failIfSingleBattle) - .unimplemented(), + .condition((user, target, move) => !target.turnData.acted) + .attr(ForceLastAttr), new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.isTransferable).reduce((v, m) => v + m.stackCount, 0))), new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5) diff --git a/src/phases/move-phase.ts b/src/phases/move-phase.ts index 73468dc1690..c1e4aeaf497 100644 --- a/src/phases/move-phase.ts +++ b/src/phases/move-phase.ts @@ -56,6 +56,7 @@ export class MovePhase extends BattlePhase { protected _targets: BattlerIndex[]; protected followUp: boolean; protected ignorePp: boolean; + protected forcedLast: boolean; protected failed: boolean = false; protected cancelled: boolean = false; protected reflected: boolean = false; @@ -90,7 +91,8 @@ export class MovePhase extends BattlePhase { * @param reflected Indicates that the move was reflected by Magic Coat or Magic Bounce. * Reflected moves cannot be reflected again and will not trigger Dancer. */ - constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false) { + + constructor(pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp: boolean = false, ignorePp: boolean = false, reflected: boolean = false, forcedLast: boolean = false) { super(); this.pokemon = pokemon; @@ -99,6 +101,7 @@ export class MovePhase extends BattlePhase { this.followUp = followUp; this.ignorePp = ignorePp; this.reflected = reflected; + this.forcedLast = forcedLast; } /** @@ -120,6 +123,15 @@ export class MovePhase extends BattlePhase { this.cancelled = true; } + /** + * Shows whether the current move has been forced to the end of the turn + * Needed for speed order, see {@linkcode Moves.QUASH} + * */ + public isForcedLast(): boolean { + return this.forcedLast; + } + + public start(): void { super.start(); diff --git a/src/test/moves/quash.test.ts b/src/test/moves/quash.test.ts new file mode 100644 index 00000000000..3cbe79d7bfe --- /dev/null +++ b/src/test/moves/quash.test.ts @@ -0,0 +1,99 @@ +import { Species } from "#enums/species"; +import { Moves } from "#enums/moves"; +import { Abilities } from "#app/enums/abilities"; +import { BattlerIndex } from "#app/battle"; +import { WeatherType } from "#enums/weather-type"; +import { MoveResult } from "#app/field/pokemon"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { describe, beforeAll, afterEach, beforeEach, it, expect } from "vitest"; + +describe("Moves - Quash", () => { + 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 + .battleType("double") + .enemyLevel(1) + .enemySpecies(Species.SLOWPOKE) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset([ Moves.RAIN_DANCE, Moves.SPLASH ]) + .ability(Abilities.BALL_FETCH) + .moveset([ Moves.QUASH, Moves.SUNNY_DAY, Moves.RAIN_DANCE, Moves.SPLASH ]); + }); + + it("makes the target move last in a turn, ignoring priority", async () => { + await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]); + + game.move.select(Moves.QUASH, 0, BattlerIndex.PLAYER_2); + game.move.select(Moves.SUNNY_DAY, 1); + await game.forceEnemyMove(Moves.SPLASH); + await game.forceEnemyMove(Moves.RAIN_DANCE); + + await game.phaseInterceptor.to("TurnEndPhase", false); + // will be sunny if player_2 moved last because of quash, rainy otherwise + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY); + }); + + it("fails if the target has already moved", async () => { + await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]); + game.move.select(Moves.SPLASH, 0); + game.move.select(Moves.QUASH, 1, BattlerIndex.PLAYER); + + await game.phaseInterceptor.to("MoveEndPhase"); + await game.phaseInterceptor.to("MoveEndPhase"); + + expect(game.scene.getPlayerField()[1].getLastXMoves(1)[0].result).toBe(MoveResult.FAIL); + }); + + it("makes multiple quashed targets move in speed order at the end of the turn", async () => { + game.override.enemySpecies(Species.NINJASK) + .enemyLevel(100); + + await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]); + + // both users are quashed - rattata is slower so sun should be up at end of turn + game.move.select(Moves.RAIN_DANCE, 0); + game.move.select(Moves.SUNNY_DAY, 1); + + await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER_2); + + await game.phaseInterceptor.to("TurnEndPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SUNNY); + }); + + it("respects trick room", async () => { + game.override.enemyMoveset([ Moves.RAIN_DANCE, Moves.SPLASH, Moves.TRICK_ROOM ]); + + await game.classicMode.startBattle([ Species.ACCELGOR, Species.RATTATA ]); + game.move.select(Moves.SPLASH, 0); + game.move.select(Moves.SPLASH, 1); + + await game.forceEnemyMove(Moves.TRICK_ROOM); + await game.forceEnemyMove(Moves.SPLASH); + await game.phaseInterceptor.to("TurnInitPhase"); + // both users are quashed - accelgor should move last w/ TR so rain should be up at end of turn + game.move.select(Moves.RAIN_DANCE, 0); + game.move.select(Moves.SUNNY_DAY, 1); + + await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.QUASH, BattlerIndex.PLAYER_2); + + await game.phaseInterceptor.to("TurnEndPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.RAIN); + }); + +}); From c3045b58b6dca55cb6d95cfcac731dead6da39e0 Mon Sep 17 00:00:00 2001 From: Lugiad <2070109+Adri1@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:06:12 +0100 Subject: [PATCH 4/6] [Localization] Localizable Lv UP pop-up bar (#5336) * Update party-exp-bar.ts localizable * Update party-exp-bar.ts --- src/ui/party-exp-bar.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/ui/party-exp-bar.ts b/src/ui/party-exp-bar.ts index 75970a5908b..b9da9320fcb 100644 --- a/src/ui/party-exp-bar.ts +++ b/src/ui/party-exp-bar.ts @@ -1,6 +1,7 @@ import { globalScene } from "#app/global-scene"; import type Pokemon from "../field/pokemon"; import { TextStyle, addTextObject } from "./text"; +import i18next from "i18next"; export default class PartyExpBar extends Phaser.GameObjects.Container { private bg: Phaser.GameObjects.NineSlice; @@ -43,9 +44,9 @@ export default class PartyExpBar extends Phaser.GameObjects.Container { // if we want to only display the level in the small frame if (showOnlyLevelUp) { if (newLevel > 200) { // if the level is greater than 200, we only display Lv. UP - this.expText.setText("Lv. UP"); + this.expText.setText(i18next.t("battleScene:levelUp")); } else { // otherwise we display Lv. Up and the new level - this.expText.setText(`Lv. UP: ${newLevel.toString()}`); + this.expText.setText(i18next.t("battleScene:levelUpWithLevel", { level: newLevel })); } } else { // if we want to display the exp From 3a4c8eb92e2f1972b749bbfe5e37fa62e9f80d45 Mon Sep 17 00:00:00 2001 From: Lugiad <2070109+Adri1@users.noreply.github.com> Date: Thu, 20 Feb 2025 16:08:48 +0100 Subject: [PATCH 5/6] [UX/UI] [Localization] Summary removed Lv and ID No. as image texts (#5361) * summary-ui-handler.ts Lv. and ID No. Text * Remove Lv and ID No. * Remove Lv and ID No. --- public/images/ui/legacy/summary_bg.png | Bin 4217 -> 2696 bytes public/images/ui/legacy/summary_profile.png | Bin 1562 -> 2589 bytes public/images/ui/summary_bg.png | Bin 2126 -> 3457 bytes public/images/ui/summary_profile.png | Bin 1673 -> 2935 bytes src/ui/summary-ui-handler.ts | 6 +++--- 5 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/images/ui/legacy/summary_bg.png b/public/images/ui/legacy/summary_bg.png index e4da2dd5da235dbded64b2c036088bfb6125ba9e..c482b5a1bf1d946e8f2a242bb2e03b86de6d5b60 100644 GIT binary patch literal 2696 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 BWC8#H diff --git a/public/images/ui/legacy/summary_profile.png b/public/images/ui/legacy/summary_profile.png index 1d184023ca8f77c937883b6ca5453140635bee2c..77d59dac1775bbcff8783be7657be197d81419b9 100644 GIT binary patch literal 2589 zcmbVO3s4hh9#1V!Me5^p%mLbC8|sOp$?hg0Br5@dP=XqCwhr<9JCW6{_BbN7{0>PR-1h-Tn6W z{r>;o|26yOp0JP=%iWi=SgaMHa@k(y+QF=nWlNa<atJv<_Yj#2DF)TDujYDg8vvlxsFn#J<+a zq%a0yI$WM%!ooAcBGefvYKey9?+5r=5Qe~j(I{Xs=m`q3NIA2-2y<`W206eigieui z0_+BX7{wkSh%{k!$LmbBjLd!NWvFx1^5u`142F^EZ{;g0*MfT1epJE7;7et z7TGI%ecl%HB<1L6+K7Ol*=**S1w7J}48jtL1cdk?pU-6wTq>QQQ45!#JQo;b7^ODh zMj9sxz|M%O$W&U&VLY9aU@$sp32Ht}Ou#@3Y6M{(WS2Ax)TkXeW2#9%Yh0rSF+FC$ z2%2KBumfwsC^|Tekue{Tr=qEdbR!1t#V9h> zq{f2N7&krbYK%yb2}5bp6hV^ug+%RHa2W^+0=%MeLPMIV%|C%-GL*)o96rSN=0bcf z>>U9M5N|O8i?-O42hBkhqz2cfFM|3+zzj}^@P$8vG6kbSY4n+3jT+ICCIiaI#SLgO z1{#TE4&VqP5=81r6Jwb1PB80*LV<)56pa#UEL0}tFp7CNu0dc-1xv&dlq=8(#9Wa| z$mgOWF{4I=iM1*W;)}HWh4V5}oodhP!gX*l!S_;W{Kj@GQh7}8%!_Z*BOwX_*EVFAfZw4PB+ zAmch{@4-10;9`NZ?|#y+3vgx#*l!)vhk0@-N7lqX0trB8D6QpS-DkeaHV+;y+1-~_h#pP+fIRW(YrDeUQ5FE5 ziFtp_8`*ioH_7R>W$>EAuFipZPOrXhUF{igG#_Y`uU0K=mrkq8hJ4eDr7owfo4Ks8M3t7XKP+Ds-u92Kssqh` znvU&y|HPHE?tZuLn=IRw{J$@;qb`1}^*5woPWD?Zm+tJ?SXVpQ?Hab$Z?taU6HnK5 zH(ojPhq+a^?$yQKFB0Wzc3Q)%=$xl(U4&i;!Ee}(DM+f~(k=h`+u_T%u3I1SJTkdw zioGhk!~b1r$X8y@ip{&q*%dme%IrV;&Zq1jE3ooE+xwIM9xoa`?VX!ZIP(-Y%0 z#J1iT>JpTHR~#R*a??!7t;$Sc(~+*!?Rokn>%drP%FO3Q0j0{}YvuVmTSdL@K1{vG zhw3Je-5&1=DK7b-Jz;pXHQckjQL^^sY}-wF?IX^LE&6|+Ogq@peLel9J6~s<6$Ct4 za?>w+sHxg@V_R&bW$@JT+?2F$T`v2VJnT%nxjv?PKy@tPN&4G1wvrxvTpw#*^`K-w z{MVXW)#r-}E}r-3mY#X#>gI2lDxPvvQa$)_;W?yfQ`du_Ril0TP=BI%U*P>|ZJDzv z#r^c!zQ3e7ckmjAFD%_1*cdD&iznmz9%eUw_r~vJMUl>?N1y5HyS~qSM&6$zlfT>% zj&g`}&ONd8Mc&7gZuF0tJ|(tix~}1_QPTR>$P;(5*HkX|cDelnE;KkyRvwsS{U4OD B*CGG_ literal 1562 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 Gzu{M%7iJ&; literal 2126 zcmZWrdstH07C*=d4=QxhFEccS#zKwEX-Kn@mo;IQ^x0$VDb6&ebIB~yv?2+)IK7n8 zNlo)nPJLsRjE_{T38HVT)KO_nys6UJuf5LR=i<(= ztpq!g9Rxvy5box12*R*{T!rAkv;Y0XBJje;bN2}$i1^$(Fh8v#xc zjaN;t!Z*HrV_)vC21C!;--InD?%rdKg~BYK@}9E|Q>GrZTJ3=^;+#!;Izxso_>9Dg zSq~3| zH8dO-7W9XBFz0g2h<$XPeqy4$E(0sjcN%S9579dUFC_>Q3sxGA4sH0EKArEcbW4xN z^Lx%Un}nG4q^pnSqDAW&=Upon{2|jPX;65q4LYzKF5V+7op)&W@1zryLd40&Nz9)ojz zo#vE9+b)w)DV~>tec!w z9~%AQW0GhVP7 z=&GW)$Yp1~g*V*A$WG<1Jbw7BfCyw#w$QQ2#p^;uTan z5i6#QOm1XL$z@xyZeA3G_ZkZR!BF!I&rZt6t&iuRd##G8DI0rsL>RVAib}TtvLpT~ znMmTt!vX?l!v31<=% zy(e?_$BIj`q~rupt`9XBe<``T4a)rj{^cU@D8T7Z$)6nT5pN2YKv&7fKqBlW=rXFXaDFSP+N zI)WmquxMIqSjS5I76|jZ_9{k9$kt?S!3}|vsTL7#ify~+ff_$fx zECRr#mDVze+8}%gqMdiJQq!&@$Xli4L%=}`9mTViw=v-`g}VggrK`R-I+YwFk+=NH zllz0k#fmdd1a3b-XI=7yV^eW1*KxH(z~ikgw;k-x(?+m6K!wlN2tu((P$~Iss};0{ z0(WNNUKAm0|6+`+1oM%DRQM9)Y)461FxDXiD@aA(dy~kiE8*lTRsh{f$_pP7c@HX~ z-Ld*7su^r|E%eJJk)&xqqPlQU}<8rI2o_*)k0AOPJ9lX6_F{DXewJFOY+I&1{te9MsmIg_Qz%uSEw*2+6 ze*CFruD+Z}tZZR6kIiC2iBNS5@-xa z>1CbgC?d_3{|HQTO-)A#!l7{#9%k?E?)b$L^fGGfnXLpz*nbyEx^EtkkztBEMR)7p z$Nc_LMJS1(;ArM3M60GScI+P;80y3zCAtUbZt~9-bRnzf1Ja+QrlvCAOh0@1e#@Ej z=Wjer`8)6`)ADAbPQ0#ZF0U)PkUug>T)42qPL&l-%Aj&kN$zYwY@t5d26tX33^Ft0_@;{JixNQIc diff --git a/public/images/ui/summary_profile.png b/public/images/ui/summary_profile.png index 38bb5e84dfda3cb13df2bc1cddd8a532e5a4bad2..55cc70dc4d09fb43382213ab5321a6ac53efb435 100644 GIT binary patch literal 2935 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 diff --git a/src/ui/summary-ui-handler.ts b/src/ui/summary-ui-handler.ts index 1526ae982e5..b952037a079 100644 --- a/src/ui/summary-ui-handler.ts +++ b/src/ui/summary-ui-handler.ts @@ -225,7 +225,7 @@ export default class SummaryUiHandler extends UiHandler { this.summaryContainer.add(this.championRibbon); this.championRibbon.setVisible(false); - this.levelText = addTextObject(36, -17, "", TextStyle.SUMMARY_ALT); + this.levelText = addTextObject(24, -17, "", TextStyle.SUMMARY_ALT); this.levelText.setOrigin(0, 1); this.summaryContainer.add(this.levelText); @@ -413,7 +413,7 @@ export default class SummaryUiHandler extends UiHandler { } this.pokeball.setFrame(getPokeballAtlasKey(this.pokemon.pokeball)); - this.levelText.setText(this.pokemon.level.toString()); + this.levelText.setText(`${i18next.t("pokemonSummary:lv")}${this.pokemon.level.toString()}`); this.genderText.setText(getGenderSymbol(this.pokemon.getGender(true))); this.genderText.setColor(getGenderColor(this.pokemon.getGender(true))); this.genderText.setShadowColor(getGenderColor(this.pokemon.getGender(true), true)); @@ -756,7 +756,7 @@ export default class SummaryUiHandler extends UiHandler { trainerText.setOrigin(0, 0); profileContainer.add(trainerText); - const trainerIdText = addTextObject(174, 12, globalScene.gameData.trainerId.toString(), TextStyle.SUMMARY_ALT); + const trainerIdText = addTextObject(141, 12, `${i18next.t("pokemonSummary:idNo")}${globalScene.gameData.trainerId.toString()}`, TextStyle.SUMMARY_ALT); trainerIdText.setOrigin(0, 0); profileContainer.add(trainerIdText); From 2d067ec7ce82e9559f4117cd2896b8bec3d6fa6d Mon Sep 17 00:00:00 2001 From: Xavion3 Date: Fri, 21 Feb 2025 07:23:08 +1100 Subject: [PATCH 6/6] [UI][Enhancement] Allow tera type choice on starter select (#5366) * Allow tera type choice on starter select * Make tera type actually apply in run * Remove logging statement * Add support for alternate forms --- index.css | 2 +- index.html | 2 +- src/configs/inputs/cfg_keyboard_qwerty.ts | 6 +- src/configs/inputs/pad_dualshock.ts | 4 +- src/configs/inputs/pad_generic.ts | 4 +- src/configs/inputs/pad_procon.ts | 4 +- src/configs/inputs/pad_unlicensedSNES.ts | 4 +- src/configs/inputs/pad_xbox360.ts | 4 +- src/data/challenge.ts | 1 + src/enums/buttons.ts | 4 +- src/phases/select-starter-phase.ts | 7 ++ src/system/game-data.ts | 2 + src/system/settings/settings-gamepad.ts | 8 +- src/system/settings/settings-keyboard.ts | 16 ++-- .../settingMenu/rebinding_setting.test.ts | 8 +- src/test/utils/fakeMobile.html | 4 +- src/ui-inputs.ts | 6 +- src/ui/pokedex-ui-handler.ts | 5 +- src/ui/starter-select-ui-handler.ts | 79 +++++++++++++++++-- 19 files changed, 121 insertions(+), 49 deletions(-) diff --git a/index.css b/index.css index d32d9da9bb1..9226f968e3e 100644 --- a/index.css +++ b/index.css @@ -170,7 +170,7 @@ input:-internal-autofill-selected { #touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT']) #apadCycleNature, #touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX_PAGE'], [data-ui-mode='RUN_INFO']) #apadCycleAbility, #touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX_PAGE']) #apadCycleGender, -#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX']) #apadCycleVariant { +#touchControls:not(.config-mode):not([data-ui-mode='STARTER_SELECT'], [data-ui-mode='POKEDEX']) #apadCycleTera { display: none; } diff --git a/index.html b/index.html index 390a29fb365..91367cf73ec 100644 --- a/index.html +++ b/index.html @@ -129,7 +129,7 @@
N
-
+
V
diff --git a/src/configs/inputs/cfg_keyboard_qwerty.ts b/src/configs/inputs/cfg_keyboard_qwerty.ts index 5ddc12e8784..c1b00a833c0 100644 --- a/src/configs/inputs/cfg_keyboard_qwerty.ts +++ b/src/configs/inputs/cfg_keyboard_qwerty.ts @@ -180,9 +180,9 @@ const cfg_keyboard_qwerty = { [SettingKeyboard.Button_Cycle_Gender]: Button.CYCLE_GENDER, [SettingKeyboard.Button_Cycle_Ability]: Button.CYCLE_ABILITY, [SettingKeyboard.Button_Cycle_Nature]: Button.CYCLE_NATURE, - [SettingKeyboard.Button_Cycle_Variant]: Button.V, [SettingKeyboard.Button_Speed_Up]: Button.SPEED_UP, [SettingKeyboard.Button_Slow_Down]: Button.SLOW_DOWN, + [SettingKeyboard.Button_Cycle_Tera]: Button.CYCLE_TERA, [SettingKeyboard.Alt_Button_Up]: Button.UP, [SettingKeyboard.Alt_Button_Down]: Button.DOWN, [SettingKeyboard.Alt_Button_Left]: Button.LEFT, @@ -197,9 +197,9 @@ const cfg_keyboard_qwerty = { [SettingKeyboard.Alt_Button_Cycle_Gender]: Button.CYCLE_GENDER, [SettingKeyboard.Alt_Button_Cycle_Ability]: Button.CYCLE_ABILITY, [SettingKeyboard.Alt_Button_Cycle_Nature]: Button.CYCLE_NATURE, - [SettingKeyboard.Alt_Button_Cycle_Variant]: Button.V, [SettingKeyboard.Alt_Button_Speed_Up]: Button.SPEED_UP, [SettingKeyboard.Alt_Button_Slow_Down]: Button.SLOW_DOWN, + [SettingKeyboard.Alt_Button_Cycle_Tera]: Button.CYCLE_TERA, }, default: { KEY_ARROW_UP: SettingKeyboard.Button_Up, @@ -216,7 +216,7 @@ const cfg_keyboard_qwerty = { KEY_G: SettingKeyboard.Button_Cycle_Gender, KEY_E: SettingKeyboard.Button_Cycle_Ability, KEY_N: SettingKeyboard.Button_Cycle_Nature, - KEY_V: SettingKeyboard.Button_Cycle_Variant, + KEY_V: SettingKeyboard.Button_Cycle_Tera, KEY_PLUS: -1, KEY_MINUS: -1, KEY_A: SettingKeyboard.Alt_Button_Left, diff --git a/src/configs/inputs/pad_dualshock.ts b/src/configs/inputs/pad_dualshock.ts index 2fbdd0ddfaa..265b39fdad5 100644 --- a/src/configs/inputs/pad_dualshock.ts +++ b/src/configs/inputs/pad_dualshock.ts @@ -53,7 +53,7 @@ const pad_dualshock = { [SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, - [SettingGamepad.Button_Cycle_Variant]: Button.V, + [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA, [SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, @@ -72,7 +72,7 @@ const pad_dualshock = { RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, RC_W: SettingGamepad.Button_Cycle_Nature, - RC_N: SettingGamepad.Button_Cycle_Variant, + RC_N: SettingGamepad.Button_Cycle_Tera, START: SettingGamepad.Button_Menu, SELECT: SettingGamepad.Button_Stats, LB: SettingGamepad.Button_Cycle_Form, diff --git a/src/configs/inputs/pad_generic.ts b/src/configs/inputs/pad_generic.ts index 256af8f0fe3..cd91fcd8b17 100644 --- a/src/configs/inputs/pad_generic.ts +++ b/src/configs/inputs/pad_generic.ts @@ -51,7 +51,7 @@ const pad_generic = { [SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, - [SettingGamepad.Button_Cycle_Variant]: Button.V, + [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA, [SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, @@ -69,7 +69,7 @@ const pad_generic = { RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, RC_W: SettingGamepad.Button_Cycle_Nature, - RC_N: SettingGamepad.Button_Cycle_Variant, + RC_N: SettingGamepad.Button_Cycle_Tera, START: SettingGamepad.Button_Menu, SELECT: SettingGamepad.Button_Stats, LB: SettingGamepad.Button_Cycle_Form, diff --git a/src/configs/inputs/pad_procon.ts b/src/configs/inputs/pad_procon.ts index 98d17c4ef57..a7ae5383fbe 100644 --- a/src/configs/inputs/pad_procon.ts +++ b/src/configs/inputs/pad_procon.ts @@ -52,7 +52,7 @@ const pad_procon = { [SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, - [SettingGamepad.Button_Cycle_Variant]: Button.V, + [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA, [SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, @@ -70,7 +70,7 @@ const pad_procon = { RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, RC_W: SettingGamepad.Button_Cycle_Nature, - RC_N: SettingGamepad.Button_Cycle_Variant, + RC_N: SettingGamepad.Button_Cycle_Tera, START: SettingGamepad.Button_Menu, SELECT: SettingGamepad.Button_Stats, LB: SettingGamepad.Button_Cycle_Form, diff --git a/src/configs/inputs/pad_unlicensedSNES.ts b/src/configs/inputs/pad_unlicensedSNES.ts index 77e68e6a644..fbde98b3fa2 100644 --- a/src/configs/inputs/pad_unlicensedSNES.ts +++ b/src/configs/inputs/pad_unlicensedSNES.ts @@ -43,7 +43,7 @@ const pad_unlicensedSNES = { [SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, - [SettingGamepad.Button_Cycle_Variant]: Button.V, + [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA, [SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, @@ -61,7 +61,7 @@ const pad_unlicensedSNES = { RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, RC_W: SettingGamepad.Button_Cycle_Nature, - RC_N: SettingGamepad.Button_Cycle_Variant, + RC_N: SettingGamepad.Button_Cycle_Tera, START: SettingGamepad.Button_Menu, SELECT: SettingGamepad.Button_Stats, LB: SettingGamepad.Button_Cycle_Form, diff --git a/src/configs/inputs/pad_xbox360.ts b/src/configs/inputs/pad_xbox360.ts index 6afc452f50b..88fee731d1d 100644 --- a/src/configs/inputs/pad_xbox360.ts +++ b/src/configs/inputs/pad_xbox360.ts @@ -51,7 +51,7 @@ const pad_xbox360 = { [SettingGamepad.Button_Action]: Button.ACTION, [SettingGamepad.Button_Cancel]: Button.CANCEL, [SettingGamepad.Button_Cycle_Nature]: Button.CYCLE_NATURE, - [SettingGamepad.Button_Cycle_Variant]: Button.V, + [SettingGamepad.Button_Cycle_Tera]: Button.CYCLE_TERA, [SettingGamepad.Button_Menu]: Button.MENU, [SettingGamepad.Button_Stats]: Button.STATS, [SettingGamepad.Button_Cycle_Form]: Button.CYCLE_FORM, @@ -69,7 +69,7 @@ const pad_xbox360 = { RC_S: SettingGamepad.Button_Action, RC_E: SettingGamepad.Button_Cancel, RC_W: SettingGamepad.Button_Cycle_Nature, - RC_N: SettingGamepad.Button_Cycle_Variant, + RC_N: SettingGamepad.Button_Cycle_Tera, START: SettingGamepad.Button_Menu, SELECT: SettingGamepad.Button_Stats, LB: SettingGamepad.Button_Cycle_Form, diff --git a/src/data/challenge.ts b/src/data/challenge.ts index c6e85be2389..30c2c9a6ce4 100644 --- a/src/data/challenge.ts +++ b/src/data/challenge.ts @@ -708,6 +708,7 @@ export class FreshStartChallenge extends Challenge { pokemon.variant = 0; // Not shiny pokemon.formIndex = 0; // Froakie should be base form pokemon.ivs = [ 15, 15, 15, 15, 15, 15 ]; // Default IVs of 15 for all stats (Updated to 15 from 10 in 1.2.0) + pokemon.teraType = pokemon.species.type1; // Always primary tera type return true; } diff --git a/src/enums/buttons.ts b/src/enums/buttons.ts index fe26023f8e7..f828b280d45 100644 --- a/src/enums/buttons.ts +++ b/src/enums/buttons.ts @@ -13,7 +13,7 @@ export enum Button { CYCLE_GENDER, CYCLE_ABILITY, CYCLE_NATURE, - V, + CYCLE_TERA, SPEED_UP, - SLOW_DOWN + SLOW_DOWN, } diff --git a/src/phases/select-starter-phase.ts b/src/phases/select-starter-phase.ts index 79012901a3a..b7ad15533a6 100644 --- a/src/phases/select-starter-phase.ts +++ b/src/phases/select-starter-phase.ts @@ -12,6 +12,7 @@ import type { Starter } from "#app/ui/starter-select-ui-handler"; import { Mode } from "#app/ui/ui"; import type { Species } from "#enums/species"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; +import * as Utils from "../utils"; export class SelectStarterPhase extends Phase { @@ -79,6 +80,12 @@ export class SelectStarterPhase extends Phase { starterPokemon.nickname = starter.nickname; } + if (!Utils.isNullOrUndefined(starter.teraType)) { + starterPokemon.teraType = starter.teraType; + } else { + starterPokemon.teraType = starterPokemon.species.type1; + } + if (globalScene.gameMode.isSplicedOnly || Overrides.STARTER_FUSION_OVERRIDE) { starterPokemon.generateFusionSpecies(true); } diff --git a/src/system/game-data.ts b/src/system/game-data.ts index 63d79d47fba..6b25013795f 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -55,6 +55,7 @@ import { MysteryEncounterSaveData } from "#app/data/mystery-encounters/mystery-e import type { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { pokerogueApi } from "#app/plugins/api/pokerogue-api"; import { ArenaTrapTag } from "#app/data/arena-tag"; +import type { Type } from "#enums/type"; export const defaultStarterSpecies: Species[] = [ Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE, @@ -229,6 +230,7 @@ export interface StarterAttributes { shiny?: boolean; favorite?: boolean; nickname?: string; + tera?: Type; } export interface StarterPreferences { diff --git a/src/system/settings/settings-gamepad.ts b/src/system/settings/settings-gamepad.ts index d26c0c9f019..840304ab1ba 100644 --- a/src/system/settings/settings-gamepad.ts +++ b/src/system/settings/settings-gamepad.ts @@ -21,7 +21,7 @@ export enum SettingGamepad { Button_Cycle_Gender = "BUTTON_CYCLE_GENDER", Button_Cycle_Ability = "BUTTON_CYCLE_ABILITY", Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", - Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", + Button_Cycle_Tera = "BUTTON_CYCLE_TERA", Button_Speed_Up = "BUTTON_SPEED_UP", Button_Slow_Down = "BUTTON_SLOW_DOWN", Button_Submit = "BUTTON_SUBMIT", @@ -45,7 +45,7 @@ export const settingGamepadOptions = { [SettingGamepad.Button_Cycle_Gender]: [ `KEY ${Button.CYCLE_GENDER.toString()}`, pressAction ], [SettingGamepad.Button_Cycle_Ability]: [ `KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction ], [SettingGamepad.Button_Cycle_Nature]: [ `KEY ${Button.CYCLE_NATURE.toString()}`, pressAction ], - [SettingGamepad.Button_Cycle_Variant]: [ `KEY ${Button.V.toString()}`, pressAction ], + [SettingGamepad.Button_Cycle_Tera]: [ `KEY ${Button.CYCLE_TERA.toString()}`, pressAction ], [SettingGamepad.Button_Speed_Up]: [ `KEY ${Button.SPEED_UP.toString()}`, pressAction ], [SettingGamepad.Button_Slow_Down]: [ `KEY ${Button.SLOW_DOWN.toString()}`, pressAction ], [SettingGamepad.Button_Submit]: [ `KEY ${Button.SUBMIT.toString()}`, pressAction ], @@ -67,7 +67,7 @@ export const settingGamepadDefaults = { [SettingGamepad.Button_Cycle_Gender]: 0, [SettingGamepad.Button_Cycle_Ability]: 0, [SettingGamepad.Button_Cycle_Nature]: 0, - [SettingGamepad.Button_Cycle_Variant]: 0, + [SettingGamepad.Button_Cycle_Tera]: 0, [SettingGamepad.Button_Speed_Up]: 0, [SettingGamepad.Button_Slow_Down]: 0, [SettingGamepad.Button_Submit]: 0, @@ -96,7 +96,7 @@ export function setSettingGamepad(setting: SettingGamepad, value: number): boole case SettingGamepad.Button_Cycle_Gender: case SettingGamepad.Button_Cycle_Ability: case SettingGamepad.Button_Cycle_Nature: - case SettingGamepad.Button_Cycle_Variant: + case SettingGamepad.Button_Cycle_Tera: case SettingGamepad.Button_Speed_Up: case SettingGamepad.Button_Slow_Down: case SettingGamepad.Button_Submit: diff --git a/src/system/settings/settings-keyboard.ts b/src/system/settings/settings-keyboard.ts index 95ad4ba41f1..1a7db1b10c2 100644 --- a/src/system/settings/settings-keyboard.ts +++ b/src/system/settings/settings-keyboard.ts @@ -32,8 +32,8 @@ export enum SettingKeyboard { Alt_Button_Cycle_Ability = "ALT_BUTTON_CYCLE_ABILITY", Button_Cycle_Nature = "BUTTON_CYCLE_NATURE", Alt_Button_Cycle_Nature = "ALT_BUTTON_CYCLE_NATURE", - Button_Cycle_Variant = "BUTTON_CYCLE_VARIANT", - Alt_Button_Cycle_Variant = "ALT_BUTTON_CYCLE_VARIANT", + Button_Cycle_Tera = "BUTTON_CYCLE_TERA", + Alt_Button_Cycle_Tera = "ALT_BUTTON_CYCLE_TERA", Button_Speed_Up = "BUTTON_SPEED_UP", Alt_Button_Speed_Up = "ALT_BUTTON_SPEED_UP", Button_Slow_Down = "BUTTON_SLOW_DOWN", @@ -73,8 +73,8 @@ export const settingKeyboardOptions = { [SettingKeyboard.Alt_Button_Cycle_Ability]: [ `KEY ${Button.CYCLE_ABILITY.toString()}`, pressAction ], [SettingKeyboard.Button_Cycle_Nature]: [ `KEY ${Button.CYCLE_NATURE.toString()}`, pressAction ], [SettingKeyboard.Alt_Button_Cycle_Nature]: [ `KEY ${Button.CYCLE_NATURE.toString()}`, pressAction ], - [SettingKeyboard.Button_Cycle_Variant]: [ `KEY ${Button.V.toString()}`, pressAction ], - [SettingKeyboard.Alt_Button_Cycle_Variant]: [ `KEY ${Button.V.toString()}`, pressAction ], + [SettingKeyboard.Button_Cycle_Tera]: [ `KEY ${Button.CYCLE_TERA.toString()}`, pressAction ], + [SettingKeyboard.Alt_Button_Cycle_Tera]: [ `KEY ${Button.CYCLE_TERA.toString()}`, pressAction ], [SettingKeyboard.Button_Speed_Up]: [ `KEY ${Button.SPEED_UP.toString()}`, pressAction ], [SettingKeyboard.Alt_Button_Speed_Up]: [ `KEY ${Button.SPEED_UP.toString()}`, pressAction ], [SettingKeyboard.Button_Slow_Down]: [ `KEY ${Button.SLOW_DOWN.toString()}`, pressAction ], @@ -112,8 +112,8 @@ export const settingKeyboardDefaults = { [SettingKeyboard.Alt_Button_Cycle_Ability]: 0, [SettingKeyboard.Button_Cycle_Nature]: 0, [SettingKeyboard.Alt_Button_Cycle_Nature]: 0, - [SettingKeyboard.Button_Cycle_Variant]: 0, - [SettingKeyboard.Alt_Button_Cycle_Variant]: 0, + [SettingKeyboard.Button_Cycle_Tera]: 0, + [SettingKeyboard.Alt_Button_Cycle_Tera]: 0, [SettingKeyboard.Button_Speed_Up]: 0, [SettingKeyboard.Alt_Button_Speed_Up]: 0, [SettingKeyboard.Button_Slow_Down]: 0, @@ -148,7 +148,7 @@ export function setSettingKeyboard(setting: SettingKeyboard, value: number): boo case SettingKeyboard.Button_Cycle_Gender: case SettingKeyboard.Button_Cycle_Ability: case SettingKeyboard.Button_Cycle_Nature: - case SettingKeyboard.Button_Cycle_Variant: + case SettingKeyboard.Button_Cycle_Tera: case SettingKeyboard.Button_Speed_Up: case SettingKeyboard.Button_Slow_Down: case SettingKeyboard.Alt_Button_Up: @@ -164,7 +164,7 @@ export function setSettingKeyboard(setting: SettingKeyboard, value: number): boo case SettingKeyboard.Alt_Button_Cycle_Gender: case SettingKeyboard.Alt_Button_Cycle_Ability: case SettingKeyboard.Alt_Button_Cycle_Nature: - case SettingKeyboard.Alt_Button_Cycle_Variant: + case SettingKeyboard.Alt_Button_Cycle_Tera: case SettingKeyboard.Alt_Button_Speed_Up: case SettingKeyboard.Alt_Button_Slow_Down: case SettingKeyboard.Alt_Button_Submit: diff --git a/src/test/settingMenu/rebinding_setting.test.ts b/src/test/settingMenu/rebinding_setting.test.ts index cae2df363d8..46a37f4e137 100644 --- a/src/test/settingMenu/rebinding_setting.test.ts +++ b/src/test/settingMenu/rebinding_setting.test.ts @@ -406,9 +406,9 @@ describe("Test Rebinding", () => { }); it("check to delete all the binds of an action", () => { - inGame.whenWePressOnKeyboard("V").weShouldTriggerTheButton("Button_Cycle_Variant"); - inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Variant").thereShouldBeNoIcon().weWantThisBindInstead("K").confirm(); - inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Variant").iconDisplayedIs("KEY_K").whenWeDelete().thereShouldBeNoIconAnymore(); - inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Variant").iconDisplayedIs("KEY_V").whenWeDelete().thereShouldBeNoIconAnymore(); + inGame.whenWePressOnKeyboard("V").weShouldTriggerTheButton("Button_Cycle_Tera"); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Tera").thereShouldBeNoIcon().weWantThisBindInstead("K").confirm(); + inTheSettingMenu.whenCursorIsOnSetting("Alt_Button_Cycle_Tera").iconDisplayedIs("KEY_K").whenWeDelete().thereShouldBeNoIconAnymore(); + inTheSettingMenu.whenCursorIsOnSetting("Button_Cycle_Tera").iconDisplayedIs("KEY_V").whenWeDelete().thereShouldBeNoIconAnymore(); }); }); diff --git a/src/test/utils/fakeMobile.html b/src/test/utils/fakeMobile.html index 4b566d592d9..263b03969d4 100644 --- a/src/test/utils/fakeMobile.html +++ b/src/test/utils/fakeMobile.html @@ -35,7 +35,7 @@
R
-
+
V
@@ -59,7 +59,7 @@
N
-
+
V
diff --git a/src/ui-inputs.ts b/src/ui-inputs.ts index e6a0ed7a69c..951aec2a3b6 100644 --- a/src/ui-inputs.ts +++ b/src/ui-inputs.ts @@ -86,7 +86,7 @@ export class UiInputs { [Button.CYCLE_GENDER]: () => this.buttonCycleOption(Button.CYCLE_GENDER), [Button.CYCLE_ABILITY]: () => this.buttonCycleOption(Button.CYCLE_ABILITY), [Button.CYCLE_NATURE]: () => this.buttonCycleOption(Button.CYCLE_NATURE), - [Button.V]: () => this.buttonCycleOption(Button.V), + [Button.CYCLE_TERA]: () => this.buttonCycleOption(Button.CYCLE_TERA), [Button.SPEED_UP]: () => this.buttonSpeedChange(), [Button.SLOW_DOWN]: () => this.buttonSpeedChange(false), }; @@ -109,7 +109,7 @@ export class UiInputs { [Button.CYCLE_GENDER]: () => undefined, [Button.CYCLE_ABILITY]: () => undefined, [Button.CYCLE_NATURE]: () => undefined, - [Button.V]: () => this.buttonInfo(false), + [Button.CYCLE_TERA]: () => undefined, [Button.SPEED_UP]: () => undefined, [Button.SLOW_DOWN]: () => undefined, }; @@ -197,7 +197,7 @@ export class UiInputs { const uiHandler = globalScene.ui?.getHandler(); if (whitelist.some(handler => uiHandler instanceof handler)) { globalScene.ui.processInput(button); - } else if (button === Button.V) { + } else if (button === Button.CYCLE_TERA) { this.buttonInfo(true); } } diff --git a/src/ui/pokedex-ui-handler.ts b/src/ui/pokedex-ui-handler.ts index 5180d7bd956..1ba1b846224 100644 --- a/src/ui/pokedex-ui-handler.ts +++ b/src/ui/pokedex-ui-handler.ts @@ -919,7 +919,7 @@ export default class PokedexUiHandler extends MessageUiHandler { } else { error = true; } - } else if (button === Button.V) { + } else if (button === Button.CYCLE_TERA) { if (!this.filterTextMode && !this.showingTray) { this.cursorObj.setVisible(false); this.setSpecies(null); @@ -1170,9 +1170,6 @@ export default class PokedexUiHandler extends MessageUiHandler { case SettingKeyboard.Button_Cycle_Shiny: iconPath = "R.png"; break; - case SettingKeyboard.Button_Cycle_Variant: - iconPath = "V.png"; - break; case SettingKeyboard.Button_Cycle_Form: iconPath = "F.png"; break; diff --git a/src/ui/starter-select-ui-handler.ts b/src/ui/starter-select-ui-handler.ts index b919679be00..a102fc9dc08 100644 --- a/src/ui/starter-select-ui-handler.ts +++ b/src/ui/starter-select-ui-handler.ts @@ -56,6 +56,8 @@ import { getPassiveCandyCount, getValueReductionCandyCounts, getSameSpeciesEggCa import { BooleanHolder, fixedInt, getLocalizedSpriteKey, isNullOrUndefined, NumberHolder, padInt, randIntRange, rgbHexToRgba, toReadableString } from "#app/utils"; import type { Nature } from "#enums/nature"; import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; +import { achvs } from "#app/system/achv"; +import * as Utils from "../utils"; export type StarterSelectCallback = (starters: Starter[]) => void; @@ -68,6 +70,7 @@ export interface Starter { moveset?: StarterMoveset; pokerus: boolean; nickname?: string; + teraType?: Type; } interface LanguageSetting { @@ -212,6 +215,7 @@ interface SpeciesDetails { abilityIndex?: number, natureIndex?: number, forSeen?: boolean, // default = false + teraType?: Type, } export default class StarterSelectUiHandler extends MessageUiHandler { @@ -262,6 +266,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private pokemonShinyIcon: Phaser.GameObjects.Sprite; private pokemonPassiveDisabledIcon: Phaser.GameObjects.Sprite; private pokemonPassiveLockedIcon: Phaser.GameObjects.Sprite; + private teraIcon: Phaser.GameObjects.Sprite; private activeTooltip: "ABILITY" | "PASSIVE" | "CANDY" | undefined; private instructionsContainer: Phaser.GameObjects.Container; @@ -271,12 +276,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private abilityIconElement: Phaser.GameObjects.Sprite; private genderIconElement: Phaser.GameObjects.Sprite; private natureIconElement: Phaser.GameObjects.Sprite; + private teraIconElement: Phaser.GameObjects.Sprite; private goFilterIconElement: Phaser.GameObjects.Sprite; private shinyLabel: Phaser.GameObjects.Text; private formLabel: Phaser.GameObjects.Text; private genderLabel: Phaser.GameObjects.Text; private abilityLabel: Phaser.GameObjects.Text; private natureLabel: Phaser.GameObjects.Text; + private teraLabel: Phaser.GameObjects.Text; private goFilterLabel: Phaser.GameObjects.Text; private starterSelectMessageBox: Phaser.GameObjects.NineSlice; @@ -292,6 +299,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private dexAttrCursor: bigint = 0n; private abilityCursor: number = -1; private natureCursor: number = -1; + private teraCursor: Type = Type.UNKNOWN; private filterBarCursor: number = 0; private starterMoveset: StarterMoveset | null; private scrollCursor: number; @@ -304,6 +312,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private starterAttr: bigint[] = []; private starterAbilityIndexes: number[] = []; private starterNatures: Nature[] = []; + private starterTeras: Type[] = []; private starterMovesets: StarterMoveset[] = []; private speciesStarterDexEntry: DexEntry | null; private speciesStarterMoves: Moves[]; @@ -312,6 +321,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { private canCycleGender: boolean; private canCycleAbility: boolean; private canCycleNature: boolean; + private canCycleTera: boolean; private assetLoadCancelled: BooleanHolder | null; public cursorObj: Phaser.GameObjects.Image; @@ -823,6 +833,11 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.starterSelectContainer.add(this.pokemonEggMovesContainer); + this.teraIcon = globalScene.add.sprite(85, 63, "button_tera"); + this.teraIcon.setName("terrastallize-icon"); + this.teraIcon.setFrame("fire"); + this.starterSelectContainer.add(this.teraIcon); + // The font size should be set per language const instructionTextSize = textSettings.instructionTextSize; @@ -867,6 +882,13 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.natureLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("starterSelectUiHandler:cycleNature"), TextStyle.PARTY, { fontSize: instructionTextSize }); this.natureLabel.setName("text-nature-label"); + this.teraIconElement = new Phaser.GameObjects.Sprite(globalScene, this.instructionRowX, this.instructionRowY, "keyboard", "V.png"); + this.teraIconElement.setName("sprite-tera-icon-element"); + this.teraIconElement.setScale(0.675); + this.teraIconElement.setOrigin(0.0, 0.0); + this.teraLabel = addTextObject(this.instructionRowX + this.instructionRowTextOffset, this.instructionRowY, i18next.t("starterSelectUiHandler:cycleTera"), TextStyle.PARTY, { fontSize: instructionTextSize }); + this.teraLabel.setName("text-tera-label"); + this.goFilterIconElement = new Phaser.GameObjects.Sprite(globalScene, this.filterInstructionRowX, this.filterInstructionRowY, "keyboard", "C.png"); this.goFilterIconElement.setName("sprite-goFilter-icon-element"); this.goFilterIconElement.setScale(0.675); @@ -1497,6 +1519,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const props = globalScene.gameData.getSpeciesDexAttrProps(randomSpecies, dexAttr); const abilityIndex = this.abilityCursor; const nature = this.natureCursor as unknown as Nature; + const teraType = this.teraCursor; const moveset = this.starterMoveset?.slice(0) as StarterMoveset; const starterCost = globalScene.gameData.getSpeciesStarterValue(randomSpecies.speciesId); const speciesForm = getPokemonSpeciesForm(randomSpecies.speciesId, props.formIndex); @@ -1505,7 +1528,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { .loadAssets(props.female, props.formIndex, props.shiny, props.variant, true) .then(() => { if (this.tryUpdateValue(starterCost, true)) { - this.addToParty(randomSpecies, dexAttr, abilityIndex, nature, moveset, true); + this.addToParty(randomSpecies, dexAttr, abilityIndex, nature, moveset, teraType, true); ui.playSelect(); } }); @@ -1585,7 +1608,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { const cursorObj = this.starterCursorObjs[this.starterSpecies.length]; cursorObj.setVisible(true); cursorObj.setPosition(this.cursorObj.x, this.cursorObj.y); - this.addToParty(this.lastSpecies, this.dexAttrCursor, this.abilityCursor, this.natureCursor as unknown as Nature, this.starterMoveset?.slice(0) as StarterMoveset); + this.addToParty(this.lastSpecies, this.dexAttrCursor, this.abilityCursor, this.natureCursor as unknown as Nature, this.starterMoveset?.slice(0) as StarterMoveset, this.teraCursor); ui.playSelect(); } else { ui.playError(); // this should be redundant as there is now a trigger for when a pokemon can't be added to party @@ -2066,7 +2089,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } } while (newFormIndex !== props.formIndex); starterAttributes.form = newFormIndex; // store the selected form - this.setSpeciesDetails(this.lastSpecies, { formIndex: newFormIndex }); + starterAttributes.tera = this.lastSpecies.forms[newFormIndex].type1; + this.setSpeciesDetails(this.lastSpecies, { formIndex: newFormIndex, teraType: starterAttributes.tera }); success = true; } break; @@ -2125,6 +2149,19 @@ export default class StarterSelectUiHandler extends MessageUiHandler { success = true; } break; + case Button.CYCLE_TERA: + if (this.canCycleTera) { + const speciesForm = getPokemonSpeciesForm(this.lastSpecies.speciesId, starterAttributes.form ?? 0); + if (speciesForm.type1 === this.teraCursor && !Utils.isNullOrUndefined(speciesForm.type2)) { + starterAttributes.tera = speciesForm.type2!; + this.setSpeciesDetails(this.lastSpecies, { teraType: speciesForm.type2! }); + } else { + starterAttributes.tera = speciesForm.type1; + this.setSpeciesDetails(this.lastSpecies, { teraType: speciesForm.type1 }); + } + success = true; + } + break; case Button.UP: if (!this.starterIconsCursorObj.visible) { if (currentRow > 0) { @@ -2289,7 +2326,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { return [ isDupe, removeIndex ]; } - addToParty(species: PokemonSpecies, dexAttr: bigint, abilityIndex: number, nature: Nature, moveset: StarterMoveset, randomSelection: boolean = false) { + addToParty(species: PokemonSpecies, dexAttr: bigint, abilityIndex: number, nature: Nature, moveset: StarterMoveset, teraType: Type, randomSelection: boolean = false) { const props = globalScene.gameData.getSpeciesDexAttrProps(species, dexAttr); this.starterIcons[this.starterSpecies.length].setTexture(species.getIconAtlasKey(props.formIndex, props.shiny, props.variant)); this.starterIcons[this.starterSpecies.length].setFrame(species.getIconId(props.female, props.formIndex, props.shiny, props.variant)); @@ -2299,6 +2336,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.starterAttr.push(dexAttr); this.starterAbilityIndexes.push(abilityIndex); this.starterNatures.push(nature); + this.starterTeras.push(teraType); this.starterMovesets.push(moveset); if (this.speciesLoaded.get(species.speciesId) || randomSelection ) { getPokemonSpeciesForm(species.speciesId, props.formIndex).cry(); @@ -2379,6 +2417,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler { case SettingKeyboard.Button_Cycle_Nature: iconPath = "N.png"; break; + case SettingKeyboard.Button_Cycle_Tera: + iconPath = "V.png"; + break; case SettingKeyboard.Button_Stats: iconPath = "C.png"; break; @@ -2459,6 +2500,9 @@ export default class StarterSelectUiHandler extends MessageUiHandler { if (this.canCycleNature) { this.updateButtonIcon(SettingKeyboard.Button_Cycle_Nature, gamepadType, this.natureIconElement, this.natureLabel); } + if (this.canCycleTera) { + this.updateButtonIcon(SettingKeyboard.Button_Cycle_Tera, gamepadType, this.teraIconElement, this.teraLabel); + } } // if filter mode is inactivated and gamepadType is not undefined, update the button icons @@ -2876,6 +2920,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.dexAttrCursor = species ? this.getCurrentDexProps(species.speciesId) : 0n; this.abilityCursor = species ? globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species) : 0; this.natureCursor = species ? globalScene.gameData.getSpeciesDefaultNature(species) : 0; + this.teraCursor = species ? species.type1 : Type.UNKNOWN; if (!species && globalScene.ui.getTooltip().visible) { globalScene.ui.hideTooltip(); @@ -2894,6 +2939,10 @@ export default class StarterSelectUiHandler extends MessageUiHandler { // load default ability from stater save data, if set this.abilityCursor = starterAttributes.ability; } + if (starterAttributes?.tera) { + // load default tera from starter save data, if set + this.teraCursor = starterAttributes.tera; + } if (this.statsMode) { if (this.speciesStarterDexEntry?.caughtAttr) { @@ -3035,7 +3084,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { female: props.female, variant: props.variant, abilityIndex: this.starterAbilityIndexes[starterIndex], - natureIndex: this.starterNatures[starterIndex] + natureIndex: this.starterNatures[starterIndex], + teraType: this.starterTeras[starterIndex] }); } else { const defaultDexAttr = this.getCurrentDexProps(species.speciesId); @@ -3083,6 +3133,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonCaughtHatchedContainer.setVisible(false); this.pokemonCandyContainer.setVisible(false); this.pokemonFormText.setVisible(false); + this.teraIcon.setVisible(false); const defaultDexAttr = globalScene.gameData.getSpeciesDefaultDexAttr(species, true, true); const defaultAbilityIndex = globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); @@ -3117,6 +3168,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonCaughtHatchedContainer.setVisible(false); this.pokemonCandyContainer.setVisible(false); this.pokemonFormText.setVisible(false); + this.teraIcon.setVisible(false); this.setSpeciesDetails(species!, { // TODO: is this bang correct? shiny: false, @@ -3131,7 +3183,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { } setSpeciesDetails(species: PokemonSpecies, options: SpeciesDetails = {}): void { - let { shiny, formIndex, female, variant, abilityIndex, natureIndex } = options; + let { shiny, formIndex, female, variant, abilityIndex, natureIndex, teraType } = options; const forSeen: boolean = options.forSeen ?? false; const oldProps = species ? globalScene.gameData.getSpeciesDexAttrProps(species, this.dexAttrCursor) : null; const oldAbilityIndex = this.abilityCursor > -1 ? this.abilityCursor : globalScene.gameData.getStarterSpeciesDefaultAbilityIndex(species); @@ -3139,6 +3191,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.dexAttrCursor = 0n; this.abilityCursor = -1; this.natureCursor = -1; + this.teraCursor = Type.UNKNOWN; // We will only update the sprite if there is a change to form, shiny/variant // or gender for species with gender sprite differences const shouldUpdateSprite = (species?.genderDiffs && !isNullOrUndefined(female)) @@ -3168,6 +3221,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.dexAttrCursor |= globalScene.gameData.getFormAttr(formIndex !== undefined ? formIndex : (formIndex = oldProps!.formIndex)); // TODO: is this bang correct? this.abilityCursor = abilityIndex !== undefined ? abilityIndex : (abilityIndex = oldAbilityIndex); this.natureCursor = natureIndex !== undefined ? natureIndex : (natureIndex = oldNatureIndex); + this.teraCursor = !Utils.isNullOrUndefined(teraType) ? teraType : (teraType = species.type1); const [ isInParty, partyIndex ]: [boolean, number] = this.isInParty(species); // we use this to firstly check if the pokemon is in the party, and if so, to get the party index in order to update the icon image if (isInParty) { this.updatePartyIcon(species, partyIndex); @@ -3179,6 +3233,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonPassiveText.setVisible(false); this.pokemonPassiveDisabledIcon.setVisible(false); this.pokemonPassiveLockedIcon.setVisible(false); + this.teraIcon.setVisible(false); if (this.assetLoadCancelled) { this.assetLoadCancelled.value = true; @@ -3230,6 +3285,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.starterAttr[starterIndex] = this.dexAttrCursor; this.starterAbilityIndexes[starterIndex] = this.abilityCursor; this.starterNatures[starterIndex] = this.natureCursor; + this.starterTeras[starterIndex] = this.teraCursor; } const assetLoadCancelled = new BooleanHolder(false); @@ -3288,7 +3344,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.canCycleForm = species.forms.filter(f => f.isStarterSelectable || !pokemonFormChanges[species.speciesId]?.find(fc => fc.formKey)) .map((_, f) => dexEntry.caughtAttr & globalScene.gameData.getFormAttr(f)).filter(f => f).length > 1; this.canCycleNature = globalScene.gameData.getNaturesForAttr(dexEntry.natureAttr).length > 1; - + this.canCycleTera = globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id) && !Utils.isNullOrUndefined(getPokemonSpeciesForm(species.speciesId, formIndex ?? 0).type2); } if (dexEntry.caughtAttr && species.malePercent !== null) { @@ -3412,10 +3468,14 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonFormText.setText(formText); this.setTypeIcons(speciesForm.type1, speciesForm.type2); + + this.teraIcon.setFrame(Type[this.teraCursor].toLowerCase()); + this.teraIcon.setVisible(!this.statsMode && globalScene.gameData.achvUnlocks.hasOwnProperty(achvs.TERASTALLIZE.id)); } else { this.pokemonAbilityText.setText(""); this.pokemonPassiveText.setText(""); this.pokemonNatureText.setText(""); + this.teraIcon.setVisible(false); this.setTypeIcons(null, null); } } else { @@ -3426,6 +3486,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.pokemonAbilityText.setText(""); this.pokemonPassiveText.setText(""); this.pokemonNatureText.setText(""); + this.teraIcon.setVisible(false); this.setTypeIcons(null, null); } @@ -3479,6 +3540,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.starterAttr.splice(index, 1); this.starterAbilityIndexes.splice(index, 1); this.starterNatures.splice(index, 1); + this.starterTeras.splice(index, 1); this.starterMovesets.splice(index, 1); for (let s = 0; s < this.starterSpecies.length; s++) { @@ -3690,6 +3752,7 @@ export default class StarterSelectUiHandler extends MessageUiHandler { abilityIndex: thisObj.starterAbilityIndexes[i], passive: !(globalScene.gameData.starterData[starterSpecies.speciesId].passiveAttr ^ (PassiveAttr.ENABLED | PassiveAttr.UNLOCKED)), nature: thisObj.starterNatures[i] as Nature, + teraType: thisObj.starterTeras[i] as Type, moveset: thisObj.starterMovesets[i], pokerus: thisObj.pokerusSpecies.includes(starterSpecies), nickname: thisObj.starterPreferences[starterSpecies.speciesId]?.nickname, @@ -3816,6 +3879,8 @@ export default class StarterSelectUiHandler extends MessageUiHandler { this.abilityLabel.setVisible(false); this.natureIconElement.setVisible(false); this.natureLabel.setVisible(false); + this.teraIconElement.setVisible(false); + this.teraLabel.setVisible(false); this.goFilterIconElement.setVisible(false); this.goFilterLabel.setVisible(false); }