Ctrl K
ComunidadGuías…

src/battle.ts

Rama beta · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
import { DAILY_BOSS_LEVEL } from "#app/constants";
import type { GameMode } from "#app/game-mode";
import { globalScene } from "#app/global-scene";
import { settings } from "#app/global-settings-manager";
import { isDailyFinalBoss } from "#data/daily-seed-utils";
import { ArenaTagType } from "#enums/arena-tag-type";
import { BattleType } from "#enums/battle-type";
import { BattlerIndex } from "#enums/battler-index";
import { BiomeId } from "#enums/biome-id";
import type { Command } from "#enums/command";
import { MoveId } from "#enums/move-id";
import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import type { MysteryEncounterType } from "#enums/mystery-encounter-type";
import type { PokeballType } from "#enums/pokeball";
import { SpeciesFormKey } from "#enums/species-form-key";
import { SpeciesId } from "#enums/species-id";
import { TrainerType } from "#enums/trainer-type";
import { TrainerVariant } from "#enums/trainer-variant";
import type { EnemyPokemon, PlayerPokemon, Pokemon } from "#field/pokemon";
import { Trainer } from "#field/trainer";
import { MoneyMultiplierModifier, type PokemonHeldItemModifier } from "#modifiers/modifier";
import type { CustomModifierSettings } from "#modifiers/modifier-type";
import type { MysteryEncounter } from "#mystery-encounters/mystery-encounter";
import { trainerConfigs } from "#trainers/trainer-config";
import type { NewBattleResolvedProps } from "#types/new-battle-props";
import type { TurnMove } from "#types/turn-move";
import {
  isBetween,
  NumberHolder,
  randInt,
  randomString,
  randSeedFloat,
  randSeedInt,
  randSeedItem,
  shiftCharCodes,
} from "#utils/common";
import { getEnumValues } from "#utils/enums";
import { randSeedUniqueItem } from "#utils/random";
import i18next from "i18next";
import Phaser from "phaser";

export interface TurnCommand {
  command: Command;
  cursor?: number | undefined;
  move?: TurnMove;
  targets?: BattlerIndex[];
  skip?: boolean;
  args?: any[];
}

export interface FaintLogEntry {
  pokemon: Pokemon;
  turn: number;
}

interface TurnCommands {
  [key: number]: TurnCommand | null;
}

export class Battle {
  protected gameMode: GameMode;
  public waveIndex: number;
  public battleType: BattleType;
  public trainer: Trainer | null;
  public enemyLevels: number[] | undefined;
  public enemyParty: EnemyPokemon[] = [];
  public seenEnemyPartyMemberIds: Set<number> = new Set<number>();
  public double: boolean;
  public started = false;
  public enemySwitchCounter = 0;
  public turn = 0;
  public preTurnCommands: TurnCommands;
  public turnCommands: TurnCommands;
  public playerParticipantIds: Set<number> = new Set<number>();
  public battleScore = 0;
  public postBattleLoot: PokemonHeldItemModifier[] = [];
  public escapeAttempts = 0;
  /**
   * A tracker of the last {@linkcode MoveId} successfully used this battle.
   */
  public lastMove: MoveId = MoveId.NONE;
  public battleSeed: string = randomString(16, true);
  private battleSeedState: string | null = null;
  public moneyScattered = 0;
  // TODO: These trackers are only used for Sticky Web + Mirror Armor edge cases
  // and are abhorrently janky.
  /** Primarily for double battles, keeps track of last enemy and player pokemon that triggered its ability or used a move */
  public lastEnemyInvolved: number;
  public lastPlayerInvolved: number;
  public lastUsedPokeball: PokeballType | null = null;
  /**
   * Saves the number of times a Pokemon on the enemy's side has fainted during this battle.
   * This is saved here since we encounter a new enemy every wave.
   * {@linkcode globalScene.arena.playerFaints} is the corresponding faint counter for the player and needs to be save across waves (reset every arena encounter).
   */
  public enemyFaints = 0;
  public playerFaintsHistory: FaintLogEntry[] = [];
  public enemyFaintsHistory: FaintLogEntry[] = [];

  public mysteryEncounterType?: MysteryEncounterType | undefined;
  /** If the current battle is a Mystery Encounter, this will always be defined */
  public mysteryEncounter?: MysteryEncounter | undefined;

  /**
   * Tracker for whether the last run attempt failed.
   * @defaultValue `false`
   */
  public failedRunAway = false;

  public successfulRun = false;

  constructor(
    gameMode: GameMode,
    { waveIndex, battleType, trainer, mysteryEncounterType, double = false }: NewBattleResolvedProps,
  ) {
    this.gameMode = gameMode;
    this.waveIndex = waveIndex;
    this.battleType = battleType;
    this.trainer = trainer ?? null;
    this.mysteryEncounterType = mysteryEncounterType;
    this.double = double;

    this.enemyLevels =
      battleType === BattleType.TRAINER
        ? trainer?.getPartyLevels(this.waveIndex)
        : // TODO: Remove array.fill.map
          new Array(double ? 2 : 1).fill(null).map(() => this.getLevelForWave());
  }

  public get isClassicFinalBoss(): boolean {
    return this.gameMode.isClassic && this.gameMode.isWaveFinal(this.waveIndex);
  }

  public getLevelForWave(): number {
    if (isDailyFinalBoss(this.waveIndex)) {
      return DAILY_BOSS_LEVEL;
    }
    const levelWaveIndex = this.gameMode.getWaveForDifficulty(this.waveIndex);
    const baseLevel = 1 + levelWaveIndex / 2 + Math.pow(levelWaveIndex / 25, 2);
    const bossMultiplier = 1.2;

    if (this.gameMode.isBoss(this.waveIndex)) {
      const ret = Math.floor(baseLevel * bossMultiplier);
      if (this.isClassicFinalBoss || !(this.waveIndex % 250)) {
        return Math.ceil(ret / 25) * 25;
      }
      let levelOffset = 0;
      if (!this.gameMode.isWaveFinal(this.waveIndex)) {
        levelOffset = Math.round(Phaser.Math.RND.realInRange(-1, 1) * Math.floor(levelWaveIndex / 10));
      }
      return ret + levelOffset;
    }

    let levelOffset = 0;

    const deviation = 10 / levelWaveIndex;
    levelOffset = Math.abs(this.randSeedGaussForLevel(deviation));

    return Math.max(Math.round(baseLevel + levelOffset), 1);
  }

  randSeedGaussForLevel(value: number): number {
    let rand = 0;
    for (let i = value; i > 0; i--) {
      rand += randSeedFloat();
    }
    return rand / value;
  }

  getBattlerCount(): number {
    return this.double ? 2 : 1;
  }

  incrementTurn(): void {
    this.turn++;
    this.turnCommands = Object.fromEntries(getEnumValues(BattlerIndex).map(bt => [bt, null]));
    this.preTurnCommands = Object.fromEntries(getEnumValues(BattlerIndex).map(bt => [bt, null]));
    this.battleSeedState = null;
  }

  addParticipant(playerPokemon: PlayerPokemon): void {
    this.playerParticipantIds.add(playerPokemon.id);
  }

  removeFaintedParticipant(playerPokemon: PlayerPokemon): void {
    this.playerParticipantIds.delete(playerPokemon.id);
  }

  addPostBattleLoot(enemyPokemon: EnemyPokemon): void {
    this.postBattleLoot.push(
      ...(globalScene.findModifiers(
        m => m.is("PokemonHeldItemModifier") && m.pokemonId === enemyPokemon.id && m.isTransferable,
        false,
      ) as PokemonHeldItemModifier[]),
    );
  }

  pickUpScatteredMoney(): void {
    const moneyAmount = new NumberHolder(globalScene.currentBattle.moneyScattered);
    globalScene.applyModifiers(MoneyMultiplierModifier, true, moneyAmount);

    if (globalScene.arena.getTag(ArenaTagType.HAPPY_HOUR)) {
      moneyAmount.value *= 2;
    }

    globalScene.addMoney(moneyAmount.value);

    const userLocale = navigator.language || "en-US";
    const formattedMoneyAmount = moneyAmount.value.toLocaleString(userLocale);
    const message = i18next.t("battle:moneyPickedUp", {
      moneyAmount: formattedMoneyAmount,
    });
    globalScene.phaseManager.queueMessage(message, undefined, true);

    globalScene.currentBattle.moneyScattered = 0;
  }

  addBattleScore(): void {
    let partyMemberTurnMultiplier = globalScene.getEnemyParty().length / 2 + 0.5;
    if (this.double) {
      partyMemberTurnMultiplier /= 1.5;
    }
    for (const p of globalScene.getEnemyParty()) {
      if (p.isBoss()) {
        partyMemberTurnMultiplier *= p.bossSegments / 1.5 / globalScene.getEnemyParty().length;
      }
    }
    const turnMultiplier = Phaser.Tweens.Builders.GetEaseFunction("Sine.easeIn")(
      1 - Math.min(this.turn - 2, 10 * partyMemberTurnMultiplier) / (10 * partyMemberTurnMultiplier),
    );
    const finalBattleScore = Math.ceil(this.battleScore * turnMultiplier);
    globalScene.score += finalBattleScore;
    console.log(
      `Battle Score: ${finalBattleScore} (${this.turn - 1} Turns x${Math.floor(turnMultiplier * 100) / 100})`,
    );
    console.log(`Total Score: ${globalScene.score}`);
    globalScene.updateScoreText();
  }

  getBgmOverride(): string | null {
    if (this.isBattleMysteryEncounter() && this.mysteryEncounter?.encounterMode === MysteryEncounterMode.DEFAULT) {
      // Music is overridden for MEs during ME onInit()
      // Should not use any BGM overrides before swapping from DEFAULT mode
      return null;
    }
    if (
      this.battleType === BattleType.TRAINER
      || this.mysteryEncounter?.encounterMode === MysteryEncounterMode.TRAINER_BATTLE
    ) {
      if (!this.started && this.trainer?.config.encounterBgm && this.trainer.getEncounterMessages().length > 0) {
        return `encounter_${this.trainer.getEncounterBgm()}`;
      }
      if (!settings.musicPreferenceAllGens) {
        return this.trainer?.getBattleBgm() ?? null;
      }
      return this.trainer?.getMixedBattleBgm() ?? null;
    }
    if (this.gameMode.isClassic) {
      if (isBetween(this.waveIndex, 191, 194)) {
        return "end";
      }
      if (isBetween(this.waveIndex, 196, 199)) {
        return "end_summit";
      }
    }
    const wildOpponents = globalScene.getEnemyParty();
    for (const pokemon of wildOpponents) {
      if (this.isClassicFinalBoss) {
        if (pokemon.species.getFormSpriteKey(pokemon.formIndex) === SpeciesFormKey.ETERNAMAX) {
          return "battle_final";
        }
        return "battle_final_encounter";
      }
      if (
        pokemon.species.legendary
        || pokemon.species.subLegendary
        || pokemon.species.mythical
        || (pokemon.species.category.startsWith("Paradox") && globalScene.arena.biomeId !== BiomeId.END)
      ) {
        if (!settings.musicPreferenceAllGens) {
          switch (pokemon.species.speciesId) {
            case SpeciesId.ARTICUNO:
            case SpeciesId.ZAPDOS:
            case SpeciesId.MOLTRES:
            case SpeciesId.MEWTWO:
            case SpeciesId.MEW:
              return "battle_legendary_mew";
            case SpeciesId.REGIROCK:
            case SpeciesId.REGICE:
            case SpeciesId.REGISTEEL:
            case SpeciesId.REGIGIGAS:
            case SpeciesId.REGIDRAGO:
            case SpeciesId.REGIELEKI:
              return "battle_legendary_regis_g5";
            case SpeciesId.KYUREM:
              return "battle_legendary_kyurem";
            default:
              if (pokemon.species.legendary) {
                return "battle_legendary_res_zek";
              }
              return "battle_legendary_unova";
          }
        }
        if (settings.musicPreferenceAllGens) {
          switch (pokemon.species.speciesId) {
            case SpeciesId.ARTICUNO:
            case SpeciesId.ZAPDOS:
            case SpeciesId.MOLTRES:
            case SpeciesId.MEWTWO:
            case SpeciesId.MEW:
              return "battle_legendary_kanto";
            case SpeciesId.RAIKOU:
              return "battle_legendary_raikou";
            case SpeciesId.ENTEI:
              return "battle_legendary_entei";
            case SpeciesId.SUICUNE:
              return "battle_legendary_suicune";
            case SpeciesId.LUGIA:
              return "battle_legendary_lugia";
            case SpeciesId.HO_OH:
              return "battle_legendary_ho_oh";
            case SpeciesId.REGIROCK:
            case SpeciesId.REGICE:
            case SpeciesId.REGISTEEL:
            case SpeciesId.REGIGIGAS:
            case SpeciesId.REGIDRAGO:
            case SpeciesId.REGIELEKI:
              return "battle_legendary_regis_g6";
            case SpeciesId.GROUDON:
            case SpeciesId.KYOGRE:
              if (pokemon.getFormKey() === SpeciesFormKey.PRIMAL) {
                return "battle_legendary_gro_kyo";
              }
              return "battle_legendary_rayquaza";
            case SpeciesId.RAYQUAZA:
              return "battle_legendary_rayquaza";
            case SpeciesId.DEOXYS:
              return "battle_legendary_deoxys";
            case SpeciesId.UXIE:
            case SpeciesId.MESPRIT:
            case SpeciesId.AZELF:
              return "battle_legendary_lake_trio";
            case SpeciesId.HEATRAN:
            case SpeciesId.CRESSELIA:
            case SpeciesId.DARKRAI:
            case SpeciesId.SHAYMIN:
              return "battle_legendary_sinnoh";
            case SpeciesId.DIALGA:
            case SpeciesId.PALKIA:
              if (pokemon.getFormKey() === SpeciesFormKey.ORIGIN) {
                return "battle_legendary_origin_forme";
              }
              return "battle_legendary_dia_pal";
            case SpeciesId.GIRATINA:
              return "battle_legendary_giratina";
            case SpeciesId.ARCEUS:
              return "battle_legendary_arceus";
            case SpeciesId.COBALION:
            case SpeciesId.TERRAKION:
            case SpeciesId.VIRIZION:
            case SpeciesId.KELDEO:
            case SpeciesId.TORNADUS:
            case SpeciesId.LANDORUS:
            case SpeciesId.THUNDURUS:
            case SpeciesId.MELOETTA:
            case SpeciesId.GENESECT:
              return "battle_legendary_unova";
            case SpeciesId.KYUREM:
              return "battle_legendary_kyurem";
            case SpeciesId.XERNEAS:
            case SpeciesId.YVELTAL:
            case SpeciesId.ZYGARDE:
              return "battle_legendary_xern_yvel";
            case SpeciesId.TAPU_KOKO:
            case SpeciesId.TAPU_LELE:
            case SpeciesId.TAPU_BULU:
            case SpeciesId.TAPU_FINI:
              return "battle_legendary_tapu";
            case SpeciesId.COSMOG:
            case SpeciesId.COSMOEM:
            case SpeciesId.SOLGALEO:
            case SpeciesId.LUNALA:
              return "battle_legendary_sol_lun";
            case SpeciesId.NECROZMA:
              switch (pokemon.getFormKey()) {
                case "dusk-mane":
                case "dawn-wings":
                  return "battle_legendary_dusk_dawn";
                case "ultra":
                  return "battle_legendary_ultra_nec";
                default:
                  return "battle_legendary_sol_lun";
              }
            case SpeciesId.NIHILEGO:
            case SpeciesId.PHEROMOSA:
            case SpeciesId.BUZZWOLE:
            case SpeciesId.XURKITREE:
            case SpeciesId.CELESTEELA:
            case SpeciesId.KARTANA:
            case SpeciesId.GUZZLORD:
            case SpeciesId.POIPOLE:
            case SpeciesId.NAGANADEL:
            case SpeciesId.STAKATAKA:
            case SpeciesId.BLACEPHALON:
              return "battle_legendary_ub";
            case SpeciesId.ZACIAN:
            case SpeciesId.ZAMAZENTA:
              return "battle_legendary_zac_zam";
            case SpeciesId.ETERNATUS:
              if (pokemon.getFormKey() === "eternamax") {
                return "battle_legendary_eternatus_p2";
              }
              return "battle_legendary_eternatus_p1";
            case SpeciesId.GLASTRIER:
            case SpeciesId.SPECTRIER:
              return "battle_legendary_glas_spec";
            case SpeciesId.CALYREX:
              if (pokemon.getFormKey() === "ice" || pokemon.getFormKey() === "shadow") {
                return "battle_legendary_riders";
              }
              return "battle_legendary_calyrex";
            case SpeciesId.GALAR_ARTICUNO:
            case SpeciesId.GALAR_ZAPDOS:
            case SpeciesId.GALAR_MOLTRES:
              return "battle_legendary_birds_galar";
            case SpeciesId.WO_CHIEN:
            case SpeciesId.CHIEN_PAO:
            case SpeciesId.TING_LU:
            case SpeciesId.CHI_YU:
              return "battle_legendary_ruinous";
            case SpeciesId.GREAT_TUSK:
            case SpeciesId.SCREAM_TAIL:
            case SpeciesId.BRUTE_BONNET:
            case SpeciesId.FLUTTER_MANE:
            case SpeciesId.SLITHER_WING:
            case SpeciesId.SANDY_SHOCKS:
            case SpeciesId.IRON_TREADS:
            case SpeciesId.IRON_BUNDLE:
            case SpeciesId.IRON_HANDS:
            case SpeciesId.IRON_JUGULIS:
            case SpeciesId.IRON_MOTH:
            case SpeciesId.IRON_THORNS:
            case SpeciesId.ROARING_MOON:
            case SpeciesId.IRON_VALIANT:
            case SpeciesId.WALKING_WAKE:
            case SpeciesId.IRON_LEAVES:
            case SpeciesId.GOUGING_FIRE:
            case SpeciesId.RAGING_BOLT:
            case SpeciesId.IRON_BOULDER:
            case SpeciesId.IRON_CROWN:
            case SpeciesId.KORAIDON:
            case SpeciesId.MIRAIDON:
              return "battle_legendary_kor_mir";
            case SpeciesId.OKIDOGI:
            case SpeciesId.MUNKIDORI:
            case SpeciesId.FEZANDIPITI:
              return "battle_legendary_loyal_three";
            case SpeciesId.OGERPON:
              return "battle_legendary_ogerpon";
            case SpeciesId.TERAPAGOS:
              return "battle_legendary_terapagos";
            case SpeciesId.PECHARUNT:
              return "battle_legendary_pecharunt";
            default:
              if (pokemon.species.legendary) {
                return "battle_legendary_res_zek";
              }
              return "battle_legendary_unova";
          }
        }
      }
    }

    if (globalScene.gameMode.isClassic && this.waveIndex <= 4) {
      return "battle_wild";
    }

    return null;
  }

  /**
   * Generates a random number using the current battle's seed. Calls {@linkcode randSeedInt}
   * @param range How large of a range of random numbers to choose from. If {@linkcode range} <= 1, returns {@linkcode min}
   * @param min The minimum integer to pick, default `0`
   * @returns A random integer between {@linkcode min} and ({@linkcode min} + {@linkcode range} - 1)
   */
  randSeedInt(range: number, min = 0): number {
    if (range <= 1) {
      return min;
    }
    const tempSeedOverride = globalScene.rngSeedOverride;
    const state = Phaser.Math.RND.state();
    if (this.battleSeedState) {
      Phaser.Math.RND.state(this.battleSeedState);
    } else {
      Phaser.Math.RND.sow([shiftCharCodes(this.battleSeed, this.turn << 6)]);
      console.log("Battle Seed:", this.battleSeed);
    }
    globalScene.rngSeedOverride = this.battleSeed;
    const ret = randSeedInt(range, min);
    this.battleSeedState = Phaser.Math.RND.state();
    Phaser.Math.RND.state(state);
    globalScene.rngSeedOverride = tempSeedOverride;
    return ret;
  }

  /**
   * Returns if the battle is of type {@linkcode BattleType.MYSTERY_ENCOUNTER}
   */
  isBattleMysteryEncounter(): boolean {
    return this.battleType === BattleType.MYSTERY_ENCOUNTER;
  }
}

export class FixedBattle extends Battle {
  constructor(waveIndex: number, config: FixedBattleConfig) {
    super(globalScene.gameMode, {
      waveIndex,
      battleType: config.battleType,
      trainer: config.battleType === BattleType.TRAINER ? config.getTrainer() : undefined,
      double: config.double,
    });
    if (config.getEnemyParty) {
      this.enemyParty = config.getEnemyParty();
    }
  }
}

type GetTrainerFunc = () => Trainer;
type GetEnemyPartyFunc = () => EnemyPokemon[];

export class FixedBattleConfig {
  // TODO: All fixed battles are currently trainer battles
  public battleType: Exclude<BattleType, BattleType.CLEAR>;
  public double: boolean;
  public getTrainer: GetTrainerFunc;
  public getEnemyParty: GetEnemyPartyFunc;
  public seedOffsetWaveIndex: number;
  public customModifierRewardSettings?: CustomModifierSettings;

  setBattleType(battleType: Exclude<BattleType, BattleType.CLEAR>): FixedBattleConfig {
    this.battleType = battleType;
    return this;
  }

  setDouble(double: boolean): FixedBattleConfig {
    this.double = double;
    return this;
  }

  setGetTrainerFunc(getTrainerFunc: GetTrainerFunc): FixedBattleConfig {
    this.getTrainer = getTrainerFunc;
    return this;
  }

  setGetEnemyPartyFunc(getEnemyPartyFunc: GetEnemyPartyFunc): FixedBattleConfig {
    this.getEnemyParty = getEnemyPartyFunc;
    return this;
  }

  setSeedOffsetWave(seedOffsetWaveIndex: number): FixedBattleConfig {
    this.seedOffsetWaveIndex = seedOffsetWaveIndex;
    return this;
  }

  setCustomModifierRewards(customModifierRewardSettings: CustomModifierSettings) {
    this.customModifierRewardSettings = customModifierRewardSettings;
    return this;
  }
}
/**
 * Helper function to generate a random trainer for evil team trainers and the elite 4/champion
 * @param trainerPool - The TrainerType or list of TrainerTypes that can possibly be generated
 * @param randomGender - (default `false`); Whether or not to randomly (50%) generate a female trainer (for use with evil team grunts)
 * @param seedOffset - (default `0`); A seed offset indicating the invocation count of the function to attempt to choose a random, but unique, trainer from the pool
 * @returns A function to generate a random trainer
 */
export function getRandomTrainerFunc(
  trainerPool: readonly (TrainerType | readonly TrainerType[])[],
  randomGender = false,
  seedOffset = 0,
): GetTrainerFunc {
  return () => {
    /** The chosen entry in the pool */
    let choice = randSeedItem(trainerPool);

    if (typeof choice !== "number") {
      choice = seedOffset === 0 ? randSeedItem(choice) : randSeedUniqueItem(choice, seedOffset);
    }

    let trainerGender = TrainerVariant.DEFAULT;
    if (randomGender) {
      trainerGender = randInt(2) === 0 ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT;
    }

    /* 1/3 chance for evil team grunts to be double battles */
    const evilTeamGrunts = [
      TrainerType.ROCKET_GRUNT,
      TrainerType.MAGMA_GRUNT,
      TrainerType.AQUA_GRUNT,
      TrainerType.GALACTIC_GRUNT,
      TrainerType.PLASMA_GRUNT,
      TrainerType.FLARE_GRUNT,
      TrainerType.AETHER_GRUNT,
      TrainerType.SKULL_GRUNT,
      TrainerType.MACRO_GRUNT,
      TrainerType.STAR_GRUNT,
    ];
    const isEvilTeamGrunt = evilTeamGrunts.includes(choice);

    if (trainerConfigs[choice].hasDouble && isEvilTeamGrunt) {
      return new Trainer(choice, randInt(3) === 0 ? TrainerVariant.DOUBLE : trainerGender);
    }

    return new Trainer(choice, trainerGender);
  };
}