diff --git a/.env.beta b/.env.beta index 88147215258..38888ec3d97 100644 --- a/.env.beta +++ b/.env.beta @@ -1,6 +1,6 @@ VITE_BYPASS_LOGIN=0 VITE_BYPASS_TUTORIAL=0 -VITE_SERVER_URL=https://api.beta.pokerogue.net +VITE_SERVER_URL=https://apibeta.pokerogue.net VITE_DISCORD_CLIENT_ID=1248062921129459756 VITE_GOOGLE_CLIENT_ID=955345393540-2k6lfftf0fdnb0krqmpthjnqavfvvf73.apps.googleusercontent.com VITE_I18N_DEBUG=1 diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 180c7af1240..8c724cda762 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -1 +1 @@ -github: patapancakes +github: pagefaultgames 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 ff0ca47bcac..c5283b091d4 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); @@ -3091,11 +3093,6 @@ export default class BattleScene extends SceneBase { private isWaveMysteryEncounter(newBattleType: BattleType, waveIndex: number, sessionDataEncounterType?: MysteryEncounterType): boolean { const [lowestMysteryEncounterWave, highestMysteryEncounterWave] = this.gameMode.getMysteryEncounterLegalWaves(); if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(waveIndex) && waveIndex < highestMysteryEncounterWave && waveIndex > lowestMysteryEncounterWave) { - // If ME type is already defined in session data, no need to roll RNG check - if (!isNullOrUndefined(sessionDataEncounterType)) { - return true; - } - // Base spawn weight is BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT/256, and increases by WEIGHT_INCREMENT_ON_SPAWN_MISS/256 for each missed attempt at spawning an encounter on a valid floor const sessionEncounterRate = this.mysteryEncounterSaveData.encounterSpawnChance; const encounteredEvents = this.mysteryEncounterSaveData.encounteredEvents; 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 f1c41e9ffb4..e2ae7830cee 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"; @@ -123,6 +123,7 @@ export class Ability implements Localizable { type AbAttrApplyFunc = (attr: TAttr, passive: boolean) => boolean | Promise; type AbAttrCondition = (pokemon: Pokemon) => boolean; +// TODO: Can this be improved? type PokemonAttackCondition = (user: Pokemon | null, target: Pokemon | null, move: Move) => boolean; type PokemonDefendCondition = (target: Pokemon, user: Pokemon, move: Move) => boolean; type PokemonStatStageChangeCondition = (target: Pokemon, statsChanged: BattleStat[], stages: number) => boolean; @@ -536,53 +537,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); @@ -2481,7 +2435,7 @@ export class PostSummonTransformAbAttr extends PostSummonAbAttr { pokemon.setStatStage(s, target.getStatStage(s)); } - pokemon.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m!.moveId, m!.ppUsed, m!.ppUp)); // TODO: are those bangs correct? + pokemon.summonData.moveset = target.getMoveset().map(m => new PokemonMove(m?.moveId ?? Moves.NONE, m?.ppUsed, m?.ppUp)); pokemon.summonData.types = target.getTypes(); @@ -3201,12 +3155,12 @@ export class ForewarnAbAttr extends PostSummonAbAttr { } else if (move?.getMove().power === -1) { movePower = 80; } else { - movePower = move!.getMove().power; // TODO: is this bang correct? + movePower = move?.getMove().power ?? 0; } if (movePower > maxPowerSeen) { maxPowerSeen = movePower; - maxMove = move!.getName(); // TODO: is this bang correct? + maxMove = move?.getName() ?? ""; } } } @@ -5300,8 +5254,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(), @@ -5313,8 +5266,7 @@ export function initAbilities() { .attr(IgnoreOpponentStatStagesAbAttr) .ignorable(), new Ability(Abilities.TINTED_LENS, 4) - //@ts-ignore - .attr(DamageBoostAbAttr, 2, (user, target, move) => target?.getMoveEffectiveness(user, move) <= 0.5), // TODO: fix TS issues + .attr(DamageBoostAbAttr, 2, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) <= 0.5), new Ability(Abilities.FILTER, 4) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75) .ignorable(), @@ -5432,8 +5384,9 @@ export function initAbilities() { .attr(WonderSkinAbAttr) .ignorable(), new Ability(Abilities.ANALYTIC, 5) - //@ts-ignore - .attr(MovePowerBoostAbAttr, (user, target, move) => !!target?.getLastXMoves(1).find(m => m.turn === target?.scene.currentBattle.turn) || user.scene.currentBattle.turnCommands[target.getBattlerIndex()].command !== Command.FIGHT, 1.3), // TODO fix TS issues + .attr(MovePowerBoostAbAttr, (user, target, move) => + !!target?.getLastXMoves(1).find(m => m.turn === target?.scene.currentBattle.turn) + || user?.scene.currentBattle.turnCommands[target?.getBattlerIndex() ?? BattlerIndex.ATTACKER]?.command !== Command.FIGHT, 1.3), new Ability(Abilities.ILLUSION, 5) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) @@ -5598,8 +5551,7 @@ export function initAbilities() { .bypassFaint() .partial(), new Ability(Abilities.STAKEOUT, 7) - //@ts-ignore - .attr(MovePowerBoostAbAttr, (user, target, move) => user.scene.currentBattle.turnCommands[target.getBattlerIndex()].command === Command.POKEMON, 2), // TODO: fix TS issues + .attr(MovePowerBoostAbAttr, (user, target, move) => user?.scene.currentBattle.turnCommands[target?.getBattlerIndex() ?? BattlerIndex.ATTACKER]?.command === Command.POKEMON, 2), new Ability(Abilities.WATER_BUBBLE, 7) .attr(ReceivedTypeDamageMultiplierAbAttr, Type.FIRE, 0.5) .attr(MoveTypePowerBoostAbAttr, Type.WATER, 2) @@ -5737,8 +5689,7 @@ export function initAbilities() { new Ability(Abilities.PRISM_ARMOR, 7) .attr(ReceivedMoveDamageMultiplierAbAttr, (target, user, move) => target.getMoveEffectiveness(user, move) >= 2, 0.75), new Ability(Abilities.NEUROFORCE, 7) - //@ts-ignore - .attr(MovePowerBoostAbAttr, (user, target, move) => target?.getMoveEffectiveness(user, move) >= 2, 1.25), // TODO: fix TS issues + .attr(MovePowerBoostAbAttr, (user, target, move) => (target?.getMoveEffectiveness(user!, move) ?? 1) >= 2, 1.25), new Ability(Abilities.INTREPID_SWORD, 8) .attr(PostSummonStatStageChangeAbAttr, [ Stat.ATK ], 1, true) .condition(getOncePerBattleCondition(Abilities.INTREPID_SWORD)), @@ -5759,13 +5710,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) @@ -5830,10 +5787,8 @@ export function initAbilities() { .attr(UserFieldStatusEffectImmunityAbAttr, StatusEffect.POISON, StatusEffect.TOXIC) .ignorable(), new Ability(Abilities.HUNGER_SWITCH, 8) - //@ts-ignore - .attr(PostTurnFormChangeAbAttr, p => p.getFormKey ? 0 : 1) // TODO: fix ts-ignore - //@ts-ignore - .attr(PostTurnFormChangeAbAttr, p => p.getFormKey ? 1 : 0) // TODO: fix ts-ignore + .attr(PostTurnFormChangeAbAttr, p => p.getFormKey() ? 0 : 1) + .attr(PostTurnFormChangeAbAttr, p => p.getFormKey() ? 1 : 0) .attr(UncopiableAbilityAbAttr) .attr(UnswappableAbilityAbAttr) .attr(NoTransformAbilityAbAttr) 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/egg-moves.ts b/src/data/egg-moves.ts index 813ade94020..741b707af2f 100644 --- a/src/data/egg-moves.ts +++ b/src/data/egg-moves.ts @@ -81,7 +81,7 @@ export const speciesEggMoves = { [Species.LEDYBA]: [ Moves.POLLEN_PUFF, Moves.THIEF, Moves.PARTING_SHOT, Moves.SPORE ], [Species.SPINARAK]: [ Moves.PARTING_SHOT, Moves.ATTACK_ORDER, Moves.GASTRO_ACID, Moves.STRENGTH_SAP ], [Species.CHINCHOU]: [ Moves.THUNDERCLAP, Moves.BOUNCY_BUBBLE, Moves.THUNDER_CAGE, Moves.TAIL_GLOW ], - [Species.PICHU]: [ Moves.MOONBLAST, Moves.WAVE_CRASH, Moves.AIR_SLASH, Moves.AURA_WHEEL ], + [Species.PICHU]: [ Moves.MOONBLAST, Moves.TRIPLE_AXEL, Moves.FIERY_DANCE, Moves.AURA_WHEEL ], [Species.CLEFFA]: [ Moves.CALM_MIND, Moves.EARTH_POWER, Moves.WISH, Moves.LIGHT_OF_RUIN ], [Species.IGGLYBUFF]: [ Moves.DRAIN_PUNCH, Moves.GRAV_APPLE, Moves.SOFT_BOILED, Moves.EXTREME_SPEED ], [Species.TOGEPI]: [ Moves.SCORCHING_SANDS, Moves.ROOST, Moves.RELIC_SONG, Moves.FIERY_DANCE ], @@ -195,7 +195,7 @@ export const speciesEggMoves = { [Species.REGISTEEL]: [ Moves.BODY_PRESS, Moves.SIZZLY_SLIDE, Moves.RECOVER, Moves.GIGATON_HAMMER ], [Species.LATIAS]: [ Moves.CORE_ENFORCER, Moves.FUSION_FLARE, Moves.SPARKLY_SWIRL, Moves.MYSTICAL_POWER ], [Species.LATIOS]: [ Moves.CORE_ENFORCER, Moves.BLUE_FLARE, Moves.NASTY_PLOT, Moves.TACHYON_CUTTER ], - [Species.KYOGRE]: [ Moves.RECOVER, Moves.HURRICANE, Moves.FLIP_TURN, Moves.WILDBOLT_STORM ], + [Species.KYOGRE]: [ Moves.RECOVER, Moves.HURRICANE, Moves.FREEZY_FROST, Moves.WILDBOLT_STORM ], [Species.GROUDON]: [ Moves.STONE_AXE, Moves.SOLAR_BLADE, Moves.MORNING_SUN, Moves.SACRED_FIRE ], [Species.RAYQUAZA]: [ Moves.V_CREATE, Moves.DRAGON_DARTS, Moves.CORE_ENFORCER, Moves.OBLIVION_WING ], [Species.JIRACHI]: [ Moves.TACHYON_CUTTER, Moves.TRIPLE_ARROWS, Moves.ROCK_SLIDE, Moves.SHELL_SMASH ], diff --git a/src/data/move.ts b/src/data/move.ts index 471260e3611..77673fa80a8 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -341,7 +341,8 @@ export default class Move implements Localizable { * @returns `true` if the move can bypass the target's Substitute; `false` otherwise. */ hitsSubstitute(user: Pokemon, target: Pokemon | null): boolean { - if (this.moveTarget === MoveTarget.USER || !target?.getTag(BattlerTagType.SUBSTITUTE)) { + if ([MoveTarget.USER, MoveTarget.USER_SIDE, MoveTarget.ENEMY_SIDE, MoveTarget.BOTH_SIDES].includes(this.moveTarget) + || !target?.getTag(BattlerTagType.SUBSTITUTE)) { return false; } @@ -398,220 +399,202 @@ export default class Move implements Localizable { /** * Sets the {@linkcode MoveFlags.MAKES_CONTACT} flag for the calling Move - * @param makesContact The value (boolean) to set the flag to + * @param setFlag Default `true`, set to `false` if the move doesn't make contact + * @see {@linkcode Abilities.STATIC} * @returns The {@linkcode Move} that called this function */ - makesContact(makesContact: boolean = true): this { - this.setFlag(MoveFlags.MAKES_CONTACT, makesContact); + makesContact(setFlag: boolean = true): this { + this.setFlag(MoveFlags.MAKES_CONTACT, setFlag); return this; } /** * Sets the {@linkcode MoveFlags.IGNORE_PROTECT} flag for the calling Move - * @param ignoresProtect The value (boolean) to set the flag to - * example: @see {@linkcode Moves.CURSE} + * @see {@linkcode Moves.CURSE} * @returns The {@linkcode Move} that called this function */ - ignoresProtect(ignoresProtect: boolean = true): this { - this.setFlag(MoveFlags.IGNORE_PROTECT, ignoresProtect); + ignoresProtect(): this { + this.setFlag(MoveFlags.IGNORE_PROTECT, true); return this; } /** * Sets the {@linkcode MoveFlags.IGNORE_VIRTUAL} flag for the calling Move - * @param ignoresVirtual The value (boolean) to set the flag to - * example: @see {@linkcode Moves.NATURE_POWER} + * @see {@linkcode Moves.NATURE_POWER} * @returns The {@linkcode Move} that called this function */ - ignoresVirtual(ignoresVirtual: boolean = true): this { - this.setFlag(MoveFlags.IGNORE_VIRTUAL, ignoresVirtual); + ignoresVirtual(): this { + this.setFlag(MoveFlags.IGNORE_VIRTUAL, true); return this; } /** * Sets the {@linkcode MoveFlags.SOUND_BASED} flag for the calling Move - * @param soundBased The value (boolean) to set the flag to - * example: @see {@linkcode Moves.UPROAR} + * @see {@linkcode Moves.UPROAR} * @returns The {@linkcode Move} that called this function */ - soundBased(soundBased: boolean = true): this { - this.setFlag(MoveFlags.SOUND_BASED, soundBased); + soundBased(): this { + this.setFlag(MoveFlags.SOUND_BASED, true); return this; } /** * Sets the {@linkcode MoveFlags.HIDE_USER} flag for the calling Move - * @param hidesUser The value (boolean) to set the flag to - * example: @see {@linkcode Moves.TELEPORT} + * @see {@linkcode Moves.TELEPORT} * @returns The {@linkcode Move} that called this function */ - hidesUser(hidesUser: boolean = true): this { - this.setFlag(MoveFlags.HIDE_USER, hidesUser); + hidesUser(): this { + this.setFlag(MoveFlags.HIDE_USER, true); return this; } /** * Sets the {@linkcode MoveFlags.HIDE_TARGET} flag for the calling Move - * @param hidesTarget The value (boolean) to set the flag to - * example: @see {@linkcode Moves.WHIRLWIND} + * @see {@linkcode Moves.WHIRLWIND} * @returns The {@linkcode Move} that called this function */ - hidesTarget(hidesTarget: boolean = true): this { - this.setFlag(MoveFlags.HIDE_TARGET, hidesTarget); + hidesTarget(): this { + this.setFlag(MoveFlags.HIDE_TARGET, true); return this; } /** * Sets the {@linkcode MoveFlags.BITING_MOVE} flag for the calling Move - * @param bitingMove The value (boolean) to set the flag to - * example: @see {@linkcode Moves.BITE} + * @see {@linkcode Moves.BITE} * @returns The {@linkcode Move} that called this function */ - bitingMove(bitingMove: boolean = true): this { - this.setFlag(MoveFlags.BITING_MOVE, bitingMove); + bitingMove(): this { + this.setFlag(MoveFlags.BITING_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.PULSE_MOVE} flag for the calling Move - * @param pulseMove The value (boolean) to set the flag to - * example: @see {@linkcode Moves.WATER_PULSE} + * @see {@linkcode Moves.WATER_PULSE} * @returns The {@linkcode Move} that called this function */ - pulseMove(pulseMove: boolean = true): this { - this.setFlag(MoveFlags.PULSE_MOVE, pulseMove); + pulseMove(): this { + this.setFlag(MoveFlags.PULSE_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.PUNCHING_MOVE} flag for the calling Move - * @param punchingMove The value (boolean) to set the flag to - * example: @see {@linkcode Moves.DRAIN_PUNCH} + * @see {@linkcode Moves.DRAIN_PUNCH} * @returns The {@linkcode Move} that called this function */ - punchingMove(punchingMove: boolean = true): this { - this.setFlag(MoveFlags.PUNCHING_MOVE, punchingMove); + punchingMove(): this { + this.setFlag(MoveFlags.PUNCHING_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.SLICING_MOVE} flag for the calling Move - * @param slicingMove The value (boolean) to set the flag to - * example: @see {@linkcode Moves.X_SCISSOR} + * @see {@linkcode Moves.X_SCISSOR} * @returns The {@linkcode Move} that called this function */ - slicingMove(slicingMove: boolean = true): this { - this.setFlag(MoveFlags.SLICING_MOVE, slicingMove); + slicingMove(): this { + this.setFlag(MoveFlags.SLICING_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.RECKLESS_MOVE} flag for the calling Move * @see {@linkcode Abilities.RECKLESS} - * @param recklessMove The value to set the flag to * @returns The {@linkcode Move} that called this function */ - recklessMove(recklessMove: boolean = true): this { - this.setFlag(MoveFlags.RECKLESS_MOVE, recklessMove); + recklessMove(): this { + this.setFlag(MoveFlags.RECKLESS_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.BALLBOMB_MOVE} flag for the calling Move - * @param ballBombMove The value (boolean) to set the flag to - * example: @see {@linkcode Moves.ELECTRO_BALL} + * @see {@linkcode Moves.ELECTRO_BALL} * @returns The {@linkcode Move} that called this function */ - ballBombMove(ballBombMove: boolean = true): this { - this.setFlag(MoveFlags.BALLBOMB_MOVE, ballBombMove); + ballBombMove(): this { + this.setFlag(MoveFlags.BALLBOMB_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.POWDER_MOVE} flag for the calling Move - * @param powderMove The value (boolean) to set the flag to - * example: @see {@linkcode Moves.STUN_SPORE} + * @see {@linkcode Moves.STUN_SPORE} * @returns The {@linkcode Move} that called this function */ - powderMove(powderMove: boolean = true): this { - this.setFlag(MoveFlags.POWDER_MOVE, powderMove); + powderMove(): this { + this.setFlag(MoveFlags.POWDER_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.DANCE_MOVE} flag for the calling Move - * @param danceMove The value (boolean) to set the flag to - * example: @see {@linkcode Moves.PETAL_DANCE} + * @see {@linkcode Moves.PETAL_DANCE} * @returns The {@linkcode Move} that called this function */ - danceMove(danceMove: boolean = true): this { - this.setFlag(MoveFlags.DANCE_MOVE, danceMove); + danceMove(): this { + this.setFlag(MoveFlags.DANCE_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.WIND_MOVE} flag for the calling Move - * @param windMove The value (boolean) to set the flag to - * example: @see {@linkcode Moves.HURRICANE} + * @see {@linkcode Moves.HURRICANE} * @returns The {@linkcode Move} that called this function */ - windMove(windMove: boolean = true): this { - this.setFlag(MoveFlags.WIND_MOVE, windMove); + windMove(): this { + this.setFlag(MoveFlags.WIND_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.TRIAGE_MOVE} flag for the calling Move - * @param triageMove The value (boolean) to set the flag to - * example: @see {@linkcode Moves.ABSORB} + * @see {@linkcode Moves.ABSORB} * @returns The {@linkcode Move} that called this function */ - triageMove(triageMove: boolean = true): this { - this.setFlag(MoveFlags.TRIAGE_MOVE, triageMove); + triageMove(): this { + this.setFlag(MoveFlags.TRIAGE_MOVE, true); return this; } /** * Sets the {@linkcode MoveFlags.IGNORE_ABILITIES} flag for the calling Move - * @param ignoresAbilities sThe value (boolean) to set the flag to - * example: @see {@linkcode Moves.SUNSTEEL_STRIKE} + * @see {@linkcode Moves.SUNSTEEL_STRIKE} * @returns The {@linkcode Move} that called this function */ - ignoresAbilities(ignoresAbilities: boolean = true): this { - this.setFlag(MoveFlags.IGNORE_ABILITIES, ignoresAbilities); + ignoresAbilities(): this { + this.setFlag(MoveFlags.IGNORE_ABILITIES, true); return this; } /** * Sets the {@linkcode MoveFlags.CHECK_ALL_HITS} flag for the calling Move - * @param checkAllHits The value (boolean) to set the flag to - * example: @see {@linkcode Moves.TRIPLE_AXEL} + * @see {@linkcode Moves.TRIPLE_AXEL} * @returns The {@linkcode Move} that called this function */ - checkAllHits(checkAllHits: boolean = true): this { - this.setFlag(MoveFlags.CHECK_ALL_HITS, checkAllHits); + checkAllHits(): this { + this.setFlag(MoveFlags.CHECK_ALL_HITS, true); return this; } /** * Sets the {@linkcode MoveFlags.IGNORE_SUBSTITUTE} flag for the calling Move - * @param ignoresSubstitute The value (boolean) to set the flag to - * example: @see {@linkcode Moves.WHIRLWIND} + * @see {@linkcode Moves.WHIRLWIND} * @returns The {@linkcode Move} that called this function */ - ignoresSubstitute(ignoresSubstitute: boolean = true): this { - this.setFlag(MoveFlags.IGNORE_SUBSTITUTE, ignoresSubstitute); + ignoresSubstitute(): this { + this.setFlag(MoveFlags.IGNORE_SUBSTITUTE, true); return this; } /** * Sets the {@linkcode MoveFlags.REDIRECT_COUNTER} flag for the calling Move - * @param redirectCounter The value (boolean) to set the flag to - * example: @see {@linkcode Moves.METAL_BURST} + * @see {@linkcode Moves.METAL_BURST} * @returns The {@linkcode Move} that called this function */ - redirectCounter(redirectCounter: boolean = true): this { - this.setFlag(MoveFlags.REDIRECT_COUNTER, redirectCounter); + redirectCounter(): this { + this.setFlag(MoveFlags.REDIRECT_COUNTER, true); return this; } @@ -4547,6 +4530,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: @@ -4567,6 +4551,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: @@ -5229,6 +5214,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { */ const switchOutTarget = this.selfSwitch ? user : target; if (switchOutTarget instanceof PlayerPokemon) { + if (switchOutTarget.scene.getParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + return false; + } switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); if (switchOutTarget.hp > 0) { @@ -5237,6 +5225,9 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { } return false; } else if (user.scene.currentBattle.battleType !== BattleType.WILD) { + if (switchOutTarget.scene.getEnemyParty().filter((p) => p.isAllowedInBattle() && !p.isOnField()).length < 1) { + return false; + } // Switch out logic for trainer battles switchOutTarget.leaveField(this.switchType === SwitchType.SWITCH); @@ -5247,7 +5238,7 @@ export class ForceSwitchOutAttr extends MoveEffectAttr { false, false), MoveEndPhase); } } else { - if (switchOutTarget.isBoss()) { + if (user.scene.currentBattle.waveIndex % 10 === 0) { return false; } // Switch out logic for everything else (eg: WILD battles) @@ -5345,7 +5336,7 @@ export class ChillyReceptionAttr extends ForceSwitchOutAttr { getCondition(): MoveConditionFunc { // chilly reception move will go through if the weather is change-able to snow, or the user can switch out, else move will fail - return (user, target, move) => user.scene.arena.trySetWeather(WeatherType.SNOW, true) || super.getSwitchOutCondition()(user, target, move); + return (user, target, move) => user.scene.arena.weather?.weatherType !== WeatherType.SNOW || super.getSwitchOutCondition()(user, target, move); } } export class RemoveTypeAttr extends MoveEffectAttr { @@ -6586,7 +6577,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 { @@ -7263,7 +7254,7 @@ export function initMoves() { new StatusMove(Moves.CURSE, Type.GHOST, -1, 10, -1, 0, 2) .attr(CurseAttr) .ignoresSubstitute() - .ignoresProtect(true) + .ignoresProtect() .target(MoveTarget.CURSE), new AttackMove(Moves.FLAIL, Type.NORMAL, MoveCategory.PHYSICAL, -1, 100, 15, -1, 0, 2) .attr(LowHpPowerAttr), @@ -8903,8 +8894,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() @@ -8934,7 +8925,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) @@ -9619,9 +9610,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/data/mystery-encounters/encounters/global-trade-system-encounter.ts b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts index f3765c9acba..6d6b5d9f71e 100644 --- a/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts +++ b/src/data/mystery-encounters/encounters/global-trade-system-encounter.ts @@ -159,7 +159,7 @@ export const GlobalTradeSystemEncounter: MysteryEncounter = return true; }, onHover: () => { - const formName = tradePokemon.species.forms && tradePokemon.species.forms.length > tradePokemon.formIndex ? tradePokemon.species.forms[pokemon.formIndex].formName : null; + const formName = tradePokemon.species.forms && tradePokemon.species.forms.length > tradePokemon.formIndex ? tradePokemon.species.forms[tradePokemon.formIndex].formName : null; const line1 = i18next.t("pokemonInfoContainer:ability") + " " + tradePokemon.getAbility().name + (tradePokemon.getGender() !== Gender.GENDERLESS ? " | " + i18next.t("pokemonInfoContainer:gender") + " " + getGenderSymbol(tradePokemon.getGender()) : ""); const line2 = i18next.t("pokemonInfoContainer:nature") + " " + getNatureName(tradePokemon.getNature()) + (formName ? " | " + i18next.t("pokemonInfoContainer:form") + " " + formName : ""); showEncounterText(scene, `${line1}\n${line2}`, 0, 0, false); diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index c9b30415299..fb67305c78d 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -224,7 +224,7 @@ export function getRandomSpeciesByStarterTier(starterTiers: number | [number, nu // If no filtered mons exist at specified starter tiers, will expand starter search range until there are // Starts by decrementing starter tier min until it is 0, then increments tier max up to 10 let tryFilterStarterTiers: [PokemonSpecies, number][] = filteredSpecies.filter(s => (s[1] >= min && s[1] <= max)); - while (tryFilterStarterTiers.length === 0 && (min !== 0 && max !== 10)) { + while (tryFilterStarterTiers.length === 0 && !(min === 0 && max === 10)) { if (min > 0) { min--; } else { diff --git a/src/data/pokemon-evolutions.ts b/src/data/pokemon-evolutions.ts index f9602d1386a..698bbb8e9c9 100644 --- a/src/data/pokemon-evolutions.ts +++ b/src/data/pokemon-evolutions.ts @@ -11,6 +11,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { TimeOfDay } from "#enums/time-of-day"; +import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier } from "#app/modifier/modifier"; export enum SpeciesWildEvolutionDelay { NONE, @@ -1647,8 +1648,14 @@ export const pokemonEvolutions: PokemonEvolutions = { new SpeciesEvolution(Species.FROSMOTH, 1, null, new SpeciesFriendshipEvolutionCondition(90, p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT), SpeciesWildEvolutionDelay.MEDIUM) ], [Species.GIMMIGHOUL]: [ - new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition( p => p.evoCounter > 9 ), SpeciesWildEvolutionDelay.VERY_LONG), - new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition( p => p.evoCounter > 9 ), SpeciesWildEvolutionDelay.VERY_LONG) + new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter + + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + + p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier + || m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG), + new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter + + p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + + p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier + || m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG) ] }; diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index a904f497b0f..5c2dd06f7b3 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -4,7 +4,7 @@ import { SpeciesFormKey } from "./pokemon-species"; import { StatusEffect } from "./status-effect"; import { MoveCategory, allMoves } from "./move"; import { Type } from "./type"; -import { Constructor } from "#app/utils"; +import { Constructor, nil } from "#app/utils"; import { Abilities } from "#enums/abilities"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; @@ -185,7 +185,7 @@ export class SpeciesFormChange { return true; } - findTrigger(triggerType: Constructor): SpeciesFormChangeTrigger | null { + findTrigger(triggerType: Constructor): SpeciesFormChangeTrigger | nil { if (!this.trigger.hasTriggerType(triggerType)) { return null; } @@ -193,7 +193,7 @@ export class SpeciesFormChange { const trigger = this.trigger; if (trigger instanceof SpeciesFormChangeCompoundTrigger) { - return trigger.triggers.find(t => t.hasTriggerType(triggerType))!; // TODO: is this bang correct? + return trigger.triggers.find(t => t.hasTriggerType(triggerType)); } return trigger; @@ -202,11 +202,11 @@ export class SpeciesFormChange { export class SpeciesFormChangeCondition { public predicate: SpeciesFormChangeConditionPredicate; - public enforceFunc: SpeciesFormChangeConditionEnforceFunc | null; + public enforceFunc: SpeciesFormChangeConditionEnforceFunc | nil; constructor(predicate: SpeciesFormChangeConditionPredicate, enforceFunc?: SpeciesFormChangeConditionEnforceFunc) { this.predicate = predicate; - this.enforceFunc = enforceFunc!; // TODO: is this bang correct? + this.enforceFunc = enforceFunc; } } diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 75a849574ea..296e4e9b1b5 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -18,6 +18,11 @@ import {Species} from "#enums/species"; import {TrainerType} from "#enums/trainer-type"; import {Gender} from "./gender"; +/** Minimum BST for Pokemon generated onto the Elite Four's teams */ +const ELITE_FOUR_MINIMUM_BST = 460; +/** Minimum BST for Pokemon generated onto the E4 Champion's team */ +const CHAMPION_MINIMUM_BST = 508; + export enum TrainerPoolTier { COMMON, UNCOMMON, @@ -860,10 +865,10 @@ export class TrainerConfig { // Set species filter and specialty types if provided, otherwise filter by base total. if (specialtyTypes.length) { - this.setSpeciesFilter(p => specialtyTypes.some(t => p.isOfType(t)) && p.baseTotal >= 450); + this.setSpeciesFilter(p => specialtyTypes.some(t => p.isOfType(t)) && p.baseTotal >= ELITE_FOUR_MINIMUM_BST); this.setSpecialtyTypes(...specialtyTypes); } else { - this.setSpeciesFilter(p => p.baseTotal >= 450); + this.setSpeciesFilter(p => p.baseTotal >= ELITE_FOUR_MINIMUM_BST); } // Localize the trainer's name by converting it to lowercase and replacing spaces with underscores. @@ -913,8 +918,7 @@ export class TrainerConfig { this.setPartyMemberFunc(-(s + 1), getRandomPartyMemberFunc(speciesPool)); }); - // Set species filter to only include species with a base total of 470 or higher. - this.setSpeciesFilter(p => p.baseTotal >= 470); + this.setSpeciesFilter(p => p.baseTotal >= CHAMPION_MINIMUM_BST); // Localize the trainer's name by converting it to lowercase and replacing spaces with underscores. const nameForCall = this.name.toLowerCase().replace(/\s/g, "_"); 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 458243752b4..af1bdfbaeb1 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -63,6 +63,12 @@ import { PLAYER_PARTY_MAX_SIZE } from "#app/constants"; import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { SwitchType } from "#enums/switch-type"; +/** `64/65536 -> 1/1024` */ +const BASE_SHINY_CHANCE = 64; + +/** `1/256` */ +const BASE_HIDDEN_ABILITY_CHANCE = 256; + export enum FieldPosition { CENTER, LEFT, @@ -139,7 +145,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { throw `Cannot create a player Pokemon for species '${species.getName(formIndex)}'`; } - const hiddenAbilityChance = new Utils.IntegerHolder(256); + const hiddenAbilityChance = new Utils.IntegerHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); } @@ -1512,6 +1518,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; } @@ -1822,7 +1830,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * The exact mechanic is that it calculates E as the XOR of the player's trainer ID and secret ID. * F is calculated as the XOR of the first 16 bits of the Pokemon's ID with the last 16 bits. * The XOR of E and F are then compared to the {@linkcode shinyThreshold} (or {@linkcode thresholdOverride} if set) to see whether or not to generate a shiny. - * The base shiny odds are {@linkcode baseShinyChance} / 65536 + * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536 * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @returns true if the Pokemon has been set as a shiny, false otherwise */ @@ -1838,9 +1846,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { const E = this.scene.gameData.trainerId ^ this.scene.gameData.secretId; const F = rand1 ^ rand2; - /** `64/65536 -> 1/1024` */ - const baseShinyChance = 64; - const shinyThreshold = new Utils.IntegerHolder(baseShinyChance); + const shinyThreshold = new Utils.IntegerHolder(BASE_SHINY_CHANCE); if (thresholdOverride === undefined) { if (this.scene.eventManager.isEventActive()) { shinyThreshold.value *= this.scene.eventManager.getShinyMultiplier(); @@ -1865,15 +1871,13 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { * Function that tries to set a Pokemon shiny based on seed. * For manual use only, usually to roll a Pokemon's shiny chance a second time. * - * The base shiny odds are {@linkcode baseShinyChance} / 65536 + * The base shiny odds are {@linkcode BASE_SHINY_CHANCE} / 65536 * @param thresholdOverride number that is divided by 2^16 (65536) to get the shiny chance, overrides {@linkcode shinyThreshold} if set (bypassing shiny rate modifiers such as Shiny Charm) * @param applyModifiersToOverride If {@linkcode thresholdOverride} is set and this is true, will apply Shiny Charm and event modifiers to {@linkcode thresholdOverride} * @returns true if the Pokemon has been set as a shiny, false otherwise */ trySetShinySeed(thresholdOverride?: integer, applyModifiersToOverride?: boolean): boolean { - /** `64/65536 -> 1/1024` */ - const baseShinyChance = 64; - const shinyThreshold = new Utils.IntegerHolder(baseShinyChance); + const shinyThreshold = new Utils.IntegerHolder(BASE_SHINY_CHANCE); if (thresholdOverride === undefined || applyModifiersToOverride) { if (thresholdOverride !== undefined && applyModifiersToOverride) { shinyThreshold.value = thresholdOverride; @@ -1931,7 +1935,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } generateFusionSpecies(forStarter?: boolean): void { - const hiddenAbilityChance = new Utils.IntegerHolder(256); + const hiddenAbilityChance = new Utils.IntegerHolder(BASE_HIDDEN_ABILITY_CHANCE); if (!this.hasTrainer()) { this.scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance); } @@ -3979,7 +3983,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; } @@ -4995,6 +5000,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/de/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/de/mystery-encounters/the-expert-pokemon-breeder-dialogue.json index 191fdaaf783..d2fcc91d153 100644 --- a/src/locales/de/mystery-encounters/the-expert-pokemon-breeder-dialogue.json +++ b/src/locales/de/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -25,7 +25,7 @@ "outro": "Schau wie glücklich dein {{chosenPokemon}} nun ist!$Hier, diese Pokémon-Eier kannst du auch haben.", "outro_failed": "Wie enttäuschend...$Es sieht so aus, als hättest du noch einen langen Weg vor dir, um das Vertrauen deines Pokémon zu gewinnen!", "gained_eggs": "@s{item_fanfare}Du erhählst {{numEggs}}!", - "eggs_tooltip": "\n(+) Erhalte @[TOOLTIP_TITLE]{{{eggs}}}", + "eggs_tooltip": "\n(+) Erhalte {{eggs}}", "numEggs_one": "{{count}} Ei der Stufe {{rarity}}", "numEggs_other": "{{count}} Eier der Stufe {{rarity}}" } 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/en/move-trigger.json b/src/locales/en/move-trigger.json index 93d25e506ba..7ea5fe82fc2 100644 --- a/src/locales/en/move-trigger.json +++ b/src/locales/en/move-trigger.json @@ -71,5 +71,5 @@ "safeguard": "{{targetName}} is protected by Safeguard!", "substituteOnOverlap": "{{pokemonName}} already\nhas a substitute!", "substituteNotEnoughHp": "But it does not have enough HP\nleft to make a substitute!", - "afterYou": "{{pokemonName}} took the kind offer!" + "afterYou": "{{targetName}} took the kind offer!" } \ No newline at end of file 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/es/move-trigger.json b/src/locales/es/move-trigger.json index b49a64ac42a..2e394dd72a7 100644 --- a/src/locales/es/move-trigger.json +++ b/src/locales/es/move-trigger.json @@ -67,5 +67,5 @@ "swapArenaTags": "¡{{pokemonName}} ha intercambiado los efectos del terreno de combate!", "exposedMove": "¡{{pokemonName}} ha identificado\n{{targetPokemonName}}!", "safeguard": "¡{{targetName}} está protegido por Velo Sagrado!", - "afterYou": "¡{{pokemonName}} ha decidido aprovechar la oportunidad!" + "afterYou": "¡{{targetName}} ha decidido aprovechar la oportunidad!" } 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/fr/move-trigger.json b/src/locales/fr/move-trigger.json index 7564718e7ce..458b7ed8c4f 100644 --- a/src/locales/fr/move-trigger.json +++ b/src/locales/fr/move-trigger.json @@ -71,5 +71,5 @@ "safeguard": "{{targetName}} est protégé\npar la capacité Rune Protect !", "substituteOnOverlap": "{{pokemonName}} a déjà\nun clone !", "substituteNotEnoughHp": "Mais il est trop faible\npour créer un clone !", - "afterYou": "{{pokemonName}} accepte\navec joie !" + "afterYou": "{{targetName}} accepte\navec joie !" } diff --git a/src/locales/fr/mystery-encounters/bug-type-superfan-dialogue.json b/src/locales/fr/mystery-encounters/bug-type-superfan-dialogue.json index 63195e37062..7e81b99c739 100644 --- a/src/locales/fr/mystery-encounters/bug-type-superfan-dialogue.json +++ b/src/locales/fr/mystery-encounters/bug-type-superfan-dialogue.json @@ -28,7 +28,7 @@ "select_prompt": "Choisissez un objet à donner.", "invalid_selection": "Ce Pokémon ne porte pas ce genre d’objet.", "selected": "Vous remettez l’objet {{selectedItem}} au Dresseur.", - "selected_dialogue": "Sérieux ? {{selectedItem}}, comme ça en cadeau ?\nC’est très générieux !$En guise de ma reconnaissance, laisse-moi\nte faire un cadeau un peu spécial !$Il est dans ma famille depusi des générations, et ça me ferait plaisir de te l’offrir !" + "selected_dialogue": "Sérieux ? {{selectedItem}}, comme ça en cadeau ?\nC’est très générieux !$En guise de ma reconnaissance, laisse-moi\nte faire un cadeau un peu spécial !$Il est dans ma famille depuis des générations, et ça me ferait plaisir de te l’offrir !" } }, "battle_won": "Tes connaissances et tes capacités ont totalement exploité nos faiblesses !$En remerciement de cette leçon, permets-moi\nd’apprendre une capacité Insecte à un de tes Pokémon !", diff --git a/src/locales/fr/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/fr/mystery-encounters/the-expert-pokemon-breeder-dialogue.json index c587a575887..ebd450730d6 100644 --- a/src/locales/fr/mystery-encounters/the-expert-pokemon-breeder-dialogue.json +++ b/src/locales/fr/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -25,7 +25,7 @@ "outro": "Ton {{chosenPokemon}} et toi avez\nl’air très heureux !$Tiens, prends ça aussi.", "outro_failed": "Voilà qui est bien décevant…$T’as encore visiblement bien du chemin à faire\npour acquérir la confiance de tes Pokémon !", "gained_eggs": "@s{item_fanfare}Vous recevez\n{{numEggs}} !", - "eggs_tooltip": "\n(+) Recevez @[TOOLTIP_TITLE]{{{eggs}}}", + "eggs_tooltip": "\n(+) Recevez {{eggs}}", "numEggs_one": "{{count}} Œuf {{rarity}}", "numEggs_other": "{{count}} Œufs {{rarity}}s" } diff --git a/src/locales/fr/mystery-encounters/training-session-dialogue.json b/src/locales/fr/mystery-encounters/training-session-dialogue.json index 9b06651eb59..9de246d3b6f 100644 --- a/src/locales/fr/mystery-encounters/training-session-dialogue.json +++ b/src/locales/fr/mystery-encounters/training-session-dialogue.json @@ -1,7 +1,7 @@ { "intro": "Vous tombez sur du matériel d’entrainement.", "title": "Session d’entrainement", - "description": "Ce matériel semble pouvoir être utilisé pour entrainer un membre de votre équipe ! Il existe plusieurs moyens avec lesquels vous pourriez entrainer un Pokémon, comme @[TOOLTIP_TITLE]{en le faisant combattre et vaincre le reste de votre équipe}.", + "description": "Ce matériel semble pouvoir être utilisé pour entrainer un membre de votre équipe ! Il existe plusieurs moyens avec lesquels vous pourriez entrainer un Pokémon, comme @[TOOLTIP_TITLE]{en le combattant et le vainquant avec le reste de votre équipe}.", "query": "Quel entrainement choisir ?", "invalid_selection": "Le Pokémon doit être en bonne santé.", "option": { diff --git a/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json index d5f5e7e336f..9d70d532f0a 100644 --- a/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json +++ b/src/locales/fr/mystery-encounters/uncommon-breed-dialogue.json @@ -14,13 +14,13 @@ "label": "Le nourrir", "disabled_tooltip": "Vous avez besoin de 4 Baies pour choisir cette option", "tooltip": "(-) Donner 4 Baies\n(+) Le {{enemyPokemon}} vous apprécie", - "selected": "Vous lancer quelques Baies\nau {{enemyPokemon}} !$Il les engloutit avec joie !$Le {{enemyPokemon}} veut se joindre\nà votre équipe !" + "selected": "Vous lancez quelques Baies\nau {{enemyPokemon}} !$Il les engloutit avec joie !$Le {{enemyPokemon}} veut se joindre\nà votre équipe !" }, "3": { "label": "Devenir amis", "disabled_tooltip": "Votre Pokémon doit connaitre certaines capacités pour choisir cette option", "tooltip": "(+) {{option3PrimaryName}} utilise {{option3PrimaryMove}}\n(+) Le {{enemyPokemon}} vous apprécie", - "selected": "Votre {{option3PrimaryName}} utilise {{option3PrimaryMove}} pour charmer le {{enemyPokemon}} !$The {{enemyPokemon}} veut se joindre\nà votre équipe !" + "selected": "Votre {{option3PrimaryName}} utilise {{option3PrimaryMove}} pour charmer le {{enemyPokemon}} !$Le {{enemyPokemon}} veut se joindre\nà votre équipe !" } } } diff --git a/src/locales/fr/settings.json b/src/locales/fr/settings.json index e310f5d5733..2dd150eed13 100644 --- a/src/locales/fr/settings.json +++ b/src/locales/fr/settings.json @@ -11,7 +11,7 @@ "expGainsSpeed": "Vit. barre d’Exp", "expPartyDisplay": "Afficher Exp équipe", "skipSeenDialogues": "Passer dialogues connus", - "eggSkip": "Animation d’éclosion", + "eggSkip": "Passer les éclosions", "never": "Jamais", "always": "Toujours", "ask": "Demander", 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/it/move-trigger.json b/src/locales/it/move-trigger.json index fba671a6813..04870015f2b 100644 --- a/src/locales/it/move-trigger.json +++ b/src/locales/it/move-trigger.json @@ -68,5 +68,5 @@ "exposedMove": "{{pokemonName}} ha identificato\n{{targetPokemonName}}!", "chillyReception": "{{pokemonName}} sta per fare una battuta!", "safeguard": "Salvaguardia protegge {{targetName}}!", - "afterYou": "{{pokemonName}} approfitta della cortesia!" + "afterYou": "{{targetName}} approfitta della cortesia!" } 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/ja/move-trigger.json b/src/locales/ja/move-trigger.json index afede7edfb3..4c3d19240f6 100644 --- a/src/locales/ja/move-trigger.json +++ b/src/locales/ja/move-trigger.json @@ -68,5 +68,5 @@ "chillyReception": "{{pokemonName}}は\n寒い ギャグを かました!", "swapArenaTags": "{{pokemonName}}は\nお互いの 場の 効果を 入れ替えた!", "exposedMove": "{{pokemonName}}は {{targetPokemonName}}の\n正体を 見破った!", - "afterYou": "{{pokemonName}}は\nお言葉に 甘えることにした!" + "afterYou": "{{targetName}}は\nお言葉に 甘えることにした!" } diff --git a/src/locales/ja/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/ja/mystery-encounters/the-expert-pokemon-breeder-dialogue.json index a59cf55694e..12eaebfb33c 100644 --- a/src/locales/ja/mystery-encounters/the-expert-pokemon-breeder-dialogue.json +++ b/src/locales/ja/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -25,7 +25,7 @@ "outro": "ねえ、 {{chosenPokemon}} すごく 嬉しくなったわ!$そして、 これらも どうぞ!", "outro_failed": "なんか ガッカリ ね……$手持ち ポケモンの 信用を 得るまで\nまだまだ みたいんだわ!", "gained_eggs": "@s{item_fanfare}{{numEggs}}を もらいました!", - "eggs_tooltip": "\n(+) @[TOOLTIP_TITLE]{{{eggs}}}を得る", + "eggs_tooltip": "\n(+) {{eggs}}を得る", "numEggs_one": "{{count}} {{rarity}} タマゴ", "numEggs_other": "{{count}} {{rarity}} タマゴ" } 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/ko/move-trigger.json b/src/locales/ko/move-trigger.json index 12a126baf9d..4b994f315bf 100644 --- a/src/locales/ko/move-trigger.json +++ b/src/locales/ko/move-trigger.json @@ -69,5 +69,5 @@ "chillyReception": "{{pokemonName}}[[는]] 썰렁한 개그를 선보였다!", "exposedMove": "{{pokemonName}}[[는]]\n{{targetPokemonName}}의 정체를 꿰뚫어 보았다!", "safeguard": "{{targetName}}[[는]] 신비의 베일이 지켜 주고 있다!", - "afterYou": "{{pokemonName}}[[는]]\n배려를 받아들이기로 했다!" + "afterYou": "{{targetName}}[[는]]\n배려를 받아들이기로 했다!" } diff --git a/src/locales/ko/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/ko/mystery-encounters/the-expert-pokemon-breeder-dialogue.json index 8c93868b4b4..e226741c60a 100644 --- a/src/locales/ko/mystery-encounters/the-expert-pokemon-breeder-dialogue.json +++ b/src/locales/ko/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -26,7 +26,7 @@ "outro": "{{chosenPokemon}}[[가]] 정말 행복해 보이네요!$여기, 이것도 드릴게요.", "outro_failed": "실망이네요….$포켓몬의 신뢰를 얻으려면 아직 멀었어요!", "gained_eggs": "@s{item_fanfare}{{numEggs}}[[를]] 받았습니다!", - "eggs_tooltip": "\n(+) @[TOOLTIP_TITLE]{{{eggs}}} 획득", + "eggs_tooltip": "\n(+) {{eggs}} 획득", "numEggs_one": "{{rarity}}알 {{count}}개", "numEggs_other": "{{rarity}}알 {{count}}개" } 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/pt_BR/menu-ui-handler.json b/src/locales/pt_BR/menu-ui-handler.json index df654976d68..5141a4ea612 100644 --- a/src/locales/pt_BR/menu-ui-handler.json +++ b/src/locales/pt_BR/menu-ui-handler.json @@ -25,5 +25,6 @@ "unlinkGoogle": "Desconectar Google", "cancel": "Cancelar", "losingProgressionWarning": "Você vai perder todo o progresso desde o início da batalha. Confirmar?", - "noEggs": "Você não está chocando nenhum ovo\nno momento!" + "noEggs": "Você não está chocando nenhum ovo\nno momento!", + "donate": "Contribuir" } diff --git a/src/locales/pt_BR/move-trigger.json b/src/locales/pt_BR/move-trigger.json index 307364e1b55..9a4d0b9e8b1 100644 --- a/src/locales/pt_BR/move-trigger.json +++ b/src/locales/pt_BR/move-trigger.json @@ -64,5 +64,5 @@ "chillyReception": "{{pokemonName}} está prestes a contar uma piada gelada!", "exposedMove": "{{pokemonName}} identificou\n{{targetPokemonName}}!", "safeguard": "{{targetName}} está protegido por Safeguard!", - "afterYou": "{{pokemonName}} aceitou a gentil oferta!" + "afterYou": "{{targetName}} aceitou a gentil oferta!" } diff --git a/src/locales/pt_BR/mystery-encounters/the-expert-pokemon-breeder-dialogue.json b/src/locales/pt_BR/mystery-encounters/the-expert-pokemon-breeder-dialogue.json index 7e48c9552da..d84c6c07273 100644 --- a/src/locales/pt_BR/mystery-encounters/the-expert-pokemon-breeder-dialogue.json +++ b/src/locales/pt_BR/mystery-encounters/the-expert-pokemon-breeder-dialogue.json @@ -25,7 +25,7 @@ "outro": "Veja como seu {{chosenPokemon}} está feliz agora!$Aqui, você também pode ficar com isso.", "outro_failed": "Que decepção...$Parece que você ainda tem um longo caminho\na percorrer para ganhar a confiança dos seus Pokémon!", "gained_eggs": "@s{item_fanfare}Você recebeu {{numEggs}}!", - "eggs_tooltip": "\n(+) Ganhe @[TOOLTIP_TITLE]{{{eggs}}}", + "eggs_tooltip": "\n(+) Ganhe {{eggs}}", "numEggs_one": "{{count}} Ovo {{rarity}}", "numEggs_other": "{{count}} Ovos {{rarity}}" } diff --git a/src/locales/pt_BR/party-ui-handler.json b/src/locales/pt_BR/party-ui-handler.json index ef09e09fb2b..a8c492670dc 100644 --- a/src/locales/pt_BR/party-ui-handler.json +++ b/src/locales/pt_BR/party-ui-handler.json @@ -24,7 +24,7 @@ "tooManyItems": "{{pokemonName}} já tem\nmuitos desse item!", "anyEffect": "Isso não terá nenhum efeito.", "unpausedEvolutions": "Evoluções foram despausadas para {{pokemonName}}.", - "unspliceConfirmation": "Você realmente deseja desfazer a fusão de {{fusionName}}\ncom {{pokemonName}}? {{fusionName}} será perdido.", + "unspliceConfirmation": "Você realmente deseja desfazer a fusão de\n{{fusionName}} com {{pokemonName}}? {{fusionName}} será perdido.", "wasReverted": "{{fusionName}} foi revertido para {{pokemonName}}.", "releaseConfirmation": "Você realmente deseja soltar {{pokemonName}}?", "releaseInBattle": "Você não pode soltar um Pokémon que está em batalha!", diff --git a/src/locales/pt_BR/settings.json b/src/locales/pt_BR/settings.json index 6c4eae23a82..1d743f7b05a 100644 --- a/src/locales/pt_BR/settings.json +++ b/src/locales/pt_BR/settings.json @@ -11,6 +11,10 @@ "expGainsSpeed": "Velocidade do Ganho de EXP", "expPartyDisplay": "Exibição de EXP da Equipe", "skipSeenDialogues": "Pular Diálogos Vistos", + "eggSkip": "Pular Eclosões de Ovos", + "never": "Nunca", + "always": "Sempre", + "ask": "Perguntar", "battleStyle": "Estilo de Batalha", "enableRetries": "Habilitar Novas Tentativas", "hideIvs": "Esconder scanner de IV", 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_CN/battle.json b/src/locales/zh_CN/battle.json index ef716b0893a..4b31de41f9a 100644 --- a/src/locales/zh_CN/battle.json +++ b/src/locales/zh_CN/battle.json @@ -88,6 +88,14 @@ "statHarshlyFell_other": "{{pokemonNameWithAffix}}的{{stats}}大幅降低了!", "statSeverelyFell_other": "{{pokemonNameWithAffix}}的{{stats}}极大幅降低了!", "statWontGoAnyLower_other": "{{pokemonNameWithAffix}}的{{stats}}已经无法再降低了!", + "statRose_one": "{{pokemonNameWithAffix}}的{{stats}}提高了!", + "statSharplyRose_one": "{{pokemonNameWithAffix}}的{{stats}}大幅提高了!", + "statRoseDrastically_one": "{{pokemonNameWithAffix}}的{{stats}}极大幅提高了!", + "statWontGoAnyHigher_one": "{{pokemonNameWithAffix}}的{{stats}}已经无法再提高了!", + "statFell_one": "{{pokemonNameWithAffix}}的{{stats}}降低了!", + "statHarshlyFell_one": "{{pokemonNameWithAffix}}的{{stats}}大幅降低了!", + "statSeverelyFell_one": "{{pokemonNameWithAffix}}的{{stats}}极大幅降低了!", + "statWontGoAnyLower_one": "{{pokemonNameWithAffix}}的{{stats}}已经无法再降低了!", "transformedIntoType": "{{pokemonName}}变成了\n{{type}}属性!", "ppReduced": "降低了{{targetName}}的\n{{moveName}}的PP{{reduction}}点!", "retryBattle": "你要从对战开始时重试么?", diff --git a/src/locales/zh_CN/dialogue.json b/src/locales/zh_CN/dialogue.json index 78f0845b436..f2382299179 100644 --- a/src/locales/zh_CN/dialogue.json +++ b/src/locales/zh_CN/dialogue.json @@ -299,6 +299,28 @@ "1": "真奇怪…怎么会这样…我不应该被打败的。" } }, + "snow_worker": { + "encounter": { + "1": "天冷了,便要添衣。\n只有人类会有这么聪明的想法!" + }, + "victory": { + "1": "即使是徒劳的挣扎,\n到头来也还是展示了人类的智慧啊!" + }, + "defeat": { + "1": "我来告诉你一个小技巧。\n如果你对着冻住的宝可梦用火系技能,冰就化啦!" + } + }, + "snow_worker_double": { + "encounter": { + "1": "……\n……准……准备好战……战斗了吗?" + }, + "victory": { + "1": "……\n……我冻得直,直打哆嗦!" + }, + "defeat": { + "1": "……\n……知……知道吗?\n...这……这地方蛮冷。" + } + }, "hex_maniac": { "encounter": { "1": "我通常只听古典音乐,但如果我输了,$我想我应该试试新时代的音乐!", diff --git a/src/locales/zh_CN/egg.json b/src/locales/zh_CN/egg.json index f2b95bd40cb..3691749a971 100644 --- a/src/locales/zh_CN/egg.json +++ b/src/locales/zh_CN/egg.json @@ -11,7 +11,7 @@ "gachaTypeLegendary": "传说概率上升", "gachaTypeMove": "稀有概率上升", "gachaTypeShiny": "闪光概率上升", - "eventType": "Mystery Event", + "eventType": "神秘事件", "selectMachine": "选择一个机器。", "notEnoughVouchers": "你没有足够的兑换券!", "tooManyEggs": "你的蛋太多啦!", diff --git a/src/locales/zh_CN/modifier-type.json b/src/locales/zh_CN/modifier-type.json index b02d74eadb5..2dc44f0fb34 100644 --- a/src/locales/zh_CN/modifier-type.json +++ b/src/locales/zh_CN/modifier-type.json @@ -2,7 +2,7 @@ "ModifierType": { "AddPokeballModifierType": { "name": "{{modifierCount}}x {{pokeballName}}", - "description": "获得 {{pokeballName}} x{{modifierCount}} (已有:{{pokeballAmount}}) \n捕捉倍率:{{catchRate}}。" + "description": "获得 {{pokeballName}}x {{modifierCount}} (已有:{{pokeballAmount}}) \n捕捉倍率:{{catchRate}}。" }, "AddVoucherModifierType": { "name": "{{modifierCount}}x {{voucherTypeName}}", @@ -50,7 +50,7 @@ "description": "遭遇双打概率提升四倍,持续{{battleCount}}场战斗。" }, "TempStatStageBoosterModifierType": { - "description": "提升全队的{{stat}}{{amount}}级,持续5场战斗。", + "description": "提升全队的{{stat}}{{amount}},持续5场战斗。", "extra": { "stage": "1阶", "percentage": "30%" @@ -63,24 +63,24 @@ "description": "使一只宝可梦的等级提升{{levels}}级。" }, "AllPokemonLevelIncrementModifierType": { - "description": "使一只寶可夢的等級提升{{levels}}級。" + "description": "使所有宝可梦的等级提升{{levels}}级。" }, "BaseStatBoosterModifierType": { "description": "增加10%持有者的{{stat}},\n个体值越高堆叠上限越高。" }, "PokemonBaseStatTotalModifierType": { - "name": "Shuckle Juice", - "description": "{{increaseDecrease}} all of the holder's base stats by {{statValue}}. You were {{blessCurse}} by the Shuckle.", + "name": "壶壶果汁", + "description": "{{increaseDecrease}}持有者的所有基础属性{{statValue}}级。你被壶壶{{blessCurse}}了。", "extra": { - "increase": "Increases", - "decrease": "Decreases", - "blessed": "blessed", - "cursed": "cursed" + "increase": "提升", + "decrease": "降低", + "blessed": "强化", + "cursed": "诅咒" } }, "PokemonBaseStatFlatModifierType": { - "name": "Old Gateau", - "description": "Increases the holder's {{stats}} base stats by {{statValue}}. Found after a strange dream." + "name": "森之羊羹", + "description": "增加持有者{{stats}}的基础属性{{statValue}},来自于一场怪梦。" }, "AllPokemonFullHpRestoreModifierType": { "description": "所有宝可梦完全回复HP。" @@ -109,7 +109,7 @@ "description": "招式命中率增加{{accuracyAmount}}(最大100)。" }, "PokemonMultiHitModifierType": { - "description": "攻击以60/75/82.5%的伤害造成2/3/4次伤害。" + "description": "伤害降低60/75/82.5%的同时造成2/3/4次伤害。" }, "TmModifierType": { "name": "招式学习器\n{{moveId}} - {{moveName}}", @@ -126,7 +126,7 @@ "description": "使某些宝可梦更改形态。" }, "FusePokemonModifierType": { - "description": "融合两只宝可梦 (改变特性, 平分基础点数\n和属性, 共享招式池)。" + "description": "融合两只宝可梦(改变特性, 平分基础点数\n和属性, 共享招式池)。" }, "TerastallizeModifierType": { "name": "{{teraType}}太晶碎块", @@ -359,8 +359,8 @@ "description": "触碰后会放出热量的神奇宝珠。\n携带后,在战斗时会变成灼伤状态。" }, "EVOLUTION_TRACKER_GIMMIGHOUL": { - "name": "金子宝物", - "description": "这个小精靈爱金子! 继续挑金子然后谁知道什么会发生!" + "name": "宝藏金币", + "description": "这个宝可梦最爱金币!多收集点金币的话会发生什么呢?" }, "BATON": { "name": "接力棒", @@ -421,11 +421,11 @@ "description": "增加1%野生融合宝可梦出现概率。" }, - "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "Shuckle Juice" }, - "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "Black Sludge", "description": "The stench is so powerful that shops will only sell you items at a steep cost increase." }, - "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "Macho Brace", "description": "Defeating a Pokémon grants the holder a Macho Brace stack. Each stack slightly boosts stats, with an extra bonus at max stacks." }, - "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "Old Gateau", "description": "Increases the holder's {{stats}} stats by {{statValue}}." }, - "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "Golden Bug Net", "description": "Imbues the owner with luck to find Bug Type Pokémon more often. Has a strange heft to it." } + "MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { "name": "壶壶果汁" }, + "MYSTERY_ENCOUNTER_BLACK_SLUDGE": { "name": "黑色污泥", "description": "由于恶臭扑鼻,商店会以非常高昂的价格向您出售商品。" }, + "MYSTERY_ENCOUNTER_MACHO_BRACE": { "name": "强制锻炼器", "description": "击败对手后获得一层锻炼等级。每层会略微提升属性,\n达到最大层数时还会获得额外奖励。" }, + "MYSTERY_ENCOUNTER_OLD_GATEAU": { "name": "森之羊羔", "description": "提升持有者的{{stats}}的{{statValue}}。" }, + "MYSTERY_ENCOUNTER_GOLDEN_BUG_NET": { "name": "金捕虫网", "description": "赋予主人好运,使其更容易\n找到虫属性宝可梦,手感很奇妙。" } }, "SpeciesBoosterItem": { "LIGHT_BALL": { diff --git a/src/locales/zh_CN/move-trigger.json b/src/locales/zh_CN/move-trigger.json index 60de3591915..32a4d0b4a7d 100644 --- a/src/locales/zh_CN/move-trigger.json +++ b/src/locales/zh_CN/move-trigger.json @@ -68,5 +68,5 @@ "chillyReception": "{{pokemonName}}\n说出了冷笑话!", "exposedMove": "{{pokemonName}}识破了\n{{targetPokemonName}}的原型!", "safeguard": "{{targetName}}\n正受到神秘之幕的保护!", - "afterYou": "{{pokemonName}}\n接受了对手的好意!" + "afterYou": "{{targetName}}\n接受了对手的好意!" } diff --git a/src/locales/zh_CN/mystery-encounters/delibirdy-dialogue.json b/src/locales/zh_CN/mystery-encounters/delibirdy-dialogue.json index 3a15f5f98be..6c43a95c782 100644 --- a/src/locales/zh_CN/mystery-encounters/delibirdy-dialogue.json +++ b/src/locales/zh_CN/mystery-encounters/delibirdy-dialogue.json @@ -3,7 +3,7 @@ { "intro": "一伙{{delibirdName}}出现了!", "title": "信使鸟快递", - "description": "{{delibirdName}}满怀期待地看着你,\n它们好像想要什么东西。\n\n也许给它们一件道具或一些钱能让它们满意?", + "description": "{{delibirdName}}满怀期待地看着你,\n它们好像想要什么东西。\n\n给它们一件道具或一些钱的话\n会不会让它们满意呢?", "query": "你要给它们什么?", "invalid_selection": "宝可梦身上并没有那种道具。", "option": { diff --git a/src/locales/zh_CN/mystery-encounters/department-store-sale-dialogue.json b/src/locales/zh_CN/mystery-encounters/department-store-sale-dialogue.json index 09c23762ea9..1d6adb28776 100644 --- a/src/locales/zh_CN/mystery-encounters/department-store-sale-dialogue.json +++ b/src/locales/zh_CN/mystery-encounters/department-store-sale-dialogue.json @@ -1,9 +1,9 @@ { "intro": "一位提着一大堆购物袋的女士。", "speaker": "大促销", - "intro_dialogue": "你好!你也是来参加促销活动的吗?\n在促销期间,可以领取一张特别优惠券,用于购买免费商品!\n我这有一张多余的优惠券。送你了!", + "intro_dialogue": "你好!你也是来参加促销活动的吗?\n在促销期间,可以领取一张特别优惠券!\n可以用于免费兑换商品哦!\n我这有一张多余的优惠券。送你了!", "title": "百货公司促销", - "description": "到处都是商品!\n好像有4个柜台可以让你用优惠券兑换商品。\n无尽可能!", + "description": "到处都是商品!\n共有4个柜台可以用优惠券兑换商品。\n好礼多多!", "query": "你要去哪里?", "option": { "1": { diff --git a/src/locales/zh_CN/mystery-encounters/global-trade-system-dialogue.json b/src/locales/zh_CN/mystery-encounters/global-trade-system-dialogue.json index 4b0d2e84edc..76748b8e71c 100644 --- a/src/locales/zh_CN/mystery-encounters/global-trade-system-dialogue.json +++ b/src/locales/zh_CN/mystery-encounters/global-trade-system-dialogue.json @@ -1,7 +1,7 @@ { "intro": "是GTS系统的交互面板!", "title": "全球交换系统(GTS)", - "description": "啊,GTS!科学的奇迹,\\nn你可以与世界各地的任何人联系,\n与他们交换宝可梦!\n今天你的交换会有好运吗?", + "description": "啊,GTS!科学的奇迹,\n你可以与世界各地的任何人联系,\n并与他们交换宝可梦!\n今天你的交换运会如何呢?", "query": "你要怎么做?", "option": { "1": { @@ -11,13 +11,13 @@ }, "2": { "label": "奇迹交换", - "tooltip": "(+)将一只宝可梦送至GTS,\n并获得一只随机宝可梦" + "tooltip": "(+)发送一只宝可梦,\n并获得一只随机宝可梦" }, "3": { "label": "道具交换", "trade_options_prompt": "选择要发送的道具", "invalid_selection": "这只宝可梦没有合法道具\n可以被交换。", - "tooltip": "(+)将你的一件道具发送到GTS并获得一件随机新道具" + "tooltip": "(+)发送一件道具\n并获得一件随机新道具" }, "4": { "label": "离开", diff --git a/src/locales/zh_CN/mystery-encounters/training-session-dialogue.json b/src/locales/zh_CN/mystery-encounters/training-session-dialogue.json index 7bb6229d330..ba853f80b4d 100644 --- a/src/locales/zh_CN/mystery-encounters/training-session-dialogue.json +++ b/src/locales/zh_CN/mystery-encounters/training-session-dialogue.json @@ -8,19 +8,19 @@ "1": { "label": "轻量训练", "tooltip": "(-)轻松战斗\n(+)提升随机2只\n宝可梦的个体", - "finished": "{{selectedPokemon}}回来了,感觉\n虽然疲惫不堪,但很有成就!$它的{{stat1}}和{{stat2}}的个体得到了改善!" + "finished": "{{selectedPokemon}}回来了,感觉虽然疲惫不堪,\n但很有成就感!$它的{{stat1}}和{{stat2}}的个体得到了改善!" }, "2": { "label": "适度训练", "tooltip": "(-)适中的战斗\n(+)改变宝可梦的性格", "select_prompt": "选择一种想要的性格\n来训练你的神奇宝贝。", - "finished": "{{selectedPokemon}}回来了,感觉\n虽然疲惫不堪,但很有成就!$它的性格变成了{{nature}}!" + "finished": "{{selectedPokemon}}回来了,感觉虽然疲惫不堪,\n但很有成就感!$它的性格变成了{{nature}}!" }, "3": { "label": "重磅训练", "tooltip": "(-)艰苦的战斗\n(+)改变宝可梦的特性", "select_prompt": "选择一种想要的特性\n来训练你的神奇宝贝。", - "finished": "{{selectedPokemon}}回来了,感觉\n虽然疲惫不堪,但很有成就!$它的特性变成了{{nature}}!" + "finished": "{{selectedPokemon}}回来了,感觉虽然疲惫不堪,\n但很有成就感!$它的特性变成了{{ability}}!" }, "4": { "label": "离开", diff --git a/src/locales/zh_CN/mystery-encounters/trash-to-treasure-dialogue.json b/src/locales/zh_CN/mystery-encounters/trash-to-treasure-dialogue.json index 29e7817923b..9de9b30cbd9 100644 --- a/src/locales/zh_CN/mystery-encounters/trash-to-treasure-dialogue.json +++ b/src/locales/zh_CN/mystery-encounters/trash-to-treasure-dialogue.json @@ -12,7 +12,7 @@ "2": { "label": "探究来源", "tooltip": "(?)找到垃圾的来源", - "selected": "你在这堆东西周围徘徊,寻找表明这东西可能出现在这里的任何迹象……", + "selected": "你在这堆东西周围徘徊,\n寻找表明这东西可能出现在这里的任何迹象……", "selected_2": "突然,垃圾动了!那不是垃圾,是一只宝可梦!" } } diff --git a/src/locales/zh_CN/mystery-encounters/uncommon-breed-dialogue.json b/src/locales/zh_CN/mystery-encounters/uncommon-breed-dialogue.json index f844cae5908..1d01b894e27 100644 --- a/src/locales/zh_CN/mystery-encounters/uncommon-breed-dialogue.json +++ b/src/locales/zh_CN/mystery-encounters/uncommon-breed-dialogue.json @@ -1,7 +1,7 @@ { "intro": "这可不是一只普通的宝可梦!", "title": "罕见物种", - "description": "{{enemyPokemon}}看起来与其他同种宝可梦大相径庭。\n@[TOOLTIP_TITLE]{也许它会特殊招式?}\n你可以直接进行战斗并尝试捕获它,\n但可能也有办法和它交朋友。", + "description": "{{enemyPokemon}}看起来\n与其他同种宝可梦大相径庭。\n@[TOOLTIP_TITLE]{也许它会特殊招式?}\n你可以直接进行战斗并尝试捕获它,\n但可能也有办法和它交朋友。", "query": "你要怎么做?", "option": { "1": { diff --git a/src/locales/zh_CN/party-ui-handler.json b/src/locales/zh_CN/party-ui-handler.json index 2bc74991824..079fc551a40 100644 --- a/src/locales/zh_CN/party-ui-handler.json +++ b/src/locales/zh_CN/party-ui-handler.json @@ -13,6 +13,7 @@ "ALL": "全部道具", "PASS_BATON": "接棒", "UNPAUSE_EVOLUTION": "解除进化暂停", + "PAUSE_EVOLUTION": "暂停进化", "REVIVE": "复活", "RENAME": "起名", "SELECT": "选择", diff --git a/src/locales/zh_CN/settings.json b/src/locales/zh_CN/settings.json index dd001213b9e..3c0d2ed9e12 100644 --- a/src/locales/zh_CN/settings.json +++ b/src/locales/zh_CN/settings.json @@ -11,6 +11,10 @@ "expGainsSpeed": "经验值获取动画速度", "expPartyDisplay": "显示队伍经验", "skipSeenDialogues": "跳过已读对话", + "eggSkip": "孵蛋跳过", + "never": "从不", + "always": "永久", + "ask": "询问", "battleStyle": "对战模式", "enableRetries": "允许重试", "hideIvs": "禁用个体值探测器信息", @@ -60,9 +64,10 @@ "playerGender": "玩家性别", "typeHints": "属性提示", "masterVolume": "主音量", - "bgmVolume": "音乐", + "bgmVolume": "音乐音量", "fieldVolume": "场景音量", - "seVolume": "音效", + "seVolume": "音效音量", + "uiVolume": "界面音量", "musicPreference": "音乐偏好", "mixed": "全曲混合", "gamepadPleasePlug": "请链接手柄或按任意键", 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/locales/zh_TW/move-trigger.json b/src/locales/zh_TW/move-trigger.json index 2cd33a3a416..6b80196f166 100644 --- a/src/locales/zh_TW/move-trigger.json +++ b/src/locales/zh_TW/move-trigger.json @@ -68,5 +68,5 @@ "chillyReception": "{{pokemonName}}\n說了冷笑話!", "exposedMove": "{{pokemonName}}識破了\n{{targetPokemonName}}的原形!", "safeguard": "{{targetName}}\n正受到神秘之幕的保護!", - "afterYou": "{{pokemonName}}\n接受了對手的好意!" + "afterYou": "{{targetName}}\n接受了對手的好意!" } diff --git a/src/modifier/modifier-type.ts b/src/modifier/modifier-type.ts index 9ce555a617e..fb69a96448a 100644 --- a/src/modifier/modifier-type.ts +++ b/src/modifier/modifier-type.ts @@ -1387,7 +1387,8 @@ export const modifierTypes = { FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(false), RARE_FORM_CHANGE_ITEM: () => new FormChangeItemModifierTypeGenerator(true), - EVOLUTION_TRACKER_GIMMIGHOUL: () => new PokemonHeldItemModifierType("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL", "relic_gold", (type, _args) => new Modifiers.EvoTrackerModifier(type, (_args[0] as Pokemon).id, Species.GIMMIGHOUL, 10)), + EVOLUTION_TRACKER_GIMMIGHOUL: () => new PokemonHeldItemModifierType("modifierType:ModifierType.EVOLUTION_TRACKER_GIMMIGHOUL", "relic_gold", + (type, args) => new Modifiers.EvoTrackerModifier(type, (args[0] as Pokemon).id, Species.GIMMIGHOUL, 10)), MEGA_BRACELET: () => new ModifierType("modifierType:ModifierType.MEGA_BRACELET", "mega_bracelet", (type, _args) => new Modifiers.MegaEvolutionAccessModifier(type)), DYNAMAX_BAND: () => new ModifierType("modifierType:ModifierType.DYNAMAX_BAND", "dynamax_band", (type, _args) => new Modifiers.GigantamaxAccessModifier(type)), diff --git a/src/modifier/modifier.ts b/src/modifier/modifier.ts index 221cbf8bcb5..6e3589882d1 100644 --- a/src/modifier/modifier.ts +++ b/src/modifier/modifier.ts @@ -1,5 +1,5 @@ import * as ModifierTypes from "./modifier-type"; -import { ModifierType, modifierTypes } from "./modifier-type"; +import { getModifierType, ModifierType, modifierTypes } from "./modifier-type"; import BattleScene from "../battle-scene"; import { getLevelTotalExp } from "../data/exp"; import { MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball"; @@ -852,26 +852,47 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier { } matchType(modifier: Modifier): boolean { - if (modifier instanceof EvoTrackerModifier) { - return (modifier as EvoTrackerModifier).species === this.species; - } - return false; + return modifier instanceof EvoTrackerModifier && modifier.species === this.species && modifier.required === this.required; } clone(): PersistentModifier { - return new EvoTrackerModifier(this.type, this.pokemonId, this.species, this.stackCount); + return new EvoTrackerModifier(this.type, this.pokemonId, this.species, this.required, this.stackCount); } getArgs(): any[] { - return super.getArgs().concat(this.species); + return super.getArgs().concat([this.species, this.required]); } apply(args: any[]): boolean { return true; } - getMaxHeldItemCount(_pokemon: Pokemon): integer { - return this.required; + getIconStackText(scene: BattleScene, virtual?: boolean): Phaser.GameObjects.BitmapText | null { + if (this.getMaxStackCount(scene) === 1 || (virtual && !this.virtualStackCount)) { + return null; + } + + const pokemon = scene.getPokemonById(this.pokemonId); + + this.stackCount = pokemon + ? pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length + : this.stackCount; + + const text = scene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11); + text.letterSpacing = -0.5; + if (this.getStackCount() >= this.required) { + text.setTint(0xf89890); + } + text.setOrigin(0, 0); + + return text; + } + + getMaxHeldItemCount(pokemon: Pokemon): integer { + this.stackCount = pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length + + pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length; + return 999; } } @@ -2414,9 +2435,8 @@ export class MoneyRewardModifier extends ConsumableModifier { scene.getParty().map(p => { if (p.species?.speciesId === Species.GIMMIGHOUL || p.fusionSpecies?.speciesId === Species.GIMMIGHOUL) { - p.evoCounter++; - const modifierType: ModifierType = modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL(); - const modifier = modifierType!.newModifier(p); + p.evoCounter ? p.evoCounter++ : p.evoCounter = 1; + const modifier = getModifierType(modifierTypes.EVOLUTION_TRACKER_GIMMIGHOUL).newModifier(p) as EvoTrackerModifier; scene.addModifier(modifier); } }); @@ -2586,7 +2606,7 @@ export class HealShopCostModifier extends PersistentModifier { constructor(type: ModifierType, shopMultiplier: number, stackCount?: integer) { super(type, stackCount); - this.shopMultiplier = shopMultiplier; + this.shopMultiplier = shopMultiplier ?? 2.5; } match(modifier: Modifier): boolean { @@ -2598,11 +2618,16 @@ export class HealShopCostModifier extends PersistentModifier { } apply(args: any[]): boolean { - (args[0] as Utils.IntegerHolder).value *= this.shopMultiplier; + const moneyCost = args[0] as Utils.NumberHolder; + moneyCost.value = Math.floor(moneyCost.value * this.shopMultiplier); return true; } + getArgs(): any[] { + return super.getArgs().concat(this.shopMultiplier); + } + getMaxStackCount(scene: BattleScene): integer { return 1; } 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/game-over-phase.ts b/src/phases/game-over-phase.ts index b0cfa5c55e2..1db47e9e289 100644 --- a/src/phases/game-over-phase.ts +++ b/src/phases/game-over-phase.ts @@ -1,34 +1,28 @@ import { clientSessionId } from "#app/account"; -import BattleScene from "#app/battle-scene"; import { BattleType } from "#app/battle"; +import BattleScene from "#app/battle-scene"; import { getCharVariantFromDialogue } from "#app/data/dialogue"; import { pokemonEvolutions } from "#app/data/pokemon-evolutions"; import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species"; import { trainerConfigs } from "#app/data/trainer-config"; -import { PlayerGender } from "#app/enums/player-gender"; -import { TrainerType } from "#app/enums/trainer-type"; import Pokemon from "#app/field/pokemon"; import { modifierTypes } from "#app/modifier/modifier-type"; +import { BattlePhase } from "#app/phases/battle-phase"; +import { CheckSwitchPhase } from "#app/phases/check-switch-phase"; +import { EncounterPhase } from "#app/phases/encounter-phase"; +import { EndCardPhase } from "#app/phases/end-card-phase"; +import { GameOverModifierRewardPhase } from "#app/phases/game-over-modifier-reward-phase"; +import { PostGameOverPhase } from "#app/phases/post-game-over-phase"; +import { RibbonModifierRewardPhase } from "#app/phases/ribbon-modifier-reward-phase"; +import { SummonPhase } from "#app/phases/summon-phase"; +import { UnlockPhase } from "#app/phases/unlock-phase"; import { achvs, ChallengeAchv } from "#app/system/achv"; import { Unlockables } from "#app/system/unlockables"; import { Mode } from "#app/ui/ui"; -import i18next from "i18next"; import * as Utils from "#app/utils"; -import { BattlePhase } from "./battle-phase"; -import { CheckSwitchPhase } from "./check-switch-phase"; -import { EncounterPhase } from "./encounter-phase"; -import { GameOverModifierRewardPhase } from "./game-over-modifier-reward-phase"; -import { RibbonModifierRewardPhase } from "./ribbon-modifier-reward-phase"; -import { SummonPhase } from "./summon-phase"; -import { EndCardPhase } from "./end-card-phase"; -import { PostGameOverPhase } from "./post-game-over-phase"; -import { UnlockPhase } from "./unlock-phase"; -import { SessionSaveData } from "../system/game-data"; -import TrainerData from "../system/trainer-data"; -import PokemonData from "../system/pokemon-data"; -import PersistentModifierData from "../system/modifier-data"; -import ChallengeData from "../system/challenge-data"; -import ArenaData from "../system/arena-data"; +import { PlayerGender } from "#enums/player-gender"; +import { TrainerType } from "#enums/trainer-type"; +import i18next from "i18next"; export class GameOverPhase extends BattlePhase { private victory: boolean; @@ -114,7 +108,7 @@ export class GameOverPhase extends BattlePhase { this.scene.gameData.gameStats.dailyRunSessionsWon++; } } - this.scene.gameData.saveRunHistory(this.scene, this.getFinalSessionData(), this.victory); + this.scene.gameData.saveRunHistory(this.scene, this.scene.gameData.getSessionSaveData(this.scene), this.victory); const fadeDuration = this.victory ? 10000 : 5000; this.scene.fadeOutBgm(fadeDuration, true); const activeBattlers = this.scene.getField().filter(p => p?.isActive(true)); @@ -221,34 +215,5 @@ export class GameOverPhase extends BattlePhase { this.firstRibbons.push(getPokemonSpecies(pokemon.species.getRootSpeciesId(forStarter))); } } - - /** - * This function mirrors game-data.ts' getSessionSaveData() to update the session data to reflect any changes that occurred within the last wave - * This means that level ups, item usage, evolutions, etc. will all be accurately reflected. - * @returns {@linkCode SessionSaveData} an updated version of the wave's SessionSaveData that accurately reflects the events of the wave - */ - private getFinalSessionData(): SessionSaveData { - return { - seed: this.scene.seed, - playTime: this.scene.sessionPlayTime, - gameMode: this.scene.gameMode.modeId, - party: this.scene.getParty().map(p => new PokemonData(p)), - enemyParty: this.scene.getEnemyParty().map(p => new PokemonData(p)), - modifiers: this.scene.findModifiers(() => true).map(m => new PersistentModifierData(m, true)), - enemyModifiers: this.scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), - arena: new ArenaData(this.scene.arena), - pokeballCounts: this.scene.pokeballCounts, - money: this.scene.money, - score: this.scene.score, - waveIndex: this.scene.currentBattle.waveIndex, - battleType: this.scene.currentBattle.battleType, - trainer: this.scene.currentBattle.battleType === BattleType.TRAINER ? new TrainerData(this.scene.currentBattle.trainer) : null, - gameVersion: this.scene.game.config.gameVersion, - timestamp: new Date().getTime(), - challenges: this.scene.gameMode.challenges.map(c => new ChallengeData(c)), - mysteryEncounterType: this.scene.currentBattle.mysteryEncounter?.encounterType ?? -1, - mysteryEncounterSaveData: this.scene.mysteryEncounterSaveData - } as SessionSaveData; - } } diff --git a/src/phases/mystery-encounter-phases.ts b/src/phases/mystery-encounter-phases.ts index 60755095cca..0efaf1bf4ca 100644 --- a/src/phases/mystery-encounter-phases.ts +++ b/src/phases/mystery-encounter-phases.ts @@ -25,6 +25,7 @@ import { GameOverPhase } from "#app/phases/game-over-phase"; import { SwitchPhase } from "#app/phases/switch-phase"; import { SeenEncounterData } from "#app/data/mystery-encounters/mystery-encounter-save-data"; import { SwitchType } from "#enums/switch-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; /** * Will handle (in order): @@ -218,9 +219,17 @@ export class MysteryEncounterBattleStartCleanupPhase extends Phase { start() { super.start(); + // Lapse any residual flinches/endures but ignore all other turn-end battle tags + const includedLapseTags = [BattlerTagType.FLINCHED, BattlerTagType.ENDURING]; const field = this.scene.getField(true).filter(p => p.summonData); field.forEach(pokemon => { - pokemon.lapseTags(BattlerTagLapseType.TURN_END); + const tags = pokemon.summonData.tags; + tags.filter(t => includedLapseTags.includes(t.tagType) + && t.lapseTypes.includes(BattlerTagLapseType.TURN_END) + && !(t.lapse(pokemon, BattlerTagLapseType.TURN_END))).forEach(t => { + t.onRemove(pokemon); + tags.splice(tags.indexOf(t), 1); + }); }); // Remove any status tick phases diff --git a/src/phases/pokemon-heal-phase.ts b/src/phases/pokemon-heal-phase.ts index 813d15ae87e..dc0bd235bb5 100644 --- a/src/phases/pokemon-heal-phase.ts +++ b/src/phases/pokemon-heal-phase.ts @@ -46,8 +46,7 @@ export class PokemonHealPhase extends CommonAnimPhase { const pokemon = this.getPokemon(); if (!pokemon.isOnField() || (!this.revive && !pokemon.isActive())) { - super.end(); - return; + return super.end(); } const hasMessage = !!this.message; @@ -58,7 +57,7 @@ export class PokemonHealPhase extends CommonAnimPhase { if (healBlock && this.hpHealed > 0) { this.scene.queueMessage(healBlock.onActivation(pokemon)); this.message = null; - super.end(); + return super.end(); } else if (healOrDamage) { const hpRestoreMultiplier = new Utils.IntegerHolder(1); if (!this.revive) { 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/system/game-data.ts b/src/system/game-data.ts index 63c7cb1b13a..af5e717c17b 100644 --- a/src/system/game-data.ts +++ b/src/system/game-data.ts @@ -957,7 +957,7 @@ export class GameData { enemyModifiers: scene.findModifiers(() => true, false).map(m => new PersistentModifierData(m, false)), arena: new ArenaData(scene.arena), pokeballCounts: scene.pokeballCounts, - money: scene.money, + money: Math.floor(scene.money), score: scene.score, waveIndex: scene.currentBattle.waveIndex, battleType: scene.currentBattle.battleType, @@ -1047,7 +1047,7 @@ export class GameData { scene.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs; } - scene.money = sessionData.money || 0; + scene.money = Math.floor(sessionData.money || 0); scene.updateMoneyText(); if (scene.money > this.gameStats.highestMoney) { diff --git a/src/system/version-converter.ts b/src/system/version-converter.ts index 0591647aeaa..3a4416a975e 100644 --- a/src/system/version-converter.ts +++ b/src/system/version-converter.ts @@ -6,6 +6,10 @@ const LATEST_VERSION = "1.0.5"; export function applySessionDataPatches(data: SessionSaveData) { const curVersion = data.gameVersion; + + // Always sanitize money as a safeguard + data.money = Math.floor(data.money); + if (curVersion !== LATEST_VERSION) { switch (curVersion) { case "1.0.0": 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/chilly_reception.test.ts b/src/test/moves/chilly_reception.test.ts index 1b5b5cecb4a..6c8e8172670 100644 --- a/src/test/moves/chilly_reception.test.ts +++ b/src/test/moves/chilly_reception.test.ts @@ -4,6 +4,7 @@ import { Species } from "#enums/species"; import { WeatherType } from "#enums/weather-type"; import GameManager from "#test/utils/gameManager"; import Phaser from "phaser"; +//import { TurnInitPhase } from "#app/phases/turn-init-phase"; import { afterEach, beforeAll, beforeEach, describe, expect, it } from "vitest"; describe("Moves - Chilly Reception", () => { @@ -66,4 +67,58 @@ describe("Moves - Chilly Reception", () => { expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); expect(game.scene.getPlayerField()[0].species.speciesId).toBe(Species.MEOWTH); }); + + // enemy uses another move and weather doesn't change + it("check case - enemy not selecting chilly reception doesn't change weather ", async () => { + game.override.battleType("single") + .enemyMoveset([Moves.CHILLY_RECEPTION, Moves.TACKLE]) + .enemyAbility(Abilities.NONE) + .moveset(Array(4).fill(Moves.SPLASH)); + + await game.classicMode.startBattle([Species.SLOWKING, Species.MEOWTH]); + + game.move.select(Moves.SPLASH); + await game.forceEnemyMove(Moves.TACKLE); + + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(undefined); + }); + + it("enemy trainer - expected behavior ", async () => { + game.override.battleType("single") + .startingWave(8) + .enemyMoveset(Array(4).fill(Moves.CHILLY_RECEPTION)) + .enemyAbility(Abilities.NONE) + .enemySpecies(Species.MAGIKARP) + .moveset([Moves.SPLASH, Moves.THUNDERBOLT]); + + await game.classicMode.startBattle([Species.JOLTEON]); + const RIVAL_MAGIKARP1 = game.scene.getEnemyPokemon()?.id; + + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + expect(game.scene.getEnemyPokemon()?.id !== RIVAL_MAGIKARP1); + + await game.phaseInterceptor.to("TurnInitPhase", false); + game.move.select(Moves.SPLASH); + + // second chilly reception should still switch out + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + await game.phaseInterceptor.to("TurnInitPhase", false); + expect(game.scene.getEnemyPokemon()?.id === RIVAL_MAGIKARP1); + game.move.select(Moves.THUNDERBOLT); + + // enemy chilly recep move should fail: it's snowing and no option to switch out + // no crashing + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + await game.phaseInterceptor.to("TurnInitPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + game.move.select(Moves.SPLASH); + await game.phaseInterceptor.to("BerryPhase", false); + expect(game.scene.arena.weather?.weatherType).toBe(WeatherType.SNOW); + + }); }); 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/substitute.test.ts b/src/test/moves/substitute.test.ts index d185c0ab471..3099446f081 100644 --- a/src/test/moves/substitute.test.ts +++ b/src/test/moves/substitute.test.ts @@ -1,13 +1,15 @@ import { BattlerIndex } from "#app/battle"; +import { ArenaTagSide } from "#app/data/arena-tag"; import { SubstituteTag, TrappedTag } from "#app/data/battler-tags"; import { allMoves, StealHeldItemChanceAttr } from "#app/data/move"; import { StatusEffect } from "#app/data/status-effect"; -import { Abilities } from "#app/enums/abilities"; -import { BattlerTagType } from "#app/enums/battler-tag-type"; -import { BerryType } from "#app/enums/berry-type"; -import { Moves } from "#app/enums/moves"; -import { Species } from "#app/enums/species"; -import { Stat } from "#app/enums/stat"; +import { Abilities } from "#enums/abilities"; +import { ArenaTagType } from "#enums/arena-tag-type"; +import { BattlerTagType } from "#enums/battler-tag-type"; +import { BerryType } from "#enums/berry-type"; +import { Moves } from "#enums/moves"; +import { Species } from "#enums/species"; +import { Stat } from "#enums/stat"; import { MoveResult } from "#app/field/pokemon"; import { CommandPhase } from "#app/phases/command-phase"; import GameManager from "#app/test/utils/gameManager"; @@ -194,6 +196,71 @@ describe("Moves - Substitute", () => { } ); + it( + "shouldn't block moves that target the user's side of the field", + async () => { + game.override.moveset(Moves.LIGHT_SCREEN); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + vi.spyOn(leadPokemon, "getMoveEffectiveness"); + + leadPokemon.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, leadPokemon.id); + + game.move.select(Moves.LIGHT_SCREEN); + + await game.toNextTurn(); + + expect(leadPokemon.getMoveEffectiveness).not.toHaveReturnedWith(0); + expect(game.scene.arena.getTagOnSide(ArenaTagType.LIGHT_SCREEN, ArenaTagSide.PLAYER)).toBeDefined(); + } + ); + + it( + "shouldn't block the opponent from setting hazards", + async () => { + game.override.enemyMoveset(Moves.STEALTH_ROCK); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const leadPokemon = game.scene.getPlayerPokemon()!; + vi.spyOn(leadPokemon, "getMoveEffectiveness"); + + game.move.select(Moves.SUBSTITUTE); + + await game.toNextTurn(); + + expect(leadPokemon.getMoveEffectiveness).not.toHaveReturnedWith(0); + expect(game.scene.arena.getTagOnSide(ArenaTagType.STEALTH_ROCK, ArenaTagSide.PLAYER)).toBeDefined(); + } + ); + + it( + "shouldn't block moves that target both sides of the field", + async () => { + game.override + .moveset(Moves.TRICK_ROOM) + .enemyMoveset(Moves.GRAVITY); + + await game.classicMode.startBattle([Species.BLASTOISE]); + + const pokemon = game.scene.getField(true); + pokemon.forEach(p => { + vi.spyOn(p, "getMoveEffectiveness"); + p.addTag(BattlerTagType.SUBSTITUTE, 0, Moves.NONE, p.id); + }); + + game.move.select(Moves.TRICK_ROOM); + + await game.toNextTurn(); + + pokemon.forEach(p => expect(p.getMoveEffectiveness).not.toHaveReturnedWith(0)); + expect(game.scene.arena.getTag(ArenaTagType.TRICK_ROOM)).toBeDefined(); + expect(game.scene.arena.getTag(ArenaTagType.GRAVITY)).toBeDefined(); + } + ); + it( "should protect the user from flinching", async () => { 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/timed-event-manager.ts b/src/timed-event-manager.ts index 9bfa3bdf54a..3caa84fd39b 100644 --- a/src/timed-event-manager.ts +++ b/src/timed-event-manager.ts @@ -1,5 +1,6 @@ import BattleScene from "#app/battle-scene"; import { TextStyle, addTextObject } from "#app/ui/text"; +import { nil } from "#app/utils"; import i18next from "i18next"; export enum EventType { @@ -64,19 +65,19 @@ export class TimedEventManager { let multiplier = 1; const shinyEvents = timedEvents.filter((te) => te.eventType === EventType.SHINY && this.isActive(te)); shinyEvents.forEach((se) => { - multiplier *= se.shinyMultiplier!; // TODO: is this bang correct? + multiplier *= se.shinyMultiplier ?? 1; }); return multiplier; } getEventBannerFilename(): string { - return timedEvents.find((te: TimedEvent) => this.isActive(te))?.bannerKey!; // TODO: is this bang correct? + return timedEvents.find((te: TimedEvent) => this.isActive(te))?.bannerKey ?? ""; } } export class TimedEventDisplay extends Phaser.GameObjects.Container { - private event: TimedEvent | null; + private event: TimedEvent | nil; private eventTimerText: Phaser.GameObjects.Text; private banner: Phaser.GameObjects.Image; private bannerShadow: Phaser.GameObjects.Rectangle; @@ -84,7 +85,7 @@ export class TimedEventDisplay extends Phaser.GameObjects.Container { constructor(scene: BattleScene, x: number, y: number, event?: TimedEvent) { super(scene, x, y); - this.event = event!; // TODO: is this bang correct? + this.event = event; this.setVisible(false); } 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()) { diff --git a/src/utils.ts b/src/utils.ts index b029067c8d6..68331211fc0 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,8 @@ import { MoneyFormat } from "#enums/money-format"; import { Moves } from "#enums/moves"; import i18next from "i18next"; +export type nil = null | undefined; + export const MissingTextureKey = "__MISSING"; export function toReadableString(str: string): string { @@ -443,7 +445,7 @@ export function deltaRgb(rgb1: integer[], rgb2: integer[]): integer { } export function rgbHexToRgba(hex: string) { - const color = hex.match(/^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i)!; // TODO: is this bang correct? + const color = hex.match(/^([\da-f]{2})([\da-f]{2})([\da-f]{2})$/i) ?? ["000000", "00", "00", "00"]; return { r: parseInt(color[1], 16), g: parseInt(color[2], 16),