378 lines
12 KiB
TypeScript
378 lines
12 KiB
TypeScript
import i18next from "i18next";
|
|
import { classicFixedBattles, FixedBattleConfig, FixedBattleConfigs } from "./battle";
|
|
import BattleScene from "./battle-scene";
|
|
import { allChallenges, applyChallenges, Challenge, ChallengeType, copyChallenge } from "./data/challenge";
|
|
import PokemonSpecies, { allSpecies } from "./data/pokemon-species";
|
|
import { Arena } from "./field/arena";
|
|
import Overrides from "#app/overrides";
|
|
import * as Utils from "./utils";
|
|
import { Biome } from "#enums/biome";
|
|
import { Species } from "#enums/species";
|
|
import { Challenges } from "./enums/challenges";
|
|
|
|
export enum GameModes {
|
|
CLASSIC,
|
|
ENDLESS,
|
|
SPLICED_ENDLESS,
|
|
DAILY,
|
|
CHALLENGE
|
|
}
|
|
|
|
interface GameModeConfig {
|
|
isClassic?: boolean;
|
|
isEndless?: boolean;
|
|
isDaily?: boolean;
|
|
hasTrainers?: boolean;
|
|
hasNoShop?: boolean;
|
|
hasShortBiomes?: boolean;
|
|
hasRandomBiomes?: boolean;
|
|
hasRandomBosses?: boolean;
|
|
isSplicedOnly?: boolean;
|
|
isChallenge?: boolean;
|
|
hasMysteryEncounters?: boolean;
|
|
}
|
|
|
|
// Describes min and max waves for MEs in specific game modes
|
|
export const CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [ 10, 180 ];
|
|
export const CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES: [number, number] = [ 10, 180 ];
|
|
|
|
export class GameMode implements GameModeConfig {
|
|
public modeId: GameModes;
|
|
public isClassic: boolean;
|
|
public isEndless: boolean;
|
|
public isDaily: boolean;
|
|
public hasTrainers: boolean;
|
|
public hasNoShop: boolean;
|
|
public hasShortBiomes: boolean;
|
|
public hasRandomBiomes: boolean;
|
|
public hasRandomBosses: boolean;
|
|
public isSplicedOnly: boolean;
|
|
public isChallenge: boolean;
|
|
public challenges: Challenge[];
|
|
public battleConfig: FixedBattleConfigs;
|
|
public hasMysteryEncounters: boolean;
|
|
public minMysteryEncounterWave: number;
|
|
public maxMysteryEncounterWave: number;
|
|
|
|
constructor(modeId: GameModes, config: GameModeConfig, battleConfig?: FixedBattleConfigs) {
|
|
this.modeId = modeId;
|
|
this.challenges = [];
|
|
Object.assign(this, config);
|
|
if (this.isChallenge) {
|
|
this.challenges = allChallenges.map(c => copyChallenge(c));
|
|
}
|
|
this.battleConfig = battleConfig || {};
|
|
}
|
|
|
|
/**
|
|
* Helper function to see if a GameMode has a specific challenge type
|
|
* @param challenge the Challenges it looks for
|
|
* @returns true if the game mode has that challenge
|
|
*/
|
|
hasChallenge(challenge: Challenges): boolean {
|
|
return this.challenges.some(c => c.id === challenge && c.value !== 0);
|
|
}
|
|
|
|
/**
|
|
* Helper function to see if the game mode is using fresh start
|
|
* @returns true if a fresh start challenge is being applied
|
|
*/
|
|
isFreshStartChallenge(): boolean {
|
|
return this.hasChallenge(Challenges.FRESH_START);
|
|
}
|
|
|
|
/**
|
|
* @returns either:
|
|
* - override from overrides.ts
|
|
* - 20 for Daily Runs
|
|
* - 5 for all other modes
|
|
*/
|
|
getStartingLevel(): integer {
|
|
if (Overrides.STARTING_LEVEL_OVERRIDE) {
|
|
return Overrides.STARTING_LEVEL_OVERRIDE;
|
|
}
|
|
switch (this.modeId) {
|
|
case GameModes.DAILY:
|
|
return 20;
|
|
default:
|
|
return 5;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @returns either:
|
|
* - override from overrides.ts
|
|
* - 1000
|
|
*/
|
|
getStartingMoney(): integer {
|
|
return Overrides.STARTING_MONEY_OVERRIDE || 1000;
|
|
}
|
|
|
|
/**
|
|
* @param scene current BattleScene
|
|
* @returns either:
|
|
* - random biome for Daily mode
|
|
* - override from overrides.ts
|
|
* - Town
|
|
*/
|
|
getStartingBiome(scene: BattleScene): Biome {
|
|
switch (this.modeId) {
|
|
case GameModes.DAILY:
|
|
return scene.generateRandomBiome(this.getWaveForDifficulty(1));
|
|
default:
|
|
return Overrides.STARTING_BIOME_OVERRIDE || Biome.TOWN;
|
|
}
|
|
}
|
|
|
|
getWaveForDifficulty(waveIndex: integer, ignoreCurveChanges: boolean = false): integer {
|
|
switch (this.modeId) {
|
|
case GameModes.DAILY:
|
|
return waveIndex + 30 + (!ignoreCurveChanges ? Math.floor(waveIndex / 5) : 0);
|
|
default:
|
|
return waveIndex;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Determines whether or not to generate a trainer
|
|
* @param waveIndex the current floor the player is on (trainer sprites fail to generate on X1 floors)
|
|
* @param arena the arena that contains the scene and functions
|
|
* @returns true if a trainer should be generated, false otherwise
|
|
*/
|
|
isWaveTrainer(waveIndex: integer, arena: Arena): boolean {
|
|
/**
|
|
* Daily spawns trainers on floors 5, 15, 20, 25, 30, 35, 40, and 45
|
|
*/
|
|
if (this.isDaily) {
|
|
return waveIndex % 10 === 5 || (!(waveIndex % 10) && waveIndex > 10 && !this.isWaveFinal(waveIndex));
|
|
}
|
|
if ((waveIndex % 30) === (arena.scene.offsetGym ? 0 : 20) && !this.isWaveFinal(waveIndex)) {
|
|
return true;
|
|
} else if (waveIndex % 10 !== 1 && waveIndex % 10) {
|
|
/**
|
|
* Do not check X1 floors since there's a bug that stops trainer sprites from appearing
|
|
* after a X0 full party heal
|
|
*/
|
|
|
|
const trainerChance = arena.getTrainerChance();
|
|
let allowTrainerBattle = true;
|
|
if (trainerChance) {
|
|
const waveBase = Math.floor(waveIndex / 10) * 10;
|
|
// Stop generic trainers from spawning in within 3 waves of a trainer battle
|
|
for (let w = Math.max(waveIndex - 3, waveBase + 2); w <= Math.min(waveIndex + 3, waveBase + 9); w++) {
|
|
if (w === waveIndex) {
|
|
continue;
|
|
}
|
|
if ((w % 30) === (arena.scene.offsetGym ? 0 : 20) || this.isFixedBattle(w)) {
|
|
allowTrainerBattle = false;
|
|
break;
|
|
} else if (w < waveIndex) {
|
|
arena.scene.executeWithSeedOffset(() => {
|
|
const waveTrainerChance = arena.getTrainerChance();
|
|
if (!Utils.randSeedInt(waveTrainerChance)) {
|
|
allowTrainerBattle = false;
|
|
}
|
|
}, w);
|
|
if (!allowTrainerBattle) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return Boolean(allowTrainerBattle && trainerChance && !Utils.randSeedInt(trainerChance));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
isTrainerBoss(waveIndex: integer, biomeType: Biome, offsetGym: boolean): boolean {
|
|
switch (this.modeId) {
|
|
case GameModes.DAILY:
|
|
return waveIndex > 10 && waveIndex < 50 && !(waveIndex % 10);
|
|
default:
|
|
return (waveIndex % 30) === (offsetGym ? 0 : 20) && (biomeType !== Biome.END || this.isClassic || this.isWaveFinal(waveIndex));
|
|
}
|
|
}
|
|
|
|
getOverrideSpecies(waveIndex: integer): PokemonSpecies | null {
|
|
if (this.isDaily && this.isWaveFinal(waveIndex)) {
|
|
const allFinalBossSpecies = allSpecies.filter(s => (s.subLegendary || s.legendary || s.mythical)
|
|
&& s.baseTotal >= 600 && s.speciesId !== Species.ETERNATUS && s.speciesId !== Species.ARCEUS);
|
|
return Utils.randSeedItem(allFinalBossSpecies);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Checks if wave provided is the final for current or specified game mode
|
|
* @param waveIndex
|
|
* @param modeId game mode
|
|
* @returns if the current wave is final for classic or daily OR a minor boss in endless
|
|
*/
|
|
isWaveFinal(waveIndex: integer, modeId: GameModes = this.modeId): boolean {
|
|
switch (modeId) {
|
|
case GameModes.CLASSIC:
|
|
case GameModes.CHALLENGE:
|
|
return waveIndex === 200;
|
|
case GameModes.ENDLESS:
|
|
case GameModes.SPLICED_ENDLESS:
|
|
return !(waveIndex % 250);
|
|
case GameModes.DAILY:
|
|
return waveIndex === 50;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Every 10 waves is a boss battle
|
|
* @returns true if waveIndex is a multiple of 10
|
|
*/
|
|
isBoss(waveIndex: integer): boolean {
|
|
return waveIndex % 10 === 0;
|
|
}
|
|
|
|
/**
|
|
* @returns `true` if the current battle is against classic mode's final boss
|
|
*/
|
|
isBattleClassicFinalBoss(waveIndex: number): boolean {
|
|
return (this.modeId === GameModes.CLASSIC || this.modeId === GameModes.CHALLENGE) && this.isWaveFinal(waveIndex);
|
|
}
|
|
|
|
/**
|
|
* Every 50 waves of an Endless mode is a boss
|
|
* At this time it is paradox pokemon
|
|
* @returns true if waveIndex is a multiple of 50 in Endless
|
|
*/
|
|
isEndlessBoss(waveIndex: integer): boolean {
|
|
return waveIndex % 50 === 0 &&
|
|
(this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS);
|
|
}
|
|
|
|
/**
|
|
* Every 250 waves of an Endless mode is a minor boss
|
|
* At this time it is Eternatus
|
|
* @returns true if waveIndex is a multiple of 250 in Endless
|
|
*/
|
|
isEndlessMinorBoss(waveIndex: integer): boolean {
|
|
return waveIndex % 250 === 0 &&
|
|
(this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS);
|
|
}
|
|
|
|
/**
|
|
* Every 1000 waves of an Endless mode is a major boss
|
|
* At this time it is Eternamax Eternatus
|
|
* @returns true if waveIndex is a multiple of 1000 in Endless
|
|
*/
|
|
isEndlessMajorBoss(waveIndex: integer): boolean {
|
|
return waveIndex % 1000 === 0 &&
|
|
(this.modeId === GameModes.ENDLESS || this.modeId === GameModes.SPLICED_ENDLESS);
|
|
}
|
|
|
|
/**
|
|
* Checks whether there is a fixed battle on this gamemode on a given wave.
|
|
* @param {integer} waveIndex The wave to check.
|
|
* @returns {boolean} If this game mode has a fixed battle on this wave
|
|
*/
|
|
isFixedBattle(waveIndex: integer): boolean {
|
|
const dummyConfig = new FixedBattleConfig();
|
|
return this.battleConfig.hasOwnProperty(waveIndex) || applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, dummyConfig);
|
|
}
|
|
|
|
/**
|
|
* Returns the config for the fixed battle for a particular wave.
|
|
* @param {integer} waveIndex The wave to check.
|
|
* @returns {boolean} The fixed battle for this wave.
|
|
*/
|
|
getFixedBattle(waveIndex: integer): FixedBattleConfig {
|
|
const challengeConfig = new FixedBattleConfig();
|
|
if (applyChallenges(this, ChallengeType.FIXED_BATTLES, waveIndex, challengeConfig)) {
|
|
return challengeConfig;
|
|
} else {
|
|
return this.battleConfig[waveIndex];
|
|
}
|
|
}
|
|
|
|
|
|
getClearScoreBonus(): integer {
|
|
switch (this.modeId) {
|
|
case GameModes.CLASSIC:
|
|
case GameModes.CHALLENGE:
|
|
return 5000;
|
|
case GameModes.DAILY:
|
|
return 2500;
|
|
default:
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
getEnemyModifierChance(isBoss: boolean): integer {
|
|
switch (this.modeId) {
|
|
case GameModes.CLASSIC:
|
|
case GameModes.CHALLENGE:
|
|
case GameModes.DAILY:
|
|
return !isBoss ? 18 : 6;
|
|
case GameModes.ENDLESS:
|
|
case GameModes.SPLICED_ENDLESS:
|
|
return !isBoss ? 12 : 4;
|
|
}
|
|
}
|
|
|
|
getName(): string {
|
|
switch (this.modeId) {
|
|
case GameModes.CLASSIC:
|
|
return i18next.t("gameMode:classic");
|
|
case GameModes.ENDLESS:
|
|
return i18next.t("gameMode:endless");
|
|
case GameModes.SPLICED_ENDLESS:
|
|
return i18next.t("gameMode:endlessSpliced");
|
|
case GameModes.DAILY:
|
|
return i18next.t("gameMode:dailyRun");
|
|
case GameModes.CHALLENGE:
|
|
return i18next.t("gameMode:challenge");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the wave range where MEs can spawn for the game mode [min, max]
|
|
*/
|
|
getMysteryEncounterLegalWaves(): [number, number] {
|
|
switch (this.modeId) {
|
|
default:
|
|
return [ 0, 0 ];
|
|
case GameModes.CLASSIC:
|
|
return CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES;
|
|
case GameModes.CHALLENGE:
|
|
return CHALLENGE_MODE_MYSTERY_ENCOUNTER_WAVES;
|
|
}
|
|
}
|
|
|
|
static getModeName(modeId: GameModes): string {
|
|
switch (modeId) {
|
|
case GameModes.CLASSIC:
|
|
return i18next.t("gameMode:classic");
|
|
case GameModes.ENDLESS:
|
|
return i18next.t("gameMode:endless");
|
|
case GameModes.SPLICED_ENDLESS:
|
|
return i18next.t("gameMode:endlessSpliced");
|
|
case GameModes.DAILY:
|
|
return i18next.t("gameMode:dailyRun");
|
|
case GameModes.CHALLENGE:
|
|
return i18next.t("gameMode:challenge");
|
|
}
|
|
}
|
|
}
|
|
|
|
export function getGameMode(gameMode: GameModes): GameMode {
|
|
switch (gameMode) {
|
|
case GameModes.CLASSIC:
|
|
return new GameMode(GameModes.CLASSIC, { isClassic: true, hasTrainers: true, hasMysteryEncounters: true }, classicFixedBattles);
|
|
case GameModes.ENDLESS:
|
|
return new GameMode(GameModes.ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true });
|
|
case GameModes.SPLICED_ENDLESS:
|
|
return new GameMode(GameModes.SPLICED_ENDLESS, { isEndless: true, hasShortBiomes: true, hasRandomBosses: true, isSplicedOnly: true });
|
|
case GameModes.DAILY:
|
|
return new GameMode(GameModes.DAILY, { isDaily: true, hasTrainers: true, hasNoShop: true });
|
|
case GameModes.CHALLENGE:
|
|
return new GameMode(GameModes.CHALLENGE, { isClassic: true, hasTrainers: true, isChallenge: true, hasMysteryEncounters: true }, classicFixedBattles);
|
|
}
|
|
}
|