pokerogue/src/battle-scene.ts
Opaque02 9eb57c0c66
[QoL] IV scanner update to hide the prompt and let you see the stats mid battle on the stats screen (#3285)
* IV scanner PR fix

Updated locales to have updated keys for IV scanner in menu

Updated legacy UI to work with IV scanner changes

Updated code to have player and enemy battle stats for ease of use

Updated logic to make the player side work exactly the same as previously since there should be no HP stat shown for players. Also updated the colours so there's no more grey option for unknown stats

Added HP, updated logic and colours. Need to undo changes to player pokemon to not be as squished because of no HP, and need to see what happens for trainers

Fixing up some git errors

Fixed a bug with double battles not updating the pokemon properly

Updated settings to allow for the ability to skip the IV scanning prompts

Adding functionality to IV scanner to skip prompt and see IVs mid battle

* Merged with latest + fixed bug with IV scanner vs pokemon info container getRootSpeciesId being different

* Updated as per merge comment

* Updating to fix comments

* Updated comments

* Update src/locales/fr/settings.ts

Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr>

* Update src/locales/de/settings.ts

Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>

* Update src/locales/pt_BR/settings.ts

Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br>

* Update src/locales/ko/settings.ts

Co-authored-by: Enoch <enoch.jwsong@gmail.com>

* Update src/locales/zh_CN/settings.ts

Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com>

* Update src/locales/zh_TW/settings.ts

Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com>

* Updating text of IV scanner to be gold

* Updated text colour to use one of the existing golds instead of a custom gold

* Japanese locale

* Updating docs

---------

Co-authored-by: Lugiad' <adrien.grivel@hotmail.fr>
Co-authored-by: Jannik Tappert <38758606+CodeTappert@users.noreply.github.com>
Co-authored-by: José Ricardo Fleury Oliveira <josefleury@discente.ufg.br>
Co-authored-by: Enoch <enoch.jwsong@gmail.com>
Co-authored-by: mercurius-00 <80205689+mercurius-00@users.noreply.github.com>
2024-08-08 01:04:12 -04:00

2680 lines
97 KiB
TypeScript

import Phaser from "phaser";
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 PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species";
import { Constructor } from "#app/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 { PokeballType } from "./data/pokeball";
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims";
import { Phase } from "./phase";
import { initGameSpeed } from "./system/game-speed";
import { Arena, ArenaBase } from "./field/arena";
import { GameData } from "./system/game-data";
import { TextStyle, addTextObject, getTextColor } from "./ui/text";
import { allMoves } from "./data/move";
import { ModifierPoolType, getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, modifierTypes } from "./modifier/modifier-type";
import AbilityBar from "./ui/ability-bar";
import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, IncrementMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability";
import { allAbilities } from "./data/ability";
import Battle, { BattleType, FixedBattleConfig } from "./battle";
import { GameMode, GameModes, getGameMode } from "./game-mode";
import FieldSpritePipeline from "./pipelines/field-sprite";
import SpritePipeline from "./pipelines/sprite";
import PartyExpBar from "./ui/party-exp-bar";
import { TrainerSlot, trainerConfigs } from "./data/trainer-config";
import Trainer, { TrainerVariant } from "./field/trainer";
import TrainerData from "./system/trainer-data";
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
import { pokemonPrevolutions } from "./data/pokemon-evolutions";
import PokeballTray from "./ui/pokeball-tray";
import InvertPostFX from "./pipelines/invert";
import { Achv, ModifierAchv, MoneyAchv, achvs } from "./system/achv";
import { Voucher, vouchers } from "./system/voucher";
import { Gender } from "./data/gender";
import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin";
import { addUiThemeOverrides } from "./ui/ui-theme";
import PokemonData from "./system/pokemon-data";
import { Nature } from "./data/nature";
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger, pokemonFormChanges } from "./data/pokemon-forms";
import { FormChangePhase, QuietFormChangePhase } from "./form-change-phase";
import { getTypeRgb } from "./data/type";
import PokemonSpriteSparkleHandler from "./field/pokemon-sprite-sparkle-handler";
import CharSprite from "./ui/char-sprite";
import DamageNumberHandler from "./field/damage-number-handler";
import PokemonInfoContainer from "./ui/pokemon-info-container";
import { biomeDepths, getBiomeName } from "./data/biomes";
import { SceneBase } from "./scene-base";
import CandyBar from "./ui/candy-bar";
import { Variant, variantData } from "./data/variant";
import { Localizable } from "#app/interfaces/locales";
import Overrides from "#app/overrides";
import {InputsController} from "./inputs-controller";
import {UiInputs} from "./ui-inputs";
import { NewArenaEvent } from "./events/battle-scene";
import { ArenaFlyout } from "./ui/arena-flyout";
import { EaseType } from "#enums/ease-type";
import { Abilities } from "#enums/abilities";
import { BattleSpec } from "#enums/battle-spec";
import { BattleStyle } from "#enums/battle-style";
import { Biome } from "#enums/biome";
import { ExpNotification } from "#enums/exp-notification";
import { MoneyFormat } from "#enums/money-format";
import { Moves } from "#enums/moves";
import { PlayerGender } from "#enums/player-gender";
import { Species } from "#enums/species";
import { UiTheme } from "#enums/ui-theme";
import { TimedEventManager } from "#app/timed-event-manager.js";
import i18next from "i18next";
import {TrainerType} from "#enums/trainer-type";
import { battleSpecDialogue } from "./data/dialogue";
import { LoadingScene } from "./loading-scene";
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
const DEBUG_RNG = false;
const OPP_IVS_OVERRIDE_VALIDATED : integer[] = (
Array.isArray(Overrides.OPP_IVS_OVERRIDE) ?
Overrides.OPP_IVS_OVERRIDE :
new Array(6).fill(Overrides.OPP_IVS_OVERRIDE)
).map(iv => isNaN(iv) || iv === null || iv > 31 ? -1 : iv);
export const startingWave = Overrides.STARTING_WAVE_OVERRIDE || 1;
const expSpriteKeys: string[] = [];
export let starterColors: StarterColors;
interface StarterColors {
[key: string]: [string, string]
}
export interface PokeballCounts {
[pb: string]: integer;
}
export type AnySound = Phaser.Sound.WebAudioSound | Phaser.Sound.HTML5AudioSound | Phaser.Sound.NoAudioSound;
export interface InfoToggle {
toggleInfo(force?: boolean): void;
isActive(): boolean;
}
export default class BattleScene extends SceneBase {
public rexUI: UIPlugin;
public inputController: InputsController;
public uiInputs: UiInputs;
public sessionPlayTime: integer | null = null;
public lastSavePlayTime: integer | null = null;
public masterVolume: number = 0.5;
public bgmVolume: number = 1;
public seVolume: number = 1;
public gameSpeed: integer = 1;
public damageNumbersMode: integer = 0;
public reroll: boolean = false;
public showMovesetFlyout: boolean = true;
public showArenaFlyout: boolean = true;
public showTimeOfDayWidget: boolean = true;
public timeOfDayAnimation: EaseType = EaseType.NONE;
public showLevelUpStats: boolean = true;
public enableTutorials: boolean = import.meta.env.VITE_BYPASS_TUTORIAL === "1";
public enableMoveInfo: boolean = true;
public enableRetries: boolean = false;
public hideIvs: boolean = false;
/**
* Determines the condition for a notification should be shown for Candy Upgrades
* - 0 = 'Off'
* - 1 = 'Passives Only'
* - 2 = 'On'
*/
public candyUpgradeNotification: integer = 0;
/**
* Determines what type of notification is used for Candy Upgrades
* - 0 = 'Icon'
* - 1 = 'Animation'
*/
public candyUpgradeDisplay: integer = 0;
public moneyFormat: MoneyFormat = MoneyFormat.NORMAL;
public uiTheme: UiTheme = UiTheme.DEFAULT;
public windowType: integer = 0;
public experimentalSprites: boolean = false;
public musicPreference: integer = 0;
public moveAnimations: boolean = true;
public expGainsSpeed: integer = 0;
public skipSeenDialogues: boolean = false;
/**
* Defines the experience gain display mode.
*
* @remarks
* The `expParty` can have several modes:
* - `0` - Default: The normal experience gain display, nothing changed.
* - `1` - Level Up Notification: Displays the level up in the small frame instead of a message.
* - `2` - Skip: No level up frame nor message.
*
* Modes `1` and `2` are still compatible with stats display, level up, new move, etc.
* @default 0 - Uses the default normal experience gain display.
*/
public expParty: ExpNotification = 0;
public hpBarSpeed: integer = 0;
public fusionPaletteSwaps: boolean = true;
public enableTouchControls: boolean = false;
public enableVibration: boolean = false;
public showBgmBar: boolean = true;
/**
* Determines the selected battle style.
* - 0 = 'Switch'
* - 1 = 'Set' - The option to switch the active pokemon at the start of a battle will not display.
*/
public battleStyle: integer = BattleStyle.SWITCH;
/**
* Defines whether or not to show type effectiveness hints
* - true: No hints
* - false: Show hints for moves
*/
public typeHints: boolean = false;
public disableMenu: boolean = false;
public gameData: GameData;
public sessionSlotId: integer;
/** PhaseQueue: dequeue/remove the first element to get the next phase */
public phaseQueue: Phase[];
public conditionalQueue: Array<[() => boolean, Phase]>;
/** PhaseQueuePrepend: is a temp storage of what will be added to PhaseQueue */
private phaseQueuePrepend: Phase[];
/** overrides default of inserting phases to end of phaseQueuePrepend array, useful or inserting Phases "out of order" */
private phaseQueuePrependSpliceIndex: integer;
private nextCommandPhaseQueue: Phase[];
private currentPhase: Phase | null;
private standbyPhase: Phase | null;
public field: Phaser.GameObjects.Container;
public fieldUI: Phaser.GameObjects.Container;
public charSprite: CharSprite;
public pbTray: PokeballTray;
public pbTrayEnemy: PokeballTray;
public abilityBar: AbilityBar;
public partyExpBar: PartyExpBar;
public candyBar: CandyBar;
public arenaBg: Phaser.GameObjects.Sprite;
public arenaBgTransition: Phaser.GameObjects.Sprite;
public arenaPlayer: ArenaBase;
public arenaPlayerTransition: ArenaBase;
public arenaEnemy: ArenaBase;
public arenaNextEnemy: ArenaBase;
public arena: Arena;
public gameMode: GameMode;
public score: integer;
public lockModifierTiers: boolean;
public trainer: Phaser.GameObjects.Sprite;
public lastEnemyTrainer: Trainer | null;
public currentBattle: Battle;
public pokeballCounts: PokeballCounts;
public money: integer;
public pokemonInfoContainer: PokemonInfoContainer;
private party: PlayerPokemon[];
/** Combined Biome and Wave count text */
private biomeWaveText: Phaser.GameObjects.Text;
private moneyText: Phaser.GameObjects.Text;
private scoreText: Phaser.GameObjects.Text;
private luckLabelText: Phaser.GameObjects.Text;
private luckText: Phaser.GameObjects.Text;
private modifierBar: ModifierBar;
private enemyModifierBar: ModifierBar;
public arenaFlyout: ArenaFlyout;
private fieldOverlay: Phaser.GameObjects.Rectangle;
private shopOverlay: Phaser.GameObjects.Rectangle;
private shopOverlayShown: boolean = false;
private shopOverlayOpacity: number = .8;
public modifiers: PersistentModifier[];
private enemyModifiers: PersistentModifier[];
public uiContainer: Phaser.GameObjects.Container;
public ui: UI;
public seed: string;
public waveSeed: string;
public waveCycleOffset: integer;
public offsetGym: boolean;
public damageNumberHandler: DamageNumberHandler;
private spriteSparkleHandler: PokemonSpriteSparkleHandler;
public fieldSpritePipeline: FieldSpritePipeline;
public spritePipeline: SpritePipeline;
private bgm: AnySound;
private bgmResumeTimer: Phaser.Time.TimerEvent | null;
private bgmCache: Set<string> = new Set();
private playTimeTimer: Phaser.Time.TimerEvent;
public rngCounter: integer = 0;
public rngSeedOverride: string = "";
public rngOffset: integer = 0;
public inputMethod: string;
private infoToggles: InfoToggle[] = [];
public eventManager: TimedEventManager;
/**
* Allows subscribers to listen for events
*
* Current Events:
* - {@linkcode BattleSceneEventType.MOVE_USED} {@linkcode MoveUsedEvent}
* - {@linkcode BattleSceneEventType.TURN_INIT} {@linkcode TurnInitEvent}
* - {@linkcode BattleSceneEventType.TURN_END} {@linkcode TurnEndEvent}
* - {@linkcode BattleSceneEventType.NEW_ARENA} {@linkcode NewArenaEvent}
*/
public readonly eventTarget: EventTarget = new EventTarget();
constructor() {
super("battle");
this.phaseQueue = [];
this.phaseQueuePrepend = [];
this.conditionalQueue = [];
this.phaseQueuePrependSpliceIndex = -1;
this.nextCommandPhaseQueue = [];
this.updateGameInfo();
}
loadPokemonAtlas(key: string, atlasPath: string, experimental?: boolean) {
if (experimental === undefined) {
experimental = this.experimentalSprites;
}
const variant = atlasPath.includes("variant/") || /_[0-3]$/.test(atlasPath);
if (experimental) {
experimental = this.hasExpSprite(key);
}
if (variant) {
atlasPath = atlasPath.replace("variant/", "");
}
this.load.atlas(key, `images/pokemon/${variant ? "variant/" : ""}${experimental ? "exp/" : ""}${atlasPath}.png`, `images/pokemon/${variant ? "variant/" : ""}${experimental ? "exp/" : ""}${atlasPath}.json`);
}
async preload() {
if (DEBUG_RNG) {
const scene = this;
const originalRealInRange = Phaser.Math.RND.realInRange;
Phaser.Math.RND.realInRange = function (min: number, max: number): number {
const ret = originalRealInRange.apply(this, [ min, max ]);
const args = [ "RNG", ++scene.rngCounter, ret / (max - min), `min: ${min} / max: ${max}` ];
args.push(`seed: ${scene.rngSeedOverride || scene.waveSeed || scene.seed}`);
if (scene.rngOffset) {
args.push(`offset: ${scene.rngOffset}`);
}
console.log(...args);
return ret;
};
}
populateAnims();
await this.initVariantData();
}
create() {
this.scene.remove(LoadingScene.KEY);
initGameSpeed.apply(this);
this.inputController = new InputsController(this);
this.uiInputs = new UiInputs(this, this.inputController);
this.gameData = new GameData(this);
addUiThemeOverrides(this);
this.load.setBaseURL();
this.spritePipeline = new SpritePipeline(this.game);
(this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("Sprite", this.spritePipeline);
this.fieldSpritePipeline = new FieldSpritePipeline(this.game);
(this.renderer as Phaser.Renderer.WebGL.WebGLRenderer).pipelines.add("FieldSprite", this.fieldSpritePipeline);
this.eventManager = new TimedEventManager();
this.launchBattle();
}
update() {
this.ui?.update();
}
launchBattle() {
this.arenaBg = this.add.sprite(0, 0, "plains_bg");
this.arenaBg.setName("sprite-arena-bg");
this.arenaBgTransition = this.add.sprite(0, 0, "plains_bg");
this.arenaBgTransition.setName("sprite-arena-bg-transition");
[ this.arenaBgTransition, this.arenaBg ].forEach(a => {
a.setPipeline(this.fieldSpritePipeline);
a.setScale(6);
a.setOrigin(0);
a.setSize(320, 240);
});
const field = this.add.container(0, 0);
field.setName("field");
field.setScale(6);
this.field = field;
const fieldUI = this.add.container(0, this.game.canvas.height);
fieldUI.setName("field-ui");
fieldUI.setDepth(1);
fieldUI.setScale(6);
this.fieldUI = fieldUI;
const transition = this.make.rexTransitionImagePack({
x: 0,
y: 0,
scale: 6,
key: "loading_bg",
origin: { x: 0, y: 0 }
}, true);
//@ts-ignore (the defined types in the package are incromplete...)
transition.transit({
mode: "blinds",
ease: "Cubic.easeInOut",
duration: 1250,
});
transition.once("complete", () => {
transition.destroy();
});
this.add.existing(transition);
const uiContainer = this.add.container(0, 0);
uiContainer.setName("ui");
uiContainer.setDepth(2);
uiContainer.setScale(6);
this.uiContainer = uiContainer;
const overlayWidth = this.game.canvas.width / 6;
const overlayHeight = (this.game.canvas.height / 6) - 48;
this.fieldOverlay = this.add.rectangle(0, overlayHeight * -1 - 48, overlayWidth, overlayHeight, 0x424242);
this.fieldOverlay.setName("rect-field-overlay");
this.fieldOverlay.setOrigin(0, 0);
this.fieldOverlay.setAlpha(0);
this.fieldUI.add(this.fieldOverlay);
this.shopOverlay = this.add.rectangle(0, overlayHeight * -1 - 48, overlayWidth, overlayHeight, 0x070707);
this.shopOverlay.setName("rect-shop-overlay");
this.shopOverlay.setOrigin(0, 0);
this.shopOverlay.setAlpha(0);
this.fieldUI.add(this.shopOverlay);
this.modifiers = [];
this.enemyModifiers = [];
this.modifierBar = new ModifierBar(this);
this.modifierBar.setName("modifier-bar");
this.add.existing(this.modifierBar);
uiContainer.add(this.modifierBar);
this.enemyModifierBar = new ModifierBar(this, true);
this.enemyModifierBar.setName("enemy-modifier-bar");
this.add.existing(this.enemyModifierBar);
uiContainer.add(this.enemyModifierBar);
this.charSprite = new CharSprite(this);
this.charSprite.setName("sprite-char");
this.charSprite.setup();
this.fieldUI.add(this.charSprite);
this.pbTray = new PokeballTray(this, true);
this.pbTray.setName("pb-tray");
this.pbTray.setup();
this.pbTrayEnemy = new PokeballTray(this, false);
this.pbTrayEnemy.setName("enemy-pb-tray");
this.pbTrayEnemy.setup();
this.fieldUI.add(this.pbTray);
this.fieldUI.add(this.pbTrayEnemy);
this.abilityBar = new AbilityBar(this);
this.abilityBar.setName("ability-bar");
this.abilityBar.setup();
this.fieldUI.add(this.abilityBar);
this.partyExpBar = new PartyExpBar(this);
this.partyExpBar.setName("party-exp-bar");
this.partyExpBar.setup();
this.fieldUI.add(this.partyExpBar);
this.candyBar = new CandyBar(this);
this.candyBar.setName("candy-bar");
this.candyBar.setup();
this.fieldUI.add(this.candyBar);
this.biomeWaveText = addTextObject(this, (this.game.canvas.width / 6) - 2, 0, startingWave.toString(), TextStyle.BATTLE_INFO);
this.biomeWaveText.setName("text-biome-wave");
this.biomeWaveText.setOrigin(1, 0.5);
this.fieldUI.add(this.biomeWaveText);
this.moneyText = addTextObject(this, (this.game.canvas.width / 6) - 2, 0, "", TextStyle.MONEY);
this.moneyText.setName("text-money");
this.moneyText.setOrigin(1, 0.5);
this.fieldUI.add(this.moneyText);
this.scoreText = addTextObject(this, (this.game.canvas.width / 6) - 2, 0, "", TextStyle.PARTY, { fontSize: "54px" });
this.scoreText.setName("text-score");
this.scoreText.setOrigin(1, 0.5);
this.fieldUI.add(this.scoreText);
this.luckText = addTextObject(this, (this.game.canvas.width / 6) - 2, 0, "", TextStyle.PARTY, { fontSize: "54px" });
this.luckText.setName("text-luck");
this.luckText.setOrigin(1, 0.5);
this.luckText.setVisible(false);
this.fieldUI.add(this.luckText);
this.luckLabelText = addTextObject(this, (this.game.canvas.width / 6) - 2, 0, i18next.t("common:luckIndicator"), TextStyle.PARTY, { fontSize: "54px" });
this.luckLabelText.setName("text-luck-label");
this.luckLabelText.setOrigin(1, 0.5);
this.luckLabelText.setVisible(false);
this.fieldUI.add(this.luckLabelText);
this.arenaFlyout = new ArenaFlyout(this);
this.fieldUI.add(this.arenaFlyout);
this.fieldUI.moveBelow<Phaser.GameObjects.GameObject>(this.arenaFlyout, this.fieldOverlay);
this.updateUIPositions();
this.damageNumberHandler = new DamageNumberHandler();
this.spriteSparkleHandler = new PokemonSpriteSparkleHandler();
this.spriteSparkleHandler.setup(this);
this.pokemonInfoContainer = new PokemonInfoContainer(this, (this.game.canvas.width / 6) + 52, -(this.game.canvas.height / 6) + 66);
this.pokemonInfoContainer.setup();
this.fieldUI.add(this.pokemonInfoContainer);
this.party = [];
const loadPokemonAssets = [];
this.arenaPlayer = new ArenaBase(this, true);
this.arenaPlayer.setName("arena-player");
this.arenaPlayerTransition = new ArenaBase(this, true);
this.arenaPlayerTransition.setName("arena-player-transition");
this.arenaEnemy = new ArenaBase(this, false);
this.arenaEnemy.setName("arena-enemy");
this.arenaNextEnemy = new ArenaBase(this, false);
this.arenaNextEnemy.setName("arena-next-enemy");
this.arenaBgTransition.setVisible(false);
this.arenaPlayerTransition.setVisible(false);
this.arenaNextEnemy.setVisible(false);
[ this.arenaPlayer, this.arenaPlayerTransition, this.arenaEnemy, this.arenaNextEnemy ].forEach(a => {
if (a instanceof Phaser.GameObjects.Sprite) {
a.setOrigin(0, 0);
}
field.add(a);
});
const trainer = this.addFieldSprite(0, 0, `trainer_${this.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`);
trainer.setOrigin(0.5, 1);
trainer.setName("sprite-trainer");
field.add(trainer);
this.trainer = trainer;
this.anims.create({
key: "prompt",
frames: this.anims.generateFrameNumbers("prompt", { start: 1, end: 4 }),
frameRate: 6,
repeat: -1,
showOnStart: true
});
this.anims.create({
key: "tera_sparkle",
frames: this.anims.generateFrameNumbers("tera_sparkle", { start: 0, end: 12 }),
frameRate: 18,
repeat: 0,
showOnStart: true,
hideOnComplete: true
});
this.reset(false, false, true);
const ui = new UI(this);
this.uiContainer.add(ui);
this.ui = ui;
ui.setup();
const defaultMoves = [ Moves.TACKLE, Moves.TAIL_WHIP, Moves.FOCUS_ENERGY, Moves.STRUGGLE ];
Promise.all([
Promise.all(loadPokemonAssets),
initCommonAnims(this).then(() => loadCommonAnimAssets(this, true)),
Promise.all([ Moves.TACKLE, Moves.TAIL_WHIP, Moves.FOCUS_ENERGY, Moves.STRUGGLE ].map(m => initMoveAnim(this, m))).then(() => loadMoveAnimAssets(this, defaultMoves, true)),
this.initStarterColors()
]).then(() => {
this.pushPhase(new LoginPhase(this));
this.pushPhase(new TitlePhase(this));
this.shiftPhase();
});
}
initSession(): void {
if (this.sessionPlayTime === null) {
this.sessionPlayTime = 0;
}
if (this.lastSavePlayTime === null) {
this.lastSavePlayTime = 0;
}
if (this.playTimeTimer) {
this.playTimeTimer.destroy();
}
this.playTimeTimer = this.time.addEvent({
delay: Utils.fixedInt(1000),
repeat: -1,
callback: () => {
if (this.gameData) {
this.gameData.gameStats.playTime++;
}
if (this.sessionPlayTime !== null) {
this.sessionPlayTime++;
}
if (this.lastSavePlayTime !== null) {
this.lastSavePlayTime++;
}
}
});
this.updateBiomeWaveText();
this.updateMoneyText();
this.updateScoreText();
}
async initExpSprites(): Promise<void> {
if (expSpriteKeys.length) {
return;
}
this.cachedFetch("./exp-sprites.json").then(res => res.json()).then(keys => {
if (Array.isArray(keys)) {
expSpriteKeys.push(...keys);
}
Promise.resolve();
});
}
async initVariantData(): Promise<void> {
Object.keys(variantData).forEach(key => delete variantData[key]);
await this.cachedFetch("./images/pokemon/variant/_masterlist.json").then(res => res.json())
.then(v => {
Object.keys(v).forEach(k => variantData[k] = v[k]);
if (this.experimentalSprites) {
const expVariantData = variantData["exp"];
const traverseVariantData = (keys: string[]) => {
let variantTree = variantData;
let expTree = expVariantData;
keys.map((k: string, i: integer) => {
if (i < keys.length - 1) {
variantTree = variantTree[k];
expTree = expTree[k];
} else if (variantTree.hasOwnProperty(k) && expTree.hasOwnProperty(k)) {
if ([ "back", "female" ].includes(k)) {
traverseVariantData(keys.concat(k));
} else {
variantTree[k] = expTree[k];
}
}
});
};
Object.keys(expVariantData).forEach(ek => traverseVariantData([ ek ]));
}
Promise.resolve();
});
}
cachedFetch(url: string, init?: RequestInit): Promise<Response> {
const manifest = this.game["manifest"];
if (manifest) {
const timestamp = manifest[`/${url.replace("./", "")}`];
if (timestamp) {
url += `?t=${timestamp}`;
}
}
return fetch(url, init);
}
initStarterColors(): Promise<void> {
return new Promise(resolve => {
if (starterColors) {
return resolve();
}
this.cachedFetch("./starter-colors.json").then(res => res.json()).then(sc => {
starterColors = {};
Object.keys(sc).forEach(key => {
starterColors[key] = sc[key];
});
/*const loadPokemonAssets: Promise<void>[] = [];
for (let s of Object.keys(speciesStarters)) {
const species = getPokemonSpecies(parseInt(s));
loadPokemonAssets.push(species.loadAssets(this, false, 0, false));
}
Promise.all(loadPokemonAssets).then(() => {
const starterCandyColors = {};
const rgbaToHexFunc = (r, g, b) => [r, g, b].map(x => x.toString(16).padStart(2, '0')).join('');
for (let s of Object.keys(speciesStarters)) {
const species = getPokemonSpecies(parseInt(s));
starterCandyColors[species.speciesId] = species.generateCandyColors(this).map(c => rgbaToHexFunc(c[0], c[1], c[2]));
}
console.log(JSON.stringify(starterCandyColors));
resolve();
});*/
resolve();
});
});
}
hasExpSprite(key: string): boolean {
const keyMatch = /^pkmn__?(back__)?(shiny__)?(female__)?(\d+)(\-.*?)?(?:_[1-3])?$/g.exec(key);
if (!keyMatch) {
return false;
}
let k = keyMatch[4]!;
if (keyMatch[2]) {
k += "s";
}
if (keyMatch[1]) {
k += "b";
}
if (keyMatch[3]) {
k += "f";
}
if (keyMatch[5]) {
k += keyMatch[5];
}
if (!expSpriteKeys.includes(k)) {
return false;
}
return true;
}
getParty(): PlayerPokemon[] {
return this.party;
}
getPlayerPokemon(): PlayerPokemon | undefined {
return this.getPlayerField().find(p => p.isActive());
}
/**
* Returns an array of PlayerPokemon of length 1 or 2 depending on if double battles or not
* @returns array of {@linkcode PlayerPokemon}
*/
getPlayerField(): PlayerPokemon[] {
const party = this.getParty();
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
}
getEnemyParty(): EnemyPokemon[] {
return this.currentBattle?.enemyParty || [];
}
getEnemyPokemon(): EnemyPokemon | undefined {
return this.getEnemyField().find(p => p.isActive());
}
/**
* Returns an array of EnemyPokemon of length 1 or 2 depending on if double battles or not
* @returns array of {@linkcode EnemyPokemon}
*/
getEnemyField(): EnemyPokemon[] {
const party = this.getEnemyParty();
return party.slice(0, Math.min(party.length, this.currentBattle?.double ? 2 : 1));
}
getField(activeOnly: boolean = false): Pokemon[] {
const ret = new Array(4).fill(null);
const playerField = this.getPlayerField();
const enemyField = this.getEnemyField();
ret.splice(0, playerField.length, ...playerField);
ret.splice(2, enemyField.length, ...enemyField);
return activeOnly
? ret.filter(p => p?.isActive())
: ret;
}
/**
* Used in doubles battles to redirect moves from one pokemon to another when one faints or is removed from the field
* @param removedPokemon {@linkcode Pokemon} the pokemon that is being removed from the field (flee, faint), moves to be redirected FROM
* @param allyPokemon {@linkcode Pokemon} the pokemon that will have the moves be redirected TO
*/
redirectPokemonMoves(removedPokemon: Pokemon, allyPokemon: Pokemon): void {
// failsafe: if not a double battle just return
if (this.currentBattle.double === false) {
return;
}
if (allyPokemon?.isActive(true)) {
let targetingMovePhase: MovePhase;
do {
targetingMovePhase = this.findPhase(mp => mp instanceof MovePhase && mp.targets.length === 1 && mp.targets[0] === removedPokemon.getBattlerIndex() && mp.pokemon.isPlayer() !== allyPokemon.isPlayer()) as MovePhase;
if (targetingMovePhase && targetingMovePhase.targets[0] !== allyPokemon.getBattlerIndex()) {
targetingMovePhase.targets[0] = allyPokemon.getBattlerIndex();
}
} while (targetingMovePhase);
}
}
/**
* Returns the ModifierBar of this scene, which is declared private and therefore not accessible elsewhere
* @returns {ModifierBar}
*/
getModifierBar(): ModifierBar {
return this.modifierBar;
}
// store info toggles to be accessible by the ui
addInfoToggle(infoToggle: InfoToggle): void {
this.infoToggles.push(infoToggle);
}
// return the stored info toggles; used by ui-inputs
getInfoToggles(activeOnly: boolean = false): InfoToggle[] {
return activeOnly ? this.infoToggles.filter(t => t?.isActive()) : this.infoToggles;
}
getPokemonById(pokemonId: integer): Pokemon | null {
const findInParty = (party: Pokemon[]) => party.find(p => p.id === pokemonId);
return (findInParty(this.getParty()) || findInParty(this.getEnemyParty())) ?? null;
}
addPlayerPokemon(species: PokemonSpecies, level: integer, abilityIndex?: integer, formIndex?: integer, gender?: Gender, shiny?: boolean, variant?: Variant, ivs?: integer[], nature?: Nature, dataSource?: Pokemon | PokemonData, postProcess?: (playerPokemon: PlayerPokemon) => void): PlayerPokemon {
const pokemon = new PlayerPokemon(this, species, level, abilityIndex, formIndex, gender, shiny, variant, ivs, nature, dataSource);
if (postProcess) {
postProcess(pokemon);
}
pokemon.init();
return pokemon;
}
addEnemyPokemon(species: PokemonSpecies, level: integer, trainerSlot: TrainerSlot, boss: boolean = false, dataSource?: PokemonData, postProcess?: (enemyPokemon: EnemyPokemon) => void): EnemyPokemon {
if (Overrides.OPP_SPECIES_OVERRIDE) {
species = getPokemonSpecies(Overrides.OPP_SPECIES_OVERRIDE);
}
const pokemon = new EnemyPokemon(this, species, level, trainerSlot, boss, dataSource);
if (Overrides.OPP_LEVEL_OVERRIDE !== 0) {
pokemon.level = Overrides.OPP_LEVEL_OVERRIDE;
}
if (Overrides.OPP_GENDER_OVERRIDE !== null) {
pokemon.gender = Overrides.OPP_GENDER_OVERRIDE;
}
overrideModifiers(this, false);
overrideHeldItems(this, pokemon, false);
if (boss && !dataSource) {
const secondaryIvs = Utils.getIvsFromId(Utils.randSeedInt(4294967295));
for (let s = 0; s < pokemon.ivs.length; s++) {
pokemon.ivs[s] = Math.round(Phaser.Math.Linear(Math.min(pokemon.ivs[s], secondaryIvs[s]), Math.max(pokemon.ivs[s], secondaryIvs[s]), 0.75));
}
}
if (postProcess) {
postProcess(pokemon);
}
for (let i = 0; i < pokemon.ivs.length; i++) {
if (OPP_IVS_OVERRIDE_VALIDATED[i] > -1) {
pokemon.ivs[i] = OPP_IVS_OVERRIDE_VALIDATED[i];
}
}
pokemon.init();
return pokemon;
}
addPokemonIcon(pokemon: Pokemon, x: number, y: number, originX: number = 0.5, originY: number = 0.5, ignoreOverride: boolean = false): Phaser.GameObjects.Container {
const container = this.add.container(x, y);
container.setName(`${pokemon.name}-icon`);
const icon = this.add.sprite(0, 0, pokemon.getIconAtlasKey(ignoreOverride));
icon.setName(`sprite-${pokemon.name}-icon`);
icon.setFrame(pokemon.getIconId(true));
// Temporary fix to show pokemon's default icon if variant icon doesn't exist
if (icon.frame.name !== pokemon.getIconId(true)) {
console.log(`${pokemon.name}'s variant icon does not exist. Replacing with default.`);
const temp = pokemon.shiny;
pokemon.shiny = false;
icon.setTexture(pokemon.getIconAtlasKey(ignoreOverride));
icon.setFrame(pokemon.getIconId(true));
pokemon.shiny = temp;
}
icon.setOrigin(0.5, 0);
container.add(icon);
if (pokemon.isFusion()) {
const fusionIcon = this.add.sprite(0, 0, pokemon.getFusionIconAtlasKey(ignoreOverride));
fusionIcon.setName("sprite-fusion-icon");
fusionIcon.setOrigin(0.5, 0);
fusionIcon.setFrame(pokemon.getFusionIconId(true));
const originalWidth = icon.width;
const originalHeight = icon.height;
const originalFrame = icon.frame;
const iconHeight = (icon.frame.cutHeight <= fusionIcon.frame.cutHeight ? Math.ceil : Math.floor)((icon.frame.cutHeight + fusionIcon.frame.cutHeight) / 4);
// Inefficient, but for some reason didn't work with only the unique properties as part of the name
const iconFrameId = `${icon.frame.name}f${fusionIcon.frame.name}`;
if (!icon.frame.texture.has(iconFrameId)) {
icon.frame.texture.add(iconFrameId, icon.frame.sourceIndex, icon.frame.cutX, icon.frame.cutY, icon.frame.cutWidth, iconHeight);
}
icon.setFrame(iconFrameId);
fusionIcon.y = icon.frame.cutHeight;
const originalFusionFrame = fusionIcon.frame;
const fusionIconY = fusionIcon.frame.cutY + icon.frame.cutHeight;
const fusionIconHeight = fusionIcon.frame.cutHeight - icon.frame.cutHeight;
// Inefficient, but for some reason didn't work with only the unique properties as part of the name
const fusionIconFrameId = `${fusionIcon.frame.name}f${icon.frame.name}`;
if (!fusionIcon.frame.texture.has(fusionIconFrameId)) {
fusionIcon.frame.texture.add(fusionIconFrameId, fusionIcon.frame.sourceIndex, fusionIcon.frame.cutX, fusionIconY, fusionIcon.frame.cutWidth, fusionIconHeight);
}
fusionIcon.setFrame(fusionIconFrameId);
const frameY = (originalFrame.y + originalFusionFrame.y) / 2;
icon.frame.y = fusionIcon.frame.y = frameY;
container.add(fusionIcon);
if (originX !== 0.5) {
container.x -= originalWidth * (originX - 0.5);
}
if (originY !== 0) {
container.y -= (originalHeight) * originY;
}
} else {
if (originX !== 0.5) {
container.x -= icon.width * (originX - 0.5);
}
if (originY !== 0) {
container.y -= icon.height * originY;
}
}
return container;
}
setSeed(seed: string): void {
this.seed = seed;
this.rngCounter = 0;
this.waveCycleOffset = this.getGeneratedWaveCycleOffset();
this.offsetGym = this.gameMode.isClassic && this.getGeneratedOffsetGym();
}
randBattleSeedInt(range: integer, min: integer = 0): integer {
return this.currentBattle.randSeedInt(this, range, min);
}
reset(clearScene: boolean = false, clearData: boolean = false, reloadI18n: boolean = false): void {
if (clearData) {
this.gameData = new GameData(this);
}
this.gameMode = getGameMode(GameModes.CLASSIC);
this.setSeed(Overrides.SEED_OVERRIDE || Utils.randomString(24));
console.log("Seed:", this.seed);
this.disableMenu = false;
this.score = 0;
this.money = 0;
this.lockModifierTiers = false;
this.pokeballCounts = Object.fromEntries(Utils.getEnumValues(PokeballType).filter(p => p <= PokeballType.MASTER_BALL).map(t => [ t, 0 ]));
this.pokeballCounts[PokeballType.POKEBALL] += 5;
if (Overrides.POKEBALL_OVERRIDE.active) {
this.pokeballCounts = Overrides.POKEBALL_OVERRIDE.pokeballs;
}
this.modifiers = [];
this.enemyModifiers = [];
this.modifierBar.removeAll(true);
this.enemyModifierBar.removeAll(true);
for (const p of this.getParty()) {
p.destroy();
}
this.party = [];
for (const p of this.getEnemyParty()) {
p.destroy();
}
//@ts-ignore - allowing `null` for currentBattle causes a lot of trouble
this.currentBattle = null; // TODO: resolve ts-ignore
this.biomeWaveText.setText(startingWave.toString());
this.biomeWaveText.setVisible(false);
this.updateMoneyText();
this.moneyText.setVisible(false);
this.updateScoreText();
this.scoreText.setVisible(false);
[ this.luckLabelText, this.luckText ].map(t => t.setVisible(false));
this.newArena(Overrides.STARTING_BIOME_OVERRIDE || Biome.TOWN);
this.field.setVisible(true);
this.arenaBgTransition.setPosition(0, 0);
this.arenaPlayer.setPosition(300, 0);
this.arenaPlayerTransition.setPosition(0, 0);
[ this.arenaEnemy, this.arenaNextEnemy ].forEach(a => a.setPosition(-280, 0));
this.arenaNextEnemy.setVisible(false);
this.arena.init();
this.trainer.setTexture(`trainer_${this.gameData.gender === PlayerGender.FEMALE ? "f" : "m"}_back`);
this.trainer.setPosition(406, 186);
this.trainer.setVisible(true);
this.updateGameInfo();
if (reloadI18n) {
const localizable: Localizable[] = [
...allSpecies,
...allMoves,
...allAbilities,
...Utils.getEnumValues(ModifierPoolType).map(mpt => getModifierPoolForType(mpt)).map(mp => Object.values(mp).flat().map(mt => mt.modifierType).filter(mt => "localize" in mt).map(lpb => lpb as unknown as Localizable)).flat()
];
for (const item of localizable) {
item.localize();
}
}
if (clearScene) {
// Reload variant data in case sprite set has changed
this.initVariantData();
this.fadeOutBgm(250, false);
this.tweens.add({
targets: [ this.uiContainer ],
alpha: 0,
duration: 250,
ease: "Sine.easeInOut",
onComplete: () => {
this.clearPhaseQueue();
this.children.removeAll(true);
this.game.domContainer.innerHTML = "";
this.launchBattle();
}
});
}
}
newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean): Battle | null {
const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave;
const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (_startingWave - 1)) + 1);
let newDouble: boolean | undefined;
let newBattleType: BattleType;
let newTrainer: Trainer | undefined;
let battleConfig: FixedBattleConfig | null = null;
this.resetSeed(newWaveIndex);
const playerField = this.getPlayerField();
if (this.gameMode.isFixedBattle(newWaveIndex) && trainerData === undefined) {
battleConfig = this.gameMode.getFixedBattle(newWaveIndex);
newDouble = battleConfig.double;
newBattleType = battleConfig.battleType;
this.executeWithSeedOffset(() => newTrainer = battleConfig?.getTrainer(this), (battleConfig.seedOffsetWaveIndex || newWaveIndex) << 8);
if (newTrainer) {
this.field.add(newTrainer);
}
} else {
if (!this.gameMode.hasTrainers) {
newBattleType = BattleType.WILD;
} else if (battleType === undefined) {
newBattleType = this.gameMode.isWaveTrainer(newWaveIndex, this.arena) ? BattleType.TRAINER : BattleType.WILD;
} else {
newBattleType = battleType;
}
if (newBattleType === BattleType.TRAINER) {
const trainerType = this.arena.randomTrainerType(newWaveIndex);
let doubleTrainer = false;
if (trainerConfigs[trainerType].doubleOnly) {
doubleTrainer = true;
} else if (trainerConfigs[trainerType].hasDouble) {
const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8);
this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance);
playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, doubleChance));
doubleTrainer = !Utils.randSeedInt(doubleChance.value);
// Add a check that special trainers can't be double except for tate and liza - they should use the normal double chance
if (trainerConfigs[trainerType].trainerTypeDouble && ![ TrainerType.TATE, TrainerType.LIZA ].includes(trainerType)) {
doubleTrainer = false;
}
}
newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, doubleTrainer ? TrainerVariant.DOUBLE : Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT);
this.field.add(newTrainer);
}
}
if (double === undefined && newWaveIndex > 1) {
if (newBattleType === BattleType.WILD && !this.gameMode.isWaveFinal(newWaveIndex)) {
const doubleChance = new Utils.IntegerHolder(newWaveIndex % 10 === 0 ? 32 : 8);
this.applyModifiers(DoubleBattleChanceBoosterModifier, true, doubleChance);
playerField.forEach(p => applyAbAttrs(DoubleBattleChanceAbAttr, p, null, doubleChance));
newDouble = !Utils.randSeedInt(doubleChance.value);
} else if (newBattleType === BattleType.TRAINER) {
newDouble = newTrainer?.variant === TrainerVariant.DOUBLE;
}
} else if (!battleConfig) {
newDouble = !!double;
}
if (Overrides.BATTLE_TYPE_OVERRIDE === "double") {
newDouble = true;
}
/* Override battles into single only if not fighting with trainers */
if (newBattleType !== BattleType.TRAINER && Overrides.BATTLE_TYPE_OVERRIDE === "single") {
newDouble = false;
}
const lastBattle = this.currentBattle;
if (lastBattle?.double && !newDouble) {
this.tryRemovePhase(p => p instanceof SwitchPhase);
}
const maxExpLevel = this.getMaxExpLevel();
this.lastEnemyTrainer = lastBattle?.trainer ?? null;
this.executeWithSeedOffset(() => {
this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble);
}, newWaveIndex << 3, this.waveSeed);
this.currentBattle.incrementTurn(this);
//this.pushPhase(new TrainerMessageTestPhase(this, TrainerType.RIVAL, TrainerType.RIVAL_2, TrainerType.RIVAL_3, TrainerType.RIVAL_4, TrainerType.RIVAL_5, TrainerType.RIVAL_6));
if (!waveIndex && lastBattle) {
const isWaveIndexMultipleOfTen = !(lastBattle.waveIndex % 10);
const isEndlessOrDaily = this.gameMode.hasShortBiomes || this.gameMode.isDaily;
const isEndlessFifthWave = this.gameMode.hasShortBiomes && (lastBattle.waveIndex % 5) === 0;
const isWaveIndexMultipleOfFiftyMinusOne = (lastBattle.waveIndex % 50) === 49;
const isNewBiome = isWaveIndexMultipleOfTen || isEndlessFifthWave || (isEndlessOrDaily && isWaveIndexMultipleOfFiftyMinusOne);
const resetArenaState = isNewBiome || this.currentBattle.battleType === BattleType.TRAINER || this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS;
this.getEnemyParty().forEach(enemyPokemon => enemyPokemon.destroy());
this.trySpreadPokerus();
if (!isNewBiome && (newWaveIndex % 10) === 5) {
this.arena.updatePoolsForTimeOfDay();
}
if (resetArenaState) {
this.arena.resetArenaEffects();
playerField.forEach((_, p) => this.pushPhase(new ReturnPhase(this, p)));
for (const pokemon of this.getParty()) {
// Only trigger form change when Eiscue is in Noice form
// Hardcoded Eiscue for now in case it is fused with another pokemon
if (pokemon.species.speciesId === Species.EISCUE && pokemon.hasAbility(Abilities.ICE_FACE) && pokemon.formIndex === 1) {
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger);
}
pokemon.resetBattleData();
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
}
this.pushPhase(new ShowTrainerPhase(this));
}
for (const pokemon of this.getParty()) {
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger);
}
if (!this.gameMode.hasRandomBiomes && !isNewBiome) {
this.pushPhase(new NextEncounterPhase(this));
} else {
this.pushPhase(new SelectBiomePhase(this));
this.pushPhase(new NewBiomeEncounterPhase(this));
const newMaxExpLevel = this.getMaxExpLevel();
if (newMaxExpLevel > maxExpLevel) {
this.pushPhase(new LevelCapPhase(this));
}
}
}
return this.currentBattle;
}
newArena(biome: Biome): Arena {
this.arena = new Arena(this, biome, Biome[biome].toLowerCase());
this.eventTarget.dispatchEvent(new NewArenaEvent());
this.arenaBg.pipelineData = { terrainColorRatio: this.arena.getBgTerrainColorRatioForBiome() };
return this.arena;
}
updateFieldScale(): Promise<void> {
return new Promise(resolve => {
const fieldScale = Math.floor(Math.pow(1 / this.getField(true)
.map(p => p.getSpriteScale())
.reduce((highestScale: number, scale: number) => highestScale = Math.max(scale, highestScale), 0), 0.7) * 40
) / 40;
this.setFieldScale(fieldScale).then(() => resolve());
});
}
setFieldScale(scale: number, instant: boolean = false): Promise<void> {
return new Promise(resolve => {
scale *= 6;
if (this.field.scale === scale) {
return resolve();
}
const defaultWidth = this.arenaBg.width * 6;
const defaultHeight = 132 * 6;
const scaledWidth = this.arenaBg.width * scale;
const scaledHeight = 132 * scale;
this.tweens.add({
targets: this.field,
scale: scale,
x: (defaultWidth - scaledWidth) / 2,
y: defaultHeight - scaledHeight,
duration: !instant ? Utils.fixedInt(Math.abs(this.field.scale - scale) * 200) : 0,
ease: "Sine.easeInOut",
onComplete: () => resolve()
});
});
}
getSpeciesFormIndex(species: PokemonSpecies, gender?: Gender, nature?: Nature, ignoreArena?: boolean): integer {
if (!species.forms?.length) {
return 0;
}
switch (species.speciesId) {
case Species.UNOWN:
case Species.SHELLOS:
case Species.GASTRODON:
case Species.BASCULIN:
case Species.DEERLING:
case Species.SAWSBUCK:
case Species.FROAKIE:
case Species.FROGADIER:
case Species.SCATTERBUG:
case Species.SPEWPA:
case Species.VIVILLON:
case Species.FLABEBE:
case Species.FLOETTE:
case Species.FLORGES:
case Species.FURFROU:
case Species.PUMPKABOO:
case Species.GOURGEIST:
case Species.ORICORIO:
case Species.MAGEARNA:
case Species.ZARUDE:
case Species.SQUAWKABILLY:
case Species.TATSUGIRI:
case Species.GIMMIGHOUL:
case Species.PALDEA_TAUROS:
return Utils.randSeedInt(species.forms.length);
case Species.PIKACHU:
return Utils.randSeedInt(8);
case Species.EEVEE:
return Utils.randSeedInt(2);
case Species.GRENINJA:
return Utils.randSeedInt(2);
case Species.ZYGARDE:
return Utils.randSeedInt(3);
case Species.MINIOR:
return Utils.randSeedInt(6);
case Species.ALCREMIE:
return Utils.randSeedInt(9);
case Species.MEOWSTIC:
case Species.INDEEDEE:
case Species.BASCULEGION:
case Species.OINKOLOGNE:
return gender === Gender.FEMALE ? 1 : 0;
case Species.TOXTRICITY:
const lowkeyNatures = [ Nature.LONELY, Nature.BOLD, Nature.RELAXED, Nature.TIMID, Nature.SERIOUS, Nature.MODEST, Nature.MILD, Nature.QUIET, Nature.BASHFUL, Nature.CALM, Nature.GENTLE, Nature.CAREFUL ];
if (nature !== undefined && lowkeyNatures.indexOf(nature) > -1) {
return 1;
}
return 0;
}
if (ignoreArena) {
switch (species.speciesId) {
case Species.BURMY:
case Species.WORMADAM:
case Species.ROTOM:
case Species.LYCANROC:
return Utils.randSeedInt(species.forms.length);
}
return 0;
}
return this.arena.getSpeciesFormIndex(species);
}
private getGeneratedOffsetGym(): boolean {
let ret = false;
this.executeWithSeedOffset(() => {
ret = !Utils.randSeedInt(2);
}, 0, this.seed.toString());
return ret;
}
private getGeneratedWaveCycleOffset(): integer {
let ret = 0;
this.executeWithSeedOffset(() => {
ret = Utils.randSeedInt(8) * 5;
}, 0, this.seed.toString());
return ret;
}
getEncounterBossSegments(waveIndex: integer, level: integer, species?: PokemonSpecies, forceBoss: boolean = false): integer {
if (this.gameMode.isDaily && this.gameMode.isWaveFinal(waveIndex)) {
return 5;
}
let isBoss: boolean | undefined;
if (forceBoss || (species && (species.subLegendary || species.legendary || species.mythical))) {
isBoss = true;
} else {
this.executeWithSeedOffset(() => {
isBoss = waveIndex % 10 === 0 || (this.gameMode.hasRandomBosses && Utils.randSeedInt(100) < Math.min(Math.max(Math.ceil((waveIndex - 250) / 50), 0) * 2, 30));
}, waveIndex << 2);
}
if (!isBoss) {
return 0;
}
let ret: integer = 2;
if (level >= 100) {
ret++;
}
if (species) {
if (species.baseTotal >= 670) {
ret++;
}
}
ret += Math.floor(waveIndex / 250);
return ret;
}
trySpreadPokerus(): void {
const party = this.getParty();
const infectedIndexes: integer[] = [];
const spread = (index: number, spreadTo: number) => {
const partyMember = party[index + spreadTo];
if (!partyMember.pokerus && !Utils.randSeedInt(10)) {
partyMember.pokerus = true;
infectedIndexes.push(index + spreadTo);
}
};
party.forEach((pokemon, p) => {
if (!pokemon.pokerus || infectedIndexes.indexOf(p) > -1) {
return;
}
this.executeWithSeedOffset(() => {
if (p) {
spread(p, -1);
}
if (p < party.length - 1) {
spread(p, 1);
}
}, this.currentBattle.waveIndex + (p << 8));
});
}
resetSeed(waveIndex?: integer): void {
const wave = waveIndex || this.currentBattle?.waveIndex || 0;
this.waveSeed = Utils.shiftCharCodes(this.seed, wave);
Phaser.Math.RND.sow([ this.waveSeed ]);
console.log("Wave Seed:", this.waveSeed, wave);
this.rngCounter = 0;
}
executeWithSeedOffset(func: Function, offset: integer, seedOverride?: string): void {
if (!func) {
return;
}
const tempRngCounter = this.rngCounter;
const tempRngOffset = this.rngOffset;
const tempRngSeedOverride = this.rngSeedOverride;
const state = Phaser.Math.RND.state();
Phaser.Math.RND.sow([ Utils.shiftCharCodes(seedOverride || this.seed, offset) ]);
this.rngCounter = 0;
this.rngOffset = offset;
this.rngSeedOverride = seedOverride || "";
func();
Phaser.Math.RND.state(state);
this.rngCounter = tempRngCounter;
this.rngOffset = tempRngOffset;
this.rngSeedOverride = tempRngSeedOverride;
}
addFieldSprite(x: number, y: number, texture: string | Phaser.Textures.Texture, frame?: string | number, terrainColorRatio: number = 0): Phaser.GameObjects.Sprite {
const ret = this.add.sprite(x, y, texture, frame);
ret.setPipeline(this.fieldSpritePipeline);
if (terrainColorRatio) {
ret.pipelineData["terrainColorRatio"] = terrainColorRatio;
}
return ret;
}
addPokemonSprite(pokemon: Pokemon, x: number, y: number, texture: string | Phaser.Textures.Texture, frame?: string | number, hasShadow: boolean = false, ignoreOverride: boolean = false): Phaser.GameObjects.Sprite {
const ret = this.addFieldSprite(x, y, texture, frame);
this.initPokemonSprite(ret, pokemon, hasShadow, ignoreOverride);
return ret;
}
initPokemonSprite(sprite: Phaser.GameObjects.Sprite, pokemon?: Pokemon, hasShadow: boolean = false, ignoreOverride: boolean = false): Phaser.GameObjects.Sprite {
sprite.setPipeline(this.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: hasShadow, ignoreOverride: ignoreOverride, teraColor: pokemon ? getTypeRgb(pokemon.getTeraType()) : undefined });
this.spriteSparkleHandler.add(sprite);
return sprite;
}
moveBelowOverlay<T extends Phaser.GameObjects.GameObject>(gameObject: T) {
this.fieldUI.moveBelow<any>(gameObject, this.fieldOverlay);
}
processInfoButton(pressed: boolean): void {
this.arenaFlyout.toggleFlyout(pressed);
}
showFieldOverlay(duration: integer): Promise<void> {
return new Promise(resolve => {
this.tweens.add({
targets: this.fieldOverlay,
alpha: 0.5,
ease: "Sine.easeOut",
duration: duration,
onComplete: () => resolve()
});
});
}
hideFieldOverlay(duration: integer): Promise<void> {
return new Promise(resolve => {
this.tweens.add({
targets: this.fieldOverlay,
alpha: 0,
duration: duration,
ease: "Cubic.easeIn",
onComplete: () => resolve()
});
});
}
updateShopOverlayOpacity(value: number): void {
this.shopOverlayOpacity = value;
if (this.shopOverlayShown) {
this.shopOverlay.setAlpha(this.shopOverlayOpacity);
}
}
showShopOverlay(duration: integer): Promise<void> {
this.shopOverlayShown = true;
return new Promise(resolve => {
this.tweens.add({
targets: this.shopOverlay,
alpha: this.shopOverlayOpacity,
ease: "Sine.easeOut",
duration,
onComplete: () => resolve()
});
});
}
hideShopOverlay(duration: integer): Promise<void> {
this.shopOverlayShown = false;
return new Promise(resolve => {
this.tweens.add({
targets: this.shopOverlay,
alpha: 0,
duration: duration,
ease: "Cubic.easeIn",
onComplete: () => resolve()
});
});
}
showEnemyModifierBar(): void {
this.enemyModifierBar.setVisible(true);
}
hideEnemyModifierBar(): void {
this.enemyModifierBar.setVisible(false);
}
updateBiomeWaveText(): void {
const isBoss = !(this.currentBattle.waveIndex % 10);
const biomeString: string = getBiomeName(this.arena.biomeType);
this.fieldUI.moveAbove(this.biomeWaveText, this.luckText);
this.biomeWaveText.setText( biomeString + " - " + this.currentBattle.waveIndex.toString());
this.biomeWaveText.setColor(!isBoss ? "#ffffff" : "#f89890");
this.biomeWaveText.setShadowColor(!isBoss ? "#636363" : "#984038");
this.biomeWaveText.setVisible(true);
}
updateMoneyText(forceVisible: boolean = true): void {
if (this.money === undefined) {
return;
}
const formattedMoney = Utils.formatMoney(this.moneyFormat, this.money);
this.moneyText.setText(`${formattedMoney}`);
this.fieldUI.moveAbove(this.moneyText, this.luckText);
if (forceVisible) {
this.moneyText.setVisible(true);
}
}
animateMoneyChanged(positiveChange: boolean): void {
if (this.tweens.getTweensOf(this.moneyText).length > 0) {
return;
}
const deltaScale = this.moneyText.scale * 0.14 * (positiveChange ? 1 : -1);
this.moneyText.setShadowColor(positiveChange ? "#008000" : "#FF0000");
this.tweens.add({
targets: this.moneyText,
duration: 250,
scale: this.moneyText.scale + deltaScale,
loop: 0,
yoyo: true,
onComplete: (_) => this.moneyText.setShadowColor(getTextColor(TextStyle.MONEY, true)),
});
}
updateScoreText(): void {
this.scoreText.setText(`Score: ${this.score.toString()}`);
this.scoreText.setVisible(this.gameMode.isDaily);
}
updateAndShowText(duration: integer): void {
const labels = [ this.luckLabelText, this.luckText ];
labels.forEach(t => t.setAlpha(0));
const luckValue = getPartyLuckValue(this.getParty());
this.luckText.setText(getLuckString(luckValue));
if (luckValue < 14) {
this.luckText.setTint(getLuckTextTint(luckValue));
} else {
this.luckText.setTint(0xffef5c, 0x47ff69, 0x6b6bff, 0xff6969);
}
this.luckLabelText.setX((this.game.canvas.width / 6) - 2 - (this.luckText.displayWidth + 2));
this.tweens.add({
targets: labels,
duration: duration,
alpha: 1,
onComplete: () => {
labels.forEach(t => t.setVisible(true));
}
});
}
hideLuckText(duration: integer): void {
if (this.reroll) {
return;
}
const labels = [ this.luckLabelText, this.luckText ];
this.tweens.add({
targets: labels,
duration: duration,
alpha: 0,
onComplete: () => {
labels.forEach(l => l.setVisible(false));
}
});
}
updateUIPositions(): void {
const enemyModifierCount = this.enemyModifiers.filter(m => m.isIconVisible(this)).length;
const biomeWaveTextHeight = this.biomeWaveText.getBottomLeft().y - this.biomeWaveText.getTopLeft().y;
this.biomeWaveText.setY(
-(this.game.canvas.height / 6) + (enemyModifierCount ? enemyModifierCount <= 12 ? 15 : 24 : 0) + (biomeWaveTextHeight / 2)
);
this.moneyText.setY(this.biomeWaveText.y + 10);
this.scoreText.setY(this.moneyText.y + 10);
[ this.luckLabelText, this.luckText ].map(l => l.setY((this.scoreText.visible ? this.scoreText : this.moneyText).y + 10));
const offsetY = (this.scoreText.visible ? this.scoreText : this.moneyText).y + 15;
this.partyExpBar.setY(offsetY);
this.candyBar.setY(offsetY + 15);
this.ui?.achvBar.setY(this.game.canvas.height / 6 + offsetY);
}
/**
* Pushes all {@linkcode Phaser.GameObjects.Text} objects in the top right to the bottom of the canvas
*/
sendTextToBack(): void {
this.fieldUI.sendToBack(this.biomeWaveText);
this.fieldUI.sendToBack(this.moneyText);
this.fieldUI.sendToBack(this.scoreText);
}
addFaintedEnemyScore(enemy: EnemyPokemon): void {
let scoreIncrease = enemy.getSpeciesForm().getBaseExp() * (enemy.level / this.getMaxExpLevel()) * ((enemy.ivs.reduce((iv: integer, total: integer) => total += iv, 0) / 93) * 0.2 + 0.8);
this.findModifiers(m => m instanceof PokemonHeldItemModifier && m.pokemonId === enemy.id, false).map(m => scoreIncrease *= (m as PokemonHeldItemModifier).getScoreMultiplier());
if (enemy.isBoss()) {
scoreIncrease *= Math.sqrt(enemy.bossSegments);
}
this.currentBattle.battleScore += Math.ceil(scoreIncrease);
}
getMaxExpLevel(ignoreLevelCap?: boolean): integer {
if (ignoreLevelCap) {
return Number.MAX_SAFE_INTEGER;
}
const waveIndex = Math.ceil((this.currentBattle?.waveIndex || 1) / 10) * 10;
const difficultyWaveIndex = this.gameMode.getWaveForDifficulty(waveIndex);
const baseLevel = (1 + difficultyWaveIndex / 2 + Math.pow(difficultyWaveIndex / 25, 2)) * 1.2;
return Math.ceil(baseLevel / 2) * 2 + 2;
}
randomSpecies(waveIndex: integer, level: integer, fromArenaPool?: boolean, speciesFilter?: PokemonSpeciesFilter, filterAllEvolutions?: boolean): PokemonSpecies {
if (fromArenaPool) {
return this.arena.randomSpecies(waveIndex, level, undefined , getPartyLuckValue(this.party));
}
const filteredSpecies = speciesFilter ? [...new Set(allSpecies.filter(s => s.isCatchable()).filter(speciesFilter).map(s => {
if (!filterAllEvolutions) {
while (pokemonPrevolutions.hasOwnProperty(s.speciesId)) {
s = getPokemonSpecies(pokemonPrevolutions[s.speciesId]);
}
}
return s;
}))] : allSpecies.filter(s => s.isCatchable());
return filteredSpecies[Utils.randSeedInt(filteredSpecies.length)];
}
generateRandomBiome(waveIndex: integer): Biome {
const relWave = waveIndex % 250;
const biomes = Utils.getEnumValues(Biome).slice(1, Utils.getEnumValues(Biome).filter(b => b >= 40).length * -1);
const maxDepth = biomeDepths[Biome.END][0] - 2;
const depthWeights = new Array(maxDepth + 1).fill(null)
.map((_, i: integer) => ((1 - Math.min(Math.abs((i / (maxDepth - 1)) - (relWave / 250)) + 0.25, 1)) / 0.75) * 250);
const biomeThresholds: integer[] = [];
let totalWeight = 0;
for (const biome of biomes) {
totalWeight += Math.ceil(depthWeights[biomeDepths[biome][0] - 1] / biomeDepths[biome][1]);
biomeThresholds.push(totalWeight);
}
const randInt = Utils.randSeedInt(totalWeight);
for (const biome of biomes) {
if (randInt < biomeThresholds[biome]) {
return biome;
}
}
return biomes[Utils.randSeedInt(biomes.length)];
}
isBgmPlaying(): boolean {
return this.bgm && this.bgm.isPlaying;
}
playBgm(bgmName?: string, fadeOut?: boolean): void {
if (bgmName === undefined) {
bgmName = this.currentBattle?.getBgmOverride(this) || this.arena?.bgm;
}
if (this.bgm && bgmName === this.bgm.key) {
if (!this.bgm.isPlaying) {
this.bgm.play({
volume: this.masterVolume * this.bgmVolume
});
}
return;
}
if (fadeOut && !this.bgm) {
fadeOut = false;
}
this.bgmCache.add(bgmName);
this.loadBgm(bgmName);
let loopPoint = 0;
loopPoint = bgmName === this.arena.bgm
? this.arena.getBgmLoopPoint()
: this.getBgmLoopPoint(bgmName);
let loaded = false;
const playNewBgm = () => {
this.ui.bgmBar.setBgmToBgmBar(bgmName);
if (bgmName === null && this.bgm && !this.bgm.pendingRemove) {
this.bgm.play({
volume: this.masterVolume * this.bgmVolume
});
return;
}
if (this.bgm && !this.bgm.pendingRemove && this.bgm.isPlaying) {
this.bgm.stop();
}
this.bgm = this.sound.add(bgmName, { loop: true });
this.bgm.play({
volume: this.masterVolume * this.bgmVolume
});
if (loopPoint) {
this.bgm.on("looped", () => this.bgm.play({ seek: loopPoint }));
}
};
this.load.once(Phaser.Loader.Events.COMPLETE, () => {
loaded = true;
if (!fadeOut || !this.bgm.isPlaying) {
playNewBgm();
}
});
if (fadeOut) {
const onBgmFaded = () => {
if (loaded && (!this.bgm.isPlaying || this.bgm.pendingRemove)) {
playNewBgm();
}
};
this.time.delayedCall(this.fadeOutBgm(500, true) ? 750 : 250, onBgmFaded);
}
if (!this.load.isLoading()) {
this.load.start();
}
}
pauseBgm(): boolean {
if (this.bgm && !this.bgm.pendingRemove && this.bgm.isPlaying) {
this.bgm.pause();
return true;
}
return false;
}
resumeBgm(): boolean {
if (this.bgm && !this.bgm.pendingRemove && this.bgm.isPaused) {
this.bgm.resume();
return true;
}
return false;
}
updateSoundVolume(): void {
if (this.sound) {
for (const sound of this.sound.getAllPlaying()) {
(sound as AnySound).setVolume(this.masterVolume * (this.bgmCache.has(sound.key) ? this.bgmVolume : this.seVolume));
}
}
}
fadeOutBgm(duration: integer = 500, destroy: boolean = true): boolean {
if (!this.bgm) {
return false;
}
const bgm = this.sound.getAllPlaying().find(bgm => bgm.key === this.bgm.key);
if (bgm) {
SoundFade.fadeOut(this, this.bgm, duration, destroy);
return true;
}
return false;
}
playSound(sound: string | AnySound, config?: object): AnySound {
if (config) {
if (config.hasOwnProperty("volume")) {
config["volume"] *= this.masterVolume * this.seVolume;
} else {
config["volume"] = this.masterVolume * this.seVolume;
}
} else {
config = { volume: this.masterVolume * this.seVolume };
}
// PRSFX sounds are mixed too loud
if ((typeof sound === "string" ? sound : sound.key).startsWith("PRSFX- ")) {
config["volume"] *= 0.5;
}
if (typeof sound === "string") {
this.sound.play(sound, config);
return this.sound.get(sound) as AnySound;
} else {
sound.play(config);
return sound;
}
}
playSoundWithoutBgm(soundName: string, pauseDuration?: integer): AnySound {
this.bgmCache.add(soundName);
const resumeBgm = this.pauseBgm();
this.playSound(soundName);
const sound = this.sound.get(soundName) as AnySound;
if (this.bgmResumeTimer) {
this.bgmResumeTimer.destroy();
}
if (resumeBgm) {
this.bgmResumeTimer = this.time.delayedCall((pauseDuration || Utils.fixedInt(sound.totalDuration * 1000)), () => {
this.resumeBgm();
this.bgmResumeTimer = null;
});
}
return sound;
}
getBgmLoopPoint(bgmName: string): number {
switch (bgmName) {
case "battle_kanto_champion": //B2W2 Kanto Champion Battle
return 13.950;
case "battle_johto_champion": //B2W2 Johto Champion Battle
return 23.498;
case "battle_hoenn_champion_g5": //B2W2 Hoenn Champion Battle
return 11.328;
case "battle_hoenn_champion_g6": //ORAS Hoenn Champion Battle
return 11.762;
case "battle_sinnoh_champion": //B2W2 Sinnoh Champion Battle
return 12.235;
case "battle_champion_alder": //BW Unova Champion Battle
return 27.653;
case "battle_champion_iris": //B2W2 Unova Champion Battle
return 10.145;
case "battle_kalos_champion": //XY Kalos Champion Battle
return 10.380;
case "battle_alola_champion": //USUM Alola Champion Battle
return 13.025;
case "battle_galar_champion": //SWSH Galar Champion Battle
return 61.635;
case "battle_champion_geeta": //SV Champion Geeta Battle
return 37.447;
case "battle_champion_nemona": //SV Champion Nemona Battle
return 14.914;
case "battle_champion_kieran": //SV Champion Kieran Battle
return 7.206;
case "battle_hoenn_elite": //ORAS Elite Four Battle
return 11.350;
case "battle_unova_elite": //BW Elite Four Battle
return 17.730;
case "battle_kalos_elite": //XY Elite Four Battle
return 12.340;
case "battle_alola_elite": //SM Elite Four Battle
return 19.212;
case "battle_galar_elite": //SWSH League Tournament Battle
return 164.069;
case "battle_paldea_elite": //SV Elite Four Battle
return 12.770;
case "battle_bb_elite": //SV BB League Elite Four Battle
return 19.434;
case "battle_final_encounter": //PMD RTDX Rayquaza's Domain
return 19.159;
case "battle_final": //BW Ghetsis Battle
return 16.453;
case "battle_kanto_gym": //B2W2 Kanto Gym Battle
return 13.857;
case "battle_johto_gym": //B2W2 Johto Gym Battle
return 12.911;
case "battle_hoenn_gym": //B2W2 Hoenn Gym Battle
return 12.379;
case "battle_sinnoh_gym": //B2W2 Sinnoh Gym Battle
return 13.122;
case "battle_unova_gym": //BW Unova Gym Battle
return 19.145;
case "battle_kalos_gym": //XY Kalos Gym Battle
return 44.810;
case "battle_galar_gym": //SWSH Galar Gym Battle
return 171.262;
case "battle_paldea_gym": //SV Paldea Gym Battle
return 127.489;
case "battle_legendary_kanto": //XY Kanto Legendary Battle
return 32.966;
case "battle_legendary_raikou": //HGSS Raikou Battle
return 12.632;
case "battle_legendary_entei": //HGSS Entei Battle
return 2.905;
case "battle_legendary_suicune": //HGSS Suicune Battle
return 12.636;
case "battle_legendary_lugia": //HGSS Lugia Battle
return 19.770;
case "battle_legendary_ho_oh": //HGSS Ho-oh Battle
return 17.668;
case "battle_legendary_regis_g5": //B2W2 Legendary Titan Battle
return 49.500;
case "battle_legendary_regis_g6": //ORAS Legendary Titan Battle
return 21.130;
case "battle_legendary_gro_kyo": //ORAS Groudon & Kyogre Battle
return 10.547;
case "battle_legendary_rayquaza": //ORAS Rayquaza Battle
return 10.495;
case "battle_legendary_deoxys": //ORAS Deoxys Battle
return 13.333;
case "battle_legendary_lake_trio": //ORAS Lake Guardians Battle
return 16.887;
case "battle_legendary_sinnoh": //ORAS Sinnoh Legendary Battle
return 22.770;
case "battle_legendary_dia_pal": //ORAS Dialga & Palkia Battle
return 16.009;
case "battle_legendary_giratina": //ORAS Giratina Battle
return 10.451;
case "battle_legendary_arceus": //HGSS Arceus Battle
return 9.595;
case "battle_legendary_unova": //BW Unova Legendary Battle
return 13.855;
case "battle_legendary_kyurem": //BW Kyurem Battle
return 18.314;
case "battle_legendary_res_zek": //BW Reshiram & Zekrom Battle
return 18.329;
case "battle_legendary_xern_yvel": //XY Xerneas & Yveltal Battle
return 26.468;
case "battle_legendary_tapu": //SM Tapu Battle
return 0.000;
case "battle_legendary_sol_lun": //SM Solgaleo & Lunala Battle
return 6.525;
case "battle_legendary_ub": //SM Ultra Beast Battle
return 9.818;
case "battle_legendary_dusk_dawn": //USUM Dusk Mane & Dawn Wings Necrozma Battle
return 5.211;
case "battle_legendary_ultra_nec": //USUM Ultra Necrozma Battle
return 10.344;
case "battle_legendary_zac_zam": //SWSH Zacian & Zamazenta Battle
return 11.424;
case "battle_legendary_glas_spec": //SWSH Glastrier & Spectrier Battle
return 12.503;
case "battle_legendary_calyrex": //SWSH Calyrex Battle
return 50.641;
case "battle_legendary_birds_galar": //SWSH Galarian Legendary Birds Battle
return 0.175;
case "battle_legendary_ruinous": //SV Treasures of Ruin Battle
return 6.333;
case "battle_legendary_kor_mir": //SV Depths of Area Zero Battle
return 6.442;
case "battle_legendary_loyal_three": //SV Loyal Three Battle
return 6.500;
case "battle_legendary_ogerpon": //SV Ogerpon Battle
return 14.335;
case "battle_legendary_terapagos": //SV Terapagos Battle
return 24.377;
case "battle_legendary_pecharunt": //SV Pecharunt Battle
return 6.508;
case "battle_rival": //BW Rival Battle
return 14.110;
case "battle_rival_2": //BW N Battle
return 17.714;
case "battle_rival_3": //BW Final N Battle
return 17.586;
case "battle_trainer": //BW Trainer Battle
return 13.686;
case "battle_wild": //BW Wild Battle
return 12.703;
case "battle_wild_strong": //BW Strong Wild Battle
return 13.940;
case "end_summit": //PMD RTDX Sky Tower Summit
return 30.025;
case "battle_rocket_grunt": //HGSS Team Rocket Battle
return 12.707;
case "battle_aqua_magma_grunt": //ORAS Team Aqua & Magma Battle
return 12.062;
case "battle_galactic_grunt": //BDSP Team Galactic Battle
return 13.043;
case "battle_plasma_grunt": //BW Team Plasma Battle
return 12.974;
case "battle_flare_grunt": //XY Team Flare Battle
return 4.228;
case "battle_rocket_boss": //USUM Giovanni Battle
return 9.115;
case "battle_aqua_magma_boss": //ORAS Archie & Maxie Battle
return 14.847;
case "battle_galactic_boss": //BDSP Cyrus Battle
return 106.962;
case "battle_plasma_boss": //B2W2 Ghetsis Battle
return 25.624;
case "battle_flare_boss": //XY Lysandre Battle
return 8.085;
}
return 0;
}
toggleInvert(invert: boolean): void {
if (invert) {
this.cameras.main.setPostPipeline(InvertPostFX);
} else {
this.cameras.main.removePostPipeline("InvertPostFX");
}
}
/* Phase Functions */
getCurrentPhase(): Phase | null {
return this.currentPhase;
}
getStandbyPhase(): Phase | null {
return this.standbyPhase;
}
/**
* Adds a phase to the conditional queue and ensures it is executed only when the specified condition is met.
*
* This method allows deferring the execution of a phase until certain conditions are met, which is useful for handling
* situations like abilities and entry hazards that depend on specific game states.
*
* @param {Phase} phase - The phase to be added to the conditional queue.
* @param {() => boolean} condition - A function that returns a boolean indicating whether the phase should be executed.
*
*/
pushConditionalPhase(phase: Phase, condition: () => boolean): void {
this.conditionalQueue.push([condition, phase]);
}
/**
* Adds a phase to nextCommandPhaseQueue, as long as boolean passed in is false
* @param phase {@linkcode Phase} the phase to add
* @param defer boolean on which queue to add to, defaults to false, and adds to phaseQueue
*/
pushPhase(phase: Phase, defer: boolean = false): void {
(!defer ? this.phaseQueue : this.nextCommandPhaseQueue).push(phase);
}
/**
* Adds Phase to the end of phaseQueuePrepend, or at phaseQueuePrependSpliceIndex
* @param phase {@linkcode Phase} the phase to add
*/
unshiftPhase(phase: Phase): void {
if (this.phaseQueuePrependSpliceIndex === -1) {
this.phaseQueuePrepend.push(phase);
} else {
this.phaseQueuePrepend.splice(this.phaseQueuePrependSpliceIndex, 0, phase);
}
}
/**
* Clears the phaseQueue
*/
clearPhaseQueue(): void {
this.phaseQueue.splice(0, this.phaseQueue.length);
}
/**
* Used by function unshiftPhase(), sets index to start inserting at current length instead of the end of the array, useful if phaseQueuePrepend gets longer with Phases
*/
setPhaseQueueSplice(): void {
this.phaseQueuePrependSpliceIndex = this.phaseQueuePrepend.length;
}
/**
* Resets phaseQueuePrependSpliceIndex to -1, implies that calls to unshiftPhase will insert at end of phaseQueuePrepend
*/
clearPhaseQueueSplice(): void {
this.phaseQueuePrependSpliceIndex = -1;
}
/**
* Is called by each Phase implementations "end()" by default
* We dump everything from phaseQueuePrepend to the start of of phaseQueue
* then removes first Phase and starts it
*/
shiftPhase(): void {
if (this.standbyPhase) {
this.currentPhase = this.standbyPhase;
this.standbyPhase = null;
return;
}
if (this.phaseQueuePrependSpliceIndex > -1) {
this.clearPhaseQueueSplice();
}
if (this.phaseQueuePrepend.length) {
while (this.phaseQueuePrepend.length) {
const poppedPhase = this.phaseQueuePrepend.pop();
if (poppedPhase) {
this.phaseQueue.unshift(poppedPhase);
}
}
}
if (!this.phaseQueue.length) {
this.populatePhaseQueue();
// Clear the conditionalQueue if there are no phases left in the phaseQueue
this.conditionalQueue = [];
}
this.currentPhase = this.phaseQueue.shift() ?? null;
// Check if there are any conditional phases queued
if (this.conditionalQueue?.length) {
// Retrieve the first conditional phase from the queue
const conditionalPhase = this.conditionalQueue.shift();
// Evaluate the condition associated with the phase
if (conditionalPhase?.[0]()) {
// If the condition is met, add the phase to the phase queue
this.pushPhase(conditionalPhase[1]);
} else if (conditionalPhase) {
// If the condition is not met, re-add the phase back to the front of the conditional queue
this.conditionalQueue.unshift(conditionalPhase);
} else {
console.warn("condition phase is undefined/null!", conditionalPhase);
}
}
this.currentPhase?.start();
}
overridePhase(phase: Phase): boolean {
if (this.standbyPhase) {
return false;
}
this.standbyPhase = this.currentPhase;
this.currentPhase = phase;
phase.start();
return true;
}
findPhase(phaseFilter: (phase: Phase) => boolean): Phase | undefined {
return this.phaseQueue.find(phaseFilter);
}
tryReplacePhase(phaseFilter: (phase: Phase) => boolean, phase: Phase): boolean {
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
if (phaseIndex > -1) {
this.phaseQueue[phaseIndex] = phase;
return true;
}
return false;
}
tryRemovePhase(phaseFilter: (phase: Phase) => boolean): boolean {
const phaseIndex = this.phaseQueue.findIndex(phaseFilter);
if (phaseIndex > -1) {
this.phaseQueue.splice(phaseIndex, 1);
return true;
}
return false;
}
pushMovePhase(movePhase: MovePhase, priorityOverride?: integer): void {
const movePriority = new Utils.IntegerHolder(priorityOverride !== undefined ? priorityOverride : movePhase.move.getMove().priority);
applyAbAttrs(IncrementMovePriorityAbAttr, movePhase.pokemon, null, movePhase.move.getMove(), movePriority);
const lowerPriorityPhase = this.phaseQueue.find(p => p instanceof MovePhase && p.move.getMove().priority < movePriority.value);
if (lowerPriorityPhase) {
this.phaseQueue.splice(this.phaseQueue.indexOf(lowerPriorityPhase), 0, movePhase);
} else {
this.pushPhase(movePhase);
}
}
/**
* Tries to add the input phase to index before target phase in the phaseQueue, else simply calls unshiftPhase()
* @param phase {@linkcode Phase} the phase to be added
* @param targetPhase {@linkcode Phase} the type of phase to search for in phaseQueue
* @returns boolean if a targetPhase was found and added
*/
prependToPhase(phase: Phase, targetPhase: Constructor<Phase>): boolean {
const targetIndex = this.phaseQueue.findIndex(ph => ph instanceof targetPhase);
if (targetIndex !== -1) {
this.phaseQueue.splice(targetIndex, 0, phase);
return true;
} else {
this.unshiftPhase(phase);
return false;
}
}
/**
* Adds a MessagePhase, either to PhaseQueuePrepend or nextCommandPhaseQueue
* @param message string for MessagePhase
* @param callbackDelay optional param for MessagePhase constructor
* @param prompt optional param for MessagePhase constructor
* @param promptDelay optional param for MessagePhase constructor
* @param defer boolean for which queue to add it to, false -> add to PhaseQueuePrepend, true -> nextCommandPhaseQueue
*/
queueMessage(message: string, callbackDelay?: integer | null, prompt?: boolean | null, promptDelay?: integer | null, defer?: boolean | null) {
const phase = new MessagePhase(this, message, callbackDelay, prompt, promptDelay);
if (!defer) {
// adds to the end of PhaseQueuePrepend
this.unshiftPhase(phase);
} else {
//remember that pushPhase adds it to nextCommandPhaseQueue
this.pushPhase(phase);
}
}
/**
* Moves everything from nextCommandPhaseQueue to phaseQueue (keeping order)
*/
populatePhaseQueue(): void {
if (this.nextCommandPhaseQueue.length) {
this.phaseQueue.push(...this.nextCommandPhaseQueue);
this.nextCommandPhaseQueue.splice(0, this.nextCommandPhaseQueue.length);
}
this.phaseQueue.push(new TurnInitPhase(this));
}
addMoney(amount: integer): void {
this.money = Math.min(this.money + amount, Number.MAX_SAFE_INTEGER);
this.updateMoneyText();
this.animateMoneyChanged(true);
this.validateAchvs(MoneyAchv);
}
getWaveMoneyAmount(moneyMultiplier: number): integer {
const waveIndex = this.currentBattle.waveIndex;
const waveSetIndex = Math.ceil(waveIndex / 10) - 1;
const moneyValue = Math.pow((waveSetIndex + 1 + (0.75 + (((waveIndex - 1) % 10) + 1) / 10)) * 100, 1 + 0.005 * waveSetIndex) * moneyMultiplier;
return Math.floor(moneyValue / 10) * 10;
}
addModifier(modifier: Modifier | null, ignoreUpdate?: boolean, playSound?: boolean, virtual?: boolean, instant?: boolean): Promise<boolean> {
if (!modifier) {
return Promise.resolve(false);
}
return new Promise(resolve => {
let success = false;
const soundName = modifier.type.soundName;
this.validateAchvs(ModifierAchv, modifier);
const modifiersToRemove: PersistentModifier[] = [];
const modifierPromises: Promise<boolean>[] = [];
if (modifier instanceof PersistentModifier) {
if (modifier instanceof TerastallizeModifier) {
modifiersToRemove.push(...(this.findModifiers(m => m instanceof TerastallizeModifier && m.pokemonId === modifier.pokemonId)));
}
if ((modifier as PersistentModifier).add(this.modifiers, !!virtual, this)) {
if (modifier instanceof PokemonFormChangeItemModifier || modifier instanceof TerastallizeModifier) {
success = modifier.apply([ this.getPokemonById(modifier.pokemonId), true ]);
}
if (playSound && !this.sound.get(soundName)) {
this.playSound(soundName);
}
} else if (!virtual) {
const defaultModifierType = getDefaultModifierTypeForTier(modifier.type.tier);
this.queueMessage(`The stack for this item is full.\n You will receive ${defaultModifierType.name} instead.`, undefined, true);
return this.addModifier(defaultModifierType.newModifier(), ignoreUpdate, playSound, false, instant).then(success => resolve(success));
}
for (const rm of modifiersToRemove) {
this.removeModifier(rm);
}
if (!ignoreUpdate && !virtual) {
return this.updateModifiers(true, instant).then(() => resolve(success));
}
} else if (modifier instanceof ConsumableModifier) {
if (playSound && !this.sound.get(soundName)) {
this.playSound(soundName);
}
if (modifier instanceof ConsumablePokemonModifier) {
for (const p in this.party) {
const pokemon = this.party[p];
const args: any[] = [ pokemon ];
if (modifier instanceof PokemonHpRestoreModifier) {
if (!(modifier as PokemonHpRestoreModifier).fainted) {
const hpRestoreMultiplier = new Utils.IntegerHolder(1);
this.applyModifiers(HealingBoosterModifier, true, hpRestoreMultiplier);
args.push(hpRestoreMultiplier.value);
} else {
args.push(1);
}
} else if (modifier instanceof FusePokemonModifier) {
args.push(this.getPokemonById(modifier.fusePokemonId) as PlayerPokemon);
}
if (modifier.shouldApply(args)) {
const result = modifier.apply(args);
if (result instanceof Promise) {
modifierPromises.push(result.then(s => success ||= s));
} else {
success ||= result;
}
}
}
return Promise.allSettled([this.party.map(p => p.updateInfo(instant)), ...modifierPromises]).then(() => resolve(success));
} else {
const args = [ this ];
if (modifier.shouldApply(args)) {
const result = modifier.apply(args);
if (result instanceof Promise) {
return result.then(success => resolve(success));
} else {
success ||= result;
}
}
}
}
resolve(success);
});
}
addEnemyModifier(modifier: PersistentModifier, ignoreUpdate?: boolean, instant?: boolean): Promise<void> {
return new Promise(resolve => {
const modifiersToRemove: PersistentModifier[] = [];
if (modifier instanceof TerastallizeModifier) {
modifiersToRemove.push(...(this.findModifiers(m => m instanceof TerastallizeModifier && m.pokemonId === modifier.pokemonId, false)));
}
if ((modifier as PersistentModifier).add(this.enemyModifiers, false, this)) {
if (modifier instanceof PokemonFormChangeItemModifier || modifier instanceof TerastallizeModifier) {
modifier.apply([ this.getPokemonById(modifier.pokemonId), true ]);
}
for (const rm of modifiersToRemove) {
this.removeModifier(rm, true);
}
}
if (!ignoreUpdate) {
this.updateModifiers(false, instant).then(() => resolve());
} else {
resolve();
}
});
}
/**
* Try to transfer a held item to another pokemon.
* If the recepient already has the maximum amount allowed for this item, the transfer is cancelled.
* The quantity to transfer is automatically capped at how much the recepient can take before reaching the maximum stack size for the item.
* A transfer that moves a quantity smaller than what is specified in the transferQuantity parameter is still considered successful.
* @param itemModifier {@linkcode PokemonHeldItemModifier} item to transfer (represents the whole stack)
* @param target {@linkcode Pokemon} pokemon recepient in this transfer
* @param playSound {boolean}
* @param transferQuantity {@linkcode integer} how many items of the stack to transfer. Optional, defaults to 1
* @param instant {boolean}
* @param ignoreUpdate {boolean}
* @returns true if the transfer was successful
*/
tryTransferHeldItemModifier(itemModifier: PokemonHeldItemModifier, target: Pokemon, playSound: boolean, transferQuantity: integer = 1, instant?: boolean, ignoreUpdate?: boolean): Promise<boolean> {
return new Promise(resolve => {
const source = itemModifier.pokemonId ? itemModifier.getPokemon(target.scene) : null;
const cancelled = new Utils.BooleanHolder(false);
Utils.executeIf(!!source && source.isPlayer() !== target.isPlayer(), () => applyAbAttrs(BlockItemTheftAbAttr, source! /* checked in condition*/, cancelled)).then(() => {
if (cancelled.value) {
return resolve(false);
}
const newItemModifier = itemModifier.clone() as PokemonHeldItemModifier;
newItemModifier.pokemonId = target.id;
const matchingModifier = target.scene.findModifier(m => m instanceof PokemonHeldItemModifier
&& (m as PokemonHeldItemModifier).matchType(itemModifier) && m.pokemonId === target.id, target.isPlayer()) as PokemonHeldItemModifier;
let removeOld = true;
if (matchingModifier) {
const maxStackCount = matchingModifier.getMaxStackCount(target.scene);
if (matchingModifier.stackCount >= maxStackCount) {
return resolve(false);
}
const countTaken = Math.min(transferQuantity, itemModifier.stackCount, maxStackCount - matchingModifier.stackCount);
itemModifier.stackCount -= countTaken;
newItemModifier.stackCount = matchingModifier.stackCount + countTaken;
removeOld = !itemModifier.stackCount;
} else {
const countTaken = Math.min(transferQuantity, itemModifier.stackCount);
itemModifier.stackCount -= countTaken;
newItemModifier.stackCount = countTaken;
}
removeOld = !itemModifier.stackCount;
if (!removeOld || !source || this.removeModifier(itemModifier, !source.isPlayer())) {
const addModifier = () => {
if (!matchingModifier || this.removeModifier(matchingModifier, !target.isPlayer())) {
if (target.isPlayer()) {
this.addModifier(newItemModifier, ignoreUpdate, playSound, false, instant).then(() => resolve(true));
} else {
this.addEnemyModifier(newItemModifier, ignoreUpdate, instant).then(() => resolve(true));
}
} else {
resolve(false);
}
};
if (source && source.isPlayer() !== target.isPlayer() && !ignoreUpdate) {
this.updateModifiers(source.isPlayer(), instant).then(() => addModifier());
} else {
addModifier();
}
return;
}
resolve(false);
});
});
}
removePartyMemberModifiers(partyMemberIndex: integer): Promise<void> {
return new Promise(resolve => {
const pokemonId = this.getParty()[partyMemberIndex].id;
const modifiersToRemove = this.modifiers.filter(m => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === pokemonId);
for (const m of modifiersToRemove) {
this.modifiers.splice(this.modifiers.indexOf(m), 1);
}
this.updateModifiers().then(() => resolve());
});
}
generateEnemyModifiers(): Promise<void> {
return new Promise(resolve => {
if (this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
return resolve();
}
const difficultyWaveIndex = this.gameMode.getWaveForDifficulty(this.currentBattle.waveIndex);
const isFinalBoss = this.gameMode.isWaveFinal(this.currentBattle.waveIndex);
let chances = Math.ceil(difficultyWaveIndex / 10);
if (isFinalBoss) {
chances = Math.ceil(chances * 2.5);
}
const party = this.getEnemyParty();
if (this.currentBattle.trainer) {
const modifiers = this.currentBattle.trainer.genModifiers(party);
for (const modifier of modifiers) {
this.addEnemyModifier(modifier, true, true);
}
}
party.forEach((enemyPokemon: EnemyPokemon, i: integer) => {
const isBoss = enemyPokemon.isBoss() || (this.currentBattle.battleType === BattleType.TRAINER && this.currentBattle.trainer?.config.isBoss);
let upgradeChance = 32;
if (isBoss) {
upgradeChance /= 2;
}
if (isFinalBoss) {
upgradeChance /= 8;
}
const modifierChance = this.gameMode.getEnemyModifierChance(isBoss!); // TODO: is this bang correct?
let pokemonModifierChance = modifierChance;
if (this.currentBattle.battleType === BattleType.TRAINER && this.currentBattle.trainer)
pokemonModifierChance = Math.ceil(pokemonModifierChance * this.currentBattle.trainer.getPartyMemberModifierChanceMultiplier(i)); // eslint-disable-line
let count = 0;
for (let c = 0; c < chances; c++) {
if (!Utils.randSeedInt(modifierChance)) {
count++;
}
}
if (isBoss) {
count = Math.max(count, Math.floor(chances / 2));
}
getEnemyModifierTypesForWave(difficultyWaveIndex, count, [ enemyPokemon ], this.currentBattle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD, upgradeChance)
.map(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false, this));
});
this.updateModifiers(false).then(() => resolve());
});
}
/**
* Removes all modifiers from enemy of PersistentModifier type
*/
clearEnemyModifiers(): void {
const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PersistentModifier);
for (const m of modifiersToRemove) {
this.enemyModifiers.splice(this.enemyModifiers.indexOf(m), 1);
}
this.updateModifiers(false).then(() => this.updateUIPositions());
}
/**
* Removes all modifiers from enemy of PokemonHeldItemModifier type
*/
clearEnemyHeldItemModifiers(): void {
const modifiersToRemove = this.enemyModifiers.filter(m => m instanceof PokemonHeldItemModifier);
for (const m of modifiersToRemove) {
this.enemyModifiers.splice(this.enemyModifiers.indexOf(m), 1);
}
this.updateModifiers(false).then(() => this.updateUIPositions());
}
setModifiersVisible(visible: boolean) {
[ this.modifierBar, this.enemyModifierBar ].map(m => m.setVisible(visible));
}
updateModifiers(player?: boolean, instant?: boolean): Promise<void> {
if (player === undefined) {
player = true;
}
return new Promise(resolve => {
const modifiers = player ? this.modifiers : this.enemyModifiers as PersistentModifier[];
for (let m = 0; m < modifiers.length; m++) {
const modifier = modifiers[m];
if (modifier instanceof PokemonHeldItemModifier && !this.getPokemonById((modifier as PokemonHeldItemModifier).pokemonId)) {
modifiers.splice(m--, 1);
}
}
for (const modifier of modifiers) {
if (modifier instanceof PersistentModifier) {
(modifier as PersistentModifier).virtualStackCount = 0;
}
}
const modifiersClone = modifiers.slice(0);
for (const modifier of modifiersClone) {
if (!modifier.getStackCount()) {
modifiers.splice(modifiers.indexOf(modifier), 1);
}
}
this.updatePartyForModifiers(player ? this.getParty() : this.getEnemyParty(), instant).then(() => {
(player ? this.modifierBar : this.enemyModifierBar).updateModifiers(modifiers);
if (!player) {
this.updateUIPositions();
}
resolve();
});
});
}
updatePartyForModifiers(party: Pokemon[], instant?: boolean): Promise<void> {
return new Promise(resolve => {
Promise.allSettled(party.map(p => {
if (p.scene) {
p.calculateStats();
}
return p.updateInfo(instant);
})).then(() => resolve());
});
}
removeModifier(modifier: PersistentModifier, enemy?: boolean): boolean {
const modifiers = !enemy ? this.modifiers : this.enemyModifiers;
const modifierIndex = modifiers.indexOf(modifier);
if (modifierIndex > -1) {
modifiers.splice(modifierIndex, 1);
if (modifier instanceof PokemonFormChangeItemModifier || modifier instanceof TerastallizeModifier) {
modifier.apply([ this.getPokemonById(modifier.pokemonId), false ]);
}
return true;
}
return false;
}
/**
* Get all of the modifiers that match `modifierType`
* @param modifierType The type of modifier to apply; must extend {@linkcode PersistentModifier}
* @param player Whether to search the player (`true`) or the enemy (`false`); Defaults to `true`
* @returns the list of all modifiers that matched `modifierType`.
*/
getModifiers<T extends PersistentModifier>(modifierType: Constructor<T>, player: boolean = true): T[] {
return (player ? this.modifiers : this.enemyModifiers).filter((m): m is T => m instanceof modifierType);
}
findModifiers(modifierFilter: ModifierPredicate, player: boolean = true): PersistentModifier[] {
return (player ? this.modifiers : this.enemyModifiers).filter(m => (modifierFilter as ModifierPredicate)(m));
}
findModifier(modifierFilter: ModifierPredicate, player: boolean = true): PersistentModifier | undefined {
return (player ? this.modifiers : this.enemyModifiers).find(m => (modifierFilter as ModifierPredicate)(m));
}
applyShuffledModifiers(scene: BattleScene, modifierType: Constructor<Modifier>, player: boolean = true, ...args: any[]): PersistentModifier[] {
let modifiers = (player ? this.modifiers : this.enemyModifiers).filter(m => m instanceof modifierType && m.shouldApply(args));
scene.executeWithSeedOffset(() => {
const shuffleModifiers = mods => {
if (mods.length < 1) {
return mods;
}
const rand = Math.floor(Utils.randSeedInt(mods.length));
return [mods[rand], ...shuffleModifiers(mods.filter((_, i) => i !== rand))];
};
modifiers = shuffleModifiers(modifiers);
}, scene.currentBattle.turn << 4, scene.waveSeed);
return this.applyModifiersInternal(modifiers, player, args);
}
applyModifiers(modifierType: Constructor<Modifier>, player: boolean = true, ...args: any[]): PersistentModifier[] {
const modifiers = (player ? this.modifiers : this.enemyModifiers).filter(m => m instanceof modifierType && m.shouldApply(args));
return this.applyModifiersInternal(modifiers, player, args);
}
applyModifiersInternal(modifiers: PersistentModifier[], player: boolean, args: any[]): PersistentModifier[] {
const appliedModifiers: PersistentModifier[] = [];
for (const modifier of modifiers) {
if (modifier.apply(args)) {
console.log("Applied", modifier.type.name, !player ? "(enemy)" : "");
appliedModifiers.push(modifier);
}
}
return appliedModifiers;
}
applyModifier(modifierType: Constructor<Modifier>, player: boolean = true, ...args: any[]): PersistentModifier | null {
const modifiers = (player ? this.modifiers : this.enemyModifiers).filter(m => m instanceof modifierType && m.shouldApply(args));
for (const modifier of modifiers) {
if (modifier.apply(args)) {
console.log("Applied", modifier.type.name, !player ? "(enemy)" : "");
return modifier;
}
}
return null;
}
triggerPokemonFormChange(pokemon: Pokemon, formChangeTriggerType: Constructor<SpeciesFormChangeTrigger>, delayed: boolean = false, modal: boolean = false): boolean {
if (pokemonFormChanges.hasOwnProperty(pokemon.species.speciesId)) {
const matchingFormChange = pokemonFormChanges[pokemon.species.speciesId].find(fc => fc.findTrigger(formChangeTriggerType) && fc.canChange(pokemon));
if (matchingFormChange) {
let phase: Phase;
if (pokemon instanceof PlayerPokemon && !matchingFormChange.quiet) {
phase = new FormChangePhase(this, pokemon, matchingFormChange, modal);
} else {
phase = new QuietFormChangePhase(this, pokemon, matchingFormChange);
}
if (pokemon instanceof PlayerPokemon && !matchingFormChange.quiet && modal) {
this.overridePhase(phase);
} else if (delayed) {
this.pushPhase(phase);
} else {
this.unshiftPhase(phase);
}
return true;
}
}
return false;
}
validateAchvs(achvType: Constructor<Achv>, ...args: unknown[]): void {
const filteredAchvs = Object.values(achvs).filter(a => a instanceof achvType);
for (const achv of filteredAchvs) {
this.validateAchv(achv, args);
}
}
validateAchv(achv: Achv, args?: any[]): boolean {
if (!this.gameData.achvUnlocks.hasOwnProperty(achv.id) && achv.validate(this, args)) {
this.gameData.achvUnlocks[achv.id] = new Date().getTime();
this.ui.achvBar.showAchv(achv);
if (vouchers.hasOwnProperty(achv.id)) {
this.validateVoucher(vouchers[achv.id]);
}
return true;
}
return false;
}
validateVoucher(voucher: Voucher, args?: any[]): boolean {
if (!this.gameData.voucherUnlocks.hasOwnProperty(voucher.id) && voucher.validate(this, args)) {
this.gameData.voucherUnlocks[voucher.id] = new Date().getTime();
this.ui.achvBar.showAchv(voucher);
this.gameData.voucherCounts[voucher.voucherType]++;
return true;
}
return false;
}
updateGameInfo(): void {
const gameInfo = {
playTime: this.sessionPlayTime ? this.sessionPlayTime : 0,
gameMode: this.currentBattle ? this.gameMode.getName() : "Title",
biome: this.currentBattle ? getBiomeName(this.arena.biomeType) : "",
wave: this.currentBattle?.waveIndex || 0,
party: this.party ? this.party.map(p => {
return { name: p.name, level: p.level };
}) : []
};
(window as any).gameInfo = gameInfo;
}
/**
* Initialized the 2nd phase of the final boss (e.g. form-change for Eternatus)
* @param pokemon The (enemy) pokemon
*/
initFinalBossPhaseTwo(pokemon: Pokemon): void {
if (pokemon instanceof EnemyPokemon && pokemon.isBoss() && !pokemon.formIndex && pokemon.bossSegmentIndex < 1) {
this.fadeOutBgm(Utils.fixedInt(2000), false);
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);
pokemon.generateAndPopulateMoveset(1);
this.setFieldScale(0.75);
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeManualTrigger, false);
this.currentBattle.double = true;
const availablePartyMembers = this.getParty().filter((p) => p.isAllowedInBattle());
if (availablePartyMembers.length > 1) {
this.pushPhase(new ToggleDoublePositionPhase(this, true));
if (!availablePartyMembers[1].isOnField()) {
this.pushPhase(new SummonPhase(this, 1));
}
}
this.shiftPhase();
});
return;
}
this.shiftPhase();
}
}