pokerogue/src/game-mode.ts
ImperialSympathizer acb2b66be4
[Feature] Add Mystery Encounters to the game (#3938)
* add .github/workflows/mystery-event.yml

* update mystery-event.yml

* mystery encounters: resolve review comments:

Lost at Sea:
-fix typo in handlePokemonGuidingYouPhase function

Mysterious Chest:
- remove obsolete commented code

mystery-encounter.ts
- remove unused `onDone` field from MysteryEncounterBuilder

* fix typo in CanLearnMoveRequirementOptions

* remove redundance from Pokemon.isAllowedInBattle()

* chore: jsdoc formatting

* fix lost-at-sea tests

* add fallback for biomeMysteryEncounters if empty

* lost-at-sea-encounter: fix and extend tests

* move "battle:fainted" into `koPlayerPokemon`

* add retries to quick-draw tests

* fix lost-at-sea-encounter tests

* clean up battle animation logic

* Update and rename mystery-event.yml to mystery-events.yml

* Update mystery-events.yml

* Fix typo

* Update mystery-events.yml

Fix debug runs

* clean up unit tests and utils

* attach github issues to all encounter jsdocs

* start dialogue refactor

* update sleeping snorlax encounter

* migrate encounters dialogue to new format

* cleanup and add jsdocs

* finish fiery fallout encounter

* fix unit test breaks

* add skeleton tests to fiery fallout

* commit latest test changes

* finish unit tests for fiery fallout

* bug fix for empty modifier shop

* stash working changes

* stash changes

* Update src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/test/utils/overridesHelper.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/test/utils/overridesHelper.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/test/utils/overridesHelper.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/test/utils/overridesHelper.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Update src/data/battle-anims.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* nit updates and cleanup

* Update src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* add jsdocs and more cleanup

* add more jsdoc

* add the strong stuff encounter

* add the strong stuff encounter and more unit tests

* cleanup container length checks in ME ui

* add retries to tests

* add retries to tests

* fix trainer wave disable override

* add shuckle juice modifier

* add dialogue bug fixes

* add dialogue bug fixes

* add pokemon salesman encounter and affects pokedex UI display

* add unit tests for pokemon salesman

* temp stash

* add offer you can't refuse

* add unit tests for offer you can't refuse encounter

* remove unnecessary prompt handlers

* add tests for disabled encounter options

* add delibird-y encounter

* add delibird-y encounter

* add absolute avarice encounter

* finish absolute avarice encounter

* add unit tests and enhancements for item overrides in tests

* fix unit test

* cleanup absolute avarice PR

* small bug fixes with latest sync from main

* update visuals loading for safari and stat trainer visuals

* update visuals loading for safari and stat trainer visuals

* update a trainer's test encounter and add unit tests

* add Trash to Treasure encounter

* clean up trash to treasure encounter

* clean up trash to treasure encounter

* add berries abound encounter

* start clowning around encounter

* first implementation pass at clowning around

* add unit tests for clowning around

* add unit tests for clowning around

* clean up ME unit tests

* clean up unit tests

* update unit tests

* add part timer and dancing lessons encounters

* add unit tests for Dancing Lessons and Part-Timer

* reordered biome list and adjusted redirection for project and labels

* Add Weird Dream encounter and slight reworks to Berries Abound/Fight or Flight

* adjusting yml to match new labels

* fix yml whoopsie

* Expanded 'Weird Dream' banlist and fixed a bug with the BST bump range

* adds Winstrate Challenge mystery encounter

* small cleanup for winstrates

* add unit tests for Winstrate Challenge

* fix pokemon not returning after winstrate battle

* commit latest beta merge updates

* fix ME null checks and unit tests with beta update

* fix ME null checks and unit tests with beta update

* MEs to pokerogue beta branch

* test dialogue changes

* test patch fix

* test patch fix

* test patch fix

* adds teleporting hijinks encounter

* add unit tests for Teleporting Hijinks

* small change to teleporting hijinks dialogue

* migrate ME translations to json

* add retries to berries-abound.Option1: should reward the player with X berries based on wave

* add missing ME dialogue back in

* revert template changes

* add ME unique trainer dialogue to both dialogue jsons

* fix hanging comma in json

* fix broken imports

* resolve lint issues

* fix flaky test

* balance tweaks to a few MEs, updates to bug superfan

* add unit tests for Bug-Type Superfan and clean up dialogue

* Adds Fun and Games mystery encounter

* add unit tests for Fun and Games encounter

* update jsdoc

* small ME balance changes

* small ME balance changes

* Adds Uncommon Breed ME and misc. ME bug fixes

* Update getFinalSessionData() to collect Mystery Encounter data

* adds GTS encounter

* various ME bug fixes and balance changes

* latest ME bug fixes

* clean up GTS Encounter and add unit tests

* small cleanup to MEs branch

* add BGM music names for ME music

* bug fixes and balance changes for MEs

* ME data schema updates

* balance changes and bug fixes to MEs

* balance changes and bug fixes to MEs

* update tests for MEs

* add jsdoc to party exp function

* dialogue updates and test fixes for MEs

* dialogue updates and test fixes for MEs

* PR suggestions and fixees

* stash PR feedback and bugfixes

* fix all tests for MEs and cleanup

* PR feedback

* update flaky ME test

* update tests, bug fix MEs, and sprite assets

* remove unintentional console log

* re-enable stubbed function for Phaser text styling

* handle undefined introVisuals properly

* PR feedback from NightKev

* disable Uncommon Breed tests

* locales updates and bug fixes for safari zone

* more PR feedback and update field trip with Rarer Candy

* fix unit test

* Change how reroll button gets disabled in Modifier Shop Phase

* update continue button text logic

* Update src/ui/modifier-select-ui-handler.ts

Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>

* fix money formatting and some nits

* more nits

* more nits

* update ME tsdocs with links

* update ME tsdocs with links

---------

Co-authored-by: Felix Staud <felix.staud@headwire.com>
Co-authored-by: flx-sta <50131232+flx-sta@users.noreply.github.com>
Co-authored-by: ImperialSympathizer <imperialsympathizer@gmail.com>
Co-authored-by: InnocentGameDev <asdargmng@gmail.com>
Co-authored-by: Mumble <171087428+frutescens@users.noreply.github.com>
2024-09-14 03:05:58 +01:00

372 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;
}
/**
* 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) &&
(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);
}
}