diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 40c7f6baf7c9..9a4be3a8abb9 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -6,6 +6,7 @@ on: jobs: deploy: + if: github.repository == 'pagefaultgames/pokerogue' runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/src/battle-scene-events.ts b/src/battle-scene-events.ts new file mode 100644 index 000000000000..be3b79ff609f --- /dev/null +++ b/src/battle-scene-events.ts @@ -0,0 +1,25 @@ +import Move from "./data/move"; + +export enum BattleSceneEventType { + MOVE_USED = "onMoveUsed" +} + +/** + * Container class for `onMoveUsed` events + * @extends Event +*/ +export class MoveUsedEvent extends Event { + /** The ID of the {@linkcode Pokemon} that used the {@linkcode Move} */ + public userId: number; + /** The {@linkcode Move} used */ + public move: Move; + /** The amount of PP used on the {@linkcode Move} this turn */ + public ppUsed: number; + constructor(userId: number, move: Move, ppUsed: number) { + super(BattleSceneEventType.MOVE_USED); + + this.userId = userId; + this.move = move; + this.ppUsed = ppUsed; + } +} diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 21ddc9026912..93f02c26143b 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2,7 +2,7 @@ import Phaser from "phaser"; import UI from "./ui/ui"; import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase } from "./phases"; import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon"; -import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies, initSpecies } from "./data/pokemon-species"; +import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species"; import * as Utils from "./utils"; import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems } from "./modifier/modifier"; import { PokeballType } from "./data/pokeball"; @@ -15,10 +15,9 @@ import { GameData, PlayerGender } from "./system/game-data"; import { TextStyle, addTextObject } from "./ui/text"; import { Moves } from "./data/enums/moves"; import { allMoves } from "./data/move"; -import { initMoves } from "./data/move"; import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getPartyLuckValue } from "./modifier/modifier-type"; import AbilityBar from "./ui/ability-bar"; -import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, applyAbAttrs, initAbilities } from "./data/ability"; +import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, applyAbAttrs } from "./data/ability"; import { allAbilities } from "./data/ability"; import Battle, { BattleType, FixedBattleConfig, fixedBattles } from "./battle"; import { GameMode, GameModes, gameModes } from "./game-mode"; @@ -152,7 +151,8 @@ export default class BattleScene extends SceneBase { public money: integer; public pokemonInfoContainer: PokemonInfoContainer; private party: PlayerPokemon[]; - private waveCountText: Phaser.GameObjects.Text; + /** Combined Biome and Wave count text */ + private biomeWaveText: Phaser.GameObjects.Text; private moneyText: Phaser.GameObjects.Text; private scoreText: Phaser.GameObjects.Text; private luckLabelText: Phaser.GameObjects.Text; @@ -185,13 +185,16 @@ export default class BattleScene extends SceneBase { public rngSeedOverride: string = ""; public rngOffset: integer = 0; + /** + * Allows subscribers to listen for events + * + * Current Events: + * - {@linkcode BattleSceneEventType.MOVE_USED} {@linkcode MoveUsedEvent} + */ + public readonly eventTarget: EventTarget = new EventTarget(); + constructor() { super("battle"); - - initSpecies(); - initMoves(); - initAbilities(); - this.phaseQueue = []; this.phaseQueuePrepend = []; this.phaseQueuePrependSpliceIndex = -1; @@ -348,9 +351,9 @@ export default class BattleScene extends SceneBase { this.candyBar.setup(); this.fieldUI.add(this.candyBar); - this.waveCountText = addTextObject(this, (this.game.canvas.width / 6) - 2, 0, startingWave.toString(), TextStyle.BATTLE_INFO); - this.waveCountText.setOrigin(1, 0); - this.fieldUI.add(this.waveCountText); + this.biomeWaveText = addTextObject(this, (this.game.canvas.width / 6) - 2, 0, startingWave.toString(), TextStyle.BATTLE_INFO); + this.biomeWaveText.setOrigin(1, 0); + this.fieldUI.add(this.biomeWaveText); this.moneyText = addTextObject(this, (this.game.canvas.width / 6) - 2, 0, "", TextStyle.MONEY); this.moneyText.setOrigin(1, 0); @@ -478,7 +481,7 @@ export default class BattleScene extends SceneBase { } }); - this.updateWaveCountText(); + this.updateBiomeWaveText(); this.updateMoneyText(); this.updateScoreText(); } @@ -792,8 +795,8 @@ export default class BattleScene extends SceneBase { this.currentBattle = null; - this.waveCountText.setText(startingWave.toString()); - this.waveCountText.setVisible(false); + this.biomeWaveText.setText(startingWave.toString()); + this.biomeWaveText.setVisible(false); this.updateMoneyText(); this.moneyText.setVisible(false); @@ -1242,12 +1245,13 @@ export default class BattleScene extends SceneBase { }); } - updateWaveCountText(): void { + updateBiomeWaveText(): void { const isBoss = !(this.currentBattle.waveIndex % 10); - this.waveCountText.setText(this.currentBattle.waveIndex.toString()); - this.waveCountText.setColor(!isBoss ? "#404040" : "#f89890"); - this.waveCountText.setShadowColor(!isBoss ? "#ded6b5" : "#984038"); - this.waveCountText.setVisible(true); + const biomeString: string = getBiomeName(this.arena.biomeType); + this.biomeWaveText.setText( biomeString + " - " + this.currentBattle.waveIndex.toString()); + this.biomeWaveText.setColor(!isBoss ? "#ffffff" : "#f89890"); + this.biomeWaveText.setShadowColor(!isBoss ? "#636363" : "#984038"); + this.biomeWaveText.setVisible(true); } updateMoneyText(): void { @@ -1295,8 +1299,8 @@ export default class BattleScene extends SceneBase { updateUIPositions(): void { const enemyModifierCount = this.enemyModifiers.filter(m => m.isIconVisible(this)).length; - this.waveCountText.setY(-(this.game.canvas.height / 6) + (enemyModifierCount ? enemyModifierCount <= 12 ? 15 : 24 : 0)); - this.moneyText.setY(this.waveCountText.y + 10); + this.biomeWaveText.setY(-(this.game.canvas.height / 6) + (enemyModifierCount ? enemyModifierCount <= 12 ? 15 : 24 : 0)); + this.moneyText.setY(this.biomeWaveText.y + 10); this.scoreText.setY(this.moneyText.y + 10); [ this.luckLabelText, this.luckText ].map(l => l.setY((this.scoreText.visible ? this.scoreText : this.moneyText).y + 10)); const offsetY = (this.scoreText.visible ? this.scoreText : this.moneyText).y + 15; diff --git a/src/data/ability.ts b/src/data/ability.ts index 70c1279b9d93..0ad69407e2cb 100644 --- a/src/data/ability.ts +++ b/src/data/ability.ts @@ -2,14 +2,14 @@ import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; import { Type } from "./type"; import * as Utils from "../utils"; import { BattleStat, getBattleStatName } from "./battle-stat"; -import { PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; +import { MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases"; import { getPokemonMessage, getPokemonPrefix } from "../messages"; import { Weather, WeatherType } from "./weather"; import { BattlerTag } from "./battler-tags"; import { BattlerTagType } from "./enums/battler-tag-type"; import { StatusEffect, getStatusEffectDescriptor, getStatusEffectHealText } from "./status-effect"; import { Gender } from "./gender"; -import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, StatusMoveTypeImmunityAttr, FlinchAttr, OneHitKOAttr, HitHealAttr, StrengthSapHealAttr, allMoves, StatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr } from "./move"; +import Move, { AttackMove, MoveCategory, MoveFlags, MoveTarget, StatusMoveTypeImmunityAttr, FlinchAttr, OneHitKOAttr, HitHealAttr, StrengthSapHealAttr, allMoves, StatusMove, SelfStatusMove, VariablePowerAttr, applyMoveAttrs, IncrementMovePriorityAttr } from "./move"; import { ArenaTagSide, ArenaTrapTag } from "./arena-tag"; import { ArenaTagType } from "./enums/arena-tag-type"; import { Stat } from "./pokemon-stat"; @@ -22,6 +22,7 @@ import i18next, { Localizable } from "#app/plugins/i18n.js"; import { Command } from "../ui/command-ui-handler"; import { getPokeballName } from "./pokeball"; import { BerryModifierType } from "#app/modifier/modifier-type"; +import {BattlerIndex} from "#app/battle"; export class Ability implements Localizable { public id: Abilities; @@ -2484,6 +2485,62 @@ export class PostBiomeChangeTerrainChangeAbAttr extends PostBiomeChangeAbAttr { } } +/** + * Triggers just after a move is used either by the opponent or the player + * @extends AbAttr + */ +export class PostMoveUsedAbAttr extends AbAttr { + applyPostMoveUsed(pokemon: Pokemon, move: PokemonMove, source: Pokemon, targets: BattlerIndex[], args: any[]): boolean | Promise { + return false; + } +} + +/** + * Triggers after a dance move is used either by the opponent or the player + * @extends PostMoveUsedAbAttr + */ +export class PostDancingMoveAbAttr extends PostMoveUsedAbAttr { + /** + * Resolves the Dancer ability by replicating the move used by the source of the dance + * either on the source itself or on the target of the dance + * @param dancer {@linkcode Pokemon} with Dancer ability + * @param move {@linkcode PokemonMove} Dancing move used by the source + * @param source {@linkcode Pokemon} that used the dancing move + * @param targets {@linkcode BattlerIndex}Targets of the dancing move + * @param args N/A + * + * @return true if the Dancer ability was resolved + */ + applyPostMoveUsed(dancer: Pokemon, move: PokemonMove, source: Pokemon, targets: BattlerIndex[], args: any[]): boolean | Promise { + // The move to replicate cannot come from the Dancer + if (source.getBattlerIndex() !== dancer.getBattlerIndex()) { + // If the move is an AttackMove or a StatusMove the Dancer must replicate the move on the source of the Dance + if (move.getMove() instanceof AttackMove || move.getMove() instanceof StatusMove) { + const target = this.getTarget(dancer, source, targets); + dancer.scene.unshiftPhase(new MovePhase(dancer.scene, dancer, target, move, true)); + } else if (move.getMove() instanceof SelfStatusMove) { + // If the move is a SelfStatusMove (ie. Swords Dance) the Dancer should replicate it on itself + dancer.scene.unshiftPhase(new MovePhase(dancer.scene, dancer, [dancer.getBattlerIndex()], move, true)); + } + } + return true; + } + + /** + * Get the correct targets of Dancer ability + * + * @param dancer {@linkcode Pokemon} Pokemon with Dancer ability + * @param source {@linkcode Pokemon} Source of the dancing move + * @param targets {@linkcode BattlerIndex} Targets of the dancing move + */ + getTarget(dancer: Pokemon, source: Pokemon, targets: BattlerIndex[]) : BattlerIndex[] { + if (dancer.isPlayer()) { + return source.isPlayer() ? targets : [source.getBattlerIndex()]; + } + return source.isPlayer() ? [source.getBattlerIndex()] : targets; + } +} + export class StatChangeMultiplierAbAttr extends AbAttr { private multiplier: integer; @@ -2641,7 +2698,7 @@ export class PostFaintContactDamageAbAttr extends PostFaintAbAttr { if (move.getMove().checkFlag(MoveFlags.MAKES_CONTACT, attacker, pokemon)) { const cancelled = new Utils.BooleanHolder(false); pokemon.scene.getField(true).map(p=>applyAbAttrs(FieldPreventExplosiveMovesAbAttr, p, cancelled)); - if (cancelled) { + if (cancelled.value) { return false; } attacker.damageAndUpdate(Math.ceil(attacker.getMaxHp() * (1 / this.damageRatio)), HitResult.OTHER); @@ -3013,6 +3070,11 @@ export function applyPostDefendAbAttrs(attrType: { new(...args: any[]): PostDefe return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostDefend(pokemon, passive, attacker, move, hitResult, args), args); } +export function applyPostMoveUsedAbAttrs(attrType: { new(...args: any[]): PostMoveUsedAbAttr }, + pokemon: Pokemon, move: PokemonMove, source: Pokemon, targets: BattlerIndex[], ...args: any[]): Promise { + return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyPostMoveUsed(pokemon, move, source, targets, args), args); +} + export function applyBattleStatMultiplierAbAttrs(attrType: { new(...args: any[]): BattleStatMultiplierAbAttr }, pokemon: Pokemon, battleStat: BattleStat, statValue: Utils.NumberHolder, ...args: any[]): Promise { return applyAbAttrsInternal(attrType, pokemon, (attr, passive) => attr.applyBattleStat(pokemon, passive, battleStat, statValue, args), args); @@ -3770,7 +3832,7 @@ export function initAbilities() { .attr(PostFaintHPDamageAbAttr) .bypassFaint(), new Ability(Abilities.DANCER, 7) - .unimplemented(), + .attr(PostDancingMoveAbAttr), new Ability(Abilities.BATTERY, 7) .unimplemented(), new Ability(Abilities.FLUFFY, 7) diff --git a/src/data/battler-tags.ts b/src/data/battler-tags.ts index 7b1de97e7f1b..b0fdc0c74674 100644 --- a/src/data/battler-tags.ts +++ b/src/data/battler-tags.ts @@ -255,6 +255,51 @@ export class ConfusedTag extends BattlerTag { } } +/** + * Tag applied to the {@linkcode Move.DESTINY_BOND} user. + * @extends BattlerTag + * @see {@linkcode apply} + */ +export class DestinyBondTag extends BattlerTag { + constructor(sourceMove: Moves, sourceId: integer) { + super(BattlerTagType.DESTINY_BOND, BattlerTagLapseType.PRE_MOVE, 1, sourceMove, sourceId); + } + + /** + * Lapses either before the user's move and does nothing + * or after receiving fatal damage. When the damage is fatal, + * the attacking Pokemon is taken down as well, unless it's a boss. + * + * @param {Pokemon} pokemon Pokemon that is attacking the Destiny Bond user. + * @param {BattlerTagLapseType} lapseType CUSTOM or PRE_MOVE + * @returns false if the tag source fainted or one turn has passed since the application + */ + lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean { + if (lapseType !== BattlerTagLapseType.CUSTOM) { + return super.lapse(pokemon, lapseType); + } + const source = pokemon.scene.getPokemonById(this.sourceId); + if (!source.isFainted()) { + return true; + } + + if (source.getAlly() === pokemon) { + return false; + } + + const targetMessage = getPokemonMessage(pokemon, ""); + + if (pokemon.isBossImmune()) { + pokemon.scene.queueMessage(`${targetMessage} is unaffected\nby the effects of Destiny Bond.`); + return false; + } + + pokemon.scene.queueMessage(`${getPokemonMessage(source, ` took\n${targetMessage} down with it!`)}`); + pokemon.damageAndUpdate(pokemon.hp, HitResult.ONE_HIT_KO, false, false, true); + return false; + } +} + export class InfatuatedTag extends BattlerTag { constructor(sourceMove: integer, sourceId: integer) { super(BattlerTagType.INFATUATED, BattlerTagLapseType.MOVE, 1, sourceMove, sourceId); @@ -1417,6 +1462,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc return new MagnetRisenTag(tagType, sourceMove); case BattlerTagType.MINIMIZED: return new MinimizeTag(); + case BattlerTagType.DESTINY_BOND: + return new DestinyBondTag(sourceMove, sourceId); case BattlerTagType.NONE: default: return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId); diff --git a/src/data/biomes.ts b/src/data/biomes.ts index fc8f20db5867..770101607fae 100644 --- a/src/data/biomes.ts +++ b/src/data/biomes.ts @@ -5,7 +5,7 @@ import beautify from "json-beautify"; import { TrainerType } from "./enums/trainer-type"; import { TimeOfDay } from "./enums/time-of-day"; import { Biome } from "./enums/biome"; -import { SpeciesFormEvolution } from "./pokemon-evolutions"; +import {pokemonEvolutions, SpeciesFormEvolution} from "./pokemon-evolutions"; export function getBiomeName(biome: Biome | -1) { if (biome === -1) { @@ -18,10 +18,8 @@ export function getBiomeName(biome: Biome | -1) { return "Ancient Ruins"; case Biome.ABYSS: return "The Abyss"; - case Biome.SPACE: - return "Stratosphere"; case Biome.END: - return "Final Destination"; + return "???"; default: return Utils.toReadableString(Biome[biome]); } @@ -200,7 +198,7 @@ export const biomePokemonPools: BiomePokemonPools = { [TimeOfDay.NIGHT]: [ { 1: [ Species.SHINX ], 15: [ Species.LUXIO ], 30: [ Species.LUXRAY ] } ], [TimeOfDay.ALL]: [ { 1: [ Species.ABRA ], 16: [ Species.KADABRA ] }, { 1: [ Species.BUNEARY ], 20: [ Species.LOPUNNY ] }, { 1: [ Species.ROOKIDEE ], 18: [ Species.CORVISQUIRE ], 38: [ Species.CORVIKNIGHT ] } ] }, - [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ Species.FARFETCHD, Species.LICKITUNG, Species.CHANSEY, Species.EEVEE, Species.SNORLAX, { 1: [ Species.DUNSPARCE ], 72: [ Species.DUDUNSPARCE ] } ] }, + [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ Species.FARFETCHD, Species.LICKITUNG, Species.CHANSEY, Species.EEVEE, Species.SNORLAX, { 1: [ Species.DUNSPARCE ], 62: [ Species.DUDUNSPARCE ] } ] }, [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ Species.DITTO, Species.LATIAS, Species.LATIOS ] }, [BiomePoolTier.BOSS]: { [TimeOfDay.DAWN]: [ Species.DODRIO, Species.FURRET, Species.GUMSHOOS, Species.GREEDENT ], @@ -268,7 +266,7 @@ export const biomePokemonPools: BiomePokemonPools = { [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], - [TimeOfDay.ALL]: [ Species.PINSIR, { 1: [ Species.CHIKORITA ], 16: [ Species.BAYLEEF ], 32: [ Species.MEGANIUM ] }, { 1: [ Species.GIRAFARIG ], 72: [ Species.FARIGIRAF ] }, Species.ZANGOOSE, Species.KECLEON, Species.TROPIUS ] + [TimeOfDay.ALL]: [ Species.PINSIR, { 1: [ Species.CHIKORITA ], 16: [ Species.BAYLEEF ], 32: [ Species.MEGANIUM ] }, { 1: [ Species.GIRAFARIG ], 62: [ Species.FARIGIRAF ] }, Species.ZANGOOSE, Species.KECLEON, Species.TROPIUS ] }, [BiomePoolTier.SUPER_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ Species.SCYTHER, Species.SHEDINJA, Species.ROTOM ] }, [BiomePoolTier.ULTRA_RARE]: { [TimeOfDay.DAWN]: [], [TimeOfDay.DAY]: [], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [] }, @@ -474,8 +472,8 @@ export const biomePokemonPools: BiomePokemonPools = { [TimeOfDay.ALL]: [ { 1: [ Species.TOTODILE ], 18: [ Species.CROCONAW ], 30: [ Species.FERALIGATR ] }, { 1: [ Species.MUDKIP ], 16: [ Species.MARSHTOMP ], 36: [ Species.SWAMPERT ] } ] }, [BiomePoolTier.SUPER_RARE]: { - [TimeOfDay.DAWN]: [ { 1: [ Species.GALAR_SLOWPOKE ], 40: [ Species.GALAR_SLOWBRO ] }, { 1: [ Species.HISUI_SLIGGOO ], 90: [ Species.HISUI_GOODRA ] } ], - [TimeOfDay.DAY]: [ { 1: [ Species.GALAR_SLOWPOKE ], 40: [ Species.GALAR_SLOWBRO ] }, { 1: [ Species.HISUI_SLIGGOO ], 90: [ Species.HISUI_GOODRA ] } ], + [TimeOfDay.DAWN]: [ { 1: [ Species.GALAR_SLOWPOKE ], 40: [ Species.GALAR_SLOWBRO ] }, { 1: [ Species.HISUI_SLIGGOO ], 80: [ Species.HISUI_GOODRA ] } ], + [TimeOfDay.DAY]: [ { 1: [ Species.GALAR_SLOWPOKE ], 40: [ Species.GALAR_SLOWBRO ] }, { 1: [ Species.HISUI_SLIGGOO ], 80: [ Species.HISUI_GOODRA ] } ], [TimeOfDay.DUSK]: [], [TimeOfDay.NIGHT]: [], [TimeOfDay.ALL]: [ Species.POLITOED, Species.GALAR_STUNFISK ] @@ -2012,7 +2010,7 @@ export const biomeTrainerPools: BiomeTrainerPools = { } }; -{ +export function initBiomes() { const pokemonBiomes = [ [ Species.BULBASAUR, Type.GRASS, Type.POISON, [ [ Biome.GRASS, BiomePoolTier.RARE ] @@ -7677,130 +7675,127 @@ export const biomeTrainerPools: BiomeTrainerPools = { traverseBiome(Biome.TOWN, 0); biomeDepths[Biome.END] = [ Object.values(biomeDepths).map(d => d[0]).reduce((max: integer, value: integer) => Math.max(max, value), 0) + 1, 1 ]; - import("./pokemon-evolutions").then(pe => { - const pokemonEvolutions = pe.pokemonEvolutions; - for (const biome of Utils.getEnumValues(Biome)) { - biomePokemonPools[biome] = {}; - biomeTrainerPools[biome] = {}; + for (const biome of Utils.getEnumValues(Biome)) { + biomePokemonPools[biome] = {}; + biomeTrainerPools[biome] = {}; - for (const tier of Utils.getEnumValues(BiomePoolTier)) { - biomePokemonPools[biome][tier] = {}; - biomeTrainerPools[biome][tier] = []; + for (const tier of Utils.getEnumValues(BiomePoolTier)) { + biomePokemonPools[biome][tier] = {}; + biomeTrainerPools[biome][tier] = []; - for (const tod of Utils.getEnumValues(TimeOfDay)) { - biomePokemonPools[biome][tier][tod] = []; - } + for (const tod of Utils.getEnumValues(TimeOfDay)) { + biomePokemonPools[biome][tier][tod] = []; } } + } - for (const pb of pokemonBiomes) { - const speciesId = pb[0] as Species; - const biomeEntries = pb[3] as (Biome | BiomePoolTier)[][]; + for (const pb of pokemonBiomes) { + const speciesId = pb[0] as Species; + const biomeEntries = pb[3] as (Biome | BiomePoolTier)[][]; - const speciesEvolutions: SpeciesFormEvolution[] = pokemonEvolutions.hasOwnProperty(speciesId) - ? pokemonEvolutions[speciesId] - : []; + const speciesEvolutions: SpeciesFormEvolution[] = pokemonEvolutions.hasOwnProperty(speciesId) + ? pokemonEvolutions[speciesId] + : []; - if (!biomeEntries.filter(b => b[0] !== Biome.END).length && !speciesEvolutions.filter(es => !!((pokemonBiomes.find(p => p[0] === es.speciesId))[3] as any[]).filter(b => b[0] !== Biome.END).length).length) { - uncatchableSpecies.push(speciesId); - } + if (!biomeEntries.filter(b => b[0] !== Biome.END).length && !speciesEvolutions.filter(es => !!((pokemonBiomes.find(p => p[0] === es.speciesId))[3] as any[]).filter(b => b[0] !== Biome.END).length).length) { + uncatchableSpecies.push(speciesId); + } - for (const b of biomeEntries) { - const biome = b[0]; - const tier = b[1]; - const timesOfDay = b.length > 2 - ? Array.isArray(b[2]) - ? b[2] - : [ b[2] ] - : [ TimeOfDay.ALL ]; + for (const b of biomeEntries) { + const biome = b[0]; + const tier = b[1]; + const timesOfDay = b.length > 2 + ? Array.isArray(b[2]) + ? b[2] + : [ b[2] ] + : [ TimeOfDay.ALL ]; - for (const tod of timesOfDay) { - if (!biomePokemonPools.hasOwnProperty(biome) || !biomePokemonPools[biome].hasOwnProperty(tier) || !biomePokemonPools[biome][tier].hasOwnProperty(tod)) { - continue; - } + for (const tod of timesOfDay) { + if (!biomePokemonPools.hasOwnProperty(biome) || !biomePokemonPools[biome].hasOwnProperty(tier) || !biomePokemonPools[biome][tier].hasOwnProperty(tod)) { + continue; + } - const biomeTierPool = biomePokemonPools[biome][tier][tod]; + const biomeTierPool = biomePokemonPools[biome][tier][tod]; - let treeIndex = -1; - let arrayIndex = 0; + let treeIndex = -1; + let arrayIndex = 0; - for (let t = 0; t < biomeTierPool.length; t++) { - const existingSpeciesIds = biomeTierPool[t] as unknown as Species[]; - for (let es = 0; es < existingSpeciesIds.length; es++) { - const existingSpeciesId = existingSpeciesIds[es]; - if (pokemonEvolutions.hasOwnProperty(existingSpeciesId) && (pokemonEvolutions[existingSpeciesId] as SpeciesFormEvolution[]).find(ese => ese.speciesId === speciesId)) { - treeIndex = t; - arrayIndex = es + 1; - break; - } else if (speciesEvolutions && speciesEvolutions.find(se => se.speciesId === existingSpeciesId)) { - treeIndex = t; - arrayIndex = es; - break; - } - } - if (treeIndex > -1) { + for (let t = 0; t < biomeTierPool.length; t++) { + const existingSpeciesIds = biomeTierPool[t] as unknown as Species[]; + for (let es = 0; es < existingSpeciesIds.length; es++) { + const existingSpeciesId = existingSpeciesIds[es]; + if (pokemonEvolutions.hasOwnProperty(existingSpeciesId) && (pokemonEvolutions[existingSpeciesId] as SpeciesFormEvolution[]).find(ese => ese.speciesId === speciesId)) { + treeIndex = t; + arrayIndex = es + 1; + break; + } else if (speciesEvolutions && speciesEvolutions.find(se => se.speciesId === existingSpeciesId)) { + treeIndex = t; + arrayIndex = es; break; } } - if (treeIndex > -1) { - (biomeTierPool[treeIndex] as unknown as Species[]).splice(arrayIndex, 0, speciesId); - } else { - (biomeTierPool as unknown as Species[][]).push([ speciesId ]); + break; } } + + if (treeIndex > -1) { + (biomeTierPool[treeIndex] as unknown as Species[]).splice(arrayIndex, 0, speciesId); + } else { + (biomeTierPool as unknown as Species[][]).push([ speciesId ]); + } } } + } - for (const b of Object.keys(biomePokemonPools)) { - for (const t of Object.keys(biomePokemonPools[b])) { - const tier = parseInt(t) as BiomePoolTier; - for (const tod of Object.keys(biomePokemonPools[b][t])) { - const biomeTierTimePool = biomePokemonPools[b][t][tod]; - for (let e = 0; e < biomeTierTimePool.length; e++) { - const entry = biomeTierTimePool[e]; - if (entry.length === 1) { - biomeTierTimePool[e] = entry[0]; - } else { - const newEntry = { - 1: [ entry[0] ] - }; - for (let s = 1; s < entry.length; s++) { - const speciesId = entry[s]; - const prevolution = entry.map(s => pokemonEvolutions[s]).flat().find(e => e && e.speciesId === speciesId); - const level = prevolution.level - (prevolution.level === 1 ? 1 : 0) + (prevolution.wildDelay * 10) - (tier >= BiomePoolTier.BOSS ? 10 : 0); - if (!newEntry.hasOwnProperty(level)) { - newEntry[level] = [ speciesId ]; - } else { - newEntry[level].push(speciesId); - } + for (const b of Object.keys(biomePokemonPools)) { + for (const t of Object.keys(biomePokemonPools[b])) { + const tier = parseInt(t) as BiomePoolTier; + for (const tod of Object.keys(biomePokemonPools[b][t])) { + const biomeTierTimePool = biomePokemonPools[b][t][tod]; + for (let e = 0; e < biomeTierTimePool.length; e++) { + const entry = biomeTierTimePool[e]; + if (entry.length === 1) { + biomeTierTimePool[e] = entry[0]; + } else { + const newEntry = { + 1: [ entry[0] ] + }; + for (let s = 1; s < entry.length; s++) { + const speciesId = entry[s]; + const prevolution = entry.map(s => pokemonEvolutions[s]).flat().find(e => e && e.speciesId === speciesId); + const level = prevolution.level - (prevolution.level === 1 ? 1 : 0) + (prevolution.wildDelay * 10) - (tier >= BiomePoolTier.BOSS ? 10 : 0); + if (!newEntry.hasOwnProperty(level)) { + newEntry[level] = [ speciesId ]; + } else { + newEntry[level].push(speciesId); } - biomeTierTimePool[e] = newEntry; } + biomeTierTimePool[e] = newEntry; } } } } + } - for (const tb of trainerBiomes) { - const trainerType = tb[0] as TrainerType; - const biomeEntries = tb[1] as BiomePoolTier[][]; - - for (const b of biomeEntries) { - const biome = b[0]; - const tier = b[1]; + for (const tb of trainerBiomes) { + const trainerType = tb[0] as TrainerType; + const biomeEntries = tb[1] as BiomePoolTier[][]; - if (!biomeTrainerPools.hasOwnProperty(biome) || !biomeTrainerPools[biome].hasOwnProperty(tier)) { - continue; - } + for (const b of biomeEntries) { + const biome = b[0]; + const tier = b[1]; - const biomeTierPool = biomeTrainerPools[biome][tier]; - biomeTierPool.push(trainerType); + if (!biomeTrainerPools.hasOwnProperty(biome) || !biomeTrainerPools[biome].hasOwnProperty(tier)) { + continue; } - } + const biomeTierPool = biomeTrainerPools[biome][tier]; + biomeTierPool.push(trainerType); + } //outputPools(); - }); + } + // used in a commented code // eslint-disable-next-line @typescript-eslint/no-unused-vars diff --git a/src/data/egg-moves.ts b/src/data/egg-moves.ts index dfe5dba477e3..bed989849d94 100644 --- a/src/data/egg-moves.ts +++ b/src/data/egg-moves.ts @@ -608,9 +608,11 @@ function parseEggMoves(content: string): void { console.log(output); } -const eggMovesStr = ""; -if (eggMovesStr) { - setTimeout(() => { - parseEggMoves(eggMovesStr); - }, 1000); +export function initEggMoves() { + const eggMovesStr = ""; + if (eggMovesStr) { + setTimeout(() => { + parseEggMoves(eggMovesStr); + }, 1000); + } } diff --git a/src/data/enums/battler-tag-type.ts b/src/data/enums/battler-tag-type.ts index 9411d70a6703..6d36cdafab5d 100644 --- a/src/data/enums/battler-tag-type.ts +++ b/src/data/enums/battler-tag-type.ts @@ -56,5 +56,6 @@ export enum BattlerTagType { CHARGED = "CHARGED", GROUNDED = "GROUNDED", MAGNET_RISEN = "MAGNET_RISEN", - MINIMIZED = "MINIMIZED" + MINIMIZED = "MINIMIZED", + DESTINY_BOND = "DESTINY_BOND" } diff --git a/src/data/move.ts b/src/data/move.ts index 34ae05dbd338..ffd66a05d209 100644 --- a/src/data/move.ts +++ b/src/data/move.ts @@ -4704,6 +4704,31 @@ export class MoneyAttr extends MoveEffectAttr { } } +/** + * Applies {@linkcode BattlerTagType.DESTINY_BOND} to the user. + * + * @extends MoveEffectAttr + */ +export class DestinyBondAttr extends MoveEffectAttr { + constructor() { + super(true, MoveEffectTrigger.PRE_APPLY); + } + + /** + * Applies {@linkcode BattlerTagType.DESTINY_BOND} to the user. + * @param user {@linkcode Pokemon} that is having the tag applied to. + * @param target {@linkcode Pokemon} N/A + * @param move {@linkcode Move} {@linkcode Move.DESTINY_BOND} + * @param {any[]} args N/A + * @returns true + */ + apply(user: Pokemon, target: Pokemon, move: Move, args: any[]): boolean { + user.scene.queueMessage(`${getPokemonMessage(user, " is trying\nto take its foe down with it!")}`); + user.addTag(BattlerTagType.DESTINY_BOND, undefined, move.id, user.id); + return true; + } +} + export class LastResortAttr extends MoveAttr { getCondition(): MoveConditionFunc { return (user: Pokemon, target: Pokemon, move: Move) => { @@ -5405,8 +5430,7 @@ export function initMoves() { .unimplemented(), new SelfStatusMove(Moves.DESTINY_BOND, Type.GHOST, -1, 5, -1, 0, 2) .ignoresProtect() - .condition(failOnBossCondition) - .unimplemented(), + .attr(DestinyBondAttr), new StatusMove(Moves.PERISH_SONG, Type.NORMAL, -1, 5, -1, 0, 2) .attr(FaintCountdownAttr) .ignoresProtect() @@ -6292,7 +6316,7 @@ export function initMoves() { new StatusMove(Moves.QUASH, Type.DARK, 100, 15, -1, 0, 5) .unimplemented(), new AttackMove(Moves.ACROBATICS, Type.FLYING, MoveCategory.PHYSICAL, 55, 100, 15, -1, 0, 5) - .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().reduce((v, m) => v + m.stackCount, 0))), + .attr(MovePowerMultiplierAttr, (user, target, move) => Math.max(1, 2 - 0.2 * user.getHeldItems().filter(i => i.getTransferrable(true)).reduce((v, m) => v + m.stackCount, 0))), new StatusMove(Moves.REFLECT_TYPE, Type.NORMAL, -1, 15, -1, 0, 5) .attr(CopyTypeAttr), new AttackMove(Moves.RETALIATE, Type.NORMAL, MoveCategory.PHYSICAL, 70, 100, 5, -1, 0, 5) diff --git a/src/data/pokemon-evolutions.ts b/src/data/pokemon-evolutions.ts index 3465b5bb1520..15193327547c 100644 --- a/src/data/pokemon-evolutions.ts +++ b/src/data/pokemon-evolutions.ts @@ -1619,7 +1619,7 @@ interface PokemonPrevolutions { export const pokemonPrevolutions: PokemonPrevolutions = {}; -{ +export function initPokemonPrevolutions(): void { const megaFormKeys = [ SpeciesFormKey.MEGA, "", SpeciesFormKey.MEGA_X, "", SpeciesFormKey.MEGA_Y ].map(sfk => sfk as string); const prevolutionKeys = Object.keys(pokemonEvolutions); prevolutionKeys.forEach(pk => { diff --git a/src/data/pokemon-forms.ts b/src/data/pokemon-forms.ts index 7a88025ef1ee..6e5392d9470f 100644 --- a/src/data/pokemon-forms.ts +++ b/src/data/pokemon-forms.ts @@ -729,7 +729,7 @@ export const pokemonFormChanges: PokemonFormChanges = { ] }; -{ +export function initPokemonForms() { const formChangeKeys = Object.keys(pokemonFormChanges); formChangeKeys.forEach(pk => { const formChanges = pokemonFormChanges[pk]; diff --git a/src/data/pokemon-species.ts b/src/data/pokemon-species.ts index f9702b3e64da..5db65adb6d3b 100644 --- a/src/data/pokemon-species.ts +++ b/src/data/pokemon-species.ts @@ -404,12 +404,14 @@ export abstract class PokemonSpeciesForm { console.warn = () => {}; const frameNames = scene.anims.generateFrameNames(spriteKey, { zeroPad: 4, suffix: ".png", start: 1, end: 400 }); console.warn = originalWarn; - scene.anims.create({ - key: this.getSpriteKey(female, formIndex, shiny, variant), - frames: frameNames, - frameRate: 12, - repeat: -1 - }); + if (!(scene.anims.exists(spriteKey))) { + scene.anims.create({ + key: this.getSpriteKey(female, formIndex, shiny, variant), + frames: frameNames, + frameRate: 12, + repeat: -1 + }); + } let spritePath = this.getSpriteAtlasPath(female, formIndex, shiny, variant).replace("variant/", "").replace(/_[1-3]$/, ""); const useExpSprite = scene.experimentalSprites && scene.hasExpSprite(spriteKey); if (useExpSprite) { diff --git a/src/data/trainer-config.ts b/src/data/trainer-config.ts index 518cc31fd5b3..30c9c016ad92 100644 --- a/src/data/trainer-config.ts +++ b/src/data/trainer-config.ts @@ -10,7 +10,6 @@ import PokemonSpecies, {PokemonSpeciesFilter, getPokemonSpecies} from "./pokemon import {Species} from "./enums/species"; import {tmSpecies} from "./tms"; import {Type} from "./type"; -import {initTrainerTypeDialogue} from "./dialogue"; import {PersistentModifier} from "../modifier/modifier"; import {TrainerVariant} from "../field/trainer"; import {PartyMemberStrength} from "./enums/party-member-strength"; @@ -631,13 +630,15 @@ export class TrainerConfig { ? scene.anims.generateFrameNames(partnerTrainerKey, {zeroPad: 4,suffix: ".png",start: 1,end: 128}) : null; console.warn = originalWarn; - scene.anims.create({ - key: trainerKey, - frames: frameNames, - frameRate: 24, - repeat: -1 - }); - if (isDouble) { + if (!(scene.anims.exists(trainerKey))) { + scene.anims.create({ + key: trainerKey, + frames: frameNames, + frameRate: 24, + repeat: -1 + }); + } + if (isDouble && !(scene.anims.exists(partnerTrainerKey))) { scene.anims.create({ key: partnerTrainerKey, frames: partnerFrameNames, @@ -1050,7 +1051,3 @@ export const trainerConfigs: TrainerConfigs = { return [ modifierTypes.TERA_SHARD().generateType(null, [ starter.species.type1 ]).withIdFromFunc(modifierTypes.TERA_SHARD).newModifier(starter) as PersistentModifier ]; }), }; - -(function () { - initTrainerTypeDialogue(); -})(); diff --git a/src/field/anims.ts b/src/field/anims.ts index db6a331cc205..52a15aa4f20f 100644 --- a/src/field/anims.ts +++ b/src/field/anims.ts @@ -24,12 +24,14 @@ export function addPokeballOpenParticles(scene: BattleScene, x: number, y: numbe function doDefaultPbOpenParticles(scene: BattleScene, x: number, y: number, radius: number) { const pbOpenParticlesFrameNames = scene.anims.generateFrameNames("pb_particles", { start: 0, end: 3, suffix: ".png" }); - scene.anims.create({ - key: "pb_open_particle", - frames: pbOpenParticlesFrameNames, - frameRate: 16, - repeat: -1 - }); + if (!(scene.anims.exists("pb_open_particle"))) { + scene.anims.create({ + key: "pb_open_particle", + frames: pbOpenParticlesFrameNames, + frameRate: 16, + repeat: -1 + }); + } const addParticle = (index: integer) => { const particle = scene.add.sprite(x, y, "pb_open_particle"); diff --git a/src/field/arena.ts b/src/field/arena.ts index 3668eaa440eb..b00b8054ee24 100644 --- a/src/field/arena.ts +++ b/src/field/arena.ts @@ -726,12 +726,14 @@ export class ArenaBase extends Phaser.GameObjects.Container { if (this.base.texture.frameTotal > 1) { const baseFrameNames = this.scene.anims.generateFrameNames(baseKey, { zeroPad: 4, suffix: ".png", start: 1, end: this.base.texture.frameTotal - 1 }); - this.scene.anims.create({ - key: baseKey, - frames: baseFrameNames, - frameRate: 12, - repeat: -1 - }); + if (!(this.scene.anims.exists(baseKey))) { + this.scene.anims.create({ + key: baseKey, + frames: baseFrameNames, + frameRate: 12, + repeat: -1 + }); + } this.base.play(baseKey); } else { this.base.stop(); @@ -751,12 +753,14 @@ export class ArenaBase extends Phaser.GameObjects.Container { if (hasProps && prop.texture.frameTotal > 1) { const propFrameNames = this.scene.anims.generateFrameNames(propKey, { zeroPad: 4, suffix: ".png", start: 1, end: prop.texture.frameTotal - 1 }); - this.scene.anims.create({ - key: propKey, - frames: propFrameNames, - frameRate: 12, - repeat: -1 - }); + if (!(this.scene.anims.exists(propKey))) { + this.scene.anims.create({ + key: propKey, + frames: propFrameNames, + frameRate: 12, + repeat: -1 + }); + } prop.play(propKey); } else { prop.stop(); diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index 9f081d731341..c8ba9c863194 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -322,12 +322,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { console.warn = () => {}; const battleFrameNames = this.scene.anims.generateFrameNames(this.getBattleSpriteKey(), { zeroPad: 4, suffix: ".png", start: 1, end: 400 }); console.warn = originalWarn; - this.scene.anims.create({ - key: this.getBattleSpriteKey(), - frames: battleFrameNames, - frameRate: 12, - repeat: -1 - }); + if (!(this.scene.anims.exists(this.getBattleSpriteKey()))) { + this.scene.anims.create({ + key: this.getBattleSpriteKey(), + frames: battleFrameNames, + frameRate: 12, + repeat: -1 + }); + } } this.playAnim(); const updateFusionPaletteAndResolve = () => { @@ -524,13 +526,15 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { shinySparkle.setVisible(false); shinySparkle.setOrigin(0.5, 1); const frameNames = this.scene.anims.generateFrameNames(key, { suffix: ".png", end: 34 }); - this.scene.anims.create({ - key: `sparkle${keySuffix}`, - frames: frameNames, - frameRate: 32, - showOnStart: true, - hideOnComplete: true, - }); + if (!(this.scene.anims.exists(`sparkle${keySuffix}`))) { + this.scene.anims.create({ + key: `sparkle${keySuffix}`, + frames: frameNames, + frameRate: 32, + showOnStart: true, + hideOnComplete: true, + }); + } this.add(shinySparkle); this.shinySparkle = shinySparkle; @@ -1790,6 +1794,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { console.log("damage", damage.value, move.name, power.value, sourceAtk, targetDef); + // In case of fatal damage, this tag would have gotten cleared before we could lapse it. + const destinyTag = this.getTag(BattlerTagType.DESTINY_BOND); + const oneHitKo = result === HitResult.ONE_HIT_KO; if (damage.value) { if (this.getHpRatio() === 1) { @@ -1850,6 +1857,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (damage) { this.scene.clearPhaseQueueSplice(); + + const attacker = this.scene.getPokemonById(source.id); + destinyTag?.lapse(attacker, BattlerTagLapseType.CUSTOM); } } break; diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 281d1d6e6387..77d2a87f4066 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -9,6 +9,15 @@ import { WindowVariant, getWindowVariantSuffix } from "./ui/ui-theme"; import { isMobile } from "./touch-controls"; import * as Utils from "./utils"; import { initI18n } from "./plugins/i18n"; +import {initStatsKeys} from "#app/ui/game-stats-ui-handler"; +import {initPokemonPrevolutions} from "#app/data/pokemon-evolutions"; +import {initBiomes} from "#app/data/biomes"; +import {initEggMoves} from "#app/data/egg-moves"; +import {initPokemonForms} from "#app/data/pokemon-forms"; +import {initSpecies} from "#app/data/pokemon-species"; +import {initMoves} from "#app/data/move"; +import {initAbilities} from "#app/data/ability"; +import {initTrainerTypeDialogue} from "#app/data/dialogue"; export class LoadingScene extends SceneBase { constructor() { @@ -277,6 +286,16 @@ export class LoadingScene extends SceneBase { this.load.plugin('rextexteditplugin', this.getCachedUrl('images/rextexteditplugin.min.js'), true); this.loadLoadingScreen(); + + initStatsKeys(); + initPokemonPrevolutions(); + initBiomes(); + initEggMoves(); + initPokemonForms(); + initTrainerTypeDialogue(); + initSpecies(); + initMoves(); + initAbilities(); } loadLoadingScreen() { diff --git a/src/locales/en/menu.ts b/src/locales/en/menu.ts index d074a2c4f888..274627995461 100644 --- a/src/locales/en/menu.ts +++ b/src/locales/en/menu.ts @@ -29,7 +29,7 @@ export const menu: SimpleTranslationEntries = { "confirmPassword": "Confirm Password", "registrationAgeWarning": "By registering, you confirm you are of 13 years of age or older.", "backToLogin": "Back to Login", - "failedToLoadSaveData": "Failed to load save data. Please reload the page.\nIf this continues, please contact the administrator.", + "failedToLoadSaveData": "Failed to load save data. Please reload the page.\nIf this persists, please check #announcements in Discord.", "sessionSuccess": "Session loaded successfully.", "failedToLoadSession": "Your session data could not be loaded.\nIt may be corrupted.", "boyOrGirl": "Are you a boy or a girl?", diff --git a/src/locales/en/move.ts b/src/locales/en/move.ts index fceab24b6a86..7f99517cfdae 100644 --- a/src/locales/en/move.ts +++ b/src/locales/en/move.ts @@ -2047,7 +2047,7 @@ export const move: MoveTranslationEntries = { }, "acrobatics": { name: "Acrobatics", - effect: "The user nimbly strikes the target. If the user is not holding an item, this attack inflicts massive damage." + effect: "The user nimbly strikes the target. The fewer held items, the higher the damage it inflicts." }, "reflectType": { name: "Reflect Type", diff --git a/src/locales/zh_CN/ability.ts b/src/locales/zh_CN/ability.ts index 248596687641..cdeb91b0e8f5 100644 --- a/src/locales/zh_CN/ability.ts +++ b/src/locales/zh_CN/ability.ts @@ -475,7 +475,7 @@ export const ability: AbilityTranslationEntries = { }, frisk: { name: "察觉", - description: "进入战斗时,神奇宝贝可以检查对方神奇宝贝的能力。", + description: "出场时,可以察觉对手的特性。", }, reckless: { name: "舍身", @@ -1063,7 +1063,7 @@ export const ability: AbilityTranslationEntries = { }, asOneGlastrier: { name: "人马一体", - description: "兼备蕾冠王的紧张感和灵幽\n马的漆黑嘶鸣这两种特性。", + description: "兼备蕾冠王的紧张感和雪暴\n马的苍白嘶鸣这两种特性。", }, asOneSpectrier: { name: "人马一体", @@ -1211,11 +1211,11 @@ export const ability: AbilityTranslationEntries = { }, embodyAspectTeal: { name: "面影辉映", - description: "将回忆映于心中,让水井面\n具发出光辉,提高自己的特\n防。", + description: "将回忆映于心中,让碧草面\n具发出光辉,提高自己的速\n度。", }, embodyAspectWellspring: { name: "面影辉映", - description: "将回忆映于心中,让碧草面\n具发出光辉,提高自己的速\n度。", + description: "将回忆映于心中,让水井面\n具发出光辉,提高自己的特\n防。", }, embodyAspectHearthflame: { name: "面影辉映", diff --git a/src/locales/zh_CN/battle.ts b/src/locales/zh_CN/battle.ts index 47e6b6dbbc39..1e2d1b6fc5ff 100644 --- a/src/locales/zh_CN/battle.ts +++ b/src/locales/zh_CN/battle.ts @@ -4,20 +4,20 @@ export const battle: SimpleTranslationEntries = { "bossAppeared": "{{bossName}} 出现了。", "trainerAppeared": "{{trainerName}}\n想要和你对战!", "trainerAppearedDouble": "{{trainerName}}\n想要和你对战!", - "trainerSendOut": "{{trainerName}} sent out\n{{pokemonName}}!", + "trainerSendOut": "{{trainerName}} 派出了\n{{pokemonName}}!", "singleWildAppeared": "一只野生 {{pokemonName}} 出现了!", "multiWildAppeared": "野生的 {{pokemonName1}}\n和 {{pokemonName2}} 出现了!", "playerComeBack": "回来吧, {{pokemonName}}!", "trainerComeBack": "{{trainerName}} 收回了 {{pokemonName}}!", "playerGo": "去吧! {{pokemonName}}!", - "trainerGo": "{{trainerName}} 派出了 {{pokemonName}}!", + "trainerGo": "{{trainerName}} 派出了\n{{pokemonName}}!", "switchQuestion": "要更换\n{{pokemonName}}吗?", "trainerDefeated": "你击败了\n{{trainerName}}!", "moneyWon": "你赢得了\n₽{{moneyAmount}}!", "pokemonCaught": "{{pokemonName}} 被抓住了!", - "partyFull": "你的队伍已满\n是否释放一个位置给 {{pokemonName}}?", + "partyFull": "你的队伍已满员.是否放生其他宝可梦\n为 {{pokemonName}} 腾出空间?", "pokemon": "宝可梦", - "sendOutPokemon": "上吧! {{pokemonName}}!", + "sendOutPokemon": "上吧!\n{{pokemonName}}!", "hitResultCriticalHit": "击中了要害!", "hitResultSuperEffective": "攻击效果拔群!", "hitResultNotVeryEffective": "攻击收效甚微…", @@ -26,7 +26,7 @@ export const battle: SimpleTranslationEntries = { "attackFailed": "但是失败了!", "attackHitsCount": "击中 {{count}} 次!", "expGain": "{{pokemonName}} 获得了 {{exp}} 经验值!", - "levelUp": "{{pokemonName}} 升级到 Lv. {{level}}!", + "levelUp": "{{pokemonName}} 升级到 Lv.{{level}}!", "learnMove": "{{pokemonName}} 学会了 {{moveName}}!", "learnMovePrompt": "{{pokemonName}} 想要学习 {{moveName}}。", "learnMoveLimitReached": "但是,{{pokemonName}} 已经学会了\n四个技能", @@ -35,7 +35,7 @@ export const battle: SimpleTranslationEntries = { "learnMoveNotLearned": "{{pokemonName}} 没有学会 {{moveName}}。", "learnMoveForgetQuestion": "要忘记哪个技能?", "learnMoveForgetSuccess": "{{pokemonName}} 忘记了\n如何使用 {{moveName}}。", - "countdownPoof": "@d{32}1, @d{15}2, @d{15}和@d{15}… @d{15}… @d{15}… @d{15}@s{pb_bounce_1}噗!", + "countdownPoof": "@d{32}1, @d{15}2 @d{15}… @d{15}… @d{15}@s{pb_bounce_1}空!", "learnMoveAnd": "然后...", "levelCapUp": "等级上限提升到 {{levelCap}}!", "moveNotImplemented": "{{moveName}} 尚未实装,无法选择。", diff --git a/src/locales/zh_CN/menu.ts b/src/locales/zh_CN/menu.ts index bc8735c53495..5b5b7efae2a1 100644 --- a/src/locales/zh_CN/menu.ts +++ b/src/locales/zh_CN/menu.ts @@ -18,13 +18,13 @@ export const menu: SimpleTranslationEntries = { "login": "登录", "register": "注册", "emptyUsername": "用户名不能为空", - "invalidLoginUsername": "提供的用户名无效", + "invalidLoginUsername": "输入的用户名无效", "invalidRegisterUsername": "用户名只能包含字母、数字或下划线", - "invalidLoginPassword": "提供的密码无效", + "invalidLoginPassword": "输入的密码无效", "invalidRegisterPassword": "密码必须至少包含 6 个字符", - "usernameAlreadyUsed": "提供的用户名已被使用", - "accountNonExistent": "提供的用户不存在", - "unmatchingPassword": "提供的密码不匹配", + "usernameAlreadyUsed": "输入的用户名已被使用", + "accountNonExistent": "输入的用户不存在", + "unmatchingPassword": "输入的密码不匹配", "passwordNotMatchingConfirmPassword": "密码必须与确认密码一致", "confirmPassword": "确认密码", "registrationAgeWarning": "注册即表示您确认您已年满 13 岁。", diff --git a/src/locales/zh_CN/modifier-type.ts b/src/locales/zh_CN/modifier-type.ts index f340ecaeb1b9..01459ce14b87 100644 --- a/src/locales/zh_CN/modifier-type.ts +++ b/src/locales/zh_CN/modifier-type.ts @@ -17,7 +17,7 @@ export const modifierType: ModifierTypeTranslationEntries = { } }, "PokemonHpRestoreModifierType": { - description: "为一只宝可梦回复 {{restorePoints}} HP 或 {{restorePercent}}% HP,取最大值", + description: "为一只宝可梦回复 {{restorePoints}} HP 或 {{restorePercent}}% HP,取较大值", extra: { "fully": "为一只宝可梦回复全部HP", "fullyWithStatus": "为一只宝可梦回复全部HP并消除所有负面\n状态", @@ -196,7 +196,7 @@ export const modifierType: ModifierTypeTranslationEntries = { "MULTI_LENS": { name: "多重镜" }, "HEALING_CHARM": { name: "治愈护符", description: "HP回复量增加10% (含复活)" }, - "CANDY_JAR": { name: "糖果罐", description: "神奇糖果提供的升级提升1级" }, + "CANDY_JAR": { name: "糖果罐", description: "神奇糖果提供的升级额外增加1级" }, "BERRY_POUCH": { name: "树果袋", description: "使用树果时有33%的几率不会消耗树果" }, diff --git a/src/locales/zh_CN/move.ts b/src/locales/zh_CN/move.ts index 1268bcc985c2..e8d4b847d99f 100644 --- a/src/locales/zh_CN/move.ts +++ b/src/locales/zh_CN/move.ts @@ -2915,7 +2915,7 @@ export const move: MoveTranslationEntries = { }, "zippyZap": { name: "电电加速", - effect: "The user attacks the target with bursts of electricity at high speed. This move always goes first and raises the user's evasiveness.", + effect: "迅猛无比的电击。必定能够\n先制攻击,并且提高自己的\n闪避率。", }, "splishySplash": { name: "滔滔冲浪", @@ -3809,4 +3809,4 @@ export const move: MoveTranslationEntries = { name: "邪毒锁链", effect: "用由毒形成的锁链缠住对手\n注入毒素加以侵蚀。有时会\n让对手陷入剧毒状态", } -} as const; +} as const; diff --git a/src/locales/zh_CN/tutorial.ts b/src/locales/zh_CN/tutorial.ts index 2ddd6013fffa..0e53c24b4201 100644 --- a/src/locales/zh_CN/tutorial.ts +++ b/src/locales/zh_CN/tutorial.ts @@ -4,7 +4,7 @@ export const tutorial: SimpleTranslationEntries = { "intro": `欢迎来到PokéRogue!这是一款以战斗为核心的融合了roguelite元素的宝可梦同人游戏。 $本游戏未进行商业化,我们没有\nPokémon或Pokémon使用的版 $权资产的所有权。 - $游戏仍在开发中,但已可完整游玩。如需报\n告错误,请使用 Discord 社区。 + $游戏仍在开发中,但已可完整游玩。如需报\n告错误,请通过 Discord 社区。 $如果游戏运行缓慢,请确保在浏览器设置中\n打开了“硬件加速”。`, "accessMenu": "在等待输入时,按 M 或 Escape 键可访\n问菜单。菜单包含设置和各种功能。", diff --git a/src/locales/zh_TW/pokemon.ts b/src/locales/zh_TW/pokemon.ts index 6a0ee4f319e0..cee85d8905bc 100644 --- a/src/locales/zh_TW/pokemon.ts +++ b/src/locales/zh_TW/pokemon.ts @@ -31,7 +31,7 @@ export const pokemon: SimpleTranslationEntries = { "sandslash": "穿山王", "nidoran_f": "尼多蘭", "nidorina": "尼多娜", - "nidoqueen": "尼多後", + "nidoqueen": "尼多后", "nidoran_m": "尼多朗", "nidorino": "尼多力諾", "nidoking": "尼多王", @@ -57,7 +57,7 @@ export const pokemon: SimpleTranslationEntries = { "psyduck": "可達鴨", "golduck": "哥達鴨", "mankey": "猴怪", - "primeape": "火暴猴", + "primeape": "火爆猴", "growlithe": "卡蒂狗", "arcanine": "風速狗", "poliwag": "蚊香蝌蚪", @@ -76,7 +76,7 @@ export const pokemon: SimpleTranslationEntries = { "tentacruel": "毒刺水母", "geodude": "小拳石", "graveler": "隆隆石", - "golem": "隆隆巖", + "golem": "隆隆岩", "ponyta": "小火馬", "rapidash": "烈焰馬", "slowpoke": "呆呆獸", @@ -95,7 +95,7 @@ export const pokemon: SimpleTranslationEntries = { "gastly": "鬼斯", "haunter": "鬼斯通", "gengar": "耿鬼", - "onix": "大巖蛇", + "onix": "大岩蛇", "drowzee": "催眠貘", "hypno": "引夢貘人", "krabby": "大鉗蟹", @@ -156,7 +156,7 @@ export const pokemon: SimpleTranslationEntries = { "bayleef": "月桂葉", "meganium": "大竺葵", "cyndaquil": "火球鼠", - "quilava": "火巖鼠", + "quilava": "火岩鼠", "typhlosion": "火暴獸", "totodile": "小鋸鱷", "croconaw": "藍鱷", @@ -185,7 +185,7 @@ export const pokemon: SimpleTranslationEntries = { "bellossom": "美麗花", "marill": "瑪力露", "azumarill": "瑪力露麗", - "sudowoodo": "樹纔怪", + "sudowoodo": "樹才怪", "politoed": "蚊香蛙皇", "hoppip": "毽子草", "skiploom": "毽子花", @@ -233,7 +233,7 @@ export const pokemon: SimpleTranslationEntries = { "kingdra": "刺龍王", "phanpy": "小小象", "donphan": "頓甲", - "porygon2": "多邊獸2型", + "porygon2": "多邊獸Ⅱ", "stantler": "驚角鹿", "smeargle": "圖圖犬", "tyrogue": "無畏小子", @@ -338,7 +338,7 @@ export const pokemon: SimpleTranslationEntries = { "zangoose": "貓鼬斬", "seviper": "飯匙蛇", "lunatone": "月石", - "solrock": "太陽巖", + "solrock": "太陽岩", "barboach": "泥泥鰍", "whiscash": "鯰魚王", "corphish": "龍蝦小兵", @@ -438,11 +438,11 @@ export const pokemon: SimpleTranslationEntries = { "skuntank": "坦克臭鼬", "bronzor": "銅鏡怪", "bronzong": "青銅鐘", - "bonsly": "盆纔怪", + "bonsly": "盆才怪", "mime_jr": "魔尼尼", "happiny": "小福蛋", "chatot": "聒噪鳥", - "spiritomb": "花巖怪", + "spiritomb": "花岩怪", "gible": "圓陸鯊", "gabite": "尖牙陸鯊", "garchomp": "烈咬陸鯊", @@ -474,12 +474,12 @@ export const pokemon: SimpleTranslationEntries = { "glaceon": "冰伊布", "gliscor": "天蠍王", "mamoswine": "象牙豬", - "porygon_z": "多邊獸乙型", + "porygon_z": "多邊獸Z", "gallade": "艾路雷朵", "probopass": "大朝北鼻", "dusknoir": "黑夜魔靈", "froslass": "雪妖女", - "rotom": "洛託姆", + "rotom": "洛托姆", "uxie": "由克希", "mesprit": "艾姆利多", "azelf": "亞克諾姆", @@ -525,8 +525,8 @@ export const pokemon: SimpleTranslationEntries = { "blitzle": "斑斑馬", "zebstrika": "雷電斑馬", "roggenrola": "石丸子", - "boldore": "地幔巖", - "gigalith": "龐巖怪", + "boldore": "地幔岩", + "gigalith": "龐岩怪", "woobat": "滾滾蝙蝠", "swoobat": "心蝙蝠", "drilbur": "螺釘地鼠", @@ -544,7 +544,7 @@ export const pokemon: SimpleTranslationEntries = { "swadloon": "寶包繭", "leavanny": "保姆蟲", "venipede": "百足蜈蚣", - "whirlipede": "車輪球", + "whirlipede": "車輪毬", "scolipede": "蜈蚣王", "cottonee": "木棉球", "whimsicott": "風妖精", @@ -558,12 +558,12 @@ export const pokemon: SimpleTranslationEntries = { "darmanitan": "達摩狒狒", "maractus": "沙鈴仙人掌", "dwebble": "石居蟹", - "crustle": "巖殿居蟹", + "crustle": "岩殿居蟹", "scraggy": "滑滑小子", "scrafty": "頭巾混混", "sigilyph": "象徵鳥", "yamask": "哭哭面具", - "cofagrigus": "迭失棺", + "cofagrigus": "死神棺", "tirtouga": "原蓋海龜", "carracosta": "肋骨海龜", "archen": "始祖小鳥", @@ -650,7 +650,7 @@ export const pokemon: SimpleTranslationEntries = { "keldeo": "凱路迪歐", "meloetta": "美洛耶塔", "genesect": "蓋諾賽克特", - "chespin": "哈力慄", + "chespin": "哈力栗", "quilladin": "胖胖哈力", "chesnaught": "布里卡隆", "fennekin": "火狐狸", @@ -744,8 +744,8 @@ export const pokemon: SimpleTranslationEntries = { "oricorio": "花舞鳥", "cutiefly": "萌虻", "ribombee": "蝶結萌虻", - "rockruff": "巖狗狗", - "lycanroc": "鬃巖狼人", + "rockruff": "岩狗狗", + "lycanroc": "鬃岩狼人", "wishiwashi": "弱丁魚", "mareanie": "好壞星", "toxapex": "超壞星", @@ -758,7 +758,7 @@ export const pokemon: SimpleTranslationEntries = { "morelull": "睡睡菇", "shiinotic": "燈罩夜菇", "salandit": "夜盜火蜥", - "salazzle": "焰後蜥", + "salazzle": "焰后蜥", "stufful": "童偶熊", "bewear": "穿着熊", "bounsweet": "甜竹竹", @@ -778,7 +778,7 @@ export const pokemon: SimpleTranslationEntries = { "komala": "樹枕尾熊", "turtonator": "爆焰龜獸", "togedemaru": "託戈德瑪爾", - "mimikyu": "謎擬丘", + "mimikyu": "謎擬Q", "bruxish": "磨牙彩皮魚", "drampa": "老翁龍", "dhelmise": "破破舵輪", @@ -827,8 +827,8 @@ export const pokemon: SimpleTranslationEntries = { "blipbug": "索偵蟲", "dottler": "天罩蟲", "orbeetle": "以歐路普", - "nickit": "狡小狐", - "thievul": "猾大狐", + "nickit": "偷兒狐", + "thievul": "狐大盜", "gossifleur": "幼棉棉", "eldegoss": "白蓬蓬", "wooloo": "毛辮羊", @@ -848,7 +848,7 @@ export const pokemon: SimpleTranslationEntries = { "cramorant": "古月鳥", "arrokuda": "刺梭魚", "barraskewda": "戽斗尖梭", - "toxel": "電音嬰", + "toxel": "毒電嬰", "toxtricity": "顫弦蠑螈", "sizzlipede": "燒火蚣", "centiskorch": "焚焰蚣", @@ -867,7 +867,7 @@ export const pokemon: SimpleTranslationEntries = { "cursola": "魔靈珊瑚", "sirfetchd": "蔥遊兵", "mr_rime": "踏冰人偶", - "runerigus": "迭失板", + "runerigus": "死神板", "milcery": "小仙奶", "alcremie": "霜奶仙", "falinks": "列陣兵", @@ -915,7 +915,7 @@ export const pokemon: SimpleTranslationEntries = { "quaxly": "潤水鴨", "quaxwell": "湧躍鴨", "quaquaval": "狂歡浪舞鴨", - "lechonk": "愛喫豚", + "lechonk": "愛吃豚", "oinkologne": "飄香豚", "tarountula": "團珠蛛", "spidops": "操陷蛛", @@ -1039,7 +1039,7 @@ export const pokemon: SimpleTranslationEntries = { "alola_persian": "貓老大", "alola_geodude": "小拳石", "alola_graveler": "隆隆石", - "alola_golem": "隆隆巖", + "alola_golem": "隆隆岩", "alola_grimer": "臭泥", "alola_muk": "臭臭泥", "alola_exeggutor": "椰蛋樹", diff --git a/src/phases.ts b/src/phases.ts index 722eb070fb11..413767a12dbb 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -30,7 +30,7 @@ import { Weather, WeatherType, getRandomWeatherType, getTerrainBlockMessage, get import { TempBattleStat } from "./data/temp-battle-stat"; import { ArenaTagSide, ArenaTrapTag, MistTag, TrickRoomTag } from "./data/arena-tag"; import { ArenaTagType } from "./data/enums/arena-tag-type"; -import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, applyPostBattleInitAbAttrs, PostBattleInitAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr } from "./data/ability"; +import { CheckTrappedAbAttr, IgnoreOpponentStatChangesAbAttr, IgnoreOpponentEvasionAbAttr, PostAttackAbAttr, PostBattleAbAttr, PostDefendAbAttr, PostSummonAbAttr, PostTurnAbAttr, PostWeatherLapseAbAttr, PreSwitchOutAbAttr, PreWeatherDamageAbAttr, ProtectStatAbAttr, RedirectMoveAbAttr, BlockRedirectAbAttr, RunSuccessAbAttr, StatChangeMultiplierAbAttr, SuppressWeatherEffectAbAttr, SyncEncounterNatureAbAttr, applyAbAttrs, applyCheckTrappedAbAttrs, applyPostAttackAbAttrs, applyPostBattleAbAttrs, applyPostDefendAbAttrs, applyPostSummonAbAttrs, applyPostTurnAbAttrs, applyPostWeatherLapseAbAttrs, applyPreStatChangeAbAttrs, applyPreSwitchOutAbAttrs, applyPreWeatherEffectAbAttrs, BattleStatMultiplierAbAttr, applyBattleStatMultiplierAbAttrs, IncrementMovePriorityAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr, applyPostBattleInitAbAttrs, PostBattleInitAbAttr, BlockNonDirectDamageAbAttr as BlockNonDirectDamageAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, PostBiomeChangeAbAttr, applyPostFaintAbAttrs, PostFaintAbAttr, IncreasePpAbAttr, PostStatChangeAbAttr, applyPostStatChangeAbAttrs, AlwaysHitAbAttr, PreventBerryUseAbAttr, StatChangeCopyAbAttr, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr } from "./data/ability"; import { Unlockables, getUnlockableName } from "./system/unlockables"; import { getBiomeKey } from "./field/arena"; import { BattleType, BattlerIndex, TurnCommand } from "./battle"; @@ -61,6 +61,7 @@ import { Abilities } from "./data/enums/abilities"; import * as Overrides from "./overrides"; import { TextStyle, addTextObject } from "./ui/text"; import { Type } from "./data/type"; +import { MoveUsedEvent } from "./battle-scene-events"; export class LoginPhase extends Phase { @@ -2155,13 +2156,28 @@ export class TurnStartPhase extends FieldPhase { this.scene.unshiftPhase(new AttemptCapturePhase(this.scene, turnCommand.targets[0] % 2, turnCommand.cursor)); break; case Command.POKEMON: + this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor, true, turnCommand.args[0] as boolean, pokemon.isPlayer())); + break; case Command.RUN: - const isSwitch = turnCommand.command === Command.POKEMON; - if (isSwitch) { - this.scene.unshiftPhase(new SwitchSummonPhase(this.scene, pokemon.getFieldIndex(), turnCommand.cursor, true, turnCommand.args[0] as boolean, pokemon.isPlayer())); - } else { - this.scene.unshiftPhase(new AttemptRunPhase(this.scene, pokemon.getFieldIndex())); + let runningPokemon = pokemon; + if (this.scene.currentBattle.double) { + const playerActivePokemon = field.filter(pokemon => { + if (!!pokemon) { + return pokemon.isPlayer() && pokemon.isActive(); + } else { + return; + } + }); + // if only one pokemon is alive, use that one + if (playerActivePokemon.length > 1) { + // find which active pokemon has faster speed + const fasterPokemon = playerActivePokemon[0].getStat(Stat.SPD) > playerActivePokemon[1].getStat(Stat.SPD) ? playerActivePokemon[0] : playerActivePokemon[1]; + // check if either active pokemon has the ability "Run Away" + const hasRunAway = playerActivePokemon.find(p => p.hasAbility(Abilities.RUN_AWAY)); + runningPokemon = hasRunAway !== undefined ? hasRunAway : fasterPokemon; + } } + this.scene.unshiftPhase(new AttemptRunPhase(this.scene, runningPokemon.getFieldIndex())); break; } } @@ -2388,7 +2404,7 @@ export class MovePhase extends BattlePhase { console.log(Moves[this.move.moveId]); if (!this.canMove()) { - if (this.move.moveId && this.pokemon.summonData.disabledMove === this.move.moveId) { + if (this.move.moveId && this.pokemon.summonData?.disabledMove === this.move.moveId) { this.scene.queueMessage(`${this.move.getName()} is disabled!`); } return this.end(); @@ -2466,8 +2482,9 @@ export class MovePhase extends BattlePhase { const moveQueue = this.pokemon.getMoveQueue(); if (this.cancelled || this.failed) { if (this.failed) { - this.move.usePp(ppUsed); - } // Only use PP if the move failed + this.move.usePp(ppUsed); // Only use PP if the move failed + this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed)); + } // Record a failed move so Abilities like Truant don't trigger next turn and soft-lock this.pokemon.pushMoveHistory({ move: Moves.NONE, result: MoveResult.FAIL }); @@ -2497,8 +2514,9 @@ export class MovePhase extends BattlePhase { return this.end(); } - if (!moveQueue.length || !moveQueue.shift().ignorePP) {// using .shift here clears out two turn moves once they've been used + if (!moveQueue.length || !moveQueue.shift().ignorePP) { // using .shift here clears out two turn moves once they've been used this.move.usePp(ppUsed); + this.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), ppUsed)); } if (!allMoves[this.move.moveId].getAttrs(CopyMoveAttr).length) { @@ -2525,7 +2543,16 @@ export class MovePhase extends BattlePhase { this.showFailedText(failedText); } } - + // Checks if Dancer ability is triggered + if (this.move.getMove().hasFlag(MoveFlags.DANCE_MOVE) && !this.followUp) { + // Pokemon with Dancer can be on either side of the battle so we check in both cases + this.scene.getPlayerField().forEach(pokemon => { + applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets); + }); + this.scene.getEnemyParty().forEach(pokemon => { + applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets); + }); + } this.end(); }; @@ -4395,7 +4422,7 @@ export class PokemonHealPhase extends CommonAnimPhase { } const healAmount = new Utils.NumberHolder(Math.floor(this.hpHealed * hpRestoreMultiplier.value)); if (healAmount.value < 0) { - pokemon.damageAndUpdate(healAmount.value * -1, HitResult.HEAL); + pokemon.damageAndUpdate(healAmount.value * -1, HitResult.HEAL as DamageResult); healAmount.value = 0; } // Prevent healing to full if specified (in case of healing tokens so Sturdy doesn't cause a softlock) diff --git a/src/test/achievement.test.ts b/src/test/achievement.test.ts new file mode 100644 index 000000000000..a27c2d901545 --- /dev/null +++ b/src/test/achievement.test.ts @@ -0,0 +1,9 @@ +import {describe, expect, it} from "vitest"; +import {MoneyAchv} from "#app/system/achv"; + +describe("check some Achievement related stuff", () => { + it ("should check Achievement creation", () => { + const ach = new MoneyAchv("Achievement", 1000, null, 100); + expect(ach.name).toBe("Achievement"); + }); +}); diff --git a/src/test/debugImports.test.ts b/src/test/debugImports.test.ts new file mode 100644 index 000000000000..c164813a3cd6 --- /dev/null +++ b/src/test/debugImports.test.ts @@ -0,0 +1,35 @@ +import { describe, expect, it} from "vitest"; +import {initStatsKeys} from "#app/ui/game-stats-ui-handler"; + +async function importModule() { + try { + initStatsKeys(); + const { PokemonMove } = await import("#app/field/pokemon"); + const { Species } = await import("#app/data/enums/species"); + return { + PokemonMove, + Species, + }; + // Dynamically import the module + } catch (error) { + // Log the error stack trace + console.error("Error during import:", error.stack); + // Rethrow the error to ensure the test fails + throw error; + } +} + +describe("tests to debug the import, with trace", () => { + it("import PokemonMove module", async () => { + const module = await importModule(); + // Example assertion + expect(module.PokemonMove).toBeDefined(); + }); + + it("import Species module", async () => { + const module = await importModule(); + // Example assertion + expect(module.Species).toBeDefined(); + }); +}); + diff --git a/src/test/pokemon.test.ts b/src/test/pokemon.test.ts new file mode 100644 index 000000000000..d1f7da45256d --- /dev/null +++ b/src/test/pokemon.test.ts @@ -0,0 +1,57 @@ +import {describe, expect, it} from "vitest"; +import {getPokemonSpecies} from "#app/data/pokemon-species"; +import {PokemonMove} from "#app/field/pokemon"; +import {Species} from "#app/data/enums/species"; +import {Moves} from "#app/data/enums/moves"; +import PokemonData from "#app/system/pokemon-data"; + +describe("some tests related to PokemonData and Species", () => { + it("should create a species", () => { + const species = getPokemonSpecies(Species.MEW); + expect(species).not.toBeNull(); + }); + + it("should create a pokemon", () => { + const pokemon = new PokemonData({ + species: Species.MEW, + level: 1, + }); + expect(pokemon).not.toBeNull(); + expect(pokemon.level).toEqual(1); + expect(pokemon.species).toEqual(Species.MEW); + }); + + it("should generate a moveset", () => { + const pokemon = new PokemonData({ + species: Species.MEW, + level: 1, + }); + expect(pokemon.moveset[0].moveId).toBe(Moves.TACKLE); + expect(pokemon.moveset[1].moveId).toBe(Moves.GROWL); + }); + + it("should create an ennemypokemon", () => { + const ennemyPokemon = new PokemonData({ + species: Species.MEWTWO, + level: 100, + }); + expect(ennemyPokemon).not.toBeNull(); + expect(ennemyPokemon.level).toEqual(100); + expect(ennemyPokemon.species).toEqual(Species.MEWTWO); + }); + + it("should create an ennemypokemon with specified moveset", () => { + const ennemyPokemon = new PokemonData({ + species: Species.MEWTWO, + level: 100, + moveset: [ + new PokemonMove(Moves.ACID), + new PokemonMove(Moves.ACROBATICS), + new PokemonMove(Moves.FOCUS_ENERGY), + ] + }); + expect(ennemyPokemon.moveset[0].moveId).toBe(Moves.ACID); + expect(ennemyPokemon.moveset[1].moveId).toBe(Moves.ACROBATICS); + expect(ennemyPokemon.moveset[2].moveId).toBe(Moves.FOCUS_ENERGY); + }); +}); diff --git a/src/test/vitest.setup.ts b/src/test/vitest.setup.ts index d0141ca9fc3a..f17a16caf149 100644 --- a/src/test/vitest.setup.ts +++ b/src/test/vitest.setup.ts @@ -1,2 +1,19 @@ import "vitest-canvas-mock"; import "#app/test/phaser.setup"; +import {initStatsKeys} from "#app/ui/game-stats-ui-handler"; +import {initPokemonPrevolutions} from "#app/data/pokemon-evolutions"; +import {initBiomes} from "#app/data/biomes"; +import {initEggMoves} from "#app/data/egg-moves"; +import {initPokemonForms} from "#app/data/pokemon-forms"; +import {initSpecies} from "#app/data/pokemon-species"; +import {initMoves} from "#app/data/move"; +import {initAbilities} from "#app/data/ability"; + +initStatsKeys(); +initPokemonPrevolutions(); +initBiomes(); +initEggMoves(); +initPokemonForms(); +initSpecies(); +initMoves(); +initAbilities(); diff --git a/src/ui/battle-info.ts b/src/ui/battle-info.ts index 1aebce7f457a..01f183c49c80 100644 --- a/src/ui/battle-info.ts +++ b/src/ui/battle-info.ts @@ -35,6 +35,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { private nameText: Phaser.GameObjects.Text; private genderText: Phaser.GameObjects.Text; private ownedIcon: Phaser.GameObjects.Sprite; + private championRibbon: Phaser.GameObjects.Sprite; private teraIcon: Phaser.GameObjects.Sprite; private shinyIcon: Phaser.GameObjects.Sprite; private fusionShinyIcon: Phaser.GameObjects.Sprite; @@ -78,27 +79,39 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.setVisible(false); this.box = this.scene.add.sprite(0, 0, this.getTextureName()); + this.box.setName("box"); this.box.setOrigin(1, 0.5); this.add(this.box); this.nameText = addTextObject(this.scene, player ? -115 : -124, player ? -15.2 : -11.2, "", TextStyle.BATTLE_INFO); + this.nameText.setName("text_name"); this.nameText.setOrigin(0, 0); this.add(this.nameText); this.genderText = addTextObject(this.scene, 0, 0, "", TextStyle.BATTLE_INFO); + this.genderText.setName("text_gender"); this.genderText.setOrigin(0, 0); this.genderText.setPositionRelative(this.nameText, 0, 2); this.add(this.genderText); if (!this.player) { this.ownedIcon = this.scene.add.sprite(0, 0, "icon_owned"); + this.ownedIcon.setName("icon_owned"); this.ownedIcon.setVisible(false); this.ownedIcon.setOrigin(0, 0); this.ownedIcon.setPositionRelative(this.nameText, 0, 11.75); this.add(this.ownedIcon); + + this.championRibbon = this.scene.add.sprite(0, 0, "champion_ribbon"); + this.championRibbon.setName("icon_champion_ribbon"); + this.championRibbon.setVisible(false); + this.championRibbon.setOrigin(0, 0); + this.championRibbon.setPositionRelative(this.nameText, 11.75, 11.75); + this.add(this.championRibbon); } this.teraIcon = this.scene.add.sprite(0, 0, "icon_tera"); + this.teraIcon.setName("icon_tera"); this.teraIcon.setVisible(false); this.teraIcon.setOrigin(0, 0); this.teraIcon.setScale(0.5); @@ -107,6 +120,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.add(this.teraIcon); this.shinyIcon = this.scene.add.sprite(0, 0, "shiny_star"); + this.shinyIcon.setName("icon_shiny"); this.shinyIcon.setVisible(false); this.shinyIcon.setOrigin(0, 0); this.shinyIcon.setScale(0.5); @@ -115,6 +129,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.add(this.shinyIcon); this.fusionShinyIcon = this.scene.add.sprite(0, 0, "shiny_star_2"); + this.fusionShinyIcon.setName("icon_fusion_shiny"); this.fusionShinyIcon.setVisible(false); this.fusionShinyIcon.setOrigin(0, 0); this.fusionShinyIcon.setScale(0.5); @@ -122,6 +137,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.add(this.fusionShinyIcon); this.splicedIcon = this.scene.add.sprite(0, 0, "icon_spliced"); + this.splicedIcon.setName("icon_spliced"); this.splicedIcon.setVisible(false); this.splicedIcon.setOrigin(0, 0); this.splicedIcon.setScale(0.5); @@ -130,31 +146,37 @@ export default class BattleInfo extends Phaser.GameObjects.Container { this.add(this.splicedIcon); this.statusIndicator = this.scene.add.sprite(0, 0, "statuses"); + this.statusIndicator.setName("icon_status"); this.statusIndicator.setVisible(false); this.statusIndicator.setOrigin(0, 0); this.statusIndicator.setPositionRelative(this.nameText, 0, 11.5); this.add(this.statusIndicator); this.levelContainer = this.scene.add.container(player ? -41 : -50, player ? -10 : -5); + this.levelContainer.setName("container_level"); this.add(this.levelContainer); const levelOverlay = this.scene.add.image(0, 0, "overlay_lv"); this.levelContainer.add(levelOverlay); this.hpBar = this.scene.add.image(player ? -61 : -71, player ? -1 : 4.5, "overlay_hp"); + this.hpBar.setName("hp_bar"); this.hpBar.setOrigin(0); this.add(this.hpBar); this.hpBarSegmentDividers = []; this.levelNumbersContainer = this.scene.add.container(9.5, (this.scene as BattleScene).uiTheme ? 0 : -0.5); + this.levelNumbersContainer.setName("container_level"); this.levelContainer.add(this.levelNumbersContainer); if (this.player) { this.hpNumbersContainer = this.scene.add.container(-15, 10); + this.hpNumbersContainer.setName("container_hp"); this.add(this.hpNumbersContainer); const expBar = this.scene.add.image(-98, 18, "overlay_exp"); + expBar.setName("overlay_exp"); expBar.setOrigin(0); this.add(expBar); @@ -173,10 +195,12 @@ export default class BattleInfo extends Phaser.GameObjects.Container { } this.statsContainer = this.scene.add.container(0, 0); + this.statsContainer.setName("container_stats"); this.statsContainer.setAlpha(0); this.add(this.statsContainer); this.statsBox = this.scene.add.sprite(0, 0, `${this.getTextureName()}_stats`); + this.statsBox.setName("box_stats"); this.statsBox.setOrigin(1, 0.5); this.statsContainer.add(this.statsBox); @@ -190,25 +214,30 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const statX = i > 1 ? this.statNumbers[i - 2].x + this.statNumbers[i - 2].width + 4 : -this.statsBox.width + 8; const statY = -this.statsBox.height / 2 + 4 + (i < battleStatOrder.length - 1 ? (i % 2 ? 10 : 0) : 5); const statLabel = this.scene.add.sprite(statX, statY, "pbinfo_stat", BattleStat[s]); + statLabel.setName("icon_stat_label_" + i.toString()); statLabel.setOrigin(0, 0); statLabels.push(statLabel); this.statValuesContainer.add(statLabel); const statNumber = this.scene.add.sprite(statX + statLabel.width, statY, "pbinfo_stat_numbers", "3"); + statNumber.setName("icon_stat_number_" + i.toString()); statNumber.setOrigin(0, 0); this.statNumbers.push(statNumber); this.statValuesContainer.add(statNumber); }); this.type1Icon = this.scene.add.sprite(player ? -139 : -15, player ? -17 : -15.5, `pbinfo_${player ? "player" : "enemy"}_type1`); + this.type1Icon.setName("icon_type_1"); this.type1Icon.setOrigin(0, 0); this.add(this.type1Icon); this.type2Icon = this.scene.add.sprite(player ? -139 : -15, player ? -1 : -2.5, `pbinfo_${player ? "player" : "enemy"}_type2`); + this.type2Icon.setName("icon_type_2"); this.type2Icon.setOrigin(0, 0); this.add(this.type2Icon); this.type3Icon = this.scene.add.sprite(player ? -154 : 0, player ? -17 : -15.5, `pbinfo_${player ? "player" : "enemy"}_type`); + this.type3Icon.setName("icon_type_3"); this.type3Icon.setOrigin(0, 0); this.add(this.type3Icon); } @@ -266,6 +295,11 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const dexEntry = pokemon.scene.gameData.dexData[pokemon.species.speciesId]; this.ownedIcon.setVisible(!!dexEntry.caughtAttr); const opponentPokemonDexAttr = pokemon.getDexAttr(); + if (pokemon.scene.gameMode.isClassic) { + if (pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId()].classicWinCount > 0 && pokemon.scene.gameData.starterData[pokemon.species.getRootSpeciesId(true)].classicWinCount > 0) { + this.championRibbon.setVisible(true); + } + } // Check if Player owns all genders and forms of the Pokemon const missingDexAttrs = ((dexEntry.caughtAttr & opponentPokemonDexAttr) < opponentPokemonDexAttr); @@ -378,7 +412,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { if (boss !== this.boss) { this.boss = boss; - [ this.nameText, this.genderText, this.teraIcon, this.splicedIcon, this.shinyIcon, this.ownedIcon, this.statusIndicator, this.levelContainer, this.statValuesContainer ].map(e => e.x += 48 * (boss ? -1 : 1)); + [ this.nameText, this.genderText, this.teraIcon, this.splicedIcon, this.shinyIcon, this.ownedIcon, this.championRibbon, this.statusIndicator, this.levelContainer, this.statValuesContainer ].map(e => e.x += 48 * (boss ? -1 : 1)); this.hpBar.x += 38 * (boss ? -1 : 1); this.hpBar.y += 2 * (this.boss ? -1 : 1); this.hpBar.setTexture(`overlay_hp${boss ? "_boss" : ""}`); @@ -402,6 +436,7 @@ export default class BattleInfo extends Phaser.GameObjects.Container { const dividerX = (Math.round((maxHp / this.bossSegments) * s) / maxHp) * this.hpBar.width; const divider = this.scene.add.rectangle(0, 0, 1, this.hpBar.height - (uiTheme ? 0 : 1), pokemon.bossSegmentIndex >= s ? 0xFFFFFF : 0x404040); divider.setOrigin(0.5, 0); + divider.setName("hpBar_divider_" + s.toString()); this.add(divider); this.moveBelow(divider as Phaser.GameObjects.GameObject, this.statsContainer); diff --git a/src/ui/egg-gacha-ui-handler.ts b/src/ui/egg-gacha-ui-handler.ts index 4cfd6813b419..9536df7dbed4 100644 --- a/src/ui/egg-gacha-ui-handler.ts +++ b/src/ui/egg-gacha-ui-handler.ts @@ -61,16 +61,20 @@ export default class EggGachaUiHandler extends MessageUiHandler { this.eggGachaContainer.add(bg); const hatchFrameNames = this.scene.anims.generateFrameNames("gacha_hatch", { suffix: ".png", start: 1, end: 4 }); - this.scene.anims.create({ - key: "open", - frames: hatchFrameNames, - frameRate: 12 - }); - this.scene.anims.create({ - key: "close", - frames: hatchFrameNames.reverse(), - frameRate: 12 - }); + if (!(this.scene.anims.exists("open"))) { + this.scene.anims.create({ + key: "open", + frames: hatchFrameNames, + frameRate: 12 + }); + } + if (!(this.scene.anims.exists("close"))) { + this.scene.anims.create({ + key: "close", + frames: hatchFrameNames.reverse(), + frameRate: 12 + }); + } Utils.getEnumValues(GachaType).forEach((gachaType, g) => { const gachaTypeKey = GachaType[gachaType].toString().toLowerCase(); diff --git a/src/ui/egg-hatch-scene-handler.ts b/src/ui/egg-hatch-scene-handler.ts index 2277c3a4b64c..5a4c984234ca 100644 --- a/src/ui/egg-hatch-scene-handler.ts +++ b/src/ui/egg-hatch-scene-handler.ts @@ -16,11 +16,13 @@ export default class EggHatchSceneHandler extends UiHandler { this.scene.fieldUI.add(this.eggHatchContainer); const eggLightraysAnimFrames = this.scene.anims.generateFrameNames("egg_lightrays", { start: 0, end: 3 }); - this.scene.anims.create({ - key: "egg_lightrays", - frames: eggLightraysAnimFrames, - frameRate: 32 - }); + if (!(this.scene.anims.exists("egg_lightrays"))) { + this.scene.anims.create({ + key: "egg_lightrays", + frames: eggLightraysAnimFrames, + frameRate: 32 + }); + } } show(_args: any[]): boolean { diff --git a/src/ui/egg-list-ui-handler.ts b/src/ui/egg-list-ui-handler.ts index 522e91d84e64..92c7faf54776 100644 --- a/src/ui/egg-list-ui-handler.ts +++ b/src/ui/egg-list-ui-handler.ts @@ -49,7 +49,7 @@ export default class EggListUiHandler extends MessageUiHandler { this.iconAnimHandler = new PokemonIconAnimHandler(); this.iconAnimHandler.setup(this.scene); - this.eggNameText = addTextObject(this.scene, 8, 66, "", TextStyle.SUMMARY); + this.eggNameText = addTextObject(this.scene, 8, 68, "", TextStyle.SUMMARY); this.eggNameText.setOrigin(0, 0); this.eggListContainer.add(this.eggNameText); diff --git a/src/ui/game-stats-ui-handler.ts b/src/ui/game-stats-ui-handler.ts index 2293cd7cf467..b48230107341 100644 --- a/src/ui/game-stats-ui-handler.ts +++ b/src/ui/game-stats-ui-handler.ts @@ -230,7 +230,7 @@ export default class GameStatsUiHandler extends UiHandler { } } -(function () { +export function initStatsKeys() { const statKeys = Object.keys(displayStats); for (const key of statKeys) { @@ -256,4 +256,4 @@ export default class GameStatsUiHandler extends UiHandler { (displayStats[key] as DisplayStat).label = Utils.toReadableString(`${splittableKey[0].toUpperCase()}${splittableKey.slice(1)}`); } } -})(); +} diff --git a/src/ui/unavailable-modal-ui-handler.ts b/src/ui/unavailable-modal-ui-handler.ts index d1d28d6b0367..96bf561003b8 100644 --- a/src/ui/unavailable-modal-ui-handler.ts +++ b/src/ui/unavailable-modal-ui-handler.ts @@ -6,7 +6,7 @@ import { updateUserInfo } from "#app/account"; export default class UnavailableModalUiHandler extends ModalUiHandler { private reconnectTimer: number; - private reconnectInterval: number; + private reconnectDuration: number; private reconnectCallback: () => void; private readonly minTime = 1000 * 5; @@ -16,7 +16,7 @@ export default class UnavailableModalUiHandler extends ModalUiHandler { constructor(scene: BattleScene, mode?: Mode) { super(scene, mode); - this.reconnectInterval = this.minTime; + this.reconnectDuration = this.minTime; } getModalTitle(): string { @@ -51,19 +51,17 @@ export default class UnavailableModalUiHandler extends ModalUiHandler { tryReconnect(): void { updateUserInfo().then(response => { if (response[0] || [200, 400].includes(response[1])) { - clearInterval(this.reconnectTimer); this.reconnectTimer = null; - this.reconnectInterval = this.minTime; + this.reconnectDuration = this.minTime; this.scene.playSound("pb_bounce_1"); this.reconnectCallback(); } else { - clearInterval(this.reconnectTimer); - this.reconnectInterval = Math.min(this.reconnectInterval * 2, this.maxTime); // Set a max delay so it isn't infinite + this.reconnectDuration = Math.min(this.reconnectDuration * 2, this.maxTime); // Set a max delay so it isn't infinite this.reconnectTimer = setTimeout( () => this.tryReconnect(), // Adds a random factor to avoid pendulum effect during long total breakdown - this.reconnectInterval + (Math.random() * this.randVarianceTime)); + this.reconnectDuration + (Math.random() * this.randVarianceTime)); } }); } @@ -75,8 +73,8 @@ export default class UnavailableModalUiHandler extends ModalUiHandler { }; this.reconnectCallback = args[0]; - - this.reconnectTimer = setInterval(() => this.tryReconnect(), this.reconnectInterval); + this.reconnectDuration = this.minTime; + this.reconnectTimer = setTimeout(() => this.tryReconnect(), this.reconnectDuration); return super.show([ config ]); } diff --git a/vitest.config.js b/vitest.config.js index 5a7babd42323..c73476431dd4 100644 --- a/vitest.config.js +++ b/vitest.config.js @@ -14,6 +14,7 @@ export default defineConfig(({ mode }) => { } }, threads: false, + trace: true, environmentOptions: { jsdom: { resources: 'usable',