Merge branch 'beta' into fix-turncount-reset
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 800 B |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 6.2 KiB |
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 23 KiB |
Before Width: | Height: | Size: 743 B After Width: | Height: | Size: 793 B |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 6.2 KiB |
@ -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$"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 7.2 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.6 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.2 KiB |
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 21 KiB |
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 318 B |
Before Width: | Height: | Size: 326 B After Width: | Height: | Size: 318 B |
BIN
public/images/pokemon/icons/variant/2/177_2.png
Normal file
After Width: | Height: | Size: 271 B |
BIN
public/images/pokemon/icons/variant/2/177_3.png
Normal file
After Width: | Height: | Size: 271 B |
BIN
public/images/pokemon/icons/variant/2/178_2.png
Normal file
After Width: | Height: | Size: 318 B |
BIN
public/images/pokemon/icons/variant/2/178_3.png
Normal file
After Width: | Height: | Size: 318 B |
Before Width: | Height: | Size: 976 B After Width: | Height: | Size: 1.0 KiB |
Before Width: | Height: | Size: 5.0 KiB After Width: | Height: | Size: 5.2 KiB |
@ -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,
|
||||||
|
BIN
public/images/pokemon/variant/back/177_2.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
public/images/pokemon/variant/back/177_3.png
Normal file
After Width: | Height: | Size: 7.1 KiB |
2372
public/images/pokemon/variant/back/female/178_2.json
Normal file
BIN
public/images/pokemon/variant/back/female/178_2.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
2372
public/images/pokemon/variant/back/female/178_3.json
Normal file
BIN
public/images/pokemon/variant/back/female/178_3.png
Normal file
After Width: | Height: | Size: 6.2 KiB |
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
2372
public/images/pokemon/variant/female/178_2.json
Normal file
BIN
public/images/pokemon/variant/female/178_2.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
2372
public/images/pokemon/variant/female/178_3.json
Normal file
BIN
public/images/pokemon/variant/female/178_3.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
@ -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);
|
||||||
|
@ -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),
|
||||||
|
@ -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,
|
||||||
|
@ -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,
|
||||||
|
@ -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`);
|
||||||
|
@ -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 ],
|
||||||
|
@ -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)
|
||||||
})
|
})
|
||||||
|
@ -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),
|
||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -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,
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
]
|
]
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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 {
|
||||||
|
5841
src/phases.ts
26
src/phases/add-enemy-buff-modifier-phase.ts
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
288
src/phases/attempt-capture-phase.ts
Normal 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()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
56
src/phases/attempt-run-phase.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
49
src/phases/battle-end-phase.ts
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
47
src/phases/battle-phase.ts
Normal 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
@ -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();
|
||||||
|
}
|
||||||
|
}
|
61
src/phases/check-switch-phase.ts
Normal 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
@ -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());
|
||||||
|
}
|
||||||
|
}
|
26
src/phases/common-anim-phase.ts
Normal 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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
84
src/phases/damage-phase.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
*/
|
*/
|
35
src/phases/egg-lapse-phase.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
379
src/phases/encounter-phase.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
40
src/phases/end-card-phase.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
16
src/phases/end-evolution-phase.ts
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
86
src/phases/enemy-command-phase.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
13
src/phases/enemy-party-member-pokemon-phase.ts
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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
@ -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
@ -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
@ -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));
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
27
src/phases/game-over-modifier-reward-phase.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
203
src/phases/game-over-phase.ts
Normal 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)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
14
src/phases/hide-party-exp-bar-phase.ts
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
103
src/phases/learn-move-phase.ts
Normal 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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
20
src/phases/level-cap-phase.ts
Normal 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));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
59
src/phases/level-up-phase.ts
Normal 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
@ -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());
|
||||||
|
}
|
||||||
|
}
|
38
src/phases/message-phase.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
30
src/phases/modifier-reward-phase.ts
Normal 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
34
src/phases/money-reward-phase.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
44
src/phases/move-anim-test-phase.ts
Normal 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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
447
src/phases/move-effect-phase.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
23
src/phases/move-end-phase.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
30
src/phases/move-header-phase.ts
Normal 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
@ -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();
|
||||||
|
}
|
||||||
|
}
|
11
src/phases/new-battle-phase.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { BattlePhase } from "./battle-phase";
|
||||||
|
|
||||||
|
export class NewBattlePhase extends BattlePhase {
|
||||||
|
start() {
|
||||||
|
super.start();
|
||||||
|
|
||||||
|
this.scene.newBattle();
|
||||||
|
|
||||||
|
this.end();
|
||||||
|
}
|
||||||
|
}
|
38
src/phases/new-biome-encounter-phase.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
46
src/phases/next-encounter-phase.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
48
src/phases/obtain-status-effect-phase.ts
Normal 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();
|
||||||
|
}
|
||||||
|
}
|
13
src/phases/outdated-phase.ts
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
40
src/phases/party-heal-phase.ts
Normal 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());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
27
src/phases/party-member-pokemon-phase.ts
Normal 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];
|
||||||
|
}
|
||||||
|
}
|