Merge branch 'beta' into fix-turncount-reset

This commit is contained in:
NightKev 2024-08-18 23:03:21 -07:00
commit 2608a79ffc
282 changed files with 38534 additions and 9896 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 800 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 743 B

After

Width:  |  Height:  |  Size: 793 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.8 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -2513,7 +2513,6 @@
], ],
"meta": { "meta": {
"app": "https://www.codeandweb.com/texturepacker", "app": "https://www.codeandweb.com/texturepacker",
"version": "3.0", "version": "3.0"
"smartupdate": "$TexturePacker:SmartUpdate:edb2df3a947401efb05329a2c96d5d73:f256d83ef4df17c17958acc6e0432ab0:bad05b37c157676604256a043511a6a2$"
} }
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 326 B

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 976 B

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.0 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -3815,6 +3815,11 @@
1, 1,
1 1
], ],
"178": [
0,
2,
2
],
"185": [ "185": [
0, 0,
1, 1,
@ -7833,6 +7838,11 @@
1, 1,
1 1
], ],
"178": [
0,
2,
2
],
"185": [ "185": [
0, 0,
1, 1,

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

@ -1,21 +1,5 @@
{ {
"1": { "1": {
"b0a080": "e552ec",
"f8f8e8": "ffe2ed",
"9b8259": "b021c5",
"e5e4c2": "ffb9f9",
"000000": "000000",
"bc9b4e": "900090",
"f8f8d0": "ff8ae9",
"e8e088": "ff49e7",
"d0b868": "d10cc7",
"7d673b": "510059",
"282828": "282828",
"f84040": "f84040",
"f88888": "1ae2e6",
"c81010": "00c2d2"
},
"2": {
"b0a080": "d96b23", "b0a080": "d96b23",
"f8f8e8": "ffe1b8", "f8f8e8": "ffe1b8",
"9b8259": "b43c06", "9b8259": "b43c06",
@ -30,5 +14,21 @@
"f84040": "f84040", "f84040": "f84040",
"f88888": "f88888", "f88888": "f88888",
"c81010": "c81010" "c81010": "c81010"
},
"2": {
"b0a080": "e552ec",
"f8f8e8": "ffe2ed",
"9b8259": "b021c5",
"e5e4c2": "ffb9f9",
"000000": "000000",
"bc9b4e": "900090",
"f8f8d0": "ff8ae9",
"e8e088": "ff49e7",
"d0b868": "d10cc7",
"7d673b": "510059",
"282828": "282828",
"f84040": "f84040",
"f88888": "1ae2e6",
"c81010": "00c2d2"
} }
} }

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

View File

@ -1,11 +1,10 @@
import Phaser from "phaser"; import Phaser from "phaser";
import UI from "./ui/ui"; import UI from "./ui/ui";
import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase, SummonPhase, ToggleDoublePositionPhase } from "./phases";
import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon"; import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon";
import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species"; import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species";
import { Constructor } from "#app/utils"; import { Constructor } from "#app/utils";
import * as Utils from "./utils"; 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 { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, TurnHeldItemTransferModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems } from "./modifier/modifier";
import { PokeballType } from "./data/pokeball"; import { PokeballType } from "./data/pokeball";
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims";
import { Phase } from "./phase"; import { Phase } from "./phase";
@ -37,8 +36,8 @@ import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin";
import { addUiThemeOverrides } from "./ui/ui-theme"; import { addUiThemeOverrides } from "./ui/ui-theme";
import PokemonData from "./system/pokemon-data"; import PokemonData from "./system/pokemon-data";
import { Nature } from "./data/nature"; import { Nature } from "./data/nature";
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger, pokemonFormChanges, FormChangeItem } from "./data/pokemon-forms"; import { SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger, pokemonFormChanges, FormChangeItem, SpeciesFormChange } from "./data/pokemon-forms";
import { FormChangePhase, QuietFormChangePhase } from "./form-change-phase"; import { FormChangePhase } from "./phases/form-change-phase";
import { getTypeRgb } from "./data/type"; import { getTypeRgb } from "./data/type";
import PokemonSpriteSparkleHandler from "./field/pokemon-sprite-sparkle-handler"; import PokemonSpriteSparkleHandler from "./field/pokemon-sprite-sparkle-handler";
import CharSprite from "./ui/char-sprite"; import CharSprite from "./ui/char-sprite";
@ -69,6 +68,21 @@ import i18next from "i18next";
import {TrainerType} from "#enums/trainer-type"; import {TrainerType} from "#enums/trainer-type";
import { battleSpecDialogue } from "./data/dialogue"; import { battleSpecDialogue } from "./data/dialogue";
import { LoadingScene } from "./loading-scene"; import { LoadingScene } from "./loading-scene";
import { LevelCapPhase } from "./phases/level-cap-phase";
import { LoginPhase } from "./phases/login-phase";
import { MessagePhase } from "./phases/message-phase";
import { MovePhase } from "./phases/move-phase";
import { NewBiomeEncounterPhase } from "./phases/new-biome-encounter-phase";
import { NextEncounterPhase } from "./phases/next-encounter-phase";
import { QuietFormChangePhase } from "./phases/quiet-form-change-phase";
import { ReturnPhase } from "./phases/return-phase";
import { SelectBiomePhase } from "./phases/select-biome-phase";
import { ShowTrainerPhase } from "./phases/show-trainer-phase";
import { SummonPhase } from "./phases/summon-phase";
import { SwitchPhase } from "./phases/switch-phase";
import { TitlePhase } from "./phases/title-phase";
import { ToggleDoublePositionPhase } from "./phases/toggle-double-position-phase";
import { TurnInitPhase } from "./phases/turn-init-phase";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1"; export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
@ -2421,7 +2435,6 @@ export default class BattleScene extends SceneBase {
getEnemyModifierTypesForWave(difficultyWaveIndex, count, [ enemyPokemon ], this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, upgradeChance) getEnemyModifierTypesForWave(difficultyWaveIndex, count, [ enemyPokemon ], this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, upgradeChance)
.map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false, this)); .map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false, this));
}); });
this.updateModifiers(false).then(() => resolve()); this.updateModifiers(false).then(() => resolve());
}); });
} }
@ -2579,7 +2592,7 @@ export default class BattleScene extends SceneBase {
// in case this is NECROZMA, determine which forms this // in case this is NECROZMA, determine which forms this
const matchingFormChangeOpts = pokemonFormChanges[pokemon.species.speciesId].filter(fc => fc.findTrigger(formChangeTriggerType) && fc.canChange(pokemon)); const matchingFormChangeOpts = pokemonFormChanges[pokemon.species.speciesId].filter(fc => fc.findTrigger(formChangeTriggerType) && fc.canChange(pokemon));
let matchingFormChange; let matchingFormChange: SpeciesFormChange | null;
if (pokemon.species.speciesId === Species.NECROZMA && matchingFormChangeOpts.length > 1) { if (pokemon.species.speciesId === Species.NECROZMA && matchingFormChangeOpts.length > 1) {
// Ultra Necrozma is changing its form back, so we need to figure out into which form it devolves. // Ultra Necrozma is changing its form back, so we need to figure out into which form it devolves.
const formChangeItemModifiers = (this.findModifiers(m => m instanceof PokemonFormChangeItemModifier && m.pokemonId === pokemon.id) as PokemonFormChangeItemModifier[]).filter(m => m.active).map(m => m.formChangeItem); const formChangeItemModifiers = (this.findModifiers(m => m instanceof PokemonFormChangeItemModifier && m.pokemonId === pokemon.id) as PokemonFormChangeItemModifier[]).filter(m => m.active).map(m => m.formChangeItem);
@ -2666,7 +2679,9 @@ export default class BattleScene extends SceneBase {
if (pokemon instanceof EnemyPokemon && pokemon.isBoss() && !pokemon.formIndex && pokemon.bossSegmentIndex < 1) { if (pokemon instanceof EnemyPokemon && pokemon.isBoss() && !pokemon.formIndex && pokemon.bossSegmentIndex < 1) {
this.fadeOutBgm(Utils.fixedInt(2000), false); this.fadeOutBgm(Utils.fixedInt(2000), false);
this.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].firstStageWin, pokemon.species.name, undefined, () => { this.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].firstStageWin, pokemon.species.name, undefined, () => {
this.addEnemyModifier(getModifierType(modifierTypes.MINI_BLACK_HOLE).newModifier(pokemon) as PersistentModifier, false, true); const finalBossMBH = getModifierType(modifierTypes.MINI_BLACK_HOLE).newModifier(pokemon) as TurnHeldItemTransferModifier;
finalBossMBH.setTransferrableFalse();
this.addEnemyModifier(finalBossMBH, false, true);
pokemon.generateAndPopulateMoveset(1); pokemon.generateAndPopulateMoveset(1);
this.setFieldScale(0.75); this.setFieldScale(0.75);
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false); this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false);

View File

@ -3,7 +3,6 @@ import { Type } from "./type";
import { Constructor } from "#app/utils"; import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import { BattleStat, getBattleStatName } from "./battle-stat"; import { BattleStat, getBattleStatName } from "./battle-stat";
import { MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import { Weather, WeatherType } from "./weather"; import { Weather, WeatherType } from "./weather";
import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags"; import { BattlerTag, GroundedTag, GulpMissileTag, SemiInvulnerableTag } from "./battler-tags";
@ -26,6 +25,10 @@ import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { MovePhase } from "#app/phases/move-phase.js";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js";
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
export class Ability implements Localizable { export class Ability implements Localizable {
public id: Abilities; public id: Abilities;
@ -2395,16 +2398,16 @@ export class PreStatChangeAbAttr extends AbAttr {
} }
export class ProtectStatAbAttr extends PreStatChangeAbAttr { export class ProtectStatAbAttr extends PreStatChangeAbAttr {
private protectedStat: BattleStat | null; private protectedStat?: BattleStat;
constructor(protectedStat?: BattleStat) { constructor(protectedStat?: BattleStat) {
super(); super();
this.protectedStat = protectedStat ?? null; this.protectedStat = protectedStat;
} }
applyPreStatChange(pokemon: Pokemon, passive: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean { applyPreStatChange(pokemon: Pokemon, passive: boolean, stat: BattleStat, cancelled: Utils.BooleanHolder, args: any[]): boolean {
if (!this.protectedStat || stat === this.protectedStat) { if (Utils.isNullOrUndefined(this.protectedStat) || stat === this.protectedStat) {
cancelled.value = true; cancelled.value = true;
return true; return true;
} }
@ -5039,6 +5042,7 @@ export function initAbilities() {
(pokemon, abilityName) => i18next.t("abilityTriggers:disguiseAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }), (pokemon, abilityName) => i18next.t("abilityTriggers:disguiseAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }),
(pokemon) => Math.floor(pokemon.getMaxHp() / 8)) (pokemon) => Math.floor(pokemon.getMaxHp() / 8))
.attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostBattleInitFormChangeAbAttr, () => 0)
.bypassFaint()
.ignorable(), .ignorable(),
new Ability(Abilities.BATTLE_BOND, 7) new Ability(Abilities.BATTLE_BOND, 7)
.attr(PostVictoryFormChangeAbAttr, () => 2) .attr(PostVictoryFormChangeAbAttr, () => 2)
@ -5191,6 +5195,7 @@ export function initAbilities() {
.attr(FormBlockDamageAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE), 0, BattlerTagType.ICE_FACE, .attr(FormBlockDamageAbAttr, (target, user, move) => move.category === MoveCategory.PHYSICAL && !!target.getTag(BattlerTagType.ICE_FACE), 0, BattlerTagType.ICE_FACE,
(pokemon, abilityName) => i18next.t("abilityTriggers:iceFaceAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName })) (pokemon, abilityName) => i18next.t("abilityTriggers:iceFaceAvoidedDamage", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon), abilityName: abilityName }))
.attr(PostBattleInitFormChangeAbAttr, () => 0) .attr(PostBattleInitFormChangeAbAttr, () => 0)
.bypassFaint()
.ignorable(), .ignorable(),
new Ability(Abilities.POWER_SPOT, 8) new Ability(Abilities.POWER_SPOT, 8)
.attr(AllyMoveCategoryPowerBoostAbAttr, [MoveCategory.SPECIAL, MoveCategory.PHYSICAL], 1.3), .attr(AllyMoveCategoryPowerBoostAbAttr, [MoveCategory.SPECIAL, MoveCategory.PHYSICAL], 1.3),

View File

@ -4,7 +4,6 @@ import * as Utils from "../utils";
import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "./move"; import { MoveCategory, allMoves, MoveTarget, IncrementMovePriorityAttr, applyMoveAttrs } from "./move";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { HitResult, PokemonMove } from "../field/pokemon"; import Pokemon, { HitResult, PokemonMove } from "../field/pokemon";
import { MoveEffectPhase, PokemonHealPhase, ShowAbilityPhase, StatChangePhase } from "../phases";
import { StatusEffect } from "./status-effect"; import { StatusEffect } from "./status-effect";
import { BattlerIndex } from "../battle"; import { BattlerIndex } from "../battle";
import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability"; import { BlockNonDirectDamageAbAttr, ChangeMovePriorityAbAttr, ProtectStatAbAttr, applyAbAttrs } from "./ability";
@ -15,6 +14,10 @@ import { Abilities } from "#enums/abilities";
import { ArenaTagType } from "#enums/arena-tag-type"; import { ArenaTagType } from "#enums/arena-tag-type";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js";
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
export enum ArenaTagSide { export enum ArenaTagSide {
BOTH, BOTH,

View File

@ -1,5 +1,4 @@
import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims"; import { ChargeAnim, CommonAnim, CommonBattleAnim, MoveChargeAnim } from "./battle-anims";
import { CommonAnimPhase, MoveEffectPhase, MovePhase, PokemonHealPhase, ShowAbilityPhase, StatChangeCallback, StatChangePhase } from "../phases";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { MoveResult, HitResult } from "../field/pokemon"; import Pokemon, { MoveResult, HitResult } from "../field/pokemon";
import { Stat, getStatName } from "./pokemon-stat"; import { Stat, getStatName } from "./pokemon-stat";
@ -18,6 +17,12 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import i18next from "#app/plugins/i18n.js"; import i18next from "#app/plugins/i18n.js";
import { CommonAnimPhase } from "#app/phases/common-anim-phase.js";
import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
import { MovePhase } from "#app/phases/move-phase.js";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
import { ShowAbilityPhase } from "#app/phases/show-ability-phase.js";
import { StatChangePhase, StatChangeCallback } from "#app/phases/stat-change-phase.js";
export enum BattlerTagLapseType { export enum BattlerTagLapseType {
FAINT, FAINT,

View File

@ -1,4 +1,3 @@
import { PokemonHealPhase, StatChangePhase } from "../phases";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import Pokemon, { HitResult } from "../field/pokemon"; import Pokemon, { HitResult } from "../field/pokemon";
import { BattleStat } from "./battle-stat"; import { BattleStat } from "./battle-stat";
@ -8,6 +7,8 @@ import { DoubleBerryEffectAbAttr, ReduceBerryUseThresholdAbAttr, applyAbAttrs }
import i18next from "i18next"; import i18next from "i18next";
import { BattlerTagType } from "#enums/battler-tag-type"; import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type"; import { BerryType } from "#enums/berry-type";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
export function getBerryName(berryType: BerryType): string { export function getBerryName(berryType: BerryType): string {
return i18next.t(`berry:${BerryType[berryType]}.name`); return i18next.t(`berry:${BerryType[berryType]}.name`);

View File

@ -37,7 +37,7 @@ export const speciesEggMoves = {
[Species.SLOWPOKE]: [ Moves.BOUNCY_BUBBLE, Moves.FLAMETHROWER, Moves.MYSTICAL_POWER, Moves.SHED_TAIL ], [Species.SLOWPOKE]: [ Moves.BOUNCY_BUBBLE, Moves.FLAMETHROWER, Moves.MYSTICAL_POWER, Moves.SHED_TAIL ],
[Species.MAGNEMITE]: [ Moves.PARABOLIC_CHARGE, Moves.BODY_PRESS, Moves.ICE_BEAM, Moves.THUNDERCLAP ], [Species.MAGNEMITE]: [ Moves.PARABOLIC_CHARGE, Moves.BODY_PRESS, Moves.ICE_BEAM, Moves.THUNDERCLAP ],
[Species.FARFETCHD]: [ Moves.IVY_CUDGEL, Moves.TRIPLE_ARROWS, Moves.ROOST, Moves.VICTORY_DANCE ], [Species.FARFETCHD]: [ Moves.IVY_CUDGEL, Moves.TRIPLE_ARROWS, Moves.ROOST, Moves.VICTORY_DANCE ],
[Species.DODUO]: [ Moves.ICE_SPINNER, Moves.MULTI_ATTACK, Moves.FLOATY_FALL, Moves.TRIPLE_ARROWS ], [Species.DODUO]: [ Moves.TRIPLE_AXEL, Moves.MULTI_ATTACK, Moves.FLOATY_FALL, Moves.TRIPLE_ARROWS ],
[Species.SEEL]: [ Moves.FREEZE_DRY, Moves.BOUNCY_BUBBLE, Moves.SLACK_OFF, Moves.STEAM_ERUPTION ], [Species.SEEL]: [ Moves.FREEZE_DRY, Moves.BOUNCY_BUBBLE, Moves.SLACK_OFF, Moves.STEAM_ERUPTION ],
[Species.GRIMER]: [ Moves.SUCKER_PUNCH, Moves.CURSE, Moves.STRENGTH_SAP, Moves.NOXIOUS_TORQUE ], [Species.GRIMER]: [ Moves.SUCKER_PUNCH, Moves.CURSE, Moves.STRENGTH_SAP, Moves.NOXIOUS_TORQUE ],
[Species.SHELLDER]: [ Moves.ROCK_BLAST, Moves.WATER_SHURIKEN, Moves.BANEFUL_BUNKER, Moves.BONE_RUSH ], [Species.SHELLDER]: [ Moves.ROCK_BLAST, Moves.WATER_SHURIKEN, Moves.BANEFUL_BUNKER, Moves.BONE_RUSH ],
@ -198,7 +198,7 @@ export const speciesEggMoves = {
[Species.KYOGRE]: [ Moves.BOUNCY_BUBBLE, Moves.HURRICANE, Moves.FREEZE_DRY, Moves.ELECTRO_SHOT ], [Species.KYOGRE]: [ Moves.BOUNCY_BUBBLE, Moves.HURRICANE, Moves.FREEZE_DRY, Moves.ELECTRO_SHOT ],
[Species.GROUDON]: [ Moves.STONE_AXE, Moves.SOLAR_BLADE, Moves.MORNING_SUN, Moves.SACRED_FIRE ], [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.RAYQUAZA]: [ Moves.V_CREATE, Moves.DRAGON_DARTS, Moves.CORE_ENFORCER, Moves.OBLIVION_WING ],
[Species.JIRACHI]: [ Moves.TACHYON_CUTTER, Moves.FLOATY_FALL, Moves.TRIPLE_ARROWS, Moves.SHELL_SMASH ], [Species.JIRACHI]: [ Moves.TACHYON_CUTTER, Moves.TRIPLE_ARROWS, Moves.ROCK_SLIDE, Moves.SHELL_SMASH ],
[Species.DEOXYS]: [ Moves.COLLISION_COURSE, Moves.EARTH_POWER, Moves.PARTING_SHOT, Moves.LUMINA_CRASH ], [Species.DEOXYS]: [ Moves.COLLISION_COURSE, Moves.EARTH_POWER, Moves.PARTING_SHOT, Moves.LUMINA_CRASH ],
[Species.TURTWIG]: [ Moves.SHELL_SMASH, Moves.MIGHTY_CLEAVE, Moves.ICE_SPINNER, Moves.SAPPY_SEED ], [Species.TURTWIG]: [ Moves.SHELL_SMASH, Moves.MIGHTY_CLEAVE, Moves.ICE_SPINNER, Moves.SAPPY_SEED ],
[Species.CHIMCHAR]: [ Moves.FIERY_DANCE, Moves.SECRET_SWORD, Moves.TRIPLE_AXEL, Moves.SACRED_FIRE ], [Species.CHIMCHAR]: [ Moves.FIERY_DANCE, Moves.SECRET_SWORD, Moves.TRIPLE_AXEL, Moves.SACRED_FIRE ],
@ -418,7 +418,7 @@ export const speciesEggMoves = {
[Species.CELESTEELA]: [ Moves.RECOVER, Moves.BUZZY_BUZZ, Moves.SANDSEAR_STORM, Moves.OBLIVION_WING ], [Species.CELESTEELA]: [ Moves.RECOVER, Moves.BUZZY_BUZZ, Moves.SANDSEAR_STORM, Moves.OBLIVION_WING ],
[Species.KARTANA]: [ Moves.MIGHTY_CLEAVE, Moves.PSYBLADE, Moves.BITTER_BLADE, Moves.BEHEMOTH_BLADE ], [Species.KARTANA]: [ Moves.MIGHTY_CLEAVE, Moves.PSYBLADE, Moves.BITTER_BLADE, Moves.BEHEMOTH_BLADE ],
[Species.GUZZLORD]: [ Moves.SUCKER_PUNCH, Moves.COMEUPPANCE, Moves.SLACK_OFF, Moves.SHED_TAIL ], [Species.GUZZLORD]: [ Moves.SUCKER_PUNCH, Moves.COMEUPPANCE, Moves.SLACK_OFF, Moves.SHED_TAIL ],
[Species.NECROZMA]: [ Moves.CLANGOROUS_SOUL, Moves.SACRED_FIRE, Moves.ASTRAL_BARRAGE, Moves.CLANGOROUS_SOUL ], [Species.NECROZMA]: [ Moves.CLANGOROUS_SOUL, Moves.SACRED_FIRE, Moves.ASTRAL_BARRAGE, Moves.DYNAMAX_CANNON ],
[Species.MAGEARNA]: [ Moves.STRENGTH_SAP, Moves.EARTH_POWER, Moves.MOONBLAST, Moves.MAKE_IT_RAIN ], [Species.MAGEARNA]: [ Moves.STRENGTH_SAP, Moves.EARTH_POWER, Moves.MOONBLAST, Moves.MAKE_IT_RAIN ],
[Species.MARSHADOW]: [ Moves.POWER_UP_PUNCH, Moves.TRIPLE_AXEL, Moves.METEOR_MASH, Moves.STORM_THROW ], [Species.MARSHADOW]: [ Moves.POWER_UP_PUNCH, Moves.TRIPLE_AXEL, Moves.METEOR_MASH, Moves.STORM_THROW ],
[Species.POIPOLE]: [ Moves.CORE_ENFORCER, Moves.ICE_BEAM, Moves.SEARING_SHOT, Moves.MALIGNANT_CHAIN ], [Species.POIPOLE]: [ Moves.CORE_ENFORCER, Moves.ICE_BEAM, Moves.SEARING_SHOT, Moves.MALIGNANT_CHAIN ],
@ -458,7 +458,7 @@ export const speciesEggMoves = {
[Species.MORPEKO]: [ Moves.TRIPLE_AXEL, Moves.OBSTRUCT, Moves.SWORDS_DANCE, Moves.COLLISION_COURSE ], [Species.MORPEKO]: [ Moves.TRIPLE_AXEL, Moves.OBSTRUCT, Moves.SWORDS_DANCE, Moves.COLLISION_COURSE ],
[Species.CUFANT]: [ Moves.LIQUIDATION, Moves.CURSE, Moves.COMBAT_TORQUE, Moves.GIGATON_HAMMER ], [Species.CUFANT]: [ Moves.LIQUIDATION, Moves.CURSE, Moves.COMBAT_TORQUE, Moves.GIGATON_HAMMER ],
[Species.DRACOZOLT]: [ Moves.TRIPLE_AXEL, Moves.DRAGON_HAMMER, Moves.FIRE_LASH, Moves.DRAGON_DANCE ], [Species.DRACOZOLT]: [ Moves.TRIPLE_AXEL, Moves.DRAGON_HAMMER, Moves.FIRE_LASH, Moves.DRAGON_DANCE ],
[Species.ARCTOZOLT]: [ Moves.TRIPLE_AXEL, Moves.AQUA_STEP, Moves.HIGH_HORSEPOWER, Moves.SHIFT_GEAR ], [Species.ARCTOZOLT]: [ Moves.MOUNTAIN_GALE, Moves.AQUA_STEP, Moves.HIGH_HORSEPOWER, Moves.SHIFT_GEAR ],
[Species.DRACOVISH]: [ Moves.TRIPLE_AXEL, Moves.DRAGON_HAMMER, Moves.THUNDER_FANG, Moves.DRAGON_DANCE ], [Species.DRACOVISH]: [ Moves.TRIPLE_AXEL, Moves.DRAGON_HAMMER, Moves.THUNDER_FANG, Moves.DRAGON_DANCE ],
[Species.ARCTOVISH]: [ Moves.ICE_FANG, Moves.THUNDER_FANG, Moves.HIGH_HORSEPOWER, Moves.SHIFT_GEAR ], [Species.ARCTOVISH]: [ Moves.ICE_FANG, Moves.THUNDER_FANG, Moves.HIGH_HORSEPOWER, Moves.SHIFT_GEAR ],
[Species.DURALUDON]: [ Moves.CORE_ENFORCER, Moves.BODY_PRESS, Moves.RECOVER, Moves.TACHYON_CUTTER ], [Species.DURALUDON]: [ Moves.CORE_ENFORCER, Moves.BODY_PRESS, Moves.RECOVER, Moves.TACHYON_CUTTER ],

View File

@ -1,5 +1,4 @@
import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims"; import { ChargeAnim, MoveChargeAnim, initMoveAnim, loadMoveAnimAssets } from "./battle-anims";
import { BattleEndPhase, MoveEndPhase, MovePhase, NewBattlePhase, PartyStatusCurePhase, PokemonHealPhase, StatChangePhase, SwitchPhase, SwitchSummonPhase } from "../phases";
import { BattleStat, getBattleStatName } from "./battle-stat"; import { BattleStat, getBattleStatName } from "./battle-stat";
import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags"; import { EncoreTag, GulpMissileTag, HelpingHandTag, SemiInvulnerableTag, ShellTrapTag, StockpilingTag, TrappedTag, TypeBoostTag } from "./battler-tags";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
@ -28,6 +27,15 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { MoveUsedEvent } from "#app/events/battle-scene.js"; import { MoveUsedEvent } from "#app/events/battle-scene.js";
import { PartyStatusCurePhase } from "#app/phases/party-status-cure-phase.js";
import { BattleEndPhase } from "#app/phases/battle-end-phase.js";
import { MoveEndPhase } from "#app/phases/move-end-phase.js";
import { MovePhase } from "#app/phases/move-phase.js";
import { NewBattlePhase } from "#app/phases/new-battle-phase.js";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
import { SwitchPhase } from "#app/phases/switch-phase.js";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase.js";
export enum MoveCategory { export enum MoveCategory {
PHYSICAL, PHYSICAL,
@ -4441,7 +4449,7 @@ export class CurseAttr extends MoveEffectAttr {
const curseRecoilDamage = Math.max(1, Math.floor(user.getMaxHp() / 2)); const curseRecoilDamage = Math.max(1, Math.floor(user.getMaxHp() / 2));
user.damageAndUpdate(curseRecoilDamage, HitResult.OTHER, false, true, true); user.damageAndUpdate(curseRecoilDamage, HitResult.OTHER, false, true, true);
user.scene.queueMessage( user.scene.queueMessage(
i18next.t("battle:cursedOnAdd", { i18next.t("battlerTags:cursedOnAdd", {
pokemonNameWithAffix: getPokemonNameWithAffix(user), pokemonNameWithAffix: getPokemonNameWithAffix(user),
pokemonName: getPokemonNameWithAffix(target) pokemonName: getPokemonNameWithAffix(target)
}) })

View File

@ -837,6 +837,8 @@ export const pokemonFormChanges: PokemonFormChanges = {
new SpeciesFormChange(Species.CRAMORANT, "", "gorging", new SpeciesFormChangeManualTrigger, true, new SpeciesFormChangeCondition(p => p.getHpRatio() < .5)), new SpeciesFormChange(Species.CRAMORANT, "", "gorging", new SpeciesFormChangeManualTrigger, true, new SpeciesFormChangeCondition(p => p.getHpRatio() < .5)),
new SpeciesFormChange(Species.CRAMORANT, "gulping", "", new SpeciesFormChangeManualTrigger, true), new SpeciesFormChange(Species.CRAMORANT, "gulping", "", new SpeciesFormChangeManualTrigger, true),
new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeManualTrigger, true), new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeManualTrigger, true),
new SpeciesFormChange(Species.CRAMORANT, "gulping", "", new SpeciesFormChangeActiveTrigger(false), true),
new SpeciesFormChange(Species.CRAMORANT, "gorging", "", new SpeciesFormChangeActiveTrigger(false), true),
] ]
}; };

View File

@ -3559,7 +3559,7 @@ export const starterPassiveAbilities = {
[Species.HEATRAN]: Abilities.EARTH_EATER, [Species.HEATRAN]: Abilities.EARTH_EATER,
[Species.REGIGIGAS]: Abilities.MINDS_EYE, [Species.REGIGIGAS]: Abilities.MINDS_EYE,
[Species.GIRATINA]: Abilities.SHADOW_SHIELD, [Species.GIRATINA]: Abilities.SHADOW_SHIELD,
[Species.CRESSELIA]: Abilities.MAGIC_BOUNCE, [Species.CRESSELIA]: Abilities.UNAWARE,
[Species.PHIONE]: Abilities.SIMPLE, [Species.PHIONE]: Abilities.SIMPLE,
[Species.MANAPHY]: Abilities.PRIMORDIAL_SEA, [Species.MANAPHY]: Abilities.PRIMORDIAL_SEA,
[Species.DARKRAI]: Abilities.UNNERVE, [Species.DARKRAI]: Abilities.UNNERVE,

View File

@ -4,7 +4,6 @@ import { Constructor } from "#app/utils";
import * as Utils from "../utils"; import * as Utils from "../utils";
import PokemonSpecies, { getPokemonSpecies } from "../data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies } from "../data/pokemon-species";
import { Weather, WeatherType, getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage } from "../data/weather"; import { Weather, WeatherType, getTerrainClearMessage, getTerrainStartMessage, getWeatherClearMessage, getWeatherStartMessage } from "../data/weather";
import { CommonAnimPhase } from "../phases";
import { CommonAnim } from "../data/battle-anims"; import { CommonAnim } from "../data/battle-anims";
import { Type } from "../data/type"; import { Type } from "../data/type";
import Move from "../data/move"; import Move from "../data/move";
@ -21,6 +20,7 @@ import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { TimeOfDay } from "#enums/time-of-day"; import { TimeOfDay } from "#enums/time-of-day";
import { TrainerType } from "#enums/trainer-type"; import { TrainerType } from "#enums/trainer-type";
import { CommonAnimPhase } from "#app/phases/common-anim-phase.js";
export class Arena { export class Arena {
public scene: BattleScene; public scene: BattleScene;

View File

@ -17,7 +17,6 @@ import { initMoveAnim, loadMoveAnimAssets } from "../data/battle-anims";
import { Status, StatusEffect, getRandomStatus } from "../data/status-effect"; import { Status, StatusEffect, getRandomStatus } from "../data/status-effect";
import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions"; import { pokemonEvolutions, pokemonPrevolutions, SpeciesFormEvolution, SpeciesEvolutionCondition, FusionSpeciesFormEvolution } from "../data/pokemon-evolutions";
import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms"; import { reverseCompatibleTms, tmSpecies, tmPoolTiers } from "../data/tms";
import { DamagePhase, FaintPhase, LearnMovePhase, MoveEffectPhase, ObtainStatusEffectPhase, StatChangePhase, SwitchSummonPhase, ToggleDoublePositionPhase, MoveEndPhase } from "../phases";
import { BattleStat } from "../data/battle-stat"; import { BattleStat } from "../data/battle-stat";
import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags"; import { BattlerTag, BattlerTagLapseType, EncoreTag, GroundedTag, HighestStatBoostTag, TypeImmuneTag, getBattlerTag, SemiInvulnerableTag, TypeBoostTag, ExposedTag } from "../data/battler-tags";
import { WeatherType } from "../data/weather"; import { WeatherType } from "../data/weather";
@ -51,6 +50,15 @@ import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { getPokemonNameWithAffix } from "#app/messages.js"; import { getPokemonNameWithAffix } from "#app/messages.js";
import { DamagePhase } from "#app/phases/damage-phase.js";
import { FaintPhase } from "#app/phases/faint-phase.js";
import { LearnMovePhase } from "#app/phases/learn-move-phase.js";
import { MoveEffectPhase } from "#app/phases/move-effect-phase.js";
import { MoveEndPhase } from "#app/phases/move-end-phase.js";
import { ObtainStatusEffectPhase } from "#app/phases/obtain-status-effect-phase.js";
import { StatChangePhase } from "#app/phases/stat-change-phase.js";
import { SwitchSummonPhase } from "#app/phases/switch-summon-phase.js";
import { ToggleDoublePositionPhase } from "#app/phases/toggle-double-position-phase.js";
export enum FieldPosition { export enum FieldPosition {
CENTER, CENTER,
@ -921,8 +929,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* by how many learnable moves there are for the {@linkcode Pokemon}. * by how many learnable moves there are for the {@linkcode Pokemon}.
*/ */
getLearnableLevelMoves(): Moves[] { getLearnableLevelMoves(): Moves[] {
let levelMoves = this.getLevelMoves(1, true).map(lm => lm[1]); let levelMoves = this.getLevelMoves(1, true, false, true).map(lm => lm[1]);
if (this.metBiome === -1 && !this.scene.gameMode.isFreshStartChallenge()) { if (this.metBiome === -1 && !this.scene.gameMode.isFreshStartChallenge() && !this.scene.gameMode.isDaily) {
levelMoves = this.getUnlockedEggMoves().concat(levelMoves); levelMoves = this.getUnlockedEggMoves().concat(levelMoves);
} }
return levelMoves.filter(lm => !this.moveset.some(m => m?.moveId === lm)); return levelMoves.filter(lm => !this.moveset.some(m => m?.moveId === lm));
@ -1210,11 +1218,11 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* *
* @param source - The Pokémon using the move. * @param source - The Pokémon using the move.
* @param move - The move being used. * @param move - The move being used.
* @returns The type damage multiplier or undefined if it's a status move * @returns The type damage multiplier or 1 if it's a status move
*/ */
getMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier | undefined { getMoveEffectiveness(source: Pokemon, move: PokemonMove): TypeDamageMultiplier {
if (move.getMove().category === MoveCategory.STATUS) { if (move.getMove().category === MoveCategory.STATUS) {
return undefined; return 1;
} }
return this.getAttackMoveEffectiveness(source, move, !this.battleData?.abilityRevealed); return this.getAttackMoveEffectiveness(source, move, !this.battleData?.abilityRevealed);
@ -3223,14 +3231,18 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
* Causes a Pokemon to leave the field (such as in preparation for a switch out/escape). * Causes a Pokemon to leave the field (such as in preparation for a switch out/escape).
* @param clearEffects Indicates if effects should be cleared (true) or passed * @param clearEffects Indicates if effects should be cleared (true) or passed
* to the next pokemon, such as during a baton pass (false) * to the next pokemon, such as during a baton pass (false)
* @param hideInfo Indicates if this should also play the animation to hide the Pokemon's
* info container.
*/ */
leaveField(clearEffects: boolean = true) { leaveField(clearEffects: boolean = true, hideInfo: boolean = true) {
this.resetTurnData(); this.resetTurnData();
if (clearEffects) { if (clearEffects) {
this.resetSummonData(); this.resetSummonData();
this.resetBattleData(); this.resetBattleData();
} }
if (hideInfo) {
this.hideInfo(); this.hideInfo();
}
this.setVisible(false); this.setVisible(false);
this.scene.field.remove(this); this.scene.field.remove(this);
this.scene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true); this.scene.triggerPokemonFormChange(this, SpeciesFormChangeActiveTrigger, true);
@ -3780,7 +3792,7 @@ export class EnemyPokemon extends Pokemon {
this.moveset = (formIndex !== undefined ? formIndex : this.formIndex) this.moveset = (formIndex !== undefined ? formIndex : this.formIndex)
? [ ? [
new PokemonMove(Moves.DYNAMAX_CANNON), new PokemonMove(Moves.DYNAMAX_CANNON),
new PokemonMove(Moves.CROSS_POISON), new PokemonMove(Moves.SLUDGE_BOMB),
new PokemonMove(Moves.FLAMETHROWER), new PokemonMove(Moves.FLAMETHROWER),
new PokemonMove(Moves.RECOVER, 0, -4) new PokemonMove(Moves.RECOVER, 0, -4)
] ]

View File

@ -62,7 +62,7 @@ export class GameMode implements GameModeConfig {
* @returns true if the game mode has that challenge * @returns true if the game mode has that challenge
*/ */
hasChallenge(challenge: Challenges): boolean { hasChallenge(challenge: Challenges): boolean {
return this.challenges.some(c => c.id === challenge); return this.challenges.some(c => c.id === challenge && c.value !== 0);
} }
/** /**

View File

@ -1,5 +1,4 @@
import * as ModifierTypes from "./modifier-type"; import * as ModifierTypes from "./modifier-type";
import { LearnMovePhase, LevelUpPhase, PokemonHealPhase } from "../phases";
import BattleScene from "../battle-scene"; import BattleScene from "../battle-scene";
import { getLevelTotalExp } from "../data/exp"; import { getLevelTotalExp } from "../data/exp";
import { MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball"; import { MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball";
@ -7,7 +6,7 @@ import Pokemon, { PlayerPokemon } from "../field/pokemon";
import { Stat } from "../data/pokemon-stat"; import { Stat } from "../data/pokemon-stat";
import { addTextObject, TextStyle } from "../ui/text"; import { addTextObject, TextStyle } from "../ui/text";
import { Type } from "../data/type"; import { Type } from "../data/type";
import { EvolutionPhase } from "../evolution-phase"; import { EvolutionPhase } from "../phases/evolution-phase";
import { FusionSpeciesFormEvolution, pokemonEvolutions, pokemonPrevolutions } from "../data/pokemon-evolutions"; import { FusionSpeciesFormEvolution, pokemonEvolutions, pokemonPrevolutions } from "../data/pokemon-evolutions";
import { getPokemonNameWithAffix } from "../messages"; import { getPokemonNameWithAffix } from "../messages";
import * as Utils from "../utils"; import * as Utils from "../utils";
@ -28,6 +27,9 @@ import i18next from "i18next";
import { allMoves } from "#app/data/move"; import { allMoves } from "#app/data/move";
import { Abilities } from "#app/enums/abilities"; import { Abilities } from "#app/enums/abilities";
import { LearnMovePhase } from "#app/phases/learn-move-phase.js";
import { LevelUpPhase } from "#app/phases/level-up-phase.js";
import { PokemonHealPhase } from "#app/phases/pokemon-heal-phase.js";
export type ModifierPredicate = (modifier: Modifier) => boolean; export type ModifierPredicate = (modifier: Modifier) => boolean;
@ -2338,7 +2340,7 @@ export abstract class HeldItemTransferModifier extends PokemonHeldItemModifier {
* @see {@linkcode modifierTypes[MINI_BLACK_HOLE]} * @see {@linkcode modifierTypes[MINI_BLACK_HOLE]}
*/ */
export class TurnHeldItemTransferModifier extends HeldItemTransferModifier { export class TurnHeldItemTransferModifier extends HeldItemTransferModifier {
readonly isTransferrable: boolean = true; isTransferrable: boolean = true;
constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) { constructor(type: ModifierType, pokemonId: integer, stackCount?: integer) {
super(type, pokemonId, stackCount); super(type, pokemonId, stackCount);
} }
@ -2362,6 +2364,10 @@ export class TurnHeldItemTransferModifier extends HeldItemTransferModifier {
getMaxHeldItemCount(pokemon: Pokemon): integer { getMaxHeldItemCount(pokemon: Pokemon): integer {
return 1; return 1;
} }
setTransferrableFalse(): void {
this.isTransferrable = false;
}
} }
/** /**
@ -2410,7 +2416,7 @@ export class ContactHeldItemTransferChanceModifier extends HeldItemTransferModif
} }
getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierTypes.ModifierType): string { getTransferMessage(pokemon: Pokemon, targetPokemon: Pokemon, item: ModifierTypes.ModifierType): string {
return i18next.t("modifier:contactHeldItemTransferApply", { pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), itemName: item.name, pokemonName: pokemon.name, typeName: this.type.name }); return i18next.t("modifier:contactHeldItemTransferApply", { pokemonNameWithAffix: getPokemonNameWithAffix(targetPokemon), itemName: item.name, pokemonName: getPokemonNameWithAffix(pokemon), typeName: this.type.name });
} }
getMaxHeldItemCount(pokemon: Pokemon): integer { getMaxHeldItemCount(pokemon: Pokemon): integer {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
import BattleScene from "#app/battle-scene.js";
import { ModifierTier } from "#app/modifier/modifier-tier.js";
import { regenerateModifierPoolThresholds, ModifierPoolType, getEnemyBuffModifierForWave } from "#app/modifier/modifier-type.js";
import { EnemyPersistentModifier } from "#app/modifier/modifier.js";
import { Phase } from "#app/phase.js";
export class AddEnemyBuffModifierPhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
const waveIndex = this.scene.currentBattle.waveIndex;
const tier = !(waveIndex % 1000) ? ModifierTier.ULTRA : !(waveIndex % 250) ? ModifierTier.GREAT : ModifierTier.COMMON;
regenerateModifierPoolThresholds(this.scene.getEnemyParty(), ModifierPoolType.ENEMY_BUFF);
const count = Math.ceil(waveIndex / 250);
for (let i = 0; i < count; i++) {
this.scene.addEnemyModifier(getEnemyBuffModifierForWave(tier, this.scene.findModifiers(m => m instanceof EnemyPersistentModifier, false), this.scene), true, true);
}
this.scene.updateModifiers(false, true).then(() => this.end());
}
}

View File

@ -0,0 +1,288 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { getPokeballCatchMultiplier, getPokeballAtlasKey, getPokeballTintColor, doPokeballBounceAnim } from "#app/data/pokeball.js";
import { getStatusEffectCatchRateMultiplier } from "#app/data/status-effect.js";
import { PokeballType } from "#app/enums/pokeball.js";
import { StatusEffect } from "#app/enums/status-effect.js";
import { addPokeballOpenParticles, addPokeballCaptureStars } from "#app/field/anims.js";
import { EnemyPokemon } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { PokemonHeldItemModifier } from "#app/modifier/modifier.js";
import { achvs } from "#app/system/achv.js";
import { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler.js";
import { SummaryUiMode } from "#app/ui/summary-ui-handler.js";
import { Mode } from "#app/ui/ui.js";
import i18next from "i18next";
import { PokemonPhase } from "./pokemon-phase";
import { VictoryPhase } from "./victory-phase";
export class AttemptCapturePhase extends PokemonPhase {
private pokeballType: PokeballType;
private pokeball: Phaser.GameObjects.Sprite;
private originalY: number;
constructor(scene: BattleScene, targetIndex: integer, pokeballType: PokeballType) {
super(scene, BattlerIndex.ENEMY + targetIndex);
this.pokeballType = pokeballType;
}
start() {
super.start();
const pokemon = this.getPokemon() as EnemyPokemon;
if (!pokemon?.hp) {
return this.end();
}
this.scene.pokeballCounts[this.pokeballType]--;
this.originalY = pokemon.y;
const _3m = 3 * pokemon.getMaxHp();
const _2h = 2 * pokemon.hp;
const catchRate = pokemon.species.catchRate;
const pokeballMultiplier = getPokeballCatchMultiplier(this.pokeballType);
const statusMultiplier = pokemon.status ? getStatusEffectCatchRateMultiplier(pokemon.status.effect) : 1;
const x = Math.round((((_3m - _2h) * catchRate * pokeballMultiplier) / _3m) * statusMultiplier);
const y = Math.round(65536 / Math.sqrt(Math.sqrt(255 / x)));
const fpOffset = pokemon.getFieldPositionOffset();
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
this.pokeball = this.scene.addFieldSprite(16, 80, "pb", pokeballAtlasKey);
this.pokeball.setOrigin(0.5, 0.625);
this.scene.field.add(this.pokeball);
this.scene.playSound("pb_throw");
this.scene.time.delayedCall(300, () => {
this.scene.field.moveBelow(this.pokeball as Phaser.GameObjects.GameObject, pokemon);
});
this.scene.tweens.add({
targets: this.pokeball,
x: { value: 236 + fpOffset[0], ease: "Linear" },
y: { value: 16 + fpOffset[1], ease: "Cubic.easeOut" },
duration: 500,
onComplete: () => {
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
this.scene.playSound("pb_rel");
pokemon.tint(getPokeballTintColor(this.pokeballType));
addPokeballOpenParticles(this.scene, this.pokeball.x, this.pokeball.y, this.pokeballType);
this.scene.tweens.add({
targets: pokemon,
duration: 500,
ease: "Sine.easeIn",
scale: 0.25,
y: 20,
onComplete: () => {
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
pokemon.setVisible(false);
this.scene.playSound("pb_catch");
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}`));
const doShake = () => {
let shakeCount = 0;
const pbX = this.pokeball.x;
const shakeCounter = this.scene.tweens.addCounter({
from: 0,
to: 1,
repeat: 4,
yoyo: true,
ease: "Cubic.easeOut",
duration: 250,
repeatDelay: 500,
onUpdate: t => {
if (shakeCount && shakeCount < 4) {
const value = t.getValue();
const directionMultiplier = shakeCount % 2 === 1 ? 1 : -1;
this.pokeball.setX(pbX + value * 4 * directionMultiplier);
this.pokeball.setAngle(value * 27.5 * directionMultiplier);
}
},
onRepeat: () => {
if (!pokemon.species.isObtainable()) {
shakeCounter.stop();
this.failCatch(shakeCount);
} else if (shakeCount++ < 3) {
if (pokeballMultiplier === -1 || pokemon.randSeedInt(65536) < y) {
this.scene.playSound("pb_move");
} else {
shakeCounter.stop();
this.failCatch(shakeCount);
}
} else {
this.scene.playSound("pb_lock");
addPokeballCaptureStars(this.scene, this.pokeball);
const pbTint = this.scene.add.sprite(this.pokeball.x, this.pokeball.y, "pb", "pb");
pbTint.setOrigin(this.pokeball.originX, this.pokeball.originY);
pbTint.setTintFill(0);
pbTint.setAlpha(0);
this.scene.field.add(pbTint);
this.scene.tweens.add({
targets: pbTint,
alpha: 0.375,
duration: 200,
easing: "Sine.easeOut",
onComplete: () => {
this.scene.tweens.add({
targets: pbTint,
alpha: 0,
duration: 200,
easing: "Sine.easeIn",
onComplete: () => pbTint.destroy()
});
}
});
}
},
onComplete: () => {
this.catch();
}
});
};
this.scene.time.delayedCall(250, () => doPokeballBounceAnim(this.scene, this.pokeball, 16, 72, 350, doShake));
}
});
}
});
}
failCatch(shakeCount: integer) {
const pokemon = this.getPokemon();
this.scene.playSound("pb_rel");
pokemon.setY(this.originalY);
if (pokemon.status?.effect !== StatusEffect.SLEEP) {
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
}
pokemon.tint(getPokeballTintColor(this.pokeballType));
pokemon.setVisible(true);
pokemon.untint(250, "Sine.easeOut");
const pokeballAtlasKey = getPokeballAtlasKey(this.pokeballType);
this.pokeball.setTexture("pb", `${pokeballAtlasKey}_opening`);
this.scene.time.delayedCall(17, () => this.pokeball.setTexture("pb", `${pokeballAtlasKey}_open`));
this.scene.tweens.add({
targets: pokemon,
duration: 250,
ease: "Sine.easeOut",
scale: 1
});
this.scene.currentBattle.lastUsedPokeball = this.pokeballType;
this.removePb();
this.end();
}
catch() {
const pokemon = this.getPokemon() as EnemyPokemon;
const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm();
if (speciesForm.abilityHidden && (pokemon.fusionSpecies ? pokemon.fusionAbilityIndex : pokemon.abilityIndex) === speciesForm.getAbilityCount() - 1) {
this.scene.validateAchv(achvs.HIDDEN_ABILITY);
}
if (pokemon.species.subLegendary) {
this.scene.validateAchv(achvs.CATCH_SUB_LEGENDARY);
}
if (pokemon.species.legendary) {
this.scene.validateAchv(achvs.CATCH_LEGENDARY);
}
if (pokemon.species.mythical) {
this.scene.validateAchv(achvs.CATCH_MYTHICAL);
}
this.scene.pokemonInfoContainer.show(pokemon, true);
this.scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
this.scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => {
const end = () => {
this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex));
this.scene.pokemonInfoContainer.hide();
this.removePb();
this.end();
};
const removePokemon = () => {
this.scene.addFaintedEnemyScore(pokemon);
this.scene.getPlayerField().filter(p => p.isActive(true)).forEach(playerPokemon => playerPokemon.removeTagsBySourceId(pokemon.id));
pokemon.hp = 0;
pokemon.trySetStatus(StatusEffect.FAINT);
this.scene.clearEnemyHeldItemModifiers();
this.scene.field.remove(pokemon, true);
};
const addToParty = () => {
const newPokemon = pokemon.addToParty(this.pokeballType);
const modifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier, false);
if (this.scene.getParty().filter(p => p.isShiny()).length === 6) {
this.scene.validateAchv(achvs.SHINY_PARTY);
}
Promise.all(modifiers.map(m => this.scene.addModifier(m, true))).then(() => {
this.scene.updateModifiers(true);
removePokemon();
if (newPokemon) {
newPokemon.loadAssets().then(end);
} else {
end();
}
});
};
Promise.all([pokemon.hideInfo(), this.scene.gameData.setPokemonCaught(pokemon)]).then(() => {
if (this.scene.getParty().length === 6) {
const promptRelease = () => {
this.scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => {
this.scene.pokemonInfoContainer.makeRoomForConfirmUi(1, true);
this.scene.ui.setMode(Mode.CONFIRM, () => {
const newPokemon = this.scene.addPlayerPokemon(pokemon.species, pokemon.level, pokemon.abilityIndex, pokemon.formIndex, pokemon.gender, pokemon.shiny, pokemon.variant, pokemon.ivs, pokemon.nature, pokemon);
this.scene.ui.setMode(Mode.SUMMARY, newPokemon, 0, SummaryUiMode.DEFAULT, () => {
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
promptRelease();
});
}, false);
}, () => {
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, this.fieldIndex, (slotIndex: integer, _option: PartyOption) => {
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
if (slotIndex < 6) {
addToParty();
} else {
promptRelease();
}
});
});
}, () => {
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
removePokemon();
end();
});
}, "fullParty");
});
};
promptRelease();
} else {
addToParty();
}
});
}, 0, true);
}
removePb() {
this.scene.tweens.add({
targets: this.pokeball,
duration: 250,
delay: 250,
ease: "Sine.easeIn",
alpha: 0,
onComplete: () => this.pokeball.destroy()
});
}
}

View File

@ -0,0 +1,56 @@
import BattleScene from "#app/battle-scene.js";
import { applyAbAttrs, RunSuccessAbAttr } from "#app/data/ability.js";
import { Stat } from "#app/enums/stat.js";
import { StatusEffect } from "#app/enums/status-effect.js";
import Pokemon from "#app/field/pokemon.js";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { BattleEndPhase } from "./battle-end-phase";
import { NewBattlePhase } from "./new-battle-phase";
import { PokemonPhase } from "./pokemon-phase";
export class AttemptRunPhase extends PokemonPhase {
constructor(scene: BattleScene, fieldIndex: integer) {
super(scene, fieldIndex);
}
start() {
super.start();
const playerPokemon = this.getPokemon();
const enemyField = this.scene.getEnemyField();
const enemySpeed = enemyField.reduce((total: integer, enemyPokemon: Pokemon) => total + enemyPokemon.getStat(Stat.SPD), 0) / enemyField.length;
const escapeChance = new Utils.IntegerHolder((((playerPokemon.getStat(Stat.SPD) * 128) / enemySpeed) + (30 * this.scene.currentBattle.escapeAttempts++)) % 256);
applyAbAttrs(RunSuccessAbAttr, playerPokemon, null, escapeChance);
if (playerPokemon.randSeedInt(256) < escapeChance.value) {
this.scene.playSound("flee");
this.scene.queueMessage(i18next.t("battle:runAwaySuccess"), null, true, 500);
this.scene.tweens.add({
targets: [this.scene.arenaEnemy, enemyField].flat(),
alpha: 0,
duration: 250,
ease: "Sine.easeIn",
onComplete: () => enemyField.forEach(enemyPokemon => enemyPokemon.destroy())
});
this.scene.clearEnemyHeldItemModifiers();
enemyField.forEach(enemyPokemon => {
enemyPokemon.hideInfo().then(() => enemyPokemon.destroy());
enemyPokemon.hp = 0;
enemyPokemon.trySetStatus(StatusEffect.FAINT);
});
this.scene.pushPhase(new BattleEndPhase(this.scene));
this.scene.pushPhase(new NewBattlePhase(this.scene));
} else {
this.scene.queueMessage(i18next.t("battle:runAwayCannotEscape"), null, true, 500);
}
this.end();
}
}

View File

@ -0,0 +1,49 @@
import { applyPostBattleAbAttrs, PostBattleAbAttr } from "#app/data/ability.js";
import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier.js";
import { BattlePhase } from "./battle-phase";
import { GameOverPhase } from "./game-over-phase";
export class BattleEndPhase extends BattlePhase {
start() {
super.start();
this.scene.currentBattle.addBattleScore(this.scene);
this.scene.gameData.gameStats.battles++;
if (this.scene.currentBattle.trainer) {
this.scene.gameData.gameStats.trainersDefeated++;
}
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) {
this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1;
}
// Endless graceful end
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex >= 5850) {
this.scene.clearPhaseQueue();
this.scene.unshiftPhase(new GameOverPhase(this.scene, true));
}
for (const pokemon of this.scene.getParty().filter(p => p.isAllowedInBattle())) {
applyPostBattleAbAttrs(PostBattleAbAttr, pokemon);
}
if (this.scene.currentBattle.moneyScattered) {
this.scene.currentBattle.pickUpScatteredMoney(this.scene);
}
this.scene.clearEnemyHeldItemModifiers();
const lapsingModifiers = this.scene.findModifiers(m => m instanceof LapsingPersistentModifier || m instanceof LapsingPokemonHeldItemModifier) as (LapsingPersistentModifier | LapsingPokemonHeldItemModifier)[];
for (const m of lapsingModifiers) {
const args: any[] = [];
if (m instanceof LapsingPokemonHeldItemModifier) {
args.push(this.scene.getPokemonById(m.pokemonId));
}
if (!m.lapse(args)) {
this.scene.removeModifier(m);
}
}
this.scene.updateModifiers().then(() => this.end());
}
}

View File

@ -0,0 +1,47 @@
import BattleScene from "#app/battle-scene.js";
import { TrainerSlot } from "#app/data/trainer-config.js";
import { Phase } from "#app/phase.js";
export class BattlePhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
showEnemyTrainer(trainerSlot: TrainerSlot = TrainerSlot.NONE): void {
const sprites = this.scene.currentBattle.trainer?.getSprites()!; // TODO: is this bang correct?
const tintSprites = this.scene.currentBattle.trainer?.getTintSprites()!; // TODO: is this bang correct?
for (let i = 0; i < sprites.length; i++) {
const visible = !trainerSlot || !i === (trainerSlot === TrainerSlot.TRAINER) || sprites.length < 2;
[sprites[i], tintSprites[i]].map(sprite => {
if (visible) {
sprite.x = trainerSlot || sprites.length < 2 ? 0 : i ? 16 : -16;
}
sprite.setVisible(visible);
sprite.clearTint();
});
sprites[i].setVisible(visible);
tintSprites[i].setVisible(visible);
sprites[i].clearTint();
tintSprites[i].clearTint();
}
this.scene.tweens.add({
targets: this.scene.currentBattle.trainer,
x: "-=16",
y: "+=16",
alpha: 1,
ease: "Sine.easeInOut",
duration: 750
});
}
hideEnemyTrainer(): void {
this.scene.tweens.add({
targets: this.scene.currentBattle.trainer,
x: "+=16",
y: "-=16",
alpha: 0,
ease: "Sine.easeInOut",
duration: 750
});
}
}

52
src/phases/berry-phase.ts Normal file
View File

@ -0,0 +1,52 @@
import { applyAbAttrs, PreventBerryUseAbAttr, HealFromBerryUseAbAttr } from "#app/data/ability.js";
import { CommonAnim } from "#app/data/battle-anims.js";
import { BerryUsedEvent } from "#app/events/battle-scene.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { BerryModifier } from "#app/modifier/modifier.js";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { FieldPhase } from "./field-phase";
import { CommonAnimPhase } from "./common-anim-phase";
/** The phase after attacks where the pokemon eat berries */
export class BerryPhase extends FieldPhase {
start() {
super.start();
this.executeForAll((pokemon) => {
const hasUsableBerry = !!this.scene.findModifier((m) => {
return m instanceof BerryModifier && m.shouldApply([pokemon]);
}, pokemon.isPlayer());
if (hasUsableBerry) {
const cancelled = new Utils.BooleanHolder(false);
pokemon.getOpponents().map((opp) => applyAbAttrs(PreventBerryUseAbAttr, opp, cancelled));
if (cancelled.value) {
pokemon.scene.queueMessage(i18next.t("abilityTriggers:preventBerryUse", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
} else {
this.scene.unshiftPhase(
new CommonAnimPhase(this.scene, pokemon.getBattlerIndex(), pokemon.getBattlerIndex(), CommonAnim.USE_ITEM)
);
for (const berryModifier of this.scene.applyModifiers(BerryModifier, pokemon.isPlayer(), pokemon) as BerryModifier[]) {
if (berryModifier.consumed) {
if (!--berryModifier.stackCount) {
this.scene.removeModifier(berryModifier);
} else {
berryModifier.consumed = false;
}
}
this.scene.eventTarget.dispatchEvent(new BerryUsedEvent(berryModifier)); // Announce a berry was used
}
this.scene.updateModifiers(pokemon.isPlayer());
applyAbAttrs(HealFromBerryUseAbAttr, pokemon, new Utils.BooleanHolder(false));
}
}
});
this.end();
}
}

View File

@ -0,0 +1,61 @@
import BattleScene from "#app/battle-scene.js";
import { BattleStyle } from "#app/enums/battle-style.js";
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { Mode } from "#app/ui/ui.js";
import i18next from "i18next";
import { BattlePhase } from "./battle-phase";
import { PostSummonPhase } from "./post-summon-phase";
import { SummonMissingPhase } from "./summon-missing-phase";
import { SwitchPhase } from "./switch-phase";
export class CheckSwitchPhase extends BattlePhase {
protected fieldIndex: integer;
protected useName: boolean;
constructor(scene: BattleScene, fieldIndex: integer, useName: boolean) {
super(scene);
this.fieldIndex = fieldIndex;
this.useName = useName;
}
start() {
super.start();
const pokemon = this.scene.getPlayerField()[this.fieldIndex];
if (this.scene.battleStyle === BattleStyle.SET) {
super.end();
return;
}
if (this.scene.field.getAll().indexOf(pokemon) === -1) {
this.scene.unshiftPhase(new SummonMissingPhase(this.scene, this.fieldIndex));
super.end();
return;
}
if (!this.scene.getParty().slice(1).filter(p => p.isActive()).length) {
super.end();
return;
}
if (pokemon.getTag(BattlerTagType.FRENZY)) {
super.end();
return;
}
this.scene.ui.showText(i18next.t("battle:switchQuestion", { pokemonName: this.useName ? getPokemonNameWithAffix(pokemon) : i18next.t("battle:pokemon") }), null, () => {
this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.tryRemovePhase(p => p instanceof PostSummonPhase && p.player && p.fieldIndex === this.fieldIndex);
this.scene.unshiftPhase(new SwitchPhase(this.scene, this.fieldIndex, false, true));
this.end();
}, () => {
this.scene.ui.setMode(Mode.MESSAGE);
this.end();
});
});
}
}

288
src/phases/command-phase.ts Normal file
View File

@ -0,0 +1,288 @@
import BattleScene from "#app/battle-scene.js";
import { TurnCommand, BattleType } from "#app/battle.js";
import { applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "#app/data/ability.js";
import { TrappedTag, EncoreTag } from "#app/data/battler-tags.js";
import { MoveTargetSet, getMoveTargets } from "#app/data/move.js";
import { speciesStarters } from "#app/data/pokemon-species.js";
import { Type } from "#app/data/type.js";
import { Abilities } from "#app/enums/abilities.js";
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { Biome } from "#app/enums/biome.js";
import { Moves } from "#app/enums/moves.js";
import { PokeballType } from "#app/enums/pokeball.js";
import { FieldPosition, PlayerPokemon } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { Command } from "#app/ui/command-ui-handler.js";
import { Mode } from "#app/ui/ui.js";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { FieldPhase } from "./field-phase";
import { SelectTargetPhase } from "./select-target-phase";
export class CommandPhase extends FieldPhase {
protected fieldIndex: integer;
constructor(scene: BattleScene, fieldIndex: integer) {
super(scene);
this.fieldIndex = fieldIndex;
}
start() {
super.start();
if (this.fieldIndex) {
// If we somehow are attempting to check the right pokemon but there's only one pokemon out
// Switch back to the center pokemon. This can happen rarely in double battles with mid turn switching
if (this.scene.getPlayerField().filter(p => p.isActive()).length === 1) {
this.fieldIndex = FieldPosition.CENTER;
} else {
const allyCommand = this.scene.currentBattle.turnCommands[this.fieldIndex - 1];
if (allyCommand?.command === Command.BALL || allyCommand?.command === Command.RUN) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: allyCommand?.command, skip: true };
}
}
}
if (this.scene.currentBattle.turnCommands[this.fieldIndex]?.skip) {
return this.end();
}
const playerPokemon = this.scene.getPlayerField()[this.fieldIndex];
const moveQueue = playerPokemon.getMoveQueue();
while (moveQueue.length && moveQueue[0]
&& moveQueue[0].move && (!playerPokemon.getMoveset().find(m => m?.moveId === moveQueue[0].move)
|| !playerPokemon.getMoveset()[playerPokemon.getMoveset().findIndex(m => m?.moveId === moveQueue[0].move)]!.isUsable(playerPokemon, moveQueue[0].ignorePP))) { // TODO: is the bang correct?
moveQueue.shift();
}
if (moveQueue.length) {
const queuedMove = moveQueue[0];
if (!queuedMove.move) {
this.handleCommand(Command.FIGHT, -1, false);
} else {
const moveIndex = playerPokemon.getMoveset().findIndex(m => m?.moveId === queuedMove.move);
if (moveIndex > -1 && playerPokemon.getMoveset()[moveIndex]!.isUsable(playerPokemon, queuedMove.ignorePP)) { // TODO: is the bang correct?
this.handleCommand(Command.FIGHT, moveIndex, queuedMove.ignorePP, { targets: queuedMove.targets, multiple: queuedMove.targets.length > 1 });
} else {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}
} else {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}
handleCommand(command: Command, cursor: integer, ...args: any[]): boolean {
const playerPokemon = this.scene.getPlayerField()[this.fieldIndex];
const enemyField = this.scene.getEnemyField();
let success: boolean;
switch (command) {
case Command.FIGHT:
let useStruggle = false;
if (cursor === -1 ||
playerPokemon.trySelectMove(cursor, args[0] as boolean) ||
(useStruggle = cursor > -1 && !playerPokemon.getMoveset().filter(m => m?.isUsable(playerPokemon)).length)) {
const moveId = !useStruggle ? cursor > -1 ? playerPokemon.getMoveset()[cursor]!.moveId : Moves.NONE : Moves.STRUGGLE; // TODO: is the bang correct?
const turnCommand: TurnCommand = { command: Command.FIGHT, cursor: cursor, move: { move: moveId, targets: [], ignorePP: args[0] }, args: args };
const moveTargets: MoveTargetSet = args.length < 3 ? getMoveTargets(playerPokemon, moveId) : args[2];
if (!moveId) {
turnCommand.targets = [this.fieldIndex];
}
console.log(moveTargets, getPokemonNameWithAffix(playerPokemon));
if (moveTargets.targets.length > 1 && moveTargets.multiple) {
this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex));
}
if (moveTargets.targets.length <= 1 || moveTargets.multiple) {
turnCommand.move!.targets = moveTargets.targets; //TODO: is the bang correct here?
} else if (playerPokemon.getTag(BattlerTagType.CHARGING) && playerPokemon.getMoveQueue().length >= 1) {
turnCommand.move!.targets = playerPokemon.getMoveQueue()[0].targets; //TODO: is the bang correct here?
} else {
this.scene.unshiftPhase(new SelectTargetPhase(this.scene, this.fieldIndex));
}
this.scene.currentBattle.turnCommands[this.fieldIndex] = turnCommand;
success = true;
} else if (cursor < playerPokemon.getMoveset().length) {
const move = playerPokemon.getMoveset()[cursor]!; //TODO: is this bang correct?
this.scene.ui.setMode(Mode.MESSAGE);
// Decides between a Disabled, Not Implemented, or No PP translation message
const errorMessage =
playerPokemon.summonData.disabledMove === move.moveId ? "battle:moveDisabled" :
move.getName().endsWith(" (N)") ? "battle:moveNotImplemented" : "battle:moveNoPP";
const moveName = move.getName().replace(" (N)", ""); // Trims off the indicator
this.scene.ui.showText(i18next.t(errorMessage, { moveName: moveName }), null, () => {
this.scene.ui.clearText();
this.scene.ui.setMode(Mode.FIGHT, this.fieldIndex);
}, null, true);
}
break;
case Command.BALL:
const notInDex = (this.scene.getEnemyField().filter(p => p.isActive(true)).some(p => !p.scene.gameData.dexData[p.species.speciesId].caughtAttr) && this.scene.gameData.getStarterCount(d => !!d.caughtAttr) < Object.keys(speciesStarters).length - 1);
if (this.scene.arena.biomeType === Biome.END && (!this.scene.gameMode.isClassic || this.scene.gameMode.isFreshStartChallenge() || notInDex )) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noPokeballForce"), null, () => {
this.scene.ui.showText("", 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noPokeballTrainer"), null, () => {
this.scene.ui.showText("", 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else {
const targets = this.scene.getEnemyField().filter(p => p.isActive(true)).map(p => p.getBattlerIndex());
if (targets.length > 1) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noPokeballMulti"), null, () => {
this.scene.ui.showText("", 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else if (cursor < 5) {
const targetPokemon = this.scene.getEnemyField().find(p => p.isActive(true));
if (targetPokemon?.isBoss() && targetPokemon?.bossSegmentIndex >= 1 && !targetPokemon?.hasAbility(Abilities.WONDER_GUARD, false, true) && cursor < PokeballType.MASTER_BALL) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noPokeballStrong"), null, () => {
this.scene.ui.showText("", 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else {
this.scene.currentBattle.turnCommands[this.fieldIndex] = { command: Command.BALL, cursor: cursor };
this.scene.currentBattle.turnCommands[this.fieldIndex]!.targets = targets;
if (this.fieldIndex) {
this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
success = true;
}
}
}
break;
case Command.POKEMON:
case Command.RUN:
const isSwitch = command === Command.POKEMON;
if (!isSwitch && this.scene.arena.biomeType === Biome.END) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noEscapeForce"), null, () => {
this.scene.ui.showText("", 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else if (!isSwitch && this.scene.currentBattle.battleType === BattleType.TRAINER) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(i18next.t("battle:noEscapeTrainer"), null, () => {
this.scene.ui.showText("", 0);
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}, null, true);
} else {
const trapTag = playerPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag;
const trapped = new Utils.BooleanHolder(false);
const batonPass = isSwitch && args[0] as boolean;
const trappedAbMessages: string[] = [];
if (!batonPass) {
enemyField.forEach(enemyPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, enemyPokemon, trapped, playerPokemon, true, trappedAbMessages));
}
if (batonPass || (!trapTag && !trapped.value)) {
this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
success = true;
if (!isSwitch && this.fieldIndex) {
this.scene.currentBattle.turnCommands[this.fieldIndex - 1]!.skip = true;
}
} else if (trapTag) {
if (trapTag.sourceMove === Moves.INGRAIN && trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId)?.isOfType(Type.GHOST)) {
success = true;
this.scene.currentBattle.turnCommands[this.fieldIndex] = isSwitch
? { command: Command.POKEMON, cursor: cursor, args: args }
: { command: Command.RUN };
break;
}
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
this.scene.ui.setMode(Mode.MESSAGE);
}
this.scene.ui.showText(
i18next.t("battle:noEscapePokemon", {
pokemonName: trapTag.sourceId && this.scene.getPokemonById(trapTag.sourceId) ? getPokemonNameWithAffix(this.scene.getPokemonById(trapTag.sourceId)!) : "",
moveName: trapTag.getMoveName(),
escapeVerb: isSwitch ? i18next.t("battle:escapeVerbSwitch") : i18next.t("battle:escapeVerbFlee")
}),
null,
() => {
this.scene.ui.showText("", 0);
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
} else if (trapped.value && trappedAbMessages.length > 0) {
if (!isSwitch) {
this.scene.ui.setMode(Mode.MESSAGE);
}
this.scene.ui.showText(trappedAbMessages[0], null, () => {
this.scene.ui.showText("", 0);
if (!isSwitch) {
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
}
}, null, true);
}
}
break;
}
if (success!) { // TODO: is the bang correct?
this.end();
}
return success!; // TODO: is the bang correct?
}
cancel() {
if (this.fieldIndex) {
this.scene.unshiftPhase(new CommandPhase(this.scene, 0));
this.scene.unshiftPhase(new CommandPhase(this.scene, 1));
this.end();
}
}
checkFightOverride(): boolean {
const pokemon = this.getPokemon();
const encoreTag = pokemon.getTag(EncoreTag) as EncoreTag;
if (!encoreTag) {
return false;
}
const moveIndex = pokemon.getMoveset().findIndex(m => m?.moveId === encoreTag.moveId);
if (moveIndex === -1 || !pokemon.getMoveset()[moveIndex]!.isUsable(pokemon)) { // TODO: is this bang correct?
return false;
}
this.handleCommand(Command.FIGHT, moveIndex, false);
return true;
}
getFieldIndex(): integer {
return this.fieldIndex;
}
getPokemon(): PlayerPokemon {
return this.scene.getPlayerField()[this.fieldIndex];
}
end() {
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
}
}

View File

@ -0,0 +1,26 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { CommonAnim, CommonBattleAnim } from "#app/data/battle-anims.js";
import { PokemonPhase } from "./pokemon-phase";
export class CommonAnimPhase extends PokemonPhase {
private anim: CommonAnim | null;
private targetIndex: integer | undefined;
constructor(scene: BattleScene, battlerIndex?: BattlerIndex, targetIndex?: BattlerIndex | undefined, anim?: CommonAnim) {
super(scene, battlerIndex);
this.anim = anim!; // TODO: is this bang correct?
this.targetIndex = targetIndex;
}
setAnimation(anim: CommonAnim) {
this.anim = anim;
}
start() {
new CommonBattleAnim(this.anim, this.getPokemon(), this.targetIndex !== undefined ? (this.player ? this.scene.getEnemyField() : this.scene.getPlayerField())[this.targetIndex] : this.getPokemon()).play(this.scene, () => {
this.end();
});
}
}

View File

@ -0,0 +1,84 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { BattleSpec } from "#app/enums/battle-spec.js";
import { DamageResult, HitResult } from "#app/field/pokemon.js";
import * as Utils from "#app/utils.js";
import { PokemonPhase } from "./pokemon-phase";
export class DamagePhase extends PokemonPhase {
private amount: integer;
private damageResult: DamageResult;
private critical: boolean;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, amount: integer, damageResult?: DamageResult, critical: boolean = false) {
super(scene, battlerIndex);
this.amount = amount;
this.damageResult = damageResult || HitResult.EFFECTIVE;
this.critical = critical;
}
start() {
super.start();
if (this.damageResult === HitResult.ONE_HIT_KO) {
if (this.scene.moveAnimations) {
this.scene.toggleInvert(true);
}
this.scene.time.delayedCall(Utils.fixedInt(1000), () => {
this.scene.toggleInvert(false);
this.applyDamage();
});
return;
}
this.applyDamage();
}
updateAmount(amount: integer): void {
this.amount = amount;
}
applyDamage() {
switch (this.damageResult) {
case HitResult.EFFECTIVE:
this.scene.playSound("hit");
break;
case HitResult.SUPER_EFFECTIVE:
case HitResult.ONE_HIT_KO:
this.scene.playSound("hit_strong");
break;
case HitResult.NOT_VERY_EFFECTIVE:
this.scene.playSound("hit_weak");
break;
}
if (this.amount) {
this.scene.damageNumberHandler.add(this.getPokemon(), this.amount, this.damageResult, this.critical);
}
if (this.damageResult !== HitResult.OTHER) {
const flashTimer = this.scene.time.addEvent({
delay: 100,
repeat: 5,
startAt: 200,
callback: () => {
this.getPokemon().getSprite().setVisible(flashTimer.repeatCount % 2 === 0);
if (!flashTimer.repeatCount) {
this.getPokemon().updateInfo().then(() => this.end());
}
}
});
} else {
this.getPokemon().updateInfo().then(() => this.end());
}
}
override end() {
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
this.scene.initFinalBossPhaseTwo(this.getPokemon());
} else {
super.end();
}
}
}

View File

@ -1,18 +1,17 @@
import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import BattleScene, { AnySound } from "#app/battle-scene.js";
import { Egg, EGG_SEED } from "#app/data/egg.js";
import { EggCountChangedEvent } from "#app/events/egg.js";
import { PlayerPokemon } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { Phase } from "#app/phase.js";
import { achvs } from "#app/system/achv.js";
import EggCounterContainer from "#app/ui/egg-counter-container.js";
import EggHatchSceneHandler from "#app/ui/egg-hatch-scene-handler.js";
import PokemonInfoContainer from "#app/ui/pokemon-info-container.js";
import { Mode } from "#app/ui/ui.js";
import i18next from "i18next"; import i18next from "i18next";
import { Phase } from "./phase"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import BattleScene, { AnySound } from "./battle-scene"; import * as Utils from "#app/utils.js";
import * as Utils from "./utils";
import { Mode } from "./ui/ui";
import { EGG_SEED, Egg } from "./data/egg";
import EggHatchSceneHandler from "./ui/egg-hatch-scene-handler";
import { PlayerPokemon } from "./field/pokemon";
import { achvs } from "./system/achv";
import PokemonInfoContainer from "./ui/pokemon-info-container";
import EggCounterContainer from "./ui/egg-counter-container";
import { EggCountChangedEvent } from "./events/egg";
import { getPokemonNameWithAffix } from "./messages";
/** /**
* Class that represents egg hatching * Class that represents egg hatching
*/ */

View File

@ -0,0 +1,35 @@
import BattleScene from "#app/battle-scene.js";
import { Egg } from "#app/data/egg.js";
import { Phase } from "#app/phase.js";
import i18next from "i18next";
import Overrides from "#app/overrides";
import { EggHatchPhase } from "./egg-hatch-phase";
export class EggLapsePhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
const eggsToHatch: Egg[] = this.scene.gameData.eggs.filter((egg: Egg) => {
return Overrides.EGG_IMMEDIATE_HATCH_OVERRIDE ? true : --egg.hatchWaves < 1;
});
let eggCount: integer = eggsToHatch.length;
if (eggCount) {
this.scene.queueMessage(i18next.t("battle:eggHatching"));
for (const egg of eggsToHatch) {
this.scene.unshiftPhase(new EggHatchPhase(this.scene, egg, eggCount));
if (eggCount > 0) {
eggCount--;
}
}
}
this.end();
}
}

View File

@ -0,0 +1,379 @@
import BattleScene from "#app/battle-scene.js";
import { BattleType, BattlerIndex } from "#app/battle.js";
import { applyAbAttrs, SyncEncounterNatureAbAttr } from "#app/data/ability.js";
import { getCharVariantFromDialogue } from "#app/data/dialogue.js";
import { TrainerSlot } from "#app/data/trainer-config.js";
import { getRandomWeatherType } from "#app/data/weather.js";
import { BattleSpec } from "#app/enums/battle-spec.js";
import { PlayerGender } from "#app/enums/player-gender.js";
import { Species } from "#app/enums/species.js";
import { EncounterPhaseEvent } from "#app/events/battle-scene.js";
import Pokemon, { FieldPosition } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { regenerateModifierPoolThresholds, ModifierPoolType } from "#app/modifier/modifier-type.js";
import { IvScannerModifier, TurnHeldItemTransferModifier } from "#app/modifier/modifier.js";
import { achvs } from "#app/system/achv.js";
import { handleTutorial, Tutorial } from "#app/tutorial.js";
import { Mode } from "#app/ui/ui.js";
import i18next from "i18next";
import { BattlePhase } from "./battle-phase";
import * as Utils from "#app/utils.js";
import { CheckSwitchPhase } from "./check-switch-phase";
import { GameOverPhase } from "./game-over-phase";
import { PostSummonPhase } from "./post-summon-phase";
import { ReturnPhase } from "./return-phase";
import { ScanIvsPhase } from "./scan-ivs-phase";
import { ShinySparklePhase } from "./shiny-sparkle-phase";
import { SummonPhase } from "./summon-phase";
import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
export class EncounterPhase extends BattlePhase {
private loaded: boolean;
constructor(scene: BattleScene, loaded?: boolean) {
super(scene);
this.loaded = !!loaded;
}
start() {
super.start();
this.scene.updateGameInfo();
this.scene.initSession();
this.scene.eventTarget.dispatchEvent(new EncounterPhaseEvent());
// Failsafe if players somehow skip floor 200 in classic mode
if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex > 200) {
this.scene.unshiftPhase(new GameOverPhase(this.scene));
}
const loadEnemyAssets: Promise<void>[] = [];
const battle = this.scene.currentBattle;
let totalBst = 0;
battle.enemyLevels?.forEach((level, e) => {
if (!this.loaded) {
if (battle.battleType === BattleType.TRAINER) {
battle.enemyParty[e] = battle.trainer?.genPartyMember(e)!; // TODO:: is the bang correct here?
} else {
const enemySpecies = this.scene.randomSpecies(battle.waveIndex, level, true);
battle.enemyParty[e] = this.scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, !!this.scene.getEncounterBossSegments(battle.waveIndex, level, enemySpecies));
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
battle.enemyParty[e].ivs = new Array(6).fill(31);
}
this.scene.getParty().slice(0, !battle.double ? 1 : 2).reverse().forEach(playerPokemon => {
applyAbAttrs(SyncEncounterNatureAbAttr, playerPokemon, null, battle.enemyParty[e]);
});
}
}
const enemyPokemon = this.scene.getEnemyParty()[e];
if (e < (battle.double ? 2 : 1)) {
enemyPokemon.setX(-66 + enemyPokemon.getFieldPositionOffset()[0]);
enemyPokemon.resetSummonData();
}
if (!this.loaded) {
this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER);
}
if (enemyPokemon.species.speciesId === Species.ETERNATUS) {
if (this.scene.gameMode.isClassic && (battle.battleSpec === BattleSpec.FINAL_BOSS || this.scene.gameMode.isWaveFinal(battle.waveIndex))) {
if (battle.battleSpec !== BattleSpec.FINAL_BOSS) {
enemyPokemon.formIndex = 1;
enemyPokemon.updateScale();
}
enemyPokemon.setBoss();
} else if (!(battle.waveIndex % 1000)) {
enemyPokemon.formIndex = 1;
enemyPokemon.updateScale();
const bossMBH = this.scene.findModifier(m => m instanceof TurnHeldItemTransferModifier && m.pokemonId === enemyPokemon.id, false) as TurnHeldItemTransferModifier;
this.scene.removeModifier(bossMBH!);
bossMBH?.setTransferrableFalse();
this.scene.addEnemyModifier(bossMBH!);
}
}
totalBst += enemyPokemon.getSpeciesForm().baseTotal;
loadEnemyAssets.push(enemyPokemon.loadAssets());
console.log(getPokemonNameWithAffix(enemyPokemon), enemyPokemon.species.speciesId, enemyPokemon.stats);
});
if (this.scene.getParty().filter(p => p.isShiny()).length === 6) {
this.scene.validateAchv(achvs.SHINY_PARTY);
}
if (battle.battleType === BattleType.TRAINER) {
loadEnemyAssets.push(battle.trainer?.loadAssets().then(() => battle.trainer?.initSprite())!); // TODO: is this bang correct?
} else {
// This block only applies for double battles to init the boss segments (idk why it's split up like this)
if (battle.enemyParty.filter(p => p.isBoss()).length > 1) {
for (const enemyPokemon of battle.enemyParty) {
// If the enemy pokemon is a boss and wasn't populated from data source, then set it up
if (enemyPokemon.isBoss() && !enemyPokemon.isPopulatedFromDataSource) {
enemyPokemon.setBoss(true, Math.ceil(enemyPokemon.bossSegments * (enemyPokemon.getSpeciesForm().baseTotal / totalBst)));
enemyPokemon.initBattleInfo();
}
}
}
}
Promise.all(loadEnemyAssets).then(() => {
battle.enemyParty.forEach((enemyPokemon, e) => {
if (e < (battle.double ? 2 : 1)) {
if (battle.battleType === BattleType.WILD) {
this.scene.field.add(enemyPokemon);
battle.seenEnemyPartyMemberIds.add(enemyPokemon.id);
const playerPokemon = this.scene.getPlayerPokemon();
if (playerPokemon?.visible) {
this.scene.field.moveBelow(enemyPokemon as Pokemon, playerPokemon);
}
enemyPokemon.tint(0, 0.5);
} else if (battle.battleType === BattleType.TRAINER) {
enemyPokemon.setVisible(false);
this.scene.currentBattle.trainer?.tint(0, 0.5);
}
if (battle.double) {
enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT);
}
}
});
if (!this.loaded) {
regenerateModifierPoolThresholds(this.scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD);
this.scene.generateEnemyModifiers();
}
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
if (!this.loaded) {
//@ts-ignore
this.scene.gameData.saveAll(this.scene, true, battle.waveIndex % 10 === 1 || this.scene.lastSavePlayTime >= 300).then(success => { // TODO: get rid of ts-ignore
this.scene.disableMenu = false;
if (!success) {
return this.scene.reset(true);
}
this.doEncounter();
});
} else {
this.doEncounter();
}
});
});
}
doEncounter() {
this.scene.playBgm(undefined, true);
this.scene.updateModifiers(false);
this.scene.setFieldScale(1);
/*if (startingWave > 10) {
for (let m = 0; m < Math.min(Math.floor(startingWave / 10), 99); m++)
this.scene.addModifier(getPlayerModifierTypeOptionsForWave((m + 1) * 10, 1, this.scene.getParty())[0].type.newModifier(), true);
this.scene.updateModifiers(true);
}*/
for (const pokemon of this.scene.getParty()) {
if (pokemon) {
pokemon.resetBattleData();
}
}
if (!this.loaded) {
this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false);
}
const enemyField = this.scene.getEnemyField();
this.scene.tweens.add({
targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(),
x: (_target, _key, value, fieldIndex: integer) => fieldIndex < 2 + (enemyField.length) ? value + 300 : value - 300,
duration: 2000,
onComplete: () => {
if (!this.tryOverrideForBattleSpec()) {
this.doEncounterCommon();
}
}
});
}
getEncounterMessage(): string {
const enemyField = this.scene.getEnemyField();
if (this.scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
return i18next.t("battle:bossAppeared", { bossName: getPokemonNameWithAffix(enemyField[0])});
}
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
if (this.scene.currentBattle.double) {
return i18next.t("battle:trainerAppearedDouble", { trainerName: this.scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) });
} else {
return i18next.t("battle:trainerAppeared", { trainerName: this.scene.currentBattle.trainer?.getName(TrainerSlot.NONE, true) });
}
}
return enemyField.length === 1
? i18next.t("battle:singleWildAppeared", { pokemonName: enemyField[0].getNameToRender() })
: i18next.t("battle:multiWildAppeared", { pokemonName1: enemyField[0].getNameToRender(), pokemonName2: enemyField[1].getNameToRender() });
}
doEncounterCommon(showEncounterMessage: boolean = true) {
const enemyField = this.scene.getEnemyField();
if (this.scene.currentBattle.battleType === BattleType.WILD) {
enemyField.forEach(enemyPokemon => {
enemyPokemon.untint(100, "Sine.easeOut");
enemyPokemon.cry();
enemyPokemon.showInfo();
if (enemyPokemon.isShiny()) {
this.scene.validateAchv(achvs.SEE_SHINY);
}
});
this.scene.updateFieldScale();
if (showEncounterMessage) {
this.scene.ui.showText(this.getEncounterMessage(), null, () => this.end(), 1500);
} else {
this.end();
}
} else if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
const trainer = this.scene.currentBattle.trainer;
trainer?.untint(100, "Sine.easeOut");
trainer?.playAnim();
const doSummon = () => {
this.scene.currentBattle.started = true;
this.scene.playBgm(undefined);
this.scene.pbTray.showPbTray(this.scene.getParty());
this.scene.pbTrayEnemy.showPbTray(this.scene.getEnemyParty());
const doTrainerSummon = () => {
this.hideEnemyTrainer();
const availablePartyMembers = this.scene.getEnemyParty().filter(p => !p.isFainted()).length;
this.scene.unshiftPhase(new SummonPhase(this.scene, 0, false));
if (this.scene.currentBattle.double && availablePartyMembers > 1) {
this.scene.unshiftPhase(new SummonPhase(this.scene, 1, false));
}
this.end();
};
if (showEncounterMessage) {
this.scene.ui.showText(this.getEncounterMessage(), null, doTrainerSummon, 1500, true);
} else {
doTrainerSummon();
}
};
const encounterMessages = this.scene.currentBattle.trainer?.getEncounterMessages();
if (!encounterMessages?.length) {
doSummon();
} else {
let message: string;
this.scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), this.scene.currentBattle.waveIndex);
message = message!; // tell TS compiler it's defined now
const showDialogueAndSummon = () => {
this.scene.ui.showDialogue(message, trainer?.getName(TrainerSlot.NONE, true), null, () => {
this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doSummon()));
});
};
if (this.scene.currentBattle.trainer?.config.hasCharSprite && !this.scene.ui.shouldSkipDialogue(message)) {
this.scene.showFieldOverlay(500).then(() => this.scene.charSprite.showCharacter(trainer?.getKey()!, getCharVariantFromDialogue(encounterMessages[0])).then(() => showDialogueAndSummon())); // TODO: is this bang correct?
} else {
showDialogueAndSummon();
}
}
}
}
end() {
const enemyField = this.scene.getEnemyField();
enemyField.forEach((enemyPokemon, e) => {
if (enemyPokemon.isShiny()) {
this.scene.unshiftPhase(new ShinySparklePhase(this.scene, BattlerIndex.ENEMY + e));
}
});
if (this.scene.currentBattle.battleType !== BattleType.TRAINER) {
enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => {
// if there is not a player party, we can't continue
if (!this.scene.getParty()?.length) {
return false;
}
// how many player pokemon are on the field ?
const pokemonsOnFieldCount = this.scene.getParty().filter(p => p.isOnField()).length;
// if it's a 2vs1, there will never be a 2nd pokemon on our field even
const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2);
// if it's a double, there should be 2, otherwise 1
if (this.scene.currentBattle.double) {
return pokemonsOnFieldCount === requiredPokemonsOnField;
}
return pokemonsOnFieldCount === 1;
}));
const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier);
if (ivScannerModifier) {
enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))));
}
}
if (!this.loaded) {
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle());
if (!availablePartyMembers[0].isOnField()) {
this.scene.pushPhase(new SummonPhase(this.scene, 0));
}
if (this.scene.currentBattle.double) {
if (availablePartyMembers.length > 1) {
this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, true));
if (!availablePartyMembers[1].isOnField()) {
this.scene.pushPhase(new SummonPhase(this.scene, 1));
}
}
} else {
if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) {
this.scene.pushPhase(new ReturnPhase(this.scene, 1));
}
this.scene.pushPhase(new ToggleDoublePositionPhase(this.scene, false));
}
if (this.scene.currentBattle.battleType !== BattleType.TRAINER && (this.scene.currentBattle.waveIndex > 1 || !this.scene.gameMode.isDaily)) {
const minPartySize = this.scene.currentBattle.double ? 2 : 1;
if (availablePartyMembers.length > minPartySize) {
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 0, this.scene.currentBattle.double));
if (this.scene.currentBattle.double) {
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 1, this.scene.currentBattle.double));
}
}
}
}
handleTutorial(this.scene, Tutorial.Access_Menu).then(() => super.end());
}
tryOverrideForBattleSpec(): boolean {
switch (this.scene.currentBattle.battleSpec) {
case BattleSpec.FINAL_BOSS:
const enemy = this.scene.getEnemyPokemon();
this.scene.ui.showText(this.getEncounterMessage(), null, () => {
const count = 5643853 + this.scene.gameData.gameStats.classicSessionsPlayed;
//The two lines below check if English ordinals (1st, 2nd, 3rd, Xth) are used and determine which one to use.
//Otherwise, it defaults to an empty string.
//As of 08-07-24: Spanish and Italian default to the English translations
const ordinalUse = ["en", "es", "it"];
const currentLanguage = i18next.resolvedLanguage ?? "en";
const ordinalIndex = (ordinalUse.includes(currentLanguage)) ? ["st", "nd", "rd"][((count + 90) % 100 - 10) % 10 - 1] ?? "th" : "";
const cycleCount = count.toLocaleString() + ordinalIndex;
const encounterDialogue = i18next.t(`${(this.scene.gameData.gender === PlayerGender.FEMALE) ? "PGF" : "PGM"}battleSpecDialogue:encounter`, {cycleCount: cycleCount});
this.scene.ui.showDialogue(encounterDialogue, enemy?.species.name, null, () => {
this.doEncounterCommon(false);
});
}, 1500, true);
return true;
}
return false;
}
}

View File

@ -0,0 +1,40 @@
import BattleScene from "#app/battle-scene.js";
import { PlayerGender } from "#app/enums/player-gender.js";
import { Phase } from "#app/phase.js";
import { addTextObject, TextStyle } from "#app/ui/text.js";
import i18next from "i18next";
export class EndCardPhase extends Phase {
public endCard: Phaser.GameObjects.Image;
public text: Phaser.GameObjects.Text;
constructor(scene: BattleScene) {
super(scene);
}
start(): void {
super.start();
this.scene.ui.getMessageHandler().bg.setVisible(false);
this.scene.ui.getMessageHandler().nameBoxContainer.setVisible(false);
this.endCard = this.scene.add.image(0, 0, `end_${this.scene.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}`);
this.endCard.setOrigin(0);
this.endCard.setScale(0.5);
this.scene.field.add(this.endCard);
this.text = addTextObject(this.scene, this.scene.game.canvas.width / 12, (this.scene.game.canvas.height / 6) - 16, i18next.t("battle:congratulations"), TextStyle.SUMMARY, { fontSize: "128px" });
this.text.setOrigin(0.5);
this.scene.field.add(this.text);
this.scene.ui.clearText();
this.scene.ui.fadeIn(1000).then(() => {
this.scene.ui.showText("", null, () => {
this.scene.ui.getMessageHandler().bg.setVisible(true);
this.end();
}, null, true);
});
}
}

View File

@ -0,0 +1,16 @@
import BattleScene from "#app/battle-scene.js";
import { Phase } from "#app/phase.js";
import { Mode } from "#app/ui/ui.js";
export class EndEvolutionPhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
this.scene.ui.setModeForceTransition(Mode.MESSAGE).then(() => this.end());
}
}

View File

@ -0,0 +1,86 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { applyCheckTrappedAbAttrs, CheckTrappedAbAttr } from "#app/data/ability.js";
import { TrappedTag } from "#app/data/battler-tags.js";
import { Command } from "#app/ui/command-ui-handler.js";
import * as Utils from "#app/utils.js";
import { FieldPhase } from "./field-phase";
/**
* Phase for determining an enemy AI's action for the next turn.
* During this phase, the enemy decides whether to switch (if it has a trainer)
* or to use a move from its moveset.
*
* For more information on how the Enemy AI works, see docs/enemy-ai.md
* @see {@linkcode Pokemon.getMatchupScore}
* @see {@linkcode EnemyPokemon.getNextMove}
*/
export class EnemyCommandPhase extends FieldPhase {
protected fieldIndex: integer;
constructor(scene: BattleScene, fieldIndex: integer) {
super(scene);
this.fieldIndex = fieldIndex;
}
start() {
super.start();
const enemyPokemon = this.scene.getEnemyField()[this.fieldIndex];
const battle = this.scene.currentBattle;
const trainer = battle.trainer;
/**
* If the enemy has a trainer, decide whether or not the enemy should switch
* to another member in its party.
*
* This block compares the active enemy Pokemon's {@linkcode Pokemon.getMatchupScore | matchup score}
* against the active player Pokemon with the enemy party's other non-fainted Pokemon. If a party
* member's matchup score is 3x the active enemy's score (or 2x for "boss" trainers),
* the enemy will switch to that Pokemon.
*/
if (trainer && !enemyPokemon.getMoveQueue().length) {
const opponents = enemyPokemon.getOpponents();
const trapTag = enemyPokemon.findTag(t => t instanceof TrappedTag) as TrappedTag;
const trapped = new Utils.BooleanHolder(false);
opponents.forEach(playerPokemon => applyCheckTrappedAbAttrs(CheckTrappedAbAttr, playerPokemon, trapped, enemyPokemon, true, []));
if (!trapTag && !trapped.value) {
const partyMemberScores = trainer.getPartyMemberMatchupScores(enemyPokemon.trainerSlot, true);
if (partyMemberScores.length) {
const matchupScores = opponents.map(opp => enemyPokemon.getMatchupScore(opp));
const matchupScore = matchupScores.reduce((total, score) => total += score, 0) / matchupScores.length;
const sortedPartyMemberScores = trainer.getSortedPartyMemberMatchupScores(partyMemberScores);
const switchMultiplier = 1 - (battle.enemySwitchCounter ? Math.pow(0.1, (1 / battle.enemySwitchCounter)) : 0);
if (sortedPartyMemberScores[0][1] * switchMultiplier >= matchupScore * (trainer.config.isBoss ? 2 : 3)) {
const index = trainer.getNextSummonIndex(enemyPokemon.trainerSlot, partyMemberScores);
battle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] =
{ command: Command.POKEMON, cursor: index, args: [false] };
battle.enemySwitchCounter++;
return this.end();
}
}
}
}
/** Select a move to use (and a target to use it against, if applicable) */
const nextMove = enemyPokemon.getNextMove();
this.scene.currentBattle.turnCommands[this.fieldIndex + BattlerIndex.ENEMY] =
{ command: Command.FIGHT, move: nextMove };
this.scene.currentBattle.enemySwitchCounter = Math.max(this.scene.currentBattle.enemySwitchCounter - 1, 0);
this.end();
}
}

View File

@ -0,0 +1,13 @@
import BattleScene from "#app/battle-scene.js";
import { EnemyPokemon } from "#app/field/pokemon.js";
import { PartyMemberPokemonPhase } from "./party-member-pokemon-phase";
export abstract class EnemyPartyMemberPokemonPhase extends PartyMemberPokemonPhase {
constructor(scene: BattleScene, partyMemberIndex: integer) {
super(scene, partyMemberIndex, false);
}
getEnemyPokemon(): EnemyPokemon {
return super.getPokemon() as EnemyPokemon;
}
}

View File

@ -1,16 +1,17 @@
import SoundFade from "phaser3-rex-plugins/plugins/soundfade"; import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import { Phase } from "./phase"; import { Phase } from "../phase";
import BattleScene from "./battle-scene"; import BattleScene from "../battle-scene";
import { SpeciesFormEvolution } from "./data/pokemon-evolutions"; import { SpeciesFormEvolution } from "../data/pokemon-evolutions";
import EvolutionSceneHandler from "./ui/evolution-scene-handler"; import EvolutionSceneHandler from "../ui/evolution-scene-handler";
import * as Utils from "./utils"; import * as Utils from "../utils";
import { Mode } from "./ui/ui"; import { Mode } from "../ui/ui";
import { LearnMovePhase } from "./phases"; import { cos, sin } from "../field/anims";
import { cos, sin } from "./field/anims"; import { PlayerPokemon } from "../field/pokemon";
import { PlayerPokemon } from "./field/pokemon"; import { getTypeRgb } from "../data/type";
import { getTypeRgb } from "./data/type";
import i18next from "i18next"; import i18next from "i18next";
import { getPokemonNameWithAffix } from "./messages"; import { getPokemonNameWithAffix } from "../messages";
import { LearnMovePhase } from "./learn-move-phase";
import { EndEvolutionPhase } from "./end-evolution-phase";
export class EvolutionPhase extends Phase { export class EvolutionPhase extends Phase {
protected pokemon: PlayerPokemon; protected pokemon: PlayerPokemon;
@ -530,16 +531,3 @@ export class EvolutionPhase extends Phase {
updateParticle(); updateParticle();
} }
} }
export class EndEvolutionPhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
this.scene.ui.setModeForceTransition(Mode.MESSAGE).then(() => this.end());
}
}

35
src/phases/exp-phase.ts Normal file
View File

@ -0,0 +1,35 @@
import BattleScene from "#app/battle-scene.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { ExpBoosterModifier } from "#app/modifier/modifier.js";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase";
import { LevelUpPhase } from "./level-up-phase";
export class ExpPhase extends PlayerPartyMemberPokemonPhase {
private expValue: number;
constructor(scene: BattleScene, partyMemberIndex: integer, expValue: number) {
super(scene, partyMemberIndex);
this.expValue = expValue;
}
start() {
super.start();
const pokemon = this.getPokemon();
const exp = new Utils.NumberHolder(this.expValue);
this.scene.applyModifiers(ExpBoosterModifier, true, exp);
exp.value = Math.floor(exp.value);
this.scene.ui.showText(i18next.t("battle:expGain", { pokemonName: getPokemonNameWithAffix(pokemon), exp: exp.value }), null, () => {
const lastLevel = pokemon.level;
pokemon.addExp(exp.value);
const newLevel = pokemon.level;
if (newLevel > lastLevel) {
this.scene.unshiftPhase(new LevelUpPhase(this.scene, this.partyMemberIndex, lastLevel, newLevel));
}
pokemon.updateInfo().then(() => this.end());
}, null, true);
}
}

171
src/phases/faint-phase.ts Normal file
View File

@ -0,0 +1,171 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex, BattleType } from "#app/battle.js";
import { applyPostFaintAbAttrs, PostFaintAbAttr, applyPostKnockOutAbAttrs, PostKnockOutAbAttr, applyPostVictoryAbAttrs, PostVictoryAbAttr } from "#app/data/ability.js";
import { BattlerTagLapseType } from "#app/data/battler-tags.js";
import { battleSpecDialogue } from "#app/data/dialogue.js";
import { allMoves, PostVictoryStatChangeAttr } from "#app/data/move.js";
import { BattleSpec } from "#app/enums/battle-spec.js";
import { StatusEffect } from "#app/enums/status-effect.js";
import { PokemonMove, EnemyPokemon, PlayerPokemon, HitResult } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { PokemonInstantReviveModifier } from "#app/modifier/modifier.js";
import i18next from "i18next";
import { DamagePhase } from "./damage-phase";
import { PokemonPhase } from "./pokemon-phase";
import { SwitchSummonPhase } from "./switch-summon-phase";
import { ToggleDoublePositionPhase } from "./toggle-double-position-phase";
import { GameOverPhase } from "./game-over-phase";
import { SwitchPhase } from "./switch-phase";
import { VictoryPhase } from "./victory-phase";
export class FaintPhase extends PokemonPhase {
private preventEndure: boolean;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, preventEndure?: boolean) {
super(scene, battlerIndex);
this.preventEndure = preventEndure!; // TODO: is this bang correct?
}
start() {
super.start();
if (!this.preventEndure) {
const instantReviveModifier = this.scene.applyModifier(PokemonInstantReviveModifier, this.player, this.getPokemon()) as PokemonInstantReviveModifier;
if (instantReviveModifier) {
if (!--instantReviveModifier.stackCount) {
this.scene.removeModifier(instantReviveModifier);
}
this.scene.updateModifiers(this.player);
return this.end();
}
}
if (!this.tryOverrideForBattleSpec()) {
this.doFaint();
}
}
doFaint(): void {
const pokemon = this.getPokemon();
// Track total times pokemon have been KO'd for supreme overlord/last respects
if (pokemon.isPlayer()) {
this.scene.currentBattle.playerFaints += 1;
} else {
this.scene.currentBattle.enemyFaints += 1;
}
this.scene.queueMessage(i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }), null, true);
if (pokemon.turnData?.attacksReceived?.length) {
const lastAttack = pokemon.turnData.attacksReceived[0];
applyPostFaintAbAttrs(PostFaintAbAttr, pokemon, this.scene.getPokemonById(lastAttack.sourceId)!, new PokemonMove(lastAttack.move).getMove(), lastAttack.result); // TODO: is this bang correct?
}
const alivePlayField = this.scene.getField(true);
alivePlayField.forEach(p => applyPostKnockOutAbAttrs(PostKnockOutAbAttr, p, pokemon));
if (pokemon.turnData?.attacksReceived?.length) {
const defeatSource = this.scene.getPokemonById(pokemon.turnData.attacksReceived[0].sourceId);
if (defeatSource?.isOnField()) {
applyPostVictoryAbAttrs(PostVictoryAbAttr, defeatSource);
const pvmove = allMoves[pokemon.turnData.attacksReceived[0].move];
const pvattrs = pvmove.getAttrs(PostVictoryStatChangeAttr);
if (pvattrs.length) {
for (const pvattr of pvattrs) {
pvattr.applyPostVictory(defeatSource, defeatSource, pvmove);
}
}
}
}
if (this.player) {
/** The total number of Pokemon in the player's party that can legally fight */
const legalPlayerPokemon = this.scene.getParty().filter(p => p.isAllowedInBattle());
/** The total number of legal player Pokemon that aren't currently on the field */
const legalPlayerPartyPokemon = legalPlayerPokemon.filter(p => !p.isActive(true));
if (!legalPlayerPokemon.length) {
/** If the player doesn't have any legal Pokemon, end the game */
this.scene.unshiftPhase(new GameOverPhase(this.scene));
} else if (this.scene.currentBattle.double && legalPlayerPokemon.length === 1 && legalPlayerPartyPokemon.length === 0) {
/**
* If the player has exactly one Pokemon in total at this point in a double battle, and that Pokemon
* is already on the field, unshift a phase that moves that Pokemon to center position.
*/
this.scene.unshiftPhase(new ToggleDoublePositionPhase(this.scene, true));
} else if (legalPlayerPartyPokemon.length > 0) {
/**
* If previous conditions weren't met, and the player has at least 1 legal Pokemon off the field,
* push a phase that prompts the player to summon a Pokemon from their party.
*/
this.scene.pushPhase(new SwitchPhase(this.scene, this.fieldIndex, true, false));
}
} else {
this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex));
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length;
if (hasReservePartyMember) {
this.scene.pushPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, -1, false, false, false));
}
}
}
// in double battles redirect potential moves off fainted pokemon
if (this.scene.currentBattle.double) {
const allyPokemon = pokemon.getAlly();
this.scene.redirectPokemonMoves(pokemon, allyPokemon);
}
pokemon.lapseTags(BattlerTagLapseType.FAINT);
this.scene.getField(true).filter(p => p !== pokemon).forEach(p => p.removeTagsBySourceId(pokemon.id));
pokemon.faintCry(() => {
if (pokemon instanceof PlayerPokemon) {
pokemon.addFriendship(-10);
}
pokemon.hideInfo();
this.scene.playSound("faint");
this.scene.tweens.add({
targets: pokemon,
duration: 500,
y: pokemon.y + 150,
ease: "Sine.easeIn",
onComplete: () => {
pokemon.setVisible(false);
pokemon.y -= 150;
pokemon.trySetStatus(StatusEffect.FAINT);
if (pokemon.isPlayer()) {
this.scene.currentBattle.removeFaintedParticipant(pokemon as PlayerPokemon);
} else {
this.scene.addFaintedEnemyScore(pokemon as EnemyPokemon);
this.scene.currentBattle.addPostBattleLoot(pokemon as EnemyPokemon);
}
this.scene.field.remove(pokemon);
this.end();
}
});
});
}
tryOverrideForBattleSpec(): boolean {
switch (this.scene.currentBattle.battleSpec) {
case BattleSpec.FINAL_BOSS:
if (!this.player) {
const enemy = this.getPokemon();
if (enemy.formIndex) {
this.scene.ui.showDialogue(battleSpecDialogue[BattleSpec.FINAL_BOSS].secondStageWin, enemy.species.name, null, () => this.doFaint());
} else {
// Final boss' HP threshold has been bypassed; cancel faint and force check for 2nd phase
enemy.hp++;
this.scene.unshiftPhase(new DamagePhase(this.scene, enemy.getBattlerIndex(), 0, HitResult.OTHER));
this.end();
}
return true;
}
}
return false;
}
}

44
src/phases/field-phase.ts Normal file
View File

@ -0,0 +1,44 @@
import { BattlerIndex } from "#app/battle.js";
import { TrickRoomTag } from "#app/data/arena-tag.js";
import { Stat } from "#app/enums/stat.js";
import Pokemon from "#app/field/pokemon.js";
import { BattlePhase } from "./battle-phase";
import * as Utils from "#app/utils.js";
type PokemonFunc = (pokemon: Pokemon) => void;
export abstract class FieldPhase extends BattlePhase {
getOrder(): BattlerIndex[] {
const playerField = this.scene.getPlayerField().filter(p => p.isActive()) as Pokemon[];
const enemyField = this.scene.getEnemyField().filter(p => p.isActive()) as Pokemon[];
// We shuffle the list before sorting so speed ties produce random results
let orderedTargets: Pokemon[] = playerField.concat(enemyField);
// We seed it with the current turn to prevent an inconsistency where it
// was varying based on how long since you last reloaded
this.scene.executeWithSeedOffset(() => {
orderedTargets = Utils.randSeedShuffle(orderedTargets);
}, this.scene.currentBattle.turn, this.scene.waveSeed);
orderedTargets.sort((a: Pokemon, b: Pokemon) => {
const aSpeed = a?.getBattleStat(Stat.SPD) || 0;
const bSpeed = b?.getBattleStat(Stat.SPD) || 0;
return bSpeed - aSpeed;
});
const speedReversed = new Utils.BooleanHolder(false);
this.scene.arena.applyTags(TrickRoomTag, speedReversed);
if (speedReversed.value) {
orderedTargets = orderedTargets.reverse();
}
return orderedTargets.map(t => t.getFieldIndex() + (!t.isPlayer() ? BattlerIndex.ENEMY : 0));
}
executeForAll(func: PokemonFunc): void {
const field = this.scene.getField(true).filter(p => p.summonData);
field.forEach(pokemon => func(pokemon));
}
}

View File

@ -1,17 +1,14 @@
import BattleScene from "./battle-scene"; import BattleScene from "../battle-scene";
import * as Utils from "./utils"; import * as Utils from "../utils";
import { SpeciesFormKey } from "./data/pokemon-species"; import { SpeciesFormKey } from "../data/pokemon-species";
import { achvs } from "./system/achv"; import { achvs } from "../system/achv";
import { SpeciesFormChange, getSpeciesFormChangeMessage } from "./data/pokemon-forms"; import { SpeciesFormChange, getSpeciesFormChangeMessage } from "../data/pokemon-forms";
import { EndEvolutionPhase, EvolutionPhase } from "./evolution-phase"; import { PlayerPokemon } from "../field/pokemon";
import Pokemon, { EnemyPokemon, PlayerPokemon } from "./field/pokemon"; import { Mode } from "../ui/ui";
import { Mode } from "./ui/ui"; import PartyUiHandler from "../ui/party-ui-handler";
import PartyUiHandler from "./ui/party-ui-handler"; import { getPokemonNameWithAffix } from "../messages";
import { BattleSpec } from "#enums/battle-spec"; import { EndEvolutionPhase } from "./end-evolution-phase";
import { BattlePhase, MovePhase, PokemonHealPhase } from "./phases"; import { EvolutionPhase } from "./evolution-phase";
import { getTypeRgb } from "./data/type";
import { getPokemonNameWithAffix } from "./messages";
import { SemiInvulnerableTag } from "./data/battler-tags";
export class FormChangePhase extends EvolutionPhase { export class FormChangePhase extends EvolutionPhase {
private formChange: SpeciesFormChange; private formChange: SpeciesFormChange;
@ -175,126 +172,3 @@ export class FormChangePhase extends EvolutionPhase {
} }
} }
} }
export class QuietFormChangePhase extends BattlePhase {
protected pokemon: Pokemon;
protected formChange: SpeciesFormChange;
constructor(scene: BattleScene, pokemon: Pokemon, formChange: SpeciesFormChange) {
super(scene);
this.pokemon = pokemon;
this.formChange = formChange;
}
start(): void {
super.start();
if (this.pokemon.formIndex === this.pokemon.species.forms.findIndex(f => f.formKey === this.formChange.formKey)) {
return this.end();
}
const preName = getPokemonNameWithAffix(this.pokemon);
if (!this.pokemon.isOnField() || this.pokemon.getTag(SemiInvulnerableTag)) {
this.pokemon.changeForm(this.formChange).then(() => {
this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500);
});
return;
}
const getPokemonSprite = () => {
const sprite = this.scene.addPokemonSprite(this.pokemon, this.pokemon.x + this.pokemon.getSprite().x, this.pokemon.y + this.pokemon.getSprite().y, "pkmn__sub");
sprite.setOrigin(0.5, 1);
sprite.play(this.pokemon.getBattleSpriteKey()).stop();
sprite.setPipeline(this.scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(this.pokemon.getTeraType()) });
[ "spriteColors", "fusionSpriteColors" ].map(k => {
if (this.pokemon.summonData?.speciesForm) {
k += "Base";
}
sprite.pipelineData[k] = this.pokemon.getSprite().pipelineData[k];
});
this.scene.field.add(sprite);
return sprite;
};
const [ pokemonTintSprite, pokemonFormTintSprite ] = [ getPokemonSprite(), getPokemonSprite() ];
this.pokemon.getSprite().on("animationupdate", (_anim, frame) => {
if (frame.textureKey === pokemonTintSprite.texture.key) {
pokemonTintSprite.setFrame(frame.textureFrame);
} else {
pokemonFormTintSprite.setFrame(frame.textureFrame);
}
});
pokemonTintSprite.setAlpha(0);
pokemonTintSprite.setTintFill(0xFFFFFF);
pokemonFormTintSprite.setVisible(false);
pokemonFormTintSprite.setTintFill(0xFFFFFF);
this.scene.playSound("PRSFX- Transform");
this.scene.tweens.add({
targets: pokemonTintSprite,
alpha: 1,
duration: 1000,
ease: "Cubic.easeIn",
onComplete: () => {
this.pokemon.setVisible(false);
this.pokemon.changeForm(this.formChange).then(() => {
pokemonFormTintSprite.setScale(0.01);
pokemonFormTintSprite.play(this.pokemon.getBattleSpriteKey()).stop();
pokemonFormTintSprite.setVisible(true);
this.scene.tweens.add({
targets: pokemonTintSprite,
delay: 250,
scale: 0.01,
ease: "Cubic.easeInOut",
duration: 500,
onComplete: () => pokemonTintSprite.destroy()
});
this.scene.tweens.add({
targets: pokemonFormTintSprite,
delay: 250,
scale: this.pokemon.getSpriteScale(),
ease: "Cubic.easeInOut",
duration: 500,
onComplete: () => {
this.pokemon.setVisible(true);
this.scene.tweens.add({
targets: pokemonFormTintSprite,
delay: 250,
alpha: 0,
ease: "Cubic.easeOut",
duration: 1000,
onComplete: () => {
pokemonTintSprite.setVisible(false);
this.scene.ui.showText(getSpeciesFormChangeMessage(this.pokemon, this.formChange, preName), null, () => this.end(), 1500);
}
});
}
});
});
}
});
}
end(): void {
if (this.pokemon.scene?.currentBattle.battleSpec === BattleSpec.FINAL_BOSS && this.pokemon instanceof EnemyPokemon) {
this.scene.playBgm();
this.scene.unshiftPhase(new PokemonHealPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getMaxHp(), null, false, false, false, true));
this.pokemon.findAndRemoveTags(() => true);
this.pokemon.bossSegments = 5;
this.pokemon.bossSegmentIndex = 4;
this.pokemon.initBattleInfo();
this.pokemon.cry();
const movePhase = this.scene.findPhase(p => p instanceof MovePhase && p.pokemon === this.pokemon) as MovePhase;
if (movePhase) {
movePhase.cancel();
}
}
super.end();
}
}

View File

@ -0,0 +1,27 @@
import BattleScene from "#app/battle-scene.js";
import { ModifierTypeFunc } from "#app/modifier/modifier-type.js";
import { Mode } from "#app/ui/ui.js";
import i18next from "i18next";
import { ModifierRewardPhase } from "./modifier-reward-phase";
export class GameOverModifierRewardPhase extends ModifierRewardPhase {
constructor(scene: BattleScene, modifierTypeFunc: ModifierTypeFunc) {
super(scene, modifierTypeFunc);
}
doReward(): Promise<void> {
return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier();
this.scene.addModifier(newModifier).then(() => {
this.scene.playSound("level_up_fanfare");
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.fadeIn(250).then(() => {
this.scene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => {
this.scene.time.delayedCall(1500, () => this.scene.arenaBg.setVisible(true));
resolve();
}, null, true, 1500);
});
});
});
}
}

View File

@ -0,0 +1,203 @@
import { clientSessionId } from "#app/account.js";
import BattleScene from "#app/battle-scene.js";
import { BattleType } from "#app/battle.js";
import { miscDialogue, getCharVariantFromDialogue } from "#app/data/dialogue.js";
import { pokemonEvolutions } from "#app/data/pokemon-evolutions.js";
import PokemonSpecies, { getPokemonSpecies } from "#app/data/pokemon-species.js";
import { trainerConfigs } from "#app/data/trainer-config.js";
import { PlayerGender } from "#app/enums/player-gender.js";
import { TrainerType } from "#app/enums/trainer-type.js";
import Pokemon from "#app/field/pokemon.js";
import { modifierTypes } from "#app/modifier/modifier-type.js";
import { achvs, ChallengeAchv } from "#app/system/achv.js";
import { Unlockables } from "#app/system/unlockables.js";
import { Mode } from "#app/ui/ui.js";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
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";
export class GameOverPhase extends BattlePhase {
private victory: boolean;
private firstRibbons: PokemonSpecies[] = [];
constructor(scene: BattleScene, victory?: boolean) {
super(scene);
this.victory = !!victory;
}
start() {
super.start();
// Failsafe if players somehow skip floor 200 in classic mode
if (this.scene.gameMode.isClassic && this.scene.currentBattle.waveIndex > 200) {
this.victory = true;
}
if (this.victory && this.scene.gameMode.isEndless) {
this.scene.ui.showDialogue(i18next.t("PGMmiscDialogue:ending_endless"), i18next.t("PGMmiscDialogue:ending_name"), 0, () => this.handleGameOver());
} else if (this.victory || !this.scene.enableRetries) {
this.handleGameOver();
} else {
this.scene.ui.showText(i18next.t("battle:retryBattle"), null, () => {
this.scene.ui.setMode(Mode.CONFIRM, () => {
this.scene.ui.fadeOut(1250).then(() => {
this.scene.reset();
this.scene.clearPhaseQueue();
this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => {
this.scene.pushPhase(new EncounterPhase(this.scene, true));
const availablePartyMembers = this.scene.getParty().filter(p => p.isAllowedInBattle()).length;
this.scene.pushPhase(new SummonPhase(this.scene, 0));
if (this.scene.currentBattle.double && availablePartyMembers > 1) {
this.scene.pushPhase(new SummonPhase(this.scene, 1));
}
if (this.scene.currentBattle.waveIndex > 1 && this.scene.currentBattle.battleType !== BattleType.TRAINER) {
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 0, this.scene.currentBattle.double));
if (this.scene.currentBattle.double && availablePartyMembers > 1) {
this.scene.pushPhase(new CheckSwitchPhase(this.scene, 1, this.scene.currentBattle.double));
}
}
this.scene.ui.fadeIn(1250);
this.end();
});
});
}, () => this.handleGameOver(), false, 0, 0, 1000);
});
}
}
handleGameOver(): void {
const doGameOver = (newClear: boolean) => {
this.scene.disableMenu = true;
this.scene.time.delayedCall(1000, () => {
let firstClear = false;
if (this.victory && newClear) {
if (this.scene.gameMode.isClassic) {
firstClear = this.scene.validateAchv(achvs.CLASSIC_VICTORY);
this.scene.validateAchv(achvs.UNEVOLVED_CLASSIC_VICTORY);
this.scene.gameData.gameStats.sessionsWon++;
for (const pokemon of this.scene.getParty()) {
this.awardRibbon(pokemon);
if (pokemon.species.getRootSpeciesId() !== pokemon.species.getRootSpeciesId(true)) {
this.awardRibbon(pokemon, true);
}
}
} else if (this.scene.gameMode.isDaily && newClear) {
this.scene.gameData.gameStats.dailyRunSessionsWon++;
}
}
const fadeDuration = this.victory ? 10000 : 5000;
this.scene.fadeOutBgm(fadeDuration, true);
const activeBattlers = this.scene.getField().filter(p => p?.isActive(true));
activeBattlers.map(p => p.hideInfo());
this.scene.ui.fadeOut(fadeDuration).then(() => {
activeBattlers.map(a => a.setVisible(false));
this.scene.setFieldScale(1, true);
this.scene.clearPhaseQueue();
this.scene.ui.clearText();
if (this.victory && this.scene.gameMode.isChallenge) {
this.scene.gameMode.challenges.forEach(c => this.scene.validateAchvs(ChallengeAchv, c));
}
const clear = (endCardPhase?: EndCardPhase) => {
if (newClear) {
this.handleUnlocks();
}
if (this.victory && newClear) {
for (const species of this.firstRibbons) {
this.scene.unshiftPhase(new RibbonModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PLUS, species));
}
if (!firstClear) {
this.scene.unshiftPhase(new GameOverModifierRewardPhase(this.scene, modifierTypes.VOUCHER_PREMIUM));
}
}
this.scene.pushPhase(new PostGameOverPhase(this.scene, endCardPhase));
this.end();
};
if (this.victory && this.scene.gameMode.isClassic) {
const message = miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1];
if (!this.scene.ui.shouldSkipDialogue(message)) {
this.scene.ui.fadeIn(500).then(() => {
this.scene.charSprite.showCharacter(`rival_${this.scene.gameData.gender === PlayerGender.FEMALE ? "m" : "f"}`, getCharVariantFromDialogue(miscDialogue.ending[this.scene.gameData.gender === PlayerGender.FEMALE ? 0 : 1])).then(() => {
this.scene.ui.showDialogue(message, this.scene.gameData.gender === PlayerGender.FEMALE ? trainerConfigs[TrainerType.RIVAL].name : trainerConfigs[TrainerType.RIVAL].nameFemale, null, () => {
this.scene.ui.fadeOut(500).then(() => {
this.scene.charSprite.hide().then(() => {
const endCardPhase = new EndCardPhase(this.scene);
this.scene.unshiftPhase(endCardPhase);
clear(endCardPhase);
});
});
});
});
});
} else {
const endCardPhase = new EndCardPhase(this.scene);
this.scene.unshiftPhase(endCardPhase);
clear(endCardPhase);
}
} else {
clear();
}
});
});
};
/* Added a local check to see if the game is running offline on victory
If Online, execute apiFetch as intended
If Offline, execute offlineNewClear(), a localStorage implementation of newClear daily run checks */
if (this.victory) {
if (!Utils.isLocal) {
Utils.apiFetch(`savedata/session/newclear?slot=${this.scene.sessionSlotId}&clientSessionId=${clientSessionId}`, true)
.then(response => response.json())
.then(newClear => doGameOver(newClear));
} else {
this.scene.gameData.offlineNewClear(this.scene).then(result => {
doGameOver(result);
});
}
} else {
doGameOver(false);
}
}
handleUnlocks(): void {
if (this.victory && this.scene.gameMode.isClassic) {
if (!this.scene.gameData.unlocks[Unlockables.ENDLESS_MODE]) {
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.ENDLESS_MODE));
}
if (this.scene.getParty().filter(p => p.fusionSpecies).length && !this.scene.gameData.unlocks[Unlockables.SPLICED_ENDLESS_MODE]) {
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.SPLICED_ENDLESS_MODE));
}
if (!this.scene.gameData.unlocks[Unlockables.MINI_BLACK_HOLE]) {
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.MINI_BLACK_HOLE));
}
if (!this.scene.gameData.unlocks[Unlockables.EVIOLITE] && this.scene.getParty().some(p => p.getSpeciesForm(true).speciesId in pokemonEvolutions)) {
this.scene.unshiftPhase(new UnlockPhase(this.scene, Unlockables.EVIOLITE));
}
}
}
awardRibbon(pokemon: Pokemon, forStarter: boolean = false): void {
const speciesId = getPokemonSpecies(pokemon.species.speciesId);
const speciesRibbonCount = this.scene.gameData.incrementRibbonCount(speciesId, forStarter);
// first time classic win, award voucher
if (speciesRibbonCount === 1) {
this.firstRibbons.push(getPokemonSpecies(pokemon.species.getRootSpeciesId(forStarter)));
}
}
}

View File

@ -0,0 +1,14 @@
import BattleScene from "#app/battle-scene.js";
import { BattlePhase } from "./battle-phase";
export class HidePartyExpBarPhase extends BattlePhase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
this.scene.partyExpBar.hide().then(() => this.end());
}
}

View File

@ -0,0 +1,103 @@
import BattleScene from "#app/battle-scene.js";
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims.js";
import { allMoves } from "#app/data/move.js";
import { SpeciesFormChangeMoveLearnedTrigger } from "#app/data/pokemon-forms.js";
import { Moves } from "#app/enums/moves.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import EvolutionSceneHandler from "#app/ui/evolution-scene-handler.js";
import { SummaryUiMode } from "#app/ui/summary-ui-handler.js";
import { Mode } from "#app/ui/ui.js";
import i18next from "i18next";
import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase";
export class LearnMovePhase extends PlayerPartyMemberPokemonPhase {
private moveId: Moves;
constructor(scene: BattleScene, partyMemberIndex: integer, moveId: Moves) {
super(scene, partyMemberIndex);
this.moveId = moveId;
}
start() {
super.start();
const pokemon = this.getPokemon();
const move = allMoves[this.moveId];
const existingMoveIndex = pokemon.getMoveset().findIndex(m => m?.moveId === move.id);
if (existingMoveIndex > -1) {
return this.end();
}
const emptyMoveIndex = pokemon.getMoveset().length < 4
? pokemon.getMoveset().length
: pokemon.getMoveset().findIndex(m => m === null);
const messageMode = this.scene.ui.getHandler() instanceof EvolutionSceneHandler
? Mode.EVOLUTION_SCENE
: Mode.MESSAGE;
if (emptyMoveIndex > -1) {
pokemon.setMove(emptyMoveIndex, this.moveId);
initMoveAnim(this.scene, this.moveId).then(() => {
loadMoveAnimAssets(this.scene, [this.moveId], true)
.then(() => {
this.scene.ui.setMode(messageMode).then(() => {
this.scene.playSound("level_up_fanfare");
this.scene.ui.showText(i18next.t("battle:learnMove", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => {
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeMoveLearnedTrigger, true);
this.end();
}, messageMode === Mode.EVOLUTION_SCENE ? 1000 : null, true);
});
});
});
} else {
this.scene.ui.setMode(messageMode).then(() => {
this.scene.ui.showText(i18next.t("battle:learnMovePrompt", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => {
this.scene.ui.showText(i18next.t("battle:learnMoveLimitReached", { pokemonName: getPokemonNameWithAffix(pokemon) }), null, () => {
this.scene.ui.showText(i18next.t("battle:learnMoveReplaceQuestion", { moveName: move.name }), null, () => {
const noHandler = () => {
this.scene.ui.setMode(messageMode).then(() => {
this.scene.ui.showText(i18next.t("battle:learnMoveStopTeaching", { moveName: move.name }), null, () => {
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
this.scene.ui.setMode(messageMode);
this.scene.ui.showText(i18next.t("battle:learnMoveNotLearned", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: move.name }), null, () => this.end(), null, true);
}, () => {
this.scene.ui.setMode(messageMode);
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId));
this.end();
});
});
});
};
this.scene.ui.setModeWithoutClear(Mode.CONFIRM, () => {
this.scene.ui.setMode(messageMode);
this.scene.ui.showText(i18next.t("battle:learnMoveForgetQuestion"), null, () => {
this.scene.ui.setModeWithoutClear(Mode.SUMMARY, this.getPokemon(), SummaryUiMode.LEARN_MOVE, move, (moveIndex: integer) => {
if (moveIndex === 4) {
noHandler();
return;
}
this.scene.ui.setMode(messageMode).then(() => {
this.scene.ui.showText(i18next.t("battle:countdownPoof"), null, () => {
this.scene.ui.showText(i18next.t("battle:learnMoveForgetSuccess", { pokemonName: getPokemonNameWithAffix(pokemon), moveName: pokemon.moveset[moveIndex]!.getName() }), null, () => { // TODO: is the bang correct?
this.scene.ui.showText(i18next.t("battle:learnMoveAnd"), null, () => {
pokemon.setMove(moveIndex, Moves.NONE);
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, this.moveId));
this.end();
}, null, true);
}, null, true);
}, null, true);
});
});
}, null, true);
}, noHandler);
});
}, null, true);
}, null, true);
});
}
}
}

View File

@ -0,0 +1,20 @@
import BattleScene from "#app/battle-scene.js";
import { Mode } from "#app/ui/ui.js";
import i18next from "i18next";
import { FieldPhase } from "./field-phase";
export class LevelCapPhase extends FieldPhase {
constructor(scene: BattleScene) {
super(scene);
}
start(): void {
super.start();
this.scene.ui.setMode(Mode.MESSAGE).then(() => {
this.scene.playSound("level_up_fanfare");
this.scene.ui.showText(i18next.t("battle:levelCapUp", { levelCap: this.scene.getMaxExpLevel() }), null, () => this.end(), null, true);
this.executeForAll(pokemon => pokemon.updateInfo(true));
});
}
}

View File

@ -0,0 +1,59 @@
import BattleScene from "#app/battle-scene.js";
import { ExpNotification } from "#app/enums/exp-notification.js";
import { EvolutionPhase } from "#app/phases/evolution-phase.js";
import { PlayerPokemon } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { LevelAchv } from "#app/system/achv.js";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { PlayerPartyMemberPokemonPhase } from "./player-party-member-pokemon-phase";
import { LearnMovePhase } from "./learn-move-phase";
export class LevelUpPhase extends PlayerPartyMemberPokemonPhase {
private lastLevel: integer;
private level: integer;
constructor(scene: BattleScene, partyMemberIndex: integer, lastLevel: integer, level: integer) {
super(scene, partyMemberIndex);
this.lastLevel = lastLevel;
this.level = level;
this.scene = scene;
}
start() {
super.start();
if (this.level > this.scene.gameData.gameStats.highestLevel) {
this.scene.gameData.gameStats.highestLevel = this.level;
}
this.scene.validateAchvs(LevelAchv, new Utils.IntegerHolder(this.level));
const pokemon = this.getPokemon();
const prevStats = pokemon.stats.slice(0);
pokemon.calculateStats();
pokemon.updateInfo();
if (this.scene.expParty === ExpNotification.DEFAULT) {
this.scene.playSound("level_up_fanfare");
this.scene.ui.showText(i18next.t("battle:levelUp", { pokemonName: getPokemonNameWithAffix(this.getPokemon()), level: this.level }), null, () => this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end()), null, true);
} else if (this.scene.expParty === ExpNotification.SKIP) {
this.end();
} else {
// we still want to display the stats if activated
this.scene.ui.getMessageHandler().promptLevelUpStats(this.partyMemberIndex, prevStats, false).then(() => this.end());
}
if (this.lastLevel < 100) { // this feels like an unnecessary optimization
const levelMoves = this.getPokemon().getLevelMoves(this.lastLevel + 1);
for (const lm of levelMoves) {
this.scene.unshiftPhase(new LearnMovePhase(this.scene, this.partyMemberIndex, lm[1]));
}
}
if (!pokemon.pauseEvolutions) {
const evolution = pokemon.getEvolution();
if (evolution) {
this.scene.unshiftPhase(new EvolutionPhase(this.scene, pokemon as PlayerPokemon, evolution, this.lastLevel));
}
}
}
}

116
src/phases/login-phase.ts Normal file
View File

@ -0,0 +1,116 @@
import { updateUserInfo } from "#app/account.js";
import BattleScene, { bypassLogin } from "#app/battle-scene.js";
import { Phase } from "#app/phase.js";
import { handleTutorial, Tutorial } from "#app/tutorial.js";
import { Mode } from "#app/ui/ui.js";
import i18next, { t } from "i18next";
import * as Utils from "#app/utils.js";
import { SelectGenderPhase } from "./select-gender-phase";
import { UnavailablePhase } from "./unavailable-phase";
export class LoginPhase extends Phase {
private showText: boolean;
constructor(scene: BattleScene, showText?: boolean) {
super(scene);
this.showText = showText === undefined || !!showText;
}
start(): void {
super.start();
const hasSession = !!Utils.getCookie(Utils.sessionIdKey);
this.scene.ui.setMode(Mode.LOADING, { buttonActions: [] });
Utils.executeIf(bypassLogin || hasSession, updateUserInfo).then(response => {
const success = response ? response[0] : false;
const statusCode = response ? response[1] : null;
if (!success) {
if (!statusCode || statusCode === 400) {
if (this.showText) {
this.scene.ui.showText(i18next.t("menu:logInOrCreateAccount"));
}
this.scene.playSound("menu_open");
const loadData = () => {
updateUserInfo().then(success => {
if (!success[0]) {
Utils.removeCookie(Utils.sessionIdKey);
this.scene.reset(true, true);
return;
}
this.scene.gameData.loadSystem().then(() => this.end());
});
};
this.scene.ui.setMode(Mode.LOGIN_FORM, {
buttonActions: [
() => {
this.scene.ui.playSelect();
loadData();
}, () => {
this.scene.playSound("menu_open");
this.scene.ui.setMode(Mode.REGISTRATION_FORM, {
buttonActions: [
() => {
this.scene.ui.playSelect();
updateUserInfo().then(success => {
if (!success[0]) {
Utils.removeCookie(Utils.sessionIdKey);
this.scene.reset(true, true);
return;
}
this.end();
} );
}, () => {
this.scene.unshiftPhase(new LoginPhase(this.scene, false));
this.end();
}
]
});
}, () => {
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/discord/callback`);
const discordId = import.meta.env.VITE_DISCORD_CLIENT_ID;
const discordUrl = `https://discord.com/api/oauth2/authorize?client_id=${discordId}&redirect_uri=${redirectUri}&response_type=code&scope=identify&prompt=none`;
window.open(discordUrl, "_self");
}, () => {
const redirectUri = encodeURIComponent(`${import.meta.env.VITE_SERVER_URL}/auth/google/callback`);
const googleId = import.meta.env.VITE_GOOGLE_CLIENT_ID;
const googleUrl = `https://accounts.google.com/o/oauth2/auth?client_id=${googleId}&redirect_uri=${redirectUri}&response_type=code&scope=openid`;
window.open(googleUrl, "_self");
}
]
});
} else if (statusCode === 401) {
Utils.removeCookie(Utils.sessionIdKey);
this.scene.reset(true, true);
} else {
this.scene.unshiftPhase(new UnavailablePhase(this.scene));
super.end();
}
return null;
} else {
this.scene.gameData.loadSystem().then(success => {
if (success || bypassLogin) {
this.end();
} else {
this.scene.ui.setMode(Mode.MESSAGE);
this.scene.ui.showText(t("menu:failedToLoadSaveData"));
}
});
}
});
}
end(): void {
this.scene.ui.setMode(Mode.MESSAGE);
if (!this.scene.gameData.gender) {
this.scene.unshiftPhase(new SelectGenderPhase(this.scene));
}
handleTutorial(this.scene, Tutorial.Intro).then(() => super.end());
}
}

View File

@ -0,0 +1,38 @@
import BattleScene from "#app/battle-scene.js";
import { Phase } from "#app/phase.js";
export class MessagePhase extends Phase {
private text: string;
private callbackDelay: integer | null;
private prompt: boolean | null;
private promptDelay: integer | null;
constructor(scene: BattleScene, text: string, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null) {
super(scene);
this.text = text;
this.callbackDelay = callbackDelay!; // TODO: is this bang correct?
this.prompt = prompt!; // TODO: is this bang correct?
this.promptDelay = promptDelay!; // TODO: is this bang correct?
}
start() {
super.start();
if (this.text.indexOf("$") > -1) {
const pageIndex = this.text.indexOf("$");
this.scene.unshiftPhase(new MessagePhase(this.scene, this.text.slice(pageIndex + 1), this.callbackDelay, this.prompt, this.promptDelay));
this.text = this.text.slice(0, pageIndex).trim();
}
this.scene.ui.showText(this.text, null, () => this.end(), this.callbackDelay || (this.prompt ? 0 : 1500), this.prompt, this.promptDelay);
}
end() {
if (this.scene.abilityBar.shown) {
this.scene.abilityBar.hide();
}
super.end();
}
}

View File

@ -0,0 +1,30 @@
import BattleScene from "#app/battle-scene.js";
import { ModifierType, ModifierTypeFunc, getModifierType } from "#app/modifier/modifier-type.js";
import i18next from "i18next";
import { BattlePhase } from "./battle-phase";
export class ModifierRewardPhase extends BattlePhase {
protected modifierType: ModifierType;
constructor(scene: BattleScene, modifierTypeFunc: ModifierTypeFunc) {
super(scene);
this.modifierType = getModifierType(modifierTypeFunc);
}
start() {
super.start();
this.doReward().then(() => this.end());
}
doReward(): Promise<void> {
return new Promise<void>(resolve => {
const newModifier = this.modifierType.newModifier();
this.scene.addModifier(newModifier).then(() => {
this.scene.playSound("item_fanfare");
this.scene.ui.showText(i18next.t("battle:rewardGain", { modifierName: newModifier?.type.name }), null, () => resolve(), null, true);
});
});
}
}

View File

@ -0,0 +1,34 @@
import BattleScene from "#app/battle-scene.js";
import { ArenaTagType } from "#app/enums/arena-tag-type.js";
import { MoneyMultiplierModifier } from "#app/modifier/modifier.js";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { BattlePhase } from "./battle-phase";
export class MoneyRewardPhase extends BattlePhase {
private moneyMultiplier: number;
constructor(scene: BattleScene, moneyMultiplier: number) {
super(scene);
this.moneyMultiplier = moneyMultiplier;
}
start() {
const moneyAmount = new Utils.IntegerHolder(this.scene.getWaveMoneyAmount(this.moneyMultiplier));
this.scene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount);
if (this.scene.arena.getTag(ArenaTagType.HAPPY_HOUR)) {
moneyAmount.value *= 2;
}
this.scene.addMoney(moneyAmount.value);
const userLocale = navigator.language || "en-US";
const formattedMoneyAmount = moneyAmount.value.toLocaleString(userLocale);
const message = i18next.t("battle:moneyWon", { moneyAmount: formattedMoneyAmount });
this.scene.ui.showText(message, null, () => this.end(), null, true);
}
}

View File

@ -0,0 +1,44 @@
import BattleScene from "#app/battle-scene.js";
import { initMoveAnim, loadMoveAnimAssets, MoveAnim } from "#app/data/battle-anims.js";
import { allMoves, SelfStatusMove } from "#app/data/move.js";
import { Moves } from "#app/enums/moves.js";
import * as Utils from "#app/utils.js";
import { BattlePhase } from "./battle-phase";
export class MoveAnimTestPhase extends BattlePhase {
private moveQueue: Moves[];
constructor(scene: BattleScene, moveQueue?: Moves[]) {
super(scene);
this.moveQueue = moveQueue || Utils.getEnumValues(Moves).slice(1);
}
start() {
const moveQueue = this.moveQueue.slice(0);
this.playMoveAnim(moveQueue, true);
}
playMoveAnim(moveQueue: Moves[], player: boolean) {
const moveId = player ? moveQueue[0] : moveQueue.shift();
if (moveId === undefined) {
this.playMoveAnim(this.moveQueue.slice(0), true);
return;
} else if (player) {
console.log(Moves[moveId]);
}
initMoveAnim(this.scene, moveId).then(() => {
loadMoveAnimAssets(this.scene, [moveId], true)
.then(() => {
new MoveAnim(moveId, player ? this.scene.getPlayerPokemon()! : this.scene.getEnemyPokemon()!, (player !== (allMoves[moveId] instanceof SelfStatusMove) ? this.scene.getEnemyPokemon()! : this.scene.getPlayerPokemon()!).getBattlerIndex()).play(this.scene, () => { // TODO: are the bangs correct here?
if (player) {
this.playMoveAnim(moveQueue, false);
} else {
this.playMoveAnim(moveQueue, true);
}
});
});
});
}
}

View File

@ -0,0 +1,447 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { applyPreAttackAbAttrs, AddSecondStrikeAbAttr, IgnoreMoveEffectsAbAttr, applyPostDefendAbAttrs, PostDefendAbAttr, applyPostAttackAbAttrs, PostAttackAbAttr, MaxMultiHitAbAttr, AlwaysHitAbAttr } from "#app/data/ability.js";
import { ArenaTagSide, ConditionalProtectTag } from "#app/data/arena-tag.js";
import { MoveAnim } from "#app/data/battle-anims.js";
import { BattlerTagLapseType, ProtectedTag, SemiInvulnerableTag } from "#app/data/battler-tags.js";
import { MoveTarget, applyMoveAttrs, OverrideMoveEffectAttr, MultiHitAttr, AttackMove, FixedDamageAttr, VariableTargetAttr, MissEffectAttr, MoveFlags, applyFilteredMoveAttrs, MoveAttr, MoveEffectAttr, MoveEffectTrigger, ChargeAttr, MoveCategory, NoEffectAttr, HitsTagAttr } from "#app/data/move.js";
import { SpeciesFormChangePostMoveTrigger } from "#app/data/pokemon-forms.js";
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { Moves } from "#app/enums/moves.js";
import Pokemon, { PokemonMove, MoveResult, HitResult } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { PokemonMultiHitModifier, FlinchChanceModifier, EnemyAttackStatusEffectChanceModifier, ContactHeldItemTransferChanceModifier, HitHealModifier } from "#app/modifier/modifier.js";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { PokemonPhase } from "./pokemon-phase";
export class MoveEffectPhase extends PokemonPhase {
public move: PokemonMove;
protected targets: BattlerIndex[];
constructor(scene: BattleScene, battlerIndex: BattlerIndex, targets: BattlerIndex[], move: PokemonMove) {
super(scene, battlerIndex);
this.move = move;
/**
* In double battles, if the right Pokemon selects a spread move and the left Pokemon dies
* with no party members available to switch in, then the right Pokemon takes the index
* of the left Pokemon and gets hit unless this is checked.
*/
if (targets.includes(battlerIndex) && this.move.getMove().moveTarget === MoveTarget.ALL_NEAR_OTHERS) {
const i = targets.indexOf(battlerIndex);
targets.splice(i, i + 1);
}
this.targets = targets;
}
start() {
super.start();
/** The Pokemon using this phase's invoked move */
const user = this.getUserPokemon();
/** All Pokemon targeted by this phase's invoked move */
const targets = this.getTargets();
/** If the user was somehow removed from the field, end this phase */
if (!user?.isOnField()) {
return super.end();
}
/**
* Does an effect from this move override other effects on this turn?
* e.g. Charging moves (Fly, etc.) on their first turn of use.
*/
const overridden = new Utils.BooleanHolder(false);
/** The {@linkcode Move} object from {@linkcode allMoves} invoked by this phase */
const move = this.move.getMove();
// Assume single target for override
applyMoveAttrs(OverrideMoveEffectAttr, user, this.getTarget() ?? null, move, overridden, this.move.virtual).then(() => {
// If other effects were overriden, stop this phase before they can be applied
if (overridden.value) {
return this.end();
}
user.lapseTags(BattlerTagLapseType.MOVE_EFFECT);
/**
* If this phase is for the first hit of the invoked move,
* resolve the move's total hit count. This block combines the
* effects of the move itself, Parental Bond, and Multi-Lens to do so.
*/
if (user.turnData.hitsLeft === undefined) {
const hitCount = new Utils.IntegerHolder(1);
// Assume single target for multi hit
applyMoveAttrs(MultiHitAttr, user, this.getTarget() ?? null, move, hitCount);
// If Parental Bond is applicable, double the hit count
applyPreAttackAbAttrs(AddSecondStrikeAbAttr, user, null, move, targets.length, hitCount, new Utils.IntegerHolder(0));
// If Multi-Lens is applicable, multiply the hit count by 1 + the number of Multi-Lenses held by the user
if (move instanceof AttackMove && !move.hasAttr(FixedDamageAttr)) {
this.scene.applyModifiers(PokemonMultiHitModifier, user.isPlayer(), user, hitCount, new Utils.IntegerHolder(0));
}
// Set the user's relevant turnData fields to reflect the final hit count
user.turnData.hitCount = hitCount.value;
user.turnData.hitsLeft = hitCount.value;
}
/**
* Log to be entered into the user's move history once the move result is resolved.
* Note that `result` (a {@linkcode MoveResult}) logs whether the move was successfully
* used in the sense of "Does it have an effect on the user?".
*/
const moveHistoryEntry = { move: this.move.moveId, targets: this.targets, result: MoveResult.PENDING, virtual: this.move.virtual };
/**
* Stores results of hit checks of the invoked move against all targets, organized by battler index.
* @see {@linkcode hitCheck}
*/
const targetHitChecks = Object.fromEntries(targets.map(p => [p.getBattlerIndex(), this.hitCheck(p)]));
const hasActiveTargets = targets.some(t => t.isActive(true));
/**
* If no targets are left for the move to hit (FAIL), or the invoked move is single-target
* (and not random target) and failed the hit check against its target (MISS), log the move
* as FAILed or MISSed (depending on the conditions above) and end this phase.
*/
if (!hasActiveTargets || (!move.hasAttr(VariableTargetAttr) && !move.isMultiTarget() && !targetHitChecks[this.targets[0]])) {
this.stopMultiHit();
if (hasActiveTargets) {
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: this.getTarget()? getPokemonNameWithAffix(this.getTarget()!) : "" }));
moveHistoryEntry.result = MoveResult.MISS;
applyMoveAttrs(MissEffectAttr, user, null, move);
} else {
this.scene.queueMessage(i18next.t("battle:attackFailed"));
moveHistoryEntry.result = MoveResult.FAIL;
}
user.pushMoveHistory(moveHistoryEntry);
return this.end();
}
/** All move effect attributes are chained together in this array to be applied asynchronously. */
const applyAttrs: Promise<void>[] = [];
// Move animation only needs one target
new MoveAnim(move.id as Moves, user, this.getTarget()?.getBattlerIndex()!).play(this.scene, () => { // TODO: is the bang correct here?
/** Has the move successfully hit a target (for damage) yet? */
let hasHit: boolean = false;
for (const target of targets) {
/**
* If the move missed a target, stop all future hits against that target
* and move on to the next target (if there is one).
*/
if (!targetHitChecks[target.getBattlerIndex()]) {
this.stopMultiHit(target);
this.scene.queueMessage(i18next.t("battle:attackMissed", { pokemonNameWithAffix: getPokemonNameWithAffix(target) }));
if (moveHistoryEntry.result === MoveResult.PENDING) {
moveHistoryEntry.result = MoveResult.MISS;
}
user.pushMoveHistory(moveHistoryEntry);
applyMoveAttrs(MissEffectAttr, user, null, move);
continue;
}
/** The {@linkcode ArenaTagSide} to which the target belongs */
const targetSide = target.isPlayer() ? ArenaTagSide.PLAYER : ArenaTagSide.ENEMY;
/** Has the invoked move been cancelled by conditional protection (e.g Quick Guard)? */
const hasConditionalProtectApplied = new Utils.BooleanHolder(false);
/** Does the applied conditional protection bypass Protect-ignoring effects? */
const bypassIgnoreProtect = new Utils.BooleanHolder(false);
// If the move is not targeting a Pokemon on the user's side, try to apply conditional protection effects
if (!this.move.getMove().isAllyTarget()) {
this.scene.arena.applyTagsForSide(ConditionalProtectTag, targetSide, hasConditionalProtectApplied, user, target, move.id, bypassIgnoreProtect);
}
/** Is the target protected by Protect, etc. or a relevant conditional protection effect? */
const isProtected = (bypassIgnoreProtect.value || !this.move.getMove().checkFlag(MoveFlags.IGNORE_PROTECT, user, target))
&& (hasConditionalProtectApplied.value || target.findTags(t => t instanceof ProtectedTag).find(t => target.lapseTag(t.tagType)));
/** Does this phase represent the invoked move's first strike? */
const firstHit = (user.turnData.hitsLeft === user.turnData.hitCount);
// Only log the move's result on the first strike
if (firstHit) {
user.pushMoveHistory(moveHistoryEntry);
}
/**
* Since all fail/miss checks have applied, the move is considered successfully applied.
* It's worth noting that if the move has no effect or is protected against, this assignment
* is overwritten and the move is logged as a FAIL.
*/
moveHistoryEntry.result = MoveResult.SUCCESS;
/**
* Stores the result of applying the invoked move to the target.
* If the target is protected, the result is always `NO_EFFECT`.
* Otherwise, the hit result is based on type effectiveness, immunities,
* and other factors that may negate the attack or status application.
*
* Internally, the call to {@linkcode Pokemon.apply} is where damage is calculated
* (for attack moves) and the target's HP is updated. However, this isn't
* made visible to the user until the resulting {@linkcode DamagePhase}
* is invoked.
*/
const hitResult = !isProtected ? target.apply(user, move) : HitResult.NO_EFFECT;
/** Does {@linkcode hitResult} indicate that damage was dealt to the target? */
const dealsDamage = [
HitResult.EFFECTIVE,
HitResult.SUPER_EFFECTIVE,
HitResult.NOT_VERY_EFFECTIVE,
HitResult.ONE_HIT_KO
].includes(hitResult);
/** Is this target the first one hit by the move on its current strike? */
const firstTarget = dealsDamage && !hasHit;
if (firstTarget) {
hasHit = true;
}
/**
* If the move has no effect on the target (i.e. the target is protected or immune),
* change the logged move result to FAIL.
*/
if (hitResult === HitResult.NO_EFFECT) {
moveHistoryEntry.result = MoveResult.FAIL;
}
/** Does this phase represent the invoked move's last strike? */
const lastHit = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive());
/**
* If the user can change forms by using the invoked move,
* it only changes forms after the move's last hit
* (see Relic Song's interaction with Parental Bond when used by Meloetta).
*/
if (lastHit) {
this.scene.triggerPokemonFormChange(user, SpeciesFormChangePostMoveTrigger);
}
/**
* Create a Promise that applys *all* effects from the invoked move's MoveEffectAttrs.
* These are ordered by trigger type (see {@linkcode MoveEffectTrigger}), and each trigger
* type requires different conditions to be met with respect to the move's hit result.
*/
applyAttrs.push(new Promise(resolve => {
// Apply all effects with PRE_MOVE triggers (if the target isn't immune to the move)
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.PRE_APPLY && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && hitResult !== HitResult.NO_EFFECT,
user, target, move).then(() => {
// All other effects require the move to not have failed or have been cancelled to trigger
if (hitResult !== HitResult.FAIL) {
/** Are the move's effects tied to the first turn of a charge move? */
const chargeEffect = !!move.getAttrs(ChargeAttr).find(ca => ca.usedChargeEffect(user, this.getTarget() ?? null, move));
/**
* If the invoked move's effects are meant to trigger during the move's "charge turn,"
* ignore all effects after this point.
* Otherwise, apply all self-targeted POST_APPLY effects.
*/
Utils.executeIf(!chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_APPLY
&& attr.selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, move)).then(() => {
// All effects past this point require the move to have hit the target
if (hitResult !== HitResult.NO_EFFECT) {
// Apply all non-self-targeted POST_APPLY effects
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.POST_APPLY
&& !(attr as MoveEffectAttr).selfTarget && (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit), user, target, this.move.getMove()).then(() => {
/**
* If the move hit, and the target doesn't have Shield Dust,
* apply the chance to flinch the target gained from King's Rock
*/
if (dealsDamage && !target.hasAbilityWithAttr(IgnoreMoveEffectsAbAttr)) {
const flinched = new Utils.BooleanHolder(false);
user.scene.applyModifiers(FlinchChanceModifier, user.isPlayer(), user, flinched);
if (flinched.value) {
target.addTag(BattlerTagType.FLINCHED, undefined, this.move.moveId, user.id);
}
}
// If the move was not protected against, apply all HIT effects
Utils.executeIf(!isProtected && !chargeEffect, () => applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && (attr as MoveEffectAttr).trigger === MoveEffectTrigger.HIT
&& (!attr.firstHitOnly || firstHit) && (!attr.lastHitOnly || lastHit) && (!attr.firstTargetOnly || firstTarget), user, target, this.move.getMove()).then(() => {
// Apply the target's post-defend ability effects (as long as the target is active or can otherwise apply them)
return Utils.executeIf(!target.isFainted() || target.canApplyAbility(), () => applyPostDefendAbAttrs(PostDefendAbAttr, target, user, this.move.getMove(), hitResult).then(() => {
// If the invoked move is an enemy attack, apply the enemy's status effect-inflicting tags and tokens
target.lapseTag(BattlerTagType.BEAK_BLAST_CHARGING);
if (move.category === MoveCategory.PHYSICAL && user.isPlayer() !== target.isPlayer()) {
target.lapseTag(BattlerTagType.SHELL_TRAP);
}
if (!user.isPlayer() && this.move.getMove() instanceof AttackMove) {
user.scene.applyShuffledModifiers(this.scene, EnemyAttackStatusEffectChanceModifier, false, target);
}
})).then(() => {
// Apply the user's post-attack ability effects
applyPostAttackAbAttrs(PostAttackAbAttr, user, target, this.move.getMove(), hitResult).then(() => {
/**
* If the invoked move is an attack, apply the user's chance to
* steal an item from the target granted by Grip Claw
*/
if (this.move.getMove() instanceof AttackMove) {
this.scene.applyModifiers(ContactHeldItemTransferChanceModifier, this.player, user, target);
}
resolve();
});
});
})
).then(() => resolve());
});
} else {
applyMoveAttrs(NoEffectAttr, user, null, move).then(() => resolve());
}
});
} else {
resolve();
}
});
}));
}
// Apply the move's POST_TARGET effects on the move's last hit, after all targeted effects have resolved
const postTarget = (user.turnData.hitsLeft === 1 || !this.getTarget()?.isActive()) ?
applyFilteredMoveAttrs((attr: MoveAttr) => attr instanceof MoveEffectAttr && attr.trigger === MoveEffectTrigger.POST_TARGET, user, null, move) :
null;
if (!!postTarget) {
if (applyAttrs.length) { // If there is a pending asynchronous move effect, do this after
applyAttrs[applyAttrs.length - 1]?.then(() => postTarget);
} else { // Otherwise, push a new asynchronous move effect
applyAttrs.push(postTarget);
}
}
// Wait for all move effects to finish applying, then end this phase
Promise.allSettled(applyAttrs).then(() => this.end());
});
});
}
end() {
const move = this.move.getMove();
move.type = move.defaultType;
const user = this.getUserPokemon();
/**
* If this phase isn't for the invoked move's last strike,
* unshift another MoveEffectPhase for the next strike.
* Otherwise, queue a message indicating the number of times the move has struck
* (if the move has struck more than once), then apply the heal from Shell Bell
* to the user.
*/
if (user) {
if (user.turnData.hitsLeft && --user.turnData.hitsLeft >= 1 && this.getTarget()?.isActive()) {
this.scene.unshiftPhase(this.getNewHitPhase());
} else {
// Queue message for number of hits made by multi-move
// If multi-hit attack only hits once, still want to render a message
const hitsTotal = user.turnData.hitCount! - Math.max(user.turnData.hitsLeft!, 0); // TODO: are those bangs correct?
if (hitsTotal > 1 || (user.turnData.hitsLeft && user.turnData.hitsLeft > 0)) {
// If there are multiple hits, or if there are hits of the multi-hit move left
this.scene.queueMessage(i18next.t("battle:attackHitsCount", { count: hitsTotal }));
}
this.scene.applyModifiers(HitHealModifier, this.player, user);
}
}
super.end();
}
/**
* Resolves whether this phase's invoked move hits or misses the given target
* @param target {@linkcode Pokemon} the Pokemon targeted by the invoked move
* @returns `true` if the move does not miss the target; `false` otherwise
*/
hitCheck(target: Pokemon): boolean {
// Moves targeting the user and entry hazards can't miss
if ([MoveTarget.USER, MoveTarget.ENEMY_SIDE].includes(this.move.getMove().moveTarget)) {
return true;
}
const user = this.getUserPokemon()!; // TODO: is this bang correct?
// Hit check only calculated on first hit for multi-hit moves unless flag is set to check all hits.
// However, if an ability with the MaxMultiHitAbAttr, namely Skill Link, is present, act as a normal
// multi-hit move and proceed with all hits
if (user.turnData.hitsLeft < user.turnData.hitCount) {
if (!this.move.getMove().hasFlag(MoveFlags.CHECK_ALL_HITS) || user.hasAbilityWithAttr(MaxMultiHitAbAttr)) {
return true;
}
}
if (user.hasAbilityWithAttr(AlwaysHitAbAttr) || target.hasAbilityWithAttr(AlwaysHitAbAttr)) {
return true;
}
// If the user should ignore accuracy on a target, check who the user targeted last turn and see if they match
if (user.getTag(BattlerTagType.IGNORE_ACCURACY) && (user.getLastXMoves().find(() => true)?.targets || []).indexOf(target.getBattlerIndex()) !== -1) {
return true;
}
if (target.getTag(BattlerTagType.ALWAYS_GET_HIT)) {
return true;
}
const semiInvulnerableTag = target.getTag(SemiInvulnerableTag);
if (semiInvulnerableTag && !this.move.getMove().getAttrs(HitsTagAttr).some(hta => hta.tagType === semiInvulnerableTag.tagType)) {
return false;
}
const moveAccuracy = this.move.getMove().calculateBattleAccuracy(user!, target); // TODO: is the bang correct here?
if (moveAccuracy === -1) {
return true;
}
const accuracyMultiplier = user.getAccuracyMultiplier(target, this.move.getMove());
const rand = user.randSeedInt(100, 1);
return rand <= moveAccuracy * (accuracyMultiplier!); // TODO: is this bang correct?
}
/** Returns the {@linkcode Pokemon} using this phase's invoked move */
getUserPokemon(): Pokemon | undefined {
if (this.battlerIndex > BattlerIndex.ENEMY_2) {
return this.scene.getPokemonById(this.battlerIndex) ?? undefined;
}
return (this.player ? this.scene.getPlayerField() : this.scene.getEnemyField())[this.fieldIndex];
}
/** Returns an array of all {@linkcode Pokemon} targeted by this phase's invoked move */
getTargets(): Pokemon[] {
return this.scene.getField(true).filter(p => this.targets.indexOf(p.getBattlerIndex()) > -1);
}
/** Returns the first target of this phase's invoked move */
getTarget(): Pokemon | undefined {
return this.getTargets()[0];
}
/**
* Removes the given {@linkcode Pokemon} from this phase's target list
* @param target {@linkcode Pokemon} the Pokemon to be removed
*/
removeTarget(target: Pokemon): void {
const targetIndex = this.targets.findIndex(ind => ind === target.getBattlerIndex());
if (targetIndex !== -1) {
this.targets.splice(this.targets.findIndex(ind => ind === target.getBattlerIndex()), 1);
}
}
/**
* Prevents subsequent strikes of this phase's invoked move from occurring
* @param target {@linkcode Pokemon} if defined, only stop subsequent
* strikes against this Pokemon
*/
stopMultiHit(target?: Pokemon): void {
/** If given a specific target, remove the target from subsequent strikes */
if (target) {
this.removeTarget(target);
}
/**
* If no target specified, or the specified target was the last of this move's
* targets, completely cancel all subsequent strikes.
*/
if (!target || this.targets.length === 0 ) {
this.getUserPokemon()!.turnData.hitCount = 1; // TODO: is the bang correct here?
this.getUserPokemon()!.turnData.hitsLeft = 1; // TODO: is the bang correct here?
}
}
/** Returns a new MoveEffectPhase with the same properties as this phase */
getNewHitPhase() {
return new MoveEffectPhase(this.scene, this.battlerIndex, this.targets, this.move);
}
}

View File

@ -0,0 +1,23 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { BattlerTagLapseType } from "#app/data/battler-tags.js";
import { PokemonPhase } from "./pokemon-phase";
export class MoveEndPhase extends PokemonPhase {
constructor(scene: BattleScene, battlerIndex: BattlerIndex) {
super(scene, battlerIndex);
}
start() {
super.start();
const pokemon = this.getPokemon();
if (pokemon.isActive(true)) {
pokemon.lapseTags(BattlerTagLapseType.AFTER_MOVE);
}
this.scene.arena.setIgnoreAbilities(false);
this.end();
}
}

View File

@ -0,0 +1,30 @@
import BattleScene from "#app/battle-scene.js";
import { applyMoveAttrs, MoveHeaderAttr } from "#app/data/move.js";
import Pokemon, { PokemonMove } from "#app/field/pokemon.js";
import { BattlePhase } from "./battle-phase";
export class MoveHeaderPhase extends BattlePhase {
public pokemon: Pokemon;
public move: PokemonMove;
constructor(scene: BattleScene, pokemon: Pokemon, move: PokemonMove) {
super(scene);
this.pokemon = pokemon;
this.move = move;
}
canMove(): boolean {
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon);
}
start() {
super.start();
if (this.canMove()) {
applyMoveAttrs(MoveHeaderAttr, this.pokemon, null, this.move.getMove()).then(() => this.end());
} else {
this.end();
}
}
}

329
src/phases/move-phase.ts Normal file
View File

@ -0,0 +1,329 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { applyAbAttrs, RedirectMoveAbAttr, BlockRedirectAbAttr, IncreasePpAbAttr, applyPreAttackAbAttrs, PokemonTypeChangeAbAttr, applyPostMoveUsedAbAttrs, PostMoveUsedAbAttr } from "#app/data/ability.js";
import { CommonAnim } from "#app/data/battle-anims.js";
import { CenterOfAttentionTag, BattlerTagLapseType } from "#app/data/battler-tags.js";
import { MoveFlags, BypassRedirectAttr, allMoves, CopyMoveAttr, applyMoveAttrs, BypassSleepAttr, HealStatusEffectAttr, ChargeAttr, PreMoveMessageAttr } from "#app/data/move.js";
import { SpeciesFormChangePreMoveTrigger } from "#app/data/pokemon-forms.js";
import { getStatusEffectActivationText, getStatusEffectHealText } from "#app/data/status-effect.js";
import { Type } from "#app/data/type.js";
import { getTerrainBlockMessage } from "#app/data/weather.js";
import { Abilities } from "#app/enums/abilities.js";
import { BattlerTagType } from "#app/enums/battler-tag-type.js";
import { Moves } from "#app/enums/moves.js";
import { StatusEffect } from "#app/enums/status-effect.js";
import { MoveUsedEvent } from "#app/events/battle-scene.js";
import Pokemon, { PokemonMove, MoveResult, TurnMove } from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import i18next from "i18next";
import * as Utils from "#app/utils.js";
import { BattlePhase } from "./battle-phase";
import { CommonAnimPhase } from "./common-anim-phase";
import { MoveEffectPhase } from "./move-effect-phase";
import { MoveEndPhase } from "./move-end-phase";
import { ShowAbilityPhase } from "./show-ability-phase";
export class MovePhase extends BattlePhase {
public pokemon: Pokemon;
public move: PokemonMove;
public targets: BattlerIndex[];
protected followUp: boolean;
protected ignorePp: boolean;
protected failed: boolean;
protected cancelled: boolean;
constructor(scene: BattleScene, pokemon: Pokemon, targets: BattlerIndex[], move: PokemonMove, followUp?: boolean, ignorePp?: boolean) {
super(scene);
this.pokemon = pokemon;
this.targets = targets;
this.move = move;
this.followUp = !!followUp;
this.ignorePp = !!ignorePp;
this.failed = false;
this.cancelled = false;
}
canMove(): boolean {
return this.pokemon.isActive(true) && this.move.isUsable(this.pokemon, this.ignorePp) && !!this.targets.length;
}
/**Signifies the current move should fail but still use PP */
fail(): void {
this.failed = true;
}
/**Signifies the current move should cancel and retain PP */
cancel(): void {
this.cancelled = true;
}
start() {
super.start();
console.log(Moves[this.move.moveId]);
if (!this.canMove()) {
if (this.move.moveId && this.pokemon.summonData?.disabledMove === this.move.moveId) {
this.scene.queueMessage(`${this.move.getName()} is disabled!`);
}
if (this.pokemon.isActive(true) && this.move.ppUsed >= this.move.getMovePp()) { // if the move PP was reduced from Spite or otherwise, the move fails
this.fail();
this.showMoveText();
this.showFailedText();
}
return this.end();
}
if (!this.followUp) {
if (this.move.getMove().checkFlag(MoveFlags.IGNORE_ABILITIES, this.pokemon, null)) {
this.scene.arena.setIgnoreAbilities();
}
} else {
this.pokemon.turnData.hitsLeft = 0; // TODO: is `0` correct?
this.pokemon.turnData.hitCount = 0; // TODO: is `0` correct?
}
// Move redirection abilities (ie. Storm Drain) only support single target moves
const moveTarget = this.targets.length === 1
? new Utils.IntegerHolder(this.targets[0])
: null;
if (moveTarget) {
const oldTarget = moveTarget.value;
this.scene.getField(true).filter(p => p !== this.pokemon).forEach(p => applyAbAttrs(RedirectMoveAbAttr, p, null, this.move.moveId, moveTarget));
this.pokemon.getOpponents().forEach(p => {
const redirectTag = p.getTag(CenterOfAttentionTag) as CenterOfAttentionTag;
if (redirectTag && (!redirectTag.powder || (!this.pokemon.isOfType(Type.GRASS) && !this.pokemon.hasAbility(Abilities.OVERCOAT)))) {
moveTarget.value = p.getBattlerIndex();
}
});
//Check if this move is immune to being redirected, and restore its target to the intended target if it is.
if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) || this.move.getMove().hasAttr(BypassRedirectAttr))) {
//If an ability prevented this move from being redirected, display its ability pop up.
if ((this.pokemon.hasAbilityWithAttr(BlockRedirectAbAttr) && !this.move.getMove().hasAttr(BypassRedirectAttr)) && oldTarget !== moveTarget.value) {
this.scene.unshiftPhase(new ShowAbilityPhase(this.scene, this.pokemon.getBattlerIndex(), this.pokemon.getPassiveAbility().hasAttr(BlockRedirectAbAttr)));
}
moveTarget.value = oldTarget;
}
this.targets[0] = moveTarget.value;
}
// Check for counterattack moves to switch target
if (this.targets.length === 1 && this.targets[0] === BattlerIndex.ATTACKER) {
if (this.pokemon.turnData.attacksReceived.length) {
const attack = this.pokemon.turnData.attacksReceived[0];
this.targets[0] = attack.sourceBattlerIndex;
// account for metal burst and comeuppance hitting remaining targets in double battles
// counterattack will redirect to remaining ally if original attacker faints
if (this.scene.currentBattle.double && this.move.getMove().hasFlag(MoveFlags.REDIRECT_COUNTER)) {
if (this.scene.getField()[this.targets[0]].hp === 0) {
const opposingField = this.pokemon.isPlayer() ? this.scene.getEnemyField() : this.scene.getPlayerField();
//@ts-ignore
this.targets[0] = opposingField.find(p => p.hp > 0)?.getBattlerIndex(); //TODO: fix ts-ignore
}
}
}
if (this.targets[0] === BattlerIndex.ATTACKER) {
this.fail(); // Marks the move as failed for later in doMove
this.showMoveText();
this.showFailedText();
}
}
const targets = this.scene.getField(true).filter(p => {
if (this.targets.indexOf(p.getBattlerIndex()) > -1) {
return true;
}
return false;
});
const doMove = () => {
this.pokemon.turnData.acted = true; // Record that the move was attempted, even if it fails
this.pokemon.lapseTags(BattlerTagLapseType.PRE_MOVE);
let ppUsed = 1;
// Filter all opponents to include only those this move is targeting
const targetedOpponents = this.pokemon.getOpponents().filter(o => this.targets.includes(o.getBattlerIndex()));
for (const opponent of targetedOpponents) {
if (this.move.ppUsed + ppUsed >= this.move.getMovePp()) { // If we're already at max PP usage, stop checking
break;
}
if (opponent.hasAbilityWithAttr(IncreasePpAbAttr)) { // Accounting for abilities like Pressure
ppUsed++;
}
}
if (!this.followUp && this.canMove() && !this.cancelled) {
this.pokemon.lapseTags(BattlerTagLapseType.MOVE);
}
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.scene.eventTarget.dispatchEvent(new MoveUsedEvent(this.pokemon?.id, this.move.getMove(), this.move.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 });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
moveQueue.shift(); // Remove the second turn of charge moves
return this.end();
}
this.scene.triggerPokemonFormChange(this.pokemon, SpeciesFormChangePreMoveTrigger);
if (this.move.moveId) {
this.showMoveText();
}
// This should only happen when there are no valid targets left on the field
if ((moveQueue.length && moveQueue[0].move === Moves.NONE) || !targets.length) {
this.showFailedText();
this.cancel();
// 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 });
this.pokemon.lapseTags(BattlerTagLapseType.MOVE_EFFECT); // Remove any tags from moves like Fly/Dive/etc.
moveQueue.shift();
return this.end();
}
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(), this.move.ppUsed));
}
if (!allMoves[this.move.moveId].hasAttr(CopyMoveAttr)) {
this.scene.currentBattle.lastMove = this.move.moveId;
}
// Assume conditions affecting targets only apply to moves with a single target
let success = this.move.getMove().applyConditions(this.pokemon, targets[0], this.move.getMove());
const cancelled = new Utils.BooleanHolder(false);
let failedText = this.move.getMove().getFailedText(this.pokemon, targets[0], this.move.getMove(), cancelled);
if (success && this.scene.arena.isMoveWeatherCancelled(this.move.getMove())) {
success = false;
} else if (success && this.scene.arena.isMoveTerrainCancelled(this.pokemon, this.targets, this.move.getMove())) {
success = false;
if (failedText === null) {
failedText = getTerrainBlockMessage(targets[0], this.scene.arena.terrain?.terrainType!); // TODO: is this bang correct?
}
}
/**
* Trigger pokemon type change before playing the move animation
* Will still change the user's type when using Roar, Whirlwind, Trick-or-Treat, and Forest's Curse,
* regardless of whether the move successfully executes or not.
*/
if (success || [Moves.ROAR, Moves.WHIRLWIND, Moves.TRICK_OR_TREAT, Moves.FORESTS_CURSE].includes(this.move.moveId)) {
applyPreAttackAbAttrs(PokemonTypeChangeAbAttr, this.pokemon, null, this.move.getMove());
}
if (success) {
this.scene.unshiftPhase(this.getEffectPhase());
} else {
this.pokemon.pushMoveHistory({ move: this.move.moveId, targets: this.targets, result: MoveResult.FAIL, virtual: this.move.virtual });
if (!cancelled.value) {
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.getEnemyField().forEach(pokemon => {
applyPostMoveUsedAbAttrs(PostMoveUsedAbAttr, pokemon, this.move, this.pokemon, this.targets);
});
}
this.end();
};
if (!this.followUp && this.pokemon.status && !this.pokemon.status.isPostTurn()) {
this.pokemon.status.incrementTurn();
let activated = false;
let healed = false;
switch (this.pokemon.status.effect) {
case StatusEffect.PARALYSIS:
if (!this.pokemon.randSeedInt(4)) {
activated = true;
this.cancelled = true;
}
break;
case StatusEffect.SLEEP:
applyMoveAttrs(BypassSleepAttr, this.pokemon, null, this.move.getMove());
healed = this.pokemon.status.turnCount === this.pokemon.status.cureTurn;
activated = !healed && !this.pokemon.getTag(BattlerTagType.BYPASS_SLEEP);
this.cancelled = activated;
break;
case StatusEffect.FREEZE:
healed = !!this.move.getMove().findAttr(attr => attr instanceof HealStatusEffectAttr && attr.selfTarget && attr.isOfEffect(StatusEffect.FREEZE)) || !this.pokemon.randSeedInt(5);
activated = !healed;
this.cancelled = activated;
break;
}
if (activated) {
this.scene.queueMessage(getStatusEffectActivationText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)));
this.scene.unshiftPhase(new CommonAnimPhase(this.scene, this.pokemon.getBattlerIndex(), undefined, CommonAnim.POISON + (this.pokemon.status.effect - 1)));
doMove();
} else {
if (healed) {
this.scene.queueMessage(getStatusEffectHealText(this.pokemon.status.effect, getPokemonNameWithAffix(this.pokemon)));
this.pokemon.resetStatus();
this.pokemon.updateInfo();
}
doMove();
}
} else {
doMove();
}
}
getEffectPhase(): MoveEffectPhase {
return new MoveEffectPhase(this.scene, this.pokemon.getBattlerIndex(), this.targets, this.move);
}
showMoveText(): void {
if (this.move.getMove().hasAttr(ChargeAttr)) {
const lastMove = this.pokemon.getLastXMoves() as TurnMove[];
if (!lastMove.length || lastMove[0].move !== this.move.getMove().id || lastMove[0].result !== MoveResult.OTHER) {
this.scene.queueMessage(i18next.t("battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName()
}), 500);
return;
}
}
if (this.pokemon.getTag(BattlerTagType.RECHARGING || BattlerTagType.INTERRUPTED)) {
return;
}
this.scene.queueMessage(i18next.t("battle:useMove", {
pokemonNameWithAffix: getPokemonNameWithAffix(this.pokemon),
moveName: this.move.getName()
}), 500);
applyMoveAttrs(PreMoveMessageAttr, this.pokemon, this.pokemon.getOpponents().find(() => true)!, this.move.getMove()); //TODO: is the bang correct here?
}
showFailedText(failedText: string | null = null): void {
this.scene.queueMessage(failedText || i18next.t("battle:attackFailed"));
}
end() {
if (!this.followUp && this.canMove()) {
this.scene.unshiftPhase(new MoveEndPhase(this.scene, this.pokemon.getBattlerIndex()));
}
super.end();
}
}

View File

@ -0,0 +1,11 @@
import { BattlePhase } from "./battle-phase";
export class NewBattlePhase extends BattlePhase {
start() {
super.start();
this.scene.newBattle();
this.end();
}
}

View File

@ -0,0 +1,38 @@
import BattleScene from "#app/battle-scene.js";
import { applyAbAttrs, PostBiomeChangeAbAttr } from "#app/data/ability.js";
import { getRandomWeatherType } from "#app/data/weather.js";
import { NextEncounterPhase } from "./next-encounter-phase";
export class NewBiomeEncounterPhase extends NextEncounterPhase {
constructor(scene: BattleScene) {
super(scene);
}
doEncounter(): void {
this.scene.playBgm(undefined, true);
for (const pokemon of this.scene.getParty()) {
if (pokemon) {
pokemon.resetBattleData();
}
}
this.scene.arena.trySetWeather(getRandomWeatherType(this.scene.arena), false);
for (const pokemon of this.scene.getParty().filter(p => p.isOnField())) {
applyAbAttrs(PostBiomeChangeAbAttr, pokemon, null);
}
const enemyField = this.scene.getEnemyField();
this.scene.tweens.add({
targets: [this.scene.arenaEnemy, enemyField].flat(),
x: "+=300",
duration: 2000,
onComplete: () => {
if (!this.tryOverrideForBattleSpec()) {
this.doEncounterCommon();
}
}
});
}
}

View File

@ -0,0 +1,46 @@
import BattleScene from "#app/battle-scene.js";
import { EncounterPhase } from "./encounter-phase";
export class NextEncounterPhase extends EncounterPhase {
constructor(scene: BattleScene) {
super(scene);
}
start() {
super.start();
}
doEncounter(): void {
this.scene.playBgm(undefined, true);
for (const pokemon of this.scene.getParty()) {
if (pokemon) {
pokemon.resetBattleData();
}
}
this.scene.arenaNextEnemy.setBiome(this.scene.arena.biomeType);
this.scene.arenaNextEnemy.setVisible(true);
const enemyField = this.scene.getEnemyField();
this.scene.tweens.add({
targets: [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer].flat(),
x: "+=300",
duration: 2000,
onComplete: () => {
this.scene.arenaEnemy.setBiome(this.scene.arena.biomeType);
this.scene.arenaEnemy.setX(this.scene.arenaNextEnemy.x);
this.scene.arenaEnemy.setAlpha(1);
this.scene.arenaNextEnemy.setX(this.scene.arenaNextEnemy.x - 300);
this.scene.arenaNextEnemy.setVisible(false);
if (this.scene.lastEnemyTrainer) {
this.scene.lastEnemyTrainer.destroy();
}
if (!this.tryOverrideForBattleSpec()) {
this.doEncounterCommon();
}
}
});
}
}

View File

@ -0,0 +1,48 @@
import BattleScene from "#app/battle-scene.js";
import { BattlerIndex } from "#app/battle.js";
import { CommonBattleAnim, CommonAnim } from "#app/data/battle-anims.js";
import { getStatusEffectObtainText, getStatusEffectOverlapText } from "#app/data/status-effect.js";
import { StatusEffect } from "#app/enums/status-effect.js";
import Pokemon from "#app/field/pokemon.js";
import { getPokemonNameWithAffix } from "#app/messages.js";
import { PokemonPhase } from "./pokemon-phase";
import { PostTurnStatusEffectPhase } from "./post-turn-status-effect-phase";
export class ObtainStatusEffectPhase extends PokemonPhase {
private statusEffect: StatusEffect | undefined;
private cureTurn: integer | null;
private sourceText: string | null;
private sourcePokemon: Pokemon | null;
constructor(scene: BattleScene, battlerIndex: BattlerIndex, statusEffect?: StatusEffect, cureTurn?: integer | null, sourceText?: string, sourcePokemon?: Pokemon) {
super(scene, battlerIndex);
this.statusEffect = statusEffect;
this.cureTurn = cureTurn!; // TODO: is this bang correct?
this.sourceText = sourceText!; // TODO: is this bang correct?
this.sourcePokemon = sourcePokemon!; // For tracking which Pokemon caused the status effect // TODO: is this bang correct?
}
start() {
const pokemon = this.getPokemon();
if (!pokemon?.status) {
if (pokemon?.trySetStatus(this.statusEffect, false, this.sourcePokemon)) {
if (this.cureTurn) {
pokemon.status!.cureTurn = this.cureTurn; // TODO: is this bang correct?
}
pokemon.updateInfo(true);
new CommonBattleAnim(CommonAnim.POISON + (this.statusEffect! - 1), pokemon).play(this.scene, () => {
this.scene.queueMessage(getStatusEffectObtainText(this.statusEffect, getPokemonNameWithAffix(pokemon), this.sourceText ?? undefined));
if (pokemon.status?.isPostTurn()) {
this.scene.pushPhase(new PostTurnStatusEffectPhase(this.scene, this.battlerIndex));
}
this.end();
});
return;
}
} else if (pokemon.status.effect === this.statusEffect) {
this.scene.queueMessage(getStatusEffectOverlapText(this.statusEffect, getPokemonNameWithAffix(pokemon)));
}
this.end();
}
}

View File

@ -0,0 +1,13 @@
import BattleScene from "#app/battle-scene.js";
import { Phase } from "#app/phase.js";
import { Mode } from "#app/ui/ui.js";
export class OutdatedPhase extends Phase {
constructor(scene: BattleScene) {
super(scene);
}
start(): void {
this.scene.ui.setMode(Mode.OUTDATED);
}
}

View File

@ -0,0 +1,40 @@
import BattleScene from "#app/battle-scene.js";
import * as Utils from "#app/utils.js";
import { BattlePhase } from "./battle-phase";
export class PartyHealPhase extends BattlePhase {
private resumeBgm: boolean;
constructor(scene: BattleScene, resumeBgm: boolean) {
super(scene);
this.resumeBgm = resumeBgm;
}
start() {
super.start();
const bgmPlaying = this.scene.isBgmPlaying();
if (bgmPlaying) {
this.scene.fadeOutBgm(1000, false);
}
this.scene.ui.fadeOut(1000).then(() => {
for (const pokemon of this.scene.getParty()) {
pokemon.hp = pokemon.getMaxHp();
pokemon.resetStatus();
for (const move of pokemon.moveset) {
move!.ppUsed = 0; // TODO: is this bang correct?
}
pokemon.updateInfo(true);
}
const healSong = this.scene.playSoundWithoutBgm("heal");
this.scene.time.delayedCall(Utils.fixedInt(healSong.totalDuration * 1000), () => {
healSong.destroy();
if (this.resumeBgm && bgmPlaying) {
this.scene.playBgm();
}
this.scene.ui.fadeIn(500).then(() => this.end());
});
});
}
}

View File

@ -0,0 +1,27 @@
import BattleScene from "#app/battle-scene.js";
import Pokemon from "#app/field/pokemon.js";
import { FieldPhase } from "./field-phase";
export abstract class PartyMemberPokemonPhase extends FieldPhase {
protected partyMemberIndex: integer;
protected fieldIndex: integer;
protected player: boolean;
constructor(scene: BattleScene, partyMemberIndex: integer, player: boolean) {
super(scene);
this.partyMemberIndex = partyMemberIndex;
this.fieldIndex = partyMemberIndex < this.scene.currentBattle.getBattlerCount()
? partyMemberIndex
: -1;
this.player = player;
}
getParty(): Pokemon[] {
return this.player ? this.scene.getParty() : this.scene.getEnemyParty();
}
getPokemon(): Pokemon {
return this.getParty()[this.partyMemberIndex];
}
}

Some files were not shown because too many files have changed in this diff Show More