diff --git a/package-lock.json b/package-lock.json index 316306e3299..568a57d3589 100644 --- a/package-lock.json +++ b/package-lock.json @@ -37,7 +37,7 @@ "typedoc": "^0.26.4", "typescript": "^5.5.3", "typescript-eslint": "^8.0.0-alpha.54", - "vite": "^5.3.5", + "vite": "^5.4.8", "vite-tsconfig-paths": "^4.3.2", "vitest": "^2.0.4", "vitest-canvas-mock": "^0.3.3" @@ -3650,7 +3650,6 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, - "license": "MIT", "optional": true, "os": [ "darwin" @@ -5042,7 +5041,6 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -5441,9 +5439,9 @@ } }, "node_modules/postcss": { - "version": "8.4.39", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.39.tgz", - "integrity": "sha512-0vzE+lAiG7hZl1/9I8yzKLx3aR9Xbof3fBHKunvMfOCYAtMhrsnccJY2iTURb9EZd5+pLuiNV9/c/GZJOHsgIw==", + "version": "8.4.47", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.47.tgz", + "integrity": "sha512-56rxCq7G/XfB4EkXq9Egn5GCqugWvDFjafDOThIdMBsI15iqPqR5r15TfSr1YPYeEI19YeaXMCbY6u88Y76GLQ==", "dev": true, "funding": [ { @@ -5459,16 +5457,21 @@ "url": "https://github.com/sponsors/ai" } ], - "license": "MIT", "dependencies": { "nanoid": "^3.3.7", - "picocolors": "^1.0.1", - "source-map-js": "^1.2.0" + "picocolors": "^1.1.0", + "source-map-js": "^1.2.1" }, "engines": { "node": "^10 || ^12 || >=14" } }, + "node_modules/postcss/node_modules/picocolors": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", + "dev": true + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -5865,11 +5868,10 @@ } }, "node_modules/source-map-js": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", - "integrity": "sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" } @@ -6469,15 +6471,14 @@ } }, "node_modules/vite": { - "version": "5.3.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.3.5.tgz", - "integrity": "sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA==", + "version": "5.4.8", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz", + "integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==", "dev": true, - "license": "MIT", "dependencies": { "esbuild": "^0.21.3", - "postcss": "^8.4.39", - "rollup": "^4.13.0" + "postcss": "^8.4.43", + "rollup": "^4.20.0" }, "bin": { "vite": "bin/vite.js" @@ -6496,6 +6497,7 @@ "less": "*", "lightningcss": "^1.21.0", "sass": "*", + "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" @@ -6513,6 +6515,9 @@ "sass": { "optional": true }, + "sass-embedded": { + "optional": true + }, "stylus": { "optional": true }, diff --git a/package.json b/package.json index 37418014d93..480934899cc 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "typedoc": "^0.26.4", "typescript": "^5.5.3", "typescript-eslint": "^8.0.0-alpha.54", - "vite": "^5.3.5", + "vite": "^5.4.8", "vite-tsconfig-paths": "^4.3.2", "vitest": "^2.0.4", "vitest-canvas-mock": "^0.3.3" diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 06a477f0be3..df852126bc2 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1040,10 +1040,6 @@ export default class BattleScene extends SceneBase { this.gameMode = getGameMode(GameModes.CLASSIC); - this.setSeed(Overrides.SEED_OVERRIDE || Utils.randomString(24)); - console.log("Seed:", this.seed); - this.resetSeed(); // Properly resets RNG after saving and quitting a session - this.disableMenu = false; this.score = 0; @@ -1078,6 +1074,12 @@ export default class BattleScene extends SceneBase { //@ts-ignore - allowing `null` for currentBattle causes a lot of trouble this.currentBattle = null; // TODO: resolve ts-ignore + // Reset RNG after end of game or save & quit. + // This needs to happen after clearing this.currentBattle or the seed will be affected by the last wave played + this.setSeed(Overrides.SEED_OVERRIDE || Utils.randomString(24)); + console.log("Seed:", this.seed); + this.resetSeed(); + this.biomeWaveText.setText(startingWave.toString()); this.biomeWaveText.setVisible(false); diff --git a/src/battle.ts b/src/battle.ts index 778d2467803..412fd1b5184 100644 --- a/src/battle.ts +++ b/src/battle.ts @@ -497,7 +497,7 @@ function getRandomTrainerFunc(trainerPool: (TrainerType | TrainerType[])[], rand } /* 1/3 chance for evil team grunts to be double battles */ - const evilTeamGrunts = [TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT]; + const evilTeamGrunts = [TrainerType.ROCKET_GRUNT, TrainerType.MAGMA_GRUNT, TrainerType.AQUA_GRUNT, TrainerType.GALACTIC_GRUNT, TrainerType.PLASMA_GRUNT, TrainerType.FLARE_GRUNT, TrainerType.AETHER_GRUNT, TrainerType.SKULL_GRUNT, TrainerType.MACRO_GRUNT, TrainerType.STAR_GRUNT]; const isEvilTeamGrunt = evilTeamGrunts.includes(trainerTypes[rand]); if (trainerConfigs[trainerTypes[rand]].hasDouble && isEvilTeamGrunt) { diff --git a/src/data/ability.ts b/src/data/ability.ts index 3ace872de3c..041d90ac4c0 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -4,7 +4,7 @@ import { Constructor } from "#app/utils"; import * as Utils from "../utils"; import { getPokemonNameWithAffix } from "../messages"; import { Weather, WeatherType } from "./weather"; -import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags"; +import { BattlerTag, GroundedTag } from "./battler-tags"; import { StatusEffect, getNonVolatileStatusEffects, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, FlinchAttr, OneHitKOAttr, HitHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr, VariableMoveTypeAttr, RandomMovesetMoveAttr, RandomMoveAttr, NaturePowerAttr, CopyMoveAttr, MoveAttr, MultiHitAttr, ChargeAttr, SacrificialAttr, SacrificialAttrOnHit, NeutralDamageAgainstFlyingTypeMultiplierAttr, FixedDamageAttr } from "./move"; @@ -536,53 +536,6 @@ export class PostDefendAbAttr extends AbAttr { } } -/** - * Applies the effects of Gulp Missile when the user is hit by an attack. - * @extends PostDefendAbAttr - */ -export class PostDefendGulpMissileAbAttr extends PostDefendAbAttr { - constructor() { - super(true); - } - - /** - * Damages the attacker and triggers the secondary effect based on the form or the BattlerTagType. - * @param {Pokemon} pokemon - The defending Pokemon. - * @param passive - n/a - * @param {Pokemon} attacker - The attacking Pokemon. - * @param {Move} move - The move being used. - * @param {HitResult} hitResult - n/a - * @param {any[]} args - n/a - * @returns Whether the effects of the ability are applied. - */ - applyPostDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, hitResult: HitResult, args: any[]): boolean | Promise { - const battlerTag = pokemon.getTag(GulpMissileTag); - if (!battlerTag || move.category === MoveCategory.STATUS || pokemon.getTag(SemiInvulnerableTag)) { - return false; - } - - if (simulated) { - return true; - } - - const cancelled = new Utils.BooleanHolder(false); - applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled); - - if (!cancelled.value) { - attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER); - } - - if (battlerTag.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) { - pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1)); - } else { - attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon); - } - - pokemon.removeTag(battlerTag.tagType); - return true; - } -} - export class FieldPriorityMoveImmunityAbAttr extends PreDefendAbAttr { applyPreDefend(pokemon: Pokemon, passive: boolean, simulated: boolean, attacker: Pokemon, move: Move, cancelled: Utils.BooleanHolder, args: any[]): boolean { const attackPriority = new Utils.IntegerHolder(move.priority); @@ -5210,8 +5163,7 @@ export function initAbilities() { .attr(PostSummonMessageAbAttr, (pokemon: Pokemon) => i18next.t("abilityTriggers:postSummonMoldBreaker", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })) .attr(MoveAbilityBypassAbAttr), new Ability(Abilities.SUPER_LUCK, 4) - .attr(BonusCritAbAttr) - .partial(), + .attr(BonusCritAbAttr), new Ability(Abilities.AFTERMATH, 4) .attr(PostFaintContactDamageAbAttr, 4) .bypassFaint(), @@ -5669,13 +5621,19 @@ export function initAbilities() { new Ability(Abilities.MIRROR_ARMOR, 8) .ignorable() .unimplemented(), + /** + * Right now, the logic is attached to Surf and Dive moves. Ideally, the post-defend/hit should be an + * ability attribute but the current implementation of move effects for BattlerTag does not support this- in the case + * where Cramorant is fainted. + * @see {@linkcode GulpMissileTagAttr} and {@linkcode GulpMissileTag} for Gulp Missile implementation + */ new Ability(Abilities.GULP_MISSILE, 8) .attr(UnsuppressableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) .attr(NoFusionAbilityAbAttr) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) - .attr(PostDefendGulpMissileAbAttr), + .bypassFaint(), new Ability(Abilities.STALWART, 8) .attr(BlockRedirectAbAttr), new Ability(Abilities.STEAM_ENGINE, 8) diff --git a/src/data/arena-tag.ts b/src/data/arena-tag.ts index d783ea51056..9e121b81fea 100644 --- a/src/data/arena-tag.ts +++ b/src/data/arena-tag.ts @@ -511,6 +511,39 @@ class WaterSportTag extends WeakenMoveTypeTag { } } +/** + * Arena Tag class for the secondary effect of {@link https://bulbapedia.bulbagarden.net/wiki/Plasma_Fists_(move) | Plasma Fists}. + * Converts Normal-type moves to Electric type for the rest of the turn. + */ +export class PlasmaFistsTag extends ArenaTag { + constructor() { + super(ArenaTagType.PLASMA_FISTS, 1, Moves.PLASMA_FISTS); + } + + /** Queues Plasma Fists' on-add message */ + onAdd(arena: Arena): void { + arena.scene.queueMessage(i18next.t("arenaTag:plasmaFistsOnAdd")); + } + + onRemove(arena: Arena): void { } // Removes default on-remove message + + /** + * Converts Normal-type moves to Electric type + * @param arena n/a + * @param args + * - `[0]` {@linkcode Utils.NumberHolder} A container with a move's {@linkcode Type} + * @returns `true` if the given move type changed; `false` otherwise. + */ + apply(arena: Arena, args: any[]): boolean { + const moveType = args[0]; + if (moveType instanceof Utils.NumberHolder && moveType.value === Type.NORMAL) { + moveType.value = Type.ELECTRIC; + return true; + } + return false; + } +} + /** * Abstract class to implement arena traps. */ @@ -1010,6 +1043,8 @@ export function getArenaTag(tagType: ArenaTagType, turnCount: integer, sourceMov return new MudSportTag(turnCount, sourceId); case ArenaTagType.WATER_SPORT: return new WaterSportTag(turnCount, sourceId); + case ArenaTagType.PLASMA_FISTS: + return new PlasmaFistsTag(); case ArenaTagType.SPIKES: return new SpikesTag(sourceId, side); case ArenaTagType.TOXIC_SPIKES: diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 4e688251147..d8094f96368 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -2123,7 +2123,36 @@ export class StockpilingTag extends BattlerTag { */ export class GulpMissileTag extends BattlerTag { constructor(tagType: BattlerTagType, sourceMove: Moves) { - super(tagType, BattlerTagLapseType.CUSTOM, 0, sourceMove); + super(tagType, BattlerTagLapseType.HIT, 0, sourceMove); + } + + override lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (pokemon.getTag(BattlerTagType.UNDERWATER)) { + return true; + } + + const moveEffectPhase = pokemon.scene.getCurrentPhase(); + if (moveEffectPhase instanceof MoveEffectPhase) { + const attacker = moveEffectPhase.getUserPokemon(); + + if (!attacker) { + return false; + } + + const cancelled = new Utils.BooleanHolder(false); + applyAbAttrs(BlockNonDirectDamageAbAttr, attacker, cancelled); + + if (!cancelled.value) { + attacker.damageAndUpdate(Math.max(1, Math.floor(attacker.getMaxHp() / 4)), HitResult.OTHER); + } + + if (this.tagType === BattlerTagType.GULP_MISSILE_ARROKUDA) { + pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, attacker.getBattlerIndex(), false, [ Stat.DEF ], -1)); + } else { + attacker.trySetStatus(StatusEffect.PARALYSIS, true, pokemon); + } + } + return false; } /** @@ -2589,6 +2618,43 @@ export class ImprisonTag extends MoveRestrictionBattlerTag { } } +/** + * Battler Tag that applies the effects of Syrup Bomb to the target Pokemon. + * For three turns, starting from the turn of hit, at the end of each turn, the target Pokemon's speed will decrease by 1. + * The tag can also expire by taking the target Pokemon off the field. + */ +export class SyrupBombTag extends BattlerTag { + constructor() { + super(BattlerTagType.SYRUP_BOMB, BattlerTagLapseType.TURN_END, 3, Moves.SYRUP_BOMB); + } + + /** + * Adds the Syrup Bomb battler tag to the target Pokemon. + * @param {Pokemon} pokemon the target Pokemon + */ + override onAdd(pokemon: Pokemon) { + super.onAdd(pokemon); + pokemon.scene.queueMessage(i18next.t("battlerTags:syrupBombOnAdd", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); + } + + /** + * Applies the single-stage speed down to the target Pokemon and decrements the tag's turn count + * @param {Pokemon} pokemon the target Pokemon + * @param {BattlerTagLapseType} _lapseType + * @returns `true` if the turnCount is still greater than 0 | `false` if the turnCount is 0 or the target Pokemon has been removed from the field + */ + override lapse(pokemon: Pokemon, _lapseType: BattlerTagLapseType): boolean { + if (!pokemon.isActive(true)) { + return false; + } + pokemon.scene.queueMessage(i18next.t("battlerTags:syrupBombLapse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) })); // Custom message in lieu of an animation in mainline + pokemon.scene.unshiftPhase(new StatStageChangePhase( + pokemon.scene, pokemon.getBattlerIndex(), true, + [Stat.SPD], -1, true, false, true + )); + return --this.turnCount > 0; + } +} /** * Retrieves a {@linkcode BattlerTag} based on the provided tag type, turn count, source move, and source ID. @@ -2763,6 +2829,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: number, source return new TauntTag(); case BattlerTagType.IMPRISON: return new ImprisonTag(sourceId); + case BattlerTagType.SYRUP_BOMB: + return new SyrupBombTag(); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/move.ts b/src/data/move.ts index e3cb5d935d5..e163e89a272 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4544,6 +4544,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.DROWSY: case BattlerTagType.DISABLED: case BattlerTagType.HEAL_BLOCK: + case BattlerTagType.RECEIVE_DOUBLE_DAMAGE: return -5; case BattlerTagType.SEEDED: case BattlerTagType.SALT_CURED: @@ -4564,6 +4565,7 @@ export class AddBattlerTagAttr extends MoveEffectAttr { case BattlerTagType.ENCORE: return -2; case BattlerTagType.MINIMIZED: + case BattlerTagType.ALWAYS_GET_HIT: return 0; case BattlerTagType.INGRAIN: case BattlerTagType.IGNORE_ACCURACY: @@ -6589,7 +6591,7 @@ export class MoveCondition { export class FirstMoveCondition extends MoveCondition { constructor() { - super((user, target, move) => user.battleSummonData?.turnCount === 1); + super((user, target, move) => user.battleSummonData?.waveTurnCount === 1); } getUserBenefitScore(user: Pokemon, target: Pokemon, move: Move): integer { @@ -8906,8 +8908,8 @@ export function initMoves() { .attr(HalfSacrificialAttr) .target(MoveTarget.ALL_NEAR_OTHERS), new AttackMove(Moves.PLASMA_FISTS, Type.ELECTRIC, MoveCategory.PHYSICAL, 100, 100, 15, -1, 0, 7) - .punchingMove() - .partial(), + .attr(AddArenaTagAttr, ArenaTagType.PLASMA_FISTS, 1) + .punchingMove(), new AttackMove(Moves.PHOTON_GEYSER, Type.PSYCHIC, MoveCategory.SPECIAL, 100, 100, 5, -1, 0, 7) .attr(PhotonGeyserCategoryAttr) .ignoresAbilities() @@ -8937,7 +8939,7 @@ export function initMoves() { .partial() .ignoresVirtual(), /* End Unused */ - new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, 100, 2, 7) //LGPE Implementation + new AttackMove(Moves.ZIPPY_ZAP, Type.ELECTRIC, MoveCategory.PHYSICAL, 50, 100, 15, -1, 2, 7) //LGPE Implementation .attr(CritOnlyAttr), new AttackMove(Moves.SPLISHY_SPLASH, Type.WATER, MoveCategory.SPECIAL, 90, 100, 15, 30, 0, 7) .attr(StatusEffectAttr, StatusEffect.PARALYSIS) @@ -9622,9 +9624,8 @@ export function initMoves() { .target(MoveTarget.ALL_NEAR_ENEMIES) .triageMove(), new AttackMove(Moves.SYRUP_BOMB, Type.GRASS, MoveCategory.SPECIAL, 60, 85, 10, -1, 0, 9) - .attr(StatStageChangeAttr, [ Stat.SPD ], -1) //Temporary - .ballBombMove() - .partial(), + .attr(AddBattlerTagAttr, BattlerTagType.SYRUP_BOMB, false, false, 3) + .ballBombMove(), new AttackMove(Moves.IVY_CUDGEL, Type.GRASS, MoveCategory.PHYSICAL, 100, 100, 10, -1, 0, 9) .attr(IvyCudgelTypeAttr) .attr(HighCritAttr) diff --git a/src/enums/arena-tag-type.ts b/src/enums/arena-tag-type.ts index c6f911cb493..123d70b64fa 100644 --- a/src/enums/arena-tag-type.ts +++ b/src/enums/arena-tag-type.ts @@ -25,4 +25,5 @@ export enum ArenaTagType { SAFEGUARD = "SAFEGUARD", NO_CRIT = "NO_CRIT", IMPRISON = "IMPRISON", + PLASMA_FISTS = "PLASMA_FISTS", } diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index 9ed3b629746..209d36316f9 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -85,4 +85,5 @@ export enum BattlerTagType { TORMENT = "TORMENT", TAUNT = "TAUNT", IMPRISON = "IMPRISON", + SYRUP_BOMB = "SYRUP_BOMB", } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index d03140950e9..20de5b97242 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -65,6 +65,12 @@ import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/myster import { SwitchType } from "#enums/switch-type"; import { BASE_HIDDEN_ABILITY_CHANCE, BASE_SHINY_CHANCE } from "#app/data/balance/rates"; +/** `64/65536 -> 1/1024` */ +const BASE_SHINY_CHANCE = 64; + +/** `1/256` */ +const BASE_HIDDEN_ABILITY_CHANCE = 256; + export enum FieldPosition { CENTER, LEFT, @@ -1514,6 +1520,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { applyMoveAttrs(VariableMoveTypeAttr, this, null, move, moveTypeHolder); applyPreAttackAbAttrs(MoveTypeChangeAbAttr, this, null, move, simulated, moveTypeHolder); + this.scene.arena.applyTags(ArenaTagType.PLASMA_FISTS, moveTypeHolder); + return moveTypeHolder.value as Type; } @@ -3977,7 +3985,8 @@ export class PlayerPokemon extends Pokemon { let compatible = false; for (const p of tmSpecies[tm]) { if (Array.isArray(p)) { - if (p[0] === this.species.speciesId || (this.fusionSpecies && p[0] === this.fusionSpecies.speciesId) && p.slice(1).indexOf(this.species.forms[this.formIndex]) > -1) { + const [pkm, form] = p; + if ((pkm === this.species.speciesId || this.fusionSpecies && pkm === this.fusionSpecies.speciesId) && form === this.getFormKey()) { compatible = true; break; } @@ -4993,6 +5002,8 @@ export class PokemonBattleData { export class PokemonBattleSummonData { /** The number of turns the pokemon has passed since entering the battle */ public turnCount: number = 1; + /** The number of turns the pokemon has passed since the start of the wave */ + public waveTurnCount: number = 1; /** The list of moves the pokemon has used since entering the battle */ public moveHistory: TurnMove[] = []; } diff --git a/src/locales/de/arena-tag.json b/src/locales/de/arena-tag.json index 3bed4fefbd0..93ceb06f308 100644 --- a/src/locales/de/arena-tag.json +++ b/src/locales/de/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "Lehmsuhler hört auf zu wirken!", "waterSportOnAdd": "Die Stärke aller Feuer-Attacken wurde reduziert!", "waterSportOnRemove": "Nassmacher hört auf zu wirken!", + "plasmaFistsOnAdd": "Ein elektrisch geladener Niederschlag regnet auf das Kampffeld herab!", "spikesOnAdd": "Die {{opponentDesc}} sind von Stacheln umgeben!", "spikesActivateTrap": "Die {{pokemonNameWithAffix}} wurde durch Stachler verletzt!!", "toxicSpikesOnAdd": "Die {{opponentDesc}} sind überall von giftigen Stacheln umgeben", @@ -54,4 +55,4 @@ "safeguardOnRemove": "Der mystische Schleier, der das ganze Feld umgab, hat sich gelüftet!", "safeguardOnRemovePlayer": "Der mystische Schleier, der dein Team umgab, hat sich gelüftet!", "safeguardOnRemoveEnemy": "Der mystische Schleier, der das gegnerische Team umgab, hat sich gelüftet!" -} \ No newline at end of file +} diff --git a/src/locales/en/arena-tag.json b/src/locales/en/arena-tag.json index d8fed386b24..df79693c7bb 100644 --- a/src/locales/en/arena-tag.json +++ b/src/locales/en/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "The effects of Mud Sport\nhave faded.", "waterSportOnAdd": "Fire's power was weakened!", "waterSportOnRemove": "The effects of Water Sport\nhave faded.", + "plasmaFistsOnAdd": "A deluge of ions showers the battlefield!", "spikesOnAdd": "{{moveName}} were scattered\nall around {{opponentDesc}}'s feet!", "spikesActivateTrap": "{{pokemonNameWithAffix}} is hurt\nby the spikes!", "toxicSpikesOnAdd": "{{moveName}} were scattered\nall around {{opponentDesc}}'s feet!", diff --git a/src/locales/en/battler-tags.json b/src/locales/en/battler-tags.json index 520ac2a6202..bb4b708b25f 100644 --- a/src/locales/en/battler-tags.json +++ b/src/locales/en/battler-tags.json @@ -78,5 +78,7 @@ "tormentOnAdd": "{{pokemonNameWithAffix}} was subjected to torment!", "tauntOnAdd": "{{pokemonNameWithAffix}} fell for the taunt!", "imprisonOnAdd": "{{pokemonNameWithAffix}} sealed the opponents move(s)!", - "autotomizeOnAdd": "{{pokemonNameWithAffix}} became nimble!" + "autotomizeOnAdd": "{{pokemonNameWithAffix}} became nimble!", + "syrupBombOnAdd": "{{pokemonNameWithAffix}} got covered in sticky, candy syrup!", + "syrupBombLapse": "The sticky syrup slowed down {{pokemonNameWithAffix}}!" } diff --git a/src/locales/es/arena-tag.json b/src/locales/es/arena-tag.json index 0f63b62e784..9aa37654c62 100644 --- a/src/locales/es/arena-tag.json +++ b/src/locales/es/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "Chapoteo Lodo ha dejado de surtir efecto.", "waterSportOnAdd": "¡Se han debilitado los ataques\nde tipo Fuego!", "waterSportOnRemove": "Hidrochorro ha dejado de surtir efecto.", + "plasmaFistsOnAdd": "¡Una lluvia de electrones cae sobre\nel terreno de combate!", "spikesOnAdd": "¡El equipo de {{opponentDesc}} ha sido rodeado por {{moveName}}!", "spikesActivateTrap": "¡Las púas han herido a {{pokemonNameWithAffix}}!", "toxicSpikesOnAdd": "¡El equipo de {{opponentDesc}} ha sido rodeado por {{moveName}}!", @@ -54,4 +55,4 @@ "safeguardOnRemove": "¡Velo Sagrado dejó de hacer efecto!", "safeguardOnRemovePlayer": "El efecto de Velo Sagrado en tu equipo se ha disipado.", "safeguardOnRemoveEnemy": "El efecto de Velo Sagrado en el equipo enemigo se ha disipado." -} \ No newline at end of file +} diff --git a/src/locales/fr/arena-tag.json b/src/locales/fr/arena-tag.json index 9cb2f342068..95e38cdbe9d 100644 --- a/src/locales/fr/arena-tag.json +++ b/src/locales/fr/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "L’effet de Lance-Boue se dissipe !", "waterSportOnAdd": "La puissance des capacités\nde type Feu diminue !", "waterSportOnRemove": "L’effet de Tourniquet se dissipe !", + "plasmaFistsOnAdd": "Un déluge de plasma s’abat sur le terrain !", "spikesOnAdd": "Des {{moveName}} s’éparpillent autour de {{opponentDesc}} !", "spikesActivateTrap": "{{pokemonNameWithAffix}} est blessé\npar les picots !", "toxicSpikesOnAdd": "Des {{moveName}} s’éparpillent autour de {{opponentDesc}} !", diff --git a/src/locales/it/arena-tag.json b/src/locales/it/arena-tag.json index a1c5ee5b3c9..be2a06eb898 100644 --- a/src/locales/it/arena-tag.json +++ b/src/locales/it/arena-tag.json @@ -1,8 +1,9 @@ { + "plasmaFistsOnAdd": "Una pioggia di elettroni si rovescia sui Pokémon!", "safeguardOnAdd": "Un velo mistico ricopre il campo!", "safeguardOnAddPlayer": "Un velo mistico ricopre la tua squadra!", "safeguardOnAddEnemy": "Un velo mistico ricopre la squadra avversaria!", "safeguardOnRemove": "Il campo non è più protetto da Salvaguardia!", "safeguardOnRemovePlayer": "La tua squadra non è più protetta da Salvaguardia!", "safeguardOnRemoveEnemy": "La squadra avversaria non è più protetta da Salvaguardia!" -} \ No newline at end of file +} diff --git a/src/locales/ja/ability-trigger.json b/src/locales/ja/ability-trigger.json index 26d27701aef..6897b466195 100644 --- a/src/locales/ja/ability-trigger.json +++ b/src/locales/ja/ability-trigger.json @@ -2,11 +2,11 @@ "blockRecoilDamage": "{{pokemonName}}は {{abilityName}}で 反動ダメージを 受けない!", "badDreams": "{{pokemonName}}は ナイトメアに うなされている!", "costar": "{{pokemonName}}は {{allyName}}の\n能力変化を コピーした!", - "iceFaceAvoidedDamage": "{{pokemonNameWithAffix}}は\n{{abilityName}}で ダメージを 受けない!", + "iceFaceAvoidedDamage": "{{pokemonNameWithAffix}} は\n{{abilityName}}で ダメージを 受けない!", "perishBody": "{{pokemonName}}の {{abilityName}}で\nおたがいは 3ターン後に ほろびいてしまう!", "poisonHeal": "{{pokemonName}}は {{abilityName}}で 回復した!", "trace": "{{pokemonName}}は 相手の {{targetName}}の\n{{abilityName}}を トレースした!", - "windPowerCharged": "{{pokemonNameWithAffix}}は\n{{moveName}}を 受けて じゅうでんした!", + "windPowerCharged": "{{pokemonName}} は\n{{moveName}}を 受けて じゅうでんした!", "quickDraw": "{{pokemonName}}は クイックドロウで\n行動が はやくなった!", "disguiseAvoidedDamage": "{{pokemonNameWithAffix}}の\nばけのかわが はがれた!", "blockItemTheft": "{{pokemonNameWithAffix}}の {{abilityName}}で\n道具を うばわれない!", @@ -48,8 +48,8 @@ "weatherEffectDisappeared": "天候の影響が なくなった!", "postSummonMoldBreaker": "{{pokemonNameWithAffix}}は\nかたやぶりだ!", "postSummonAnticipation": "{{pokemonNameWithAffix}}は\nみぶるいした!", - "postSummonTurboblaze": "{{pokemonNameWithAffix}}は\n燃え盛(もえさか)る オーラを 放っている!", - "postSummonTeravolt": "{{pokemonNameWithAffix}}は\n弾(はじ)ける オーラを 放っている!", + "postSummonTurboblaze": "{{pokemonNameWithAffix}}は\n燃え盛る オーラを 放っている!", + "postSummonTeravolt": "{{pokemonNameWithAffix}}は\n弾ける オーラを 放っている!", "postSummonDarkAura": "{{pokemonNameWithAffix}}は\nダークオーラを 放っている!", "postSummonFairyAura": "{{pokemonNameWithAffix}}は\nフェアリーオーラを 放っている!", "postSummonAuraBreak": "{{pokemonNameWithAffix}}は\nすべての オーラを 制圧する!", diff --git a/src/locales/ja/arena-tag.json b/src/locales/ja/arena-tag.json index a81942338fd..0da759884a5 100644 --- a/src/locales/ja/arena-tag.json +++ b/src/locales/ja/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "どろあそびの 効果が なくなった!", "waterSportOnAdd": "炎の威力が 弱まった!", "waterSportOnRemove": "みずあそびの 効果が なくなった!", + "plasmaFistsOnAdd": "電子のシャワーが 降りそそいだ!", "spikesOnAdd": "{{opponentDesc}}の 足下に\n{{moveName}}が 散らばった!", "spikesActivateTrap": "{{pokemonNameWithAffix}}は\nまきびしの ダメージを 受けた!", "toxicSpikesOnAdd": "{{opponentDesc}}の 足下に\n{{moveName}}が 散らばった!", diff --git a/src/locales/ko/arena-tag.json b/src/locales/ko/arena-tag.json index ce9922ab3bf..c1a7b1ca7ca 100644 --- a/src/locales/ko/arena-tag.json +++ b/src/locales/ko/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "흙놀이의 효과가\n없어졌다!", "waterSportOnAdd": "불꽃의 위력이 약해졌다!", "waterSportOnRemove": "물놀이의 효과가\n없어졌다!", + "plasmaFistsOnAdd": "전기 입자가 쏟아졌다!", "spikesOnAdd": "{{opponentDesc}}의 발밑에\n압정이 뿌려졌다!", "spikesActivateTrap": "{{pokemonNameWithAffix}}[[는]]\n압정뿌리기의 데미지를 입었다!", "toxicSpikesOnAdd": "{{opponentDesc}}의 발밑에\n독압정이 뿌려졌다!", @@ -54,4 +55,4 @@ "safeguardOnRemove": "필드를 감싸던 신비의 베일이 없어졌다!", "safeguardOnRemovePlayer": "우리 편을 감싸던 신비의 베일이 없어졌다!", "safeguardOnRemoveEnemy": "상대 편을 감싸던 신비의 베일이 없어졌다!" -} \ No newline at end of file +} diff --git a/src/locales/pt_BR/arena-tag.json b/src/locales/pt_BR/arena-tag.json index 3a1476dcef6..5fb8b49565f 100644 --- a/src/locales/pt_BR/arena-tag.json +++ b/src/locales/pt_BR/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "Os efeitos de Mud Sport\nsumiram.", "waterSportOnAdd": "O poder de movimentos de fogo foi enfraquecido!", "waterSportOnRemove": "Os efeitos de Water Sport\nsumiram.", + "plasmaFistsOnAdd": "Um dilúvio de íons chove sobre o campo de batalha!", "spikesOnAdd": "{{moveName}} foram espalhados\nno chão ao redor de {{opponentDesc}}!", "spikesActivateTrap": "{{pokemonNameWithAffix}} foi ferido\npelos espinhos!", "toxicSpikesOnAdd": "{{moveName}} foram espalhados\nno chão ao redor de {{opponentDesc}}!", diff --git a/src/locales/zh_CN/arena-tag.json b/src/locales/zh_CN/arena-tag.json index 74ad38ba9bf..d7ac1b9b04b 100644 --- a/src/locales/zh_CN/arena-tag.json +++ b/src/locales/zh_CN/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "玩泥巴的效果消失了!", "waterSportOnAdd": "火焰的威力减弱了!", "waterSportOnRemove": "玩水的效果消失了!", + "plasmaFistsOnAdd": "等离子雨倾盆而下!", "spikesOnAdd": "{{opponentDesc}}脚下\n散落着{{moveName}}!", "spikesActivateTrap": "{{pokemonNameWithAffix}}\n受到了撒菱的伤害!", "toxicSpikesOnAdd": "{{opponentDesc}}脚下\n散落着{{moveName}}!", @@ -54,4 +55,4 @@ "safeguardOnRemove": "包围整个场地的\n神秘之幕消失了!", "safeguardOnRemovePlayer": "包围我方的\n神秘之幕消失了!", "safeguardOnRemoveEnemy": "包围对手的\n神秘之幕消失了!" -} \ No newline at end of file +} diff --git a/src/locales/zh_TW/arena-tag.json b/src/locales/zh_TW/arena-tag.json index a6224f300a3..4a08d268e20 100644 --- a/src/locales/zh_TW/arena-tag.json +++ b/src/locales/zh_TW/arena-tag.json @@ -28,6 +28,7 @@ "mudSportOnRemove": "玩泥巴的效果消失了!", "waterSportOnAdd": "火焰的威力減弱了!", "waterSportOnRemove": "玩水的效果消失了!", + "plasmaFistsOnAdd": "等離子雨傾盆而下!", "spikesOnAdd": "{{opponentDesc}}腳下\n散落著{{moveName}}!", "spikesActivateTrap": "{{pokemonNameWithAffix}}\n受到了撒菱的傷害!", "toxicSpikesOnAdd": "{{opponentDesc}}腳下\n散落著{{moveName}}!", diff --git a/src/phases/battle-end-phase.ts b/src/phases/battle-end-phase.ts index 902a85325ad..eaa458af904 100644 --- a/src/phases/battle-end-phase.ts +++ b/src/phases/battle-end-phase.ts @@ -35,6 +35,12 @@ export class BattleEndPhase extends BattlePhase { this.scene.unshiftPhase(new GameOverPhase(this.scene, true)); } + for (const pokemon of this.scene.getField()) { + if (pokemon && pokemon.battleSummonData) { + pokemon.battleSummonData.waveTurnCount = 1; + } + } + for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) { applyPostBattleAbAttrs(PostBattleAbAttr, pokemon); } diff --git a/src/phases/switch-summon-phase.ts b/src/phases/switch-summon-phase.ts index eb1e089543b..07761b10d6e 100644 --- a/src/phases/switch-summon-phase.ts +++ b/src/phases/switch-summon-phase.ts @@ -173,6 +173,7 @@ export class SwitchSummonPhase extends SummonPhase { // Or compensate for force switch move if switched out pokemon is not fainted if (currentCommand === Command.POKEMON || lastPokemonIsForceSwitchedAndNotFainted) { pokemon.battleSummonData.turnCount--; + pokemon.battleSummonData.waveTurnCount--; } if (this.switchType === SwitchType.BATON_PASS && pokemon) { diff --git a/src/phases/turn-end-phase.ts b/src/phases/turn-end-phase.ts index 724a5206d74..60a2e6600db 100644 --- a/src/phases/turn-end-phase.ts +++ b/src/phases/turn-end-phase.ts @@ -44,6 +44,7 @@ export class TurnEndPhase extends FieldPhase { this.scene.applyModifiers(TurnHeldItemTransferModifier, pokemon.isPlayer(), pokemon); pokemon.battleSummonData.turnCount++; + pokemon.battleSummonData.waveTurnCount++; }; this.executeForAll(handlePokemon); diff --git a/src/phases/turn-start-phase.ts b/src/phases/turn-start-phase.ts index b070abb390a..95d55986185 100644 --- a/src/phases/turn-start-phase.ts +++ b/src/phases/turn-start-phase.ts @@ -43,20 +43,17 @@ export class TurnStartPhase extends FieldPhase { orderedTargets = Utils.randSeedShuffle(orderedTargets); }, this.scene.currentBattle.turn, this.scene.waveSeed); - orderedTargets.sort((a: Pokemon, b: Pokemon) => { - const aSpeed = a?.getEffectiveStat(Stat.SPD) || 0; - const bSpeed = b?.getEffectiveStat(Stat.SPD) || 0; - - return bSpeed - aSpeed; - }); - - // Next, a check for Trick Room is applied. If Trick Room is present, the order is reversed. + // Next, a check for Trick Room is applied to determine sort order. const speedReversed = new Utils.BooleanHolder(false); this.scene.arena.applyTags(TrickRoomTag, speedReversed); - if (speedReversed.value) { - orderedTargets = orderedTargets.reverse(); - } + // Adjust the sort function based on whether Trick Room is active. + orderedTargets.sort((a: Pokemon, b: Pokemon) => { + const aSpeed = a?.getEffectiveStat(Stat.SPD) ?? 0; + const bSpeed = b?.getEffectiveStat(Stat.SPD) ?? 0; + + return speedReversed.value ? aSpeed - bSpeed : bSpeed - aSpeed; + }); return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : BattlerIndex.PLAYER)); } diff --git a/src/test/abilities/gulp_missile.test.ts b/src/test/abilities/gulp_missile.test.ts index ac0efd8e8d1..d981f009974 100644 --- a/src/test/abilities/gulp_missile.test.ts +++ b/src/test/abilities/gulp_missile.test.ts @@ -1,11 +1,7 @@ -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import { StatusEffect } from "#app/enums/status-effect"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { StatusEffect } from "#enums/status-effect"; import Pokemon from "#app/field/pokemon"; -import { BerryPhase } from "#app/phases/berry-phase"; -import { MoveEndPhase } from "#app/phases/move-end-phase"; -import { TurnEndPhase } from "#app/phases/turn-end-phase"; -import { TurnStartPhase } from "#app/phases/turn-start-phase"; -import GameManager from "#app/test/utils/gameManager"; +import GameManager from "#test/utils/gameManager"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -53,13 +49,13 @@ describe("Abilities - Gulp Missile", () => { }); it("changes to Gulping Form if HP is over half when Surf or Dive is used", async () => { - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; game.move.select(Moves.DIVE); await game.toNextTurn(); game.move.select(Moves.DIVE); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getHpRatio()).toBeGreaterThanOrEqual(.5); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); @@ -67,21 +63,21 @@ describe("Abilities - Gulp Missile", () => { }); it("changes to Gorging Form if HP is under half when Surf or Dive is used", async () => { - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.49); expect(cramorant.getHpRatio()).toBe(.49); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined(); expect(cramorant.formIndex).toBe(GORGING_FORM); }); it("changes to base form when switched out after Surf or Dive is used", async () => { - await game.startBattle([Species.CRAMORANT, Species.MAGIKARP]); + await game.classicMode.startBattle([Species.CRAMORANT, Species.MAGIKARP]); const cramorant = game.scene.getPlayerPokemon()!; game.move.select(Moves.SURF); @@ -96,51 +92,51 @@ describe("Abilities - Gulp Missile", () => { }); it("changes form during Dive's charge turn", async () => { - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; game.move.select(Moves.DIVE); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); }); it("deals 1/4 of the attacker's maximum HP when hit by a damaging attack", async () => { - game.override.enemyMoveset([Moves.TACKLE]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TACKLE); + await game.classicMode.startBattle([Species.CRAMORANT]); const enemy = game.scene.getEnemyPokemon()!; vi.spyOn(enemy, "damageAndUpdate"); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); }); it("does not have any effect when hit by non-damaging attack", async () => { - game.override.enemyMoveset([Moves.TAIL_WHIP]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TAIL_WHIP); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); }); it("lowers attacker's DEF stat stage by 1 when hit in Gulping form", async () => { - game.override.enemyMoveset([Moves.TACKLE]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TACKLE); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -149,12 +145,12 @@ describe("Abilities - Gulp Missile", () => { vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); expect(enemy.getStatStage(Stat.DEF)).toBe(-1); @@ -163,8 +159,8 @@ describe("Abilities - Gulp Missile", () => { }); it("paralyzes the enemy when hit in Gorging form", async () => { - game.override.enemyMoveset([Moves.TACKLE]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TACKLE); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -173,12 +169,12 @@ describe("Abilities - Gulp Missile", () => { vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.45); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_PIKACHU)).toBeDefined(); expect(cramorant.formIndex).toBe(GORGING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.damageAndUpdate).toHaveReturnedWith(getEffectDamage(enemy)); expect(enemy.status?.effect).toBe(StatusEffect.PARALYSIS); @@ -187,21 +183,21 @@ describe("Abilities - Gulp Missile", () => { }); it("does not activate the ability when underwater", async () => { - game.override.enemyMoveset([Moves.SURF]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.SURF); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; game.move.select(Moves.DIVE); - await game.phaseInterceptor.to(BerryPhase, false); + await game.phaseInterceptor.to("BerryPhase", false); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); }); it("prevents effect damage but inflicts secondary effect on attacker with Magic Guard", async () => { - game.override.enemyMoveset([Moves.TACKLE]).enemyAbility(Abilities.MAGIC_GUARD); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.TACKLE).enemyAbility(Abilities.MAGIC_GUARD); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; const enemy = game.scene.getEnemyPokemon()!; @@ -209,13 +205,13 @@ describe("Abilities - Gulp Missile", () => { vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); const enemyHpPreEffect = enemy.hp; expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(enemy.hp).toBe(enemyHpPreEffect); expect(enemy.getStatStage(Stat.DEF)).toBe(-1); @@ -223,20 +219,36 @@ describe("Abilities - Gulp Missile", () => { expect(cramorant.formIndex).toBe(NORMAL_FORM); }); + it("activates on faint", async () => { + game.override.enemyMoveset(Moves.THUNDERBOLT); + await game.classicMode.startBattle([Species.CRAMORANT]); + + const cramorant = game.scene.getPlayerPokemon()!; + + game.move.select(Moves.SURF); + await game.phaseInterceptor.to("FaintPhase"); + + expect(cramorant.hp).toBe(0); + expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeUndefined(); + expect(cramorant.formIndex).toBe(NORMAL_FORM); + expect(game.scene.getEnemyPokemon()!.getStatStage(Stat.DEF)).toBe(-1); + }); + + it("cannot be suppressed", async () => { - game.override.enemyMoveset([Moves.GASTRO_ACID]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.GASTRO_ACID); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); @@ -244,19 +256,19 @@ describe("Abilities - Gulp Missile", () => { }); it("cannot be swapped with another ability", async () => { - game.override.enemyMoveset([Moves.SKILL_SWAP]); - await game.startBattle([Species.CRAMORANT]); + game.override.enemyMoveset(Moves.SKILL_SWAP); + await game.classicMode.startBattle([Species.CRAMORANT]); const cramorant = game.scene.getPlayerPokemon()!; vi.spyOn(cramorant, "getHpRatio").mockReturnValue(.55); game.move.select(Moves.SURF); - await game.phaseInterceptor.to(MoveEndPhase); + await game.phaseInterceptor.to("MoveEndPhase"); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); expect(cramorant.formIndex).toBe(GULPING_FORM); - await game.phaseInterceptor.to(TurnEndPhase); + await game.phaseInterceptor.to("TurnEndPhase"); expect(cramorant.hasAbility(Abilities.GULP_MISSILE)).toBe(true); expect(cramorant.getTag(BattlerTagType.GULP_MISSILE_ARROKUDA)).toBeDefined(); @@ -266,9 +278,9 @@ describe("Abilities - Gulp Missile", () => { it("cannot be copied", async () => { game.override.enemyAbility(Abilities.TRACE); - await game.startBattle([Species.CRAMORANT]); + await game.classicMode.startBattle([Species.CRAMORANT]); game.move.select(Moves.SPLASH); - await game.phaseInterceptor.to(TurnStartPhase); + await game.phaseInterceptor.to("TurnStartPhase"); expect(game.scene.getEnemyPokemon()?.hasAbility(Abilities.GULP_MISSILE)).toBe(false); }); diff --git a/src/test/field/pokemon.test.ts b/src/test/field/pokemon.test.ts index f7c1cf8bc3d..225f302ff0c 100644 --- a/src/test/field/pokemon.test.ts +++ b/src/test/field/pokemon.test.ts @@ -3,6 +3,7 @@ import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; import GameManager from "../utils/gameManager"; import { PokeballType } from "#app/enums/pokeball"; import BattleScene from "#app/battle-scene"; +import { Moves } from "#app/enums/moves"; describe("Spec - Pokemon", () => { let phaserGame: Phaser.Game; @@ -63,4 +64,15 @@ describe("Spec - Pokemon", () => { }); }); }); + + it("should not share tms between different forms", async () => { + game.override.starterForms({ [Species.ROTOM]: 4 }); + + await game.classicMode.startBattle([Species.ROTOM]); + + const fanRotom = game.scene.getPlayerPokemon()!; + + expect(fanRotom.compatibleTms).not.toContain(Moves.BLIZZARD); + expect(fanRotom.compatibleTms).toContain(Moves.AIR_SLASH); + }); }); diff --git a/src/test/moves/fake_out.test.ts b/src/test/moves/fake_out.test.ts index 04d6216b952..e306ab12a3f 100644 --- a/src/test/moves/fake_out.test.ts +++ b/src/test/moves/fake_out.test.ts @@ -23,14 +23,15 @@ describe("Moves - Fake Out", () => { game.override .battleType("single") .enemySpecies(Species.CORVIKNIGHT) - .starterSpecies(Species.FEEBAS) .moveset([Moves.FAKE_OUT, Moves.SPLASH]) .enemyMoveset(Moves.SPLASH) + .enemyLevel(10) + .startingLevel(10) // prevent LevelUpPhase from happening .disableCrits(); }); - it("can only be used on the first turn a pokemon is sent out", async() => { - await game.classicMode.startBattle(); + it("can only be used on the first turn a pokemon is sent out in a battle", async() => { + await game.classicMode.startBattle([Species.FEEBAS]); const enemy = game.scene.getEnemyPokemon()!; @@ -44,22 +45,27 @@ describe("Moves - Fake Out", () => { await game.toNextTurn(); expect(enemy.hp).toBe(postTurnOneHp); + }, 20000); - game.move.select(Moves.SPLASH); - await game.doKillOpponents(); + // This is a PokeRogue buff to Fake Out + it("can be used at the start of every wave even if the pokemon wasn't recalled", async() => { + await game.classicMode.startBattle([Species.FEEBAS]); + + const enemy = game.scene.getEnemyPokemon()!; + enemy.damageAndUpdate(enemy.getMaxHp() - 1); + + game.move.select(Moves.FAKE_OUT); await game.toNextWave(); - const newEnemy = game.scene.getEnemyPokemon()!; - game.move.select(Moves.FAKE_OUT); await game.toNextTurn(); - expect(newEnemy.hp).toBe(newEnemy.getMaxHp()); + expect(game.scene.getEnemyPokemon()!.isFullHp()).toBe(false); }, 20000); it("can be used again if recalled and sent back out", async() => { game.override.startingWave(4); - await game.classicMode.startBattle(); + await game.classicMode.startBattle([Species.FEEBAS, Species.MAGIKARP]); const enemy1 = game.scene.getEnemyPokemon()!; @@ -76,6 +82,18 @@ describe("Moves - Fake Out", () => { const enemy2 = game.scene.getEnemyPokemon()!; + expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp()); + enemy2.hp = enemy2.getMaxHp(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.doSwitchPokemon(1); + await game.toNextTurn(); + + game.move.select(Moves.FAKE_OUT); + await game.toNextTurn(); + expect(enemy2.hp).toBeLessThan(enemy2.getMaxHp()); }, 20000); }); diff --git a/src/test/moves/plasma_fists.test.ts b/src/test/moves/plasma_fists.test.ts new file mode 100644 index 00000000000..a9bd7660dfd --- /dev/null +++ b/src/test/moves/plasma_fists.test.ts @@ -0,0 +1,98 @@ +import { BattlerIndex } from "#app/battle"; +import { Type } from "#app/data/type"; +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, it, expect, vi } from "vitest"; + +describe("Moves - Plasma Fists", () => { + 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.PLASMA_FISTS, Moves.TACKLE]) + .battleType("double") + .startingLevel(100) + .enemySpecies(Species.DUSCLOPS) + .enemyAbility(Abilities.BALL_FETCH) + .enemyMoveset(Moves.TACKLE) + .enemyLevel(100); + }); + + it("should convert all subsequent Normal-type attacks to Electric-type", async () => { + await game.classicMode.startBattle([Species.DUSCLOPS, Species.BLASTOISE]); + + const field = game.scene.getField(true); + field.forEach(p => vi.spyOn(p, "getMoveType")); + + game.move.select(Moves.PLASMA_FISTS, 0, BattlerIndex.ENEMY); + game.move.select(Moves.TACKLE, 1, BattlerIndex.ENEMY_2); + + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER); + await game.forceEnemyMove(Moves.TACKLE, BattlerIndex.PLAYER_2); + + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.PLAYER_2, BattlerIndex.ENEMY, BattlerIndex.ENEMY_2]); + + await game.phaseInterceptor.to("BerryPhase", false); + + field.forEach(p => { + expect(p.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC); + expect(p.hp).toBeLessThan(p.getMaxHp()); + }); + }); + + it("should not affect Normal-type attacks boosted by Pixilate", async () => { + game.override + .battleType("single") + .enemyAbility(Abilities.PIXILATE); + + await game.classicMode.startBattle([Species.ONIX]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + vi.spyOn(enemyPokemon, "getMoveType"); + + game.move.select(Moves.PLASMA_FISTS); + + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.getMoveType).toHaveLastReturnedWith(Type.FAIRY); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + }); + + it("should affect moves that become Normal type due to Normalize", async () => { + game.override + .battleType("single") + .enemyAbility(Abilities.NORMALIZE) + .enemyMoveset(Moves.WATER_GUN); + + await game.classicMode.startBattle([Species.DUSCLOPS]); + + const playerPokemon = game.scene.getPlayerPokemon()!; + const enemyPokemon = game.scene.getEnemyPokemon()!; + vi.spyOn(enemyPokemon, "getMoveType"); + + game.move.select(Moves.PLASMA_FISTS); + + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.phaseInterceptor.to("BerryPhase", false); + + expect(enemyPokemon.getMoveType).toHaveLastReturnedWith(Type.ELECTRIC); + expect(playerPokemon.hp).toBeLessThan(playerPokemon.getMaxHp()); + }); +}); diff --git a/src/test/moves/syrup_bomb.test.ts b/src/test/moves/syrup_bomb.test.ts new file mode 100644 index 00000000000..20cd590e457 --- /dev/null +++ b/src/test/moves/syrup_bomb.test.ts @@ -0,0 +1,82 @@ +import { allMoves } from "#app/data/move"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Abilities } from "#enums/abilities"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { Stat } from "#enums/stat"; +import GameManager from "#test/utils/gameManager"; +import Phaser from "phaser"; +import { BattlerIndex } from "#app/battle"; +import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("Moves - SYRUP BOMB", () => { + 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 + .starterSpecies(Species.MAGIKARP) + .enemySpecies(Species.SNORLAX) + .startingLevel(30) + .enemyLevel(100) + .moveset([Moves.SYRUP_BOMB, Moves.SPLASH]) + .enemyMoveset(Moves.SPLASH); + vi.spyOn(allMoves[Moves.SYRUP_BOMB], "accuracy", "get").mockReturnValue(100); + }); + + //Bulbapedia Reference: https://bulbapedia.bulbagarden.net/wiki/syrup_bomb_(move) + + it("decreases the target Pokemon's speed stat once per turn for 3 turns", + async () => { + await game.startBattle([Species.MAGIKARP]); + + const targetPokemon = game.scene.getEnemyPokemon()!; + expect(targetPokemon.getStatStage(Stat.SPD)).toBe(0); + + game.move.select(Moves.SYRUP_BOMB); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.move.forceHit(); + await game.toNextTurn(); + expect(targetPokemon.getTag(BattlerTagType.SYRUP_BOMB)).toBeDefined(); + expect(targetPokemon.getStatStage(Stat.SPD)).toBe(-1); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(targetPokemon.getTag(BattlerTagType.SYRUP_BOMB)).toBeDefined(); + expect(targetPokemon.getStatStage(Stat.SPD)).toBe(-2); + + game.move.select(Moves.SPLASH); + await game.toNextTurn(); + expect(targetPokemon.getTag(BattlerTagType.SYRUP_BOMB)).toBeUndefined(); + expect(targetPokemon.getStatStage(Stat.SPD)).toBe(-3); + } + ); + + it("does not affect Pokemon with the ability Bulletproof", + async () => { + game.override.enemyAbility(Abilities.BULLETPROOF); + await game.startBattle([Species.MAGIKARP]); + + const targetPokemon = game.scene.getEnemyPokemon()!; + + game.move.select(Moves.SYRUP_BOMB); + await game.setTurnOrder([BattlerIndex.PLAYER, BattlerIndex.ENEMY]); + await game.move.forceHit(); + await game.toNextTurn(); + expect(targetPokemon.isFullHp()).toBe(true); + expect(targetPokemon.getTag(BattlerTagType.SYRUP_BOMB)).toBeUndefined(); + expect(targetPokemon.getStatStage(Stat.SPD)).toBe(0); + } + ); +}); diff --git a/src/test/utils/gameManager.ts b/src/test/utils/gameManager.ts index 6fad87df182..a2403de7e18 100644 --- a/src/test/utils/gameManager.ts +++ b/src/test/utils/gameManager.ts @@ -303,7 +303,7 @@ export default class GameManager { vi.spyOn(enemy, "getNextMove").mockReturnValueOnce({ move: moveId, - targets: (target && !legalTargets.multiple && legalTargets.targets.includes(target)) + targets: (target !== undefined && !legalTargets.multiple && legalTargets.targets.includes(target)) ? [target] : enemy.getNextTargets(moveId) }); diff --git a/src/ui/egg-summary-ui-handler.ts b/src/ui/egg-summary-ui-handler.ts index f4c3e056360..519722b1505 100644 --- a/src/ui/egg-summary-ui-handler.ts +++ b/src/ui/egg-summary-ui-handler.ts @@ -99,8 +99,9 @@ export default class EggSummaryUiHandler extends MessageUiHandler { clear() { super.clear(); - this.cursor = -1; this.scrollGridHandler.reset(); + this.cursor = -1; + this.summaryContainer.setVisible(false); this.pokemonIconsContainer.removeAll(true); this.pokemonContainers = []; @@ -164,8 +165,8 @@ export default class EggSummaryUiHandler extends MessageUiHandler { this.scrollGridHandler.setTotalElements(this.eggHatchData.length); this.updatePokemonIcons(); - this.setCursor(0); + this.scene.playSoundWithoutBgm("evolution_fanfare"); return true; } diff --git a/src/ui/title-ui-handler.ts b/src/ui/title-ui-handler.ts index 4087b397ff7..79baf407fb6 100644 --- a/src/ui/title-ui-handler.ts +++ b/src/ui/title-ui-handler.ts @@ -6,6 +6,7 @@ import { TextStyle, addTextObject, getTextStyleOptions } from "./text"; import { getSplashMessages } from "../data/splash-messages"; import i18next from "i18next"; import { TimedEventDisplay } from "#app/timed-event-manager"; +import { version } from "../../package.json"; export default class TitleUiHandler extends OptionSelectUiHandler { /** If the stats can not be retrieved, use this fallback value */ @@ -16,6 +17,7 @@ export default class TitleUiHandler extends OptionSelectUiHandler { private splashMessage: string; private splashMessageText: Phaser.GameObjects.Text; private eventDisplay: TimedEventDisplay; + private appVersionText: Phaser.GameObjects.Text; private titleStatsTimer: NodeJS.Timeout | null; @@ -68,6 +70,11 @@ export default class TitleUiHandler extends OptionSelectUiHandler { loop: -1, yoyo: true, }); + + this.appVersionText = addTextObject(this.scene, logo.x - 60, logo.y + logo.displayHeight + 4, "", TextStyle.MONEY, { fontSize: "54px" }); + this.appVersionText.setOrigin(0.5, 0.5); + this.appVersionText.setAngle(0); + this.titleContainer.add(this.appVersionText); } updateTitleStats(): void { @@ -91,6 +98,8 @@ export default class TitleUiHandler extends OptionSelectUiHandler { this.splashMessage = Utils.randItem(getSplashMessages()); this.splashMessageText.setText(i18next.t(this.splashMessage, { count: TitleUiHandler.BATTLES_WON_FALLBACK })); + this.appVersionText.setText("v"+version); + const ui = this.getUi(); if (this.scene.eventManager.isEventActive()) {