Skip to content

Commit

Permalink
Merge pull request #1 from ben-lear/encounters-beta
Browse files Browse the repository at this point in the history
Encounters beta
  • Loading branch information
ben-lear authored Jun 10, 2024
2 parents 8a0b5c5 + 2422698 commit 6185274
Show file tree
Hide file tree
Showing 58 changed files with 4,280 additions and 369 deletions.
2 changes: 1 addition & 1 deletion .env
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
VITE_BYPASS_LOGIN=0
VITE_BYPASS_LOGIN=1
VITE_BYPASS_TUTORIAL=0
VITE_SERVER_URL=http://localhost:8001
141 changes: 141 additions & 0 deletions MEs-Roadmap-Documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
# 📝 Most immediate things on the to-do platter

- ### High priority
- 🐛 Opening the chest simply moves you to a wild battle against nothingness, which you can escape after you get bored of it.

- ### Medium priority
- 🐛 PKMN Sprites and their HP/lvl bar doesn't get properly recalled when finding an ME or when meeting Rival. 🛠️
- 🐛 Wave seed generates different encounter data if you roll to a new wave, see the spawned stuff, and refresh the app
- 🐛 If a ME spawns on the first floor of a new biome (NewBiomeEncounterPhase), intro visuals do not spawn properly
- ⚙️ "Steal from player" functionality (Gholdengo ME) ❌
- ⚙️ Add some spacing logic to MEs spawning so you don't get floods/droughts of MEs ❌
- ⚙️ Push Dark Deal ME to a higher wave requirement (+30) as it seems to be functioning (mostly) bugless.


# 📝 Things to be done before Mystery Encounters ("~~~~MEs") MVP is finished:
All the things on this list should be done before the merge as a MVP (Minimum Viable Product) release.

- ## Bugless implementation of the MVP MEs
- Establish placeholder waves for MEs to happen ✔️
- Bug-ish implementation of Common ME 1 🛠️
- Bug-ish implementation of Common ME 2 🛠️
- Bug-ish implementation of Rare ME 1 ✔️
- Bug-ish implementation of Epic ME 1 🛠️
- Bug-ish implementation of Legendary ME 1 ✔️

- ## First round of playtesting (Alpha)
- Establish a placeholder odd for MEs to happen closer to real implementation ❌
- Find and eliminate as many bugs as possible 🛠️
- Tweak odds of ME spawn if needed ❌
- Tweak difficulty/rewards balance in MEs 🛠️

## Translation of MEs after playtest/balance
- EN localisation 🛠️
- ES localisation 🛠️


# 🧬 Deep dive into MEs and what has done so far

Mysterious Encounters aim to be an addition to PokeRogue that will fundamentally shift the way PokéRogue feels. It looks to improve the bet of the game into the RogueLite genre without touching the core gameplay loop of Pokémon battles/collection that we know and love already in this game. Below there are some specifications that clarify what's being worked on for ease of access for the devs, balance team, artists and others who may be interested. Beware of spoilers!

## A Mysterious Encounter __**always has**__:
### #️⃣ A wave index where they're happening -- each ME takes up a whole wave (means you miss a combat!).

### 💬 Dialogue:
- Dialogue/Message content populated in relevant locales files (namely locales/mystery-encounter.ts)
- An associated EncounterTypeDialogue object populated in allMysteryEncounterDialogue (see data/mystery-ecounter-dialogue.ts)
- This will require certain content, such as encounter description window text and option button labels, while some other fields will be optional
- Key content to be aware of when writing encounter dialogue:
- Intro dialogue or messages (shown before anything appears on screen)
- A title (shown in description box)
- A description (shown in description box)
- A prompt/query to the player, to choose the options (shown in description box)
- An option panel at the bottom, taking the space of what usually is the game dialogs + controls
- Containing at least two options, and up to four.
- ❗❗ To view what dialogue content is __**mandatory**__ for encounters, check the schema in data/mystery-ecounter-dialogue.ts

### 🕺 Intro Visuals:
- One or multiple sprites may be used. They will slide onto the field when the encounter starts
- 📚 This could be anything from a group of trainers, to a Pokemon, to a static sprite of an inanimate object
- ❗❗ To populate an encounter with intro visuals, see "Encounter Class Extending MysteryEncounterWrapper" section
- 📚 Technically, the encounter will still work if Intro Visuals are not provided, but your encounter will look very strange when an empty field slides onto the screen

### 📋 Encounter Class Implementing MysteryEncounterWrapper
- ❗❗ All encounters should have their own class files organized in the src/data/mystery-encounters folder
- ❗❗ Encounter classes can be named anything, but **must implement MysteryEncounterWrapper**
- Refer to existing MEs for examples
- ❗❗ As part of MysteryEncounterWrapper, they should implement their own get() function
- 📚 The get() function should return an object that is some concrete extension of class MysteryEncounter
- Example: can return a new OptionSelectMysteryEncounter()
- ❗❗ **This MysteryEncounter type class will be where all encounter functional/business logic will reside**
- 📚 That includes things like, what intro visuals to display, what each option does (is it a battle, getting items, skipping the encounter, etc.)
- 📚 It will also serve as the way to pull data from the encounter class when starting the game
- ❗❗ A new instance of this encounter class should be added to the initMysteryEncounters() function inside data/mystery-encounter.ts

### 🌟 **Rarity** tier of the ME, common by default.
- ⚪ Common pool
- 🔵 Rare pool
- 🟣 Epic pool
- 🟡 Legendary pool

### **Optional Requirements** for Mystery Encounters.
- 🛠️ They give granular control over whether encounters will spawn in certain situations
- Requirements might include:
- Being within a wave range
- Being a range of wave X-Y
- Having X amount of $$$
- Having X-Y party members (similar to catching logic?) ✔️/❌ (PARTIALLY COMPLETE)

### **MysteryEncounterOptions**
When selected, execute the custom logic passed in the **onSelect** function. Some **MysteryEncounterOptions** could be as simple as giving the player a pokéball, and others could be a few functions chained together, like "fight a battle, and get an item if you win"

### **Functions/ Helper functions** defined in __/utils/mystery-encounter-utils.ts__ for ME to happen, if applicable. They can be:
- Giving the player X item ✔️
- Giving the player X item from a certain tier ✔️
- Letting the player choose from items ✔️
- Letting the player choose from X items from a certain tier ✔️
- Start a combat encounter with a trainer ✔️
- Start a combat encounter with a wild pokémon (from biome) ✔️
- Start a combat encounter with a boss wild pokémon ✔️
- XP to the whole party ✔️
- Remove a PKMN from the player's party ✔️
- Steal from player ❌

# 📝 Known bugs (squash 'em all!):
- ## 🔴 __**Really bad ones**__
- 🐛 Opening the chest simply moves you to a wild battle against nothingness, which you can escape after you get bored of it.
- 🐛 Weaker trainers from Mysterious Challenger crashes the game when the reward screen should come out

- ## 🟡 __**Bad ones under certain circumstances**__
- 🐛 Needs further replication : At wave 51, wild PKMN encounter caused a freezed after pressing "ESC" key upon being asked to switch PKMNs

- ## 🟢 __**Non-game breaking**__
- 🐛 Scientist will remember the first PKMN it "did the thing on" and never ever forget it, even in future runs. Only affects dialogue.
- 🐛 Any ME that procs and wave (?)(?)(1) has its sprite removed. Only the sprite is affected.


# 🗿 Other cool things/functionalities that won't make it in the MVP but are planned to accomodate future MEs:

### QoL improvements
- Dialogue references to __**good**__ outcomes will be colored 🟢, __**bad**__ ones in 🔴 and __**ambiguous**__ or __**mixed**__, in 🟡
- Helps with quick glances when 5x speed

#### More requirements (with helper functions)
- Having X item
- Having Y amount of X item
- Being in a specific Biome
- A Pokémon X in player's party can learn Y move
- A Pokémon X in player's party knows Y move
- A Pokémon X in player's party has Y ability
- A Pokémon X in player's party belongs to a pre-defined pool (ie. "Ultrabeasts")

#### More outcomes (with helper functions)
- Status one or many Pokémon if your party -- if they can be statused
- Damage one or many Pokémon in your party
- Set a hazard (ally or foe side)
- Set a weather
- Give the player a Pokémon from a pool (useful for reg. professors/traders)
- XP to a Pokémon (similar to rare candy?)
- Add logic for choosing a Pokémon from party for some effect (trades, sacrifices, etc)
- Add logic for awarding exp to the party (outside of a normal combat)
- Encounter/pull a PKMN from a pre-defined pool (ie. "Ultrabeasts")
41 changes: 41 additions & 0 deletions public/images/mystery-encounters/mad_scientist_m.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"textures": [
{
"image": "mad_scientist_m.png",
"format": "RGBA8888",
"size": {
"w": 44,
"h": 74
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 44,
"h": 74
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 44,
"h": 74
},
"frame": {
"x": 0,
"y": 0,
"w": 44,
"h": 74
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$"
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
114 changes: 107 additions & 7 deletions src/battle-scene.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ import { addUiThemeOverrides } from "./ui/ui-theme";
import PokemonData from "./system/pokemon-data";
import { Nature } from "./data/nature";
import { SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger, pokemonFormChanges } from "./data/pokemon-forms";
import { FormChangePhase, QuietFormChangePhase } from "./form-change-phase";
import { FormChangePhase, QuietFormChangePhase } from "./phases/form-change-phase";
import { BattleSpec } from "./enums/battle-spec";
import { getTypeRgb } from "./data/type";
import PokemonSpriteSparkleHandler from "./field/pokemon-sprite-sparkle-handler";
Expand All @@ -63,6 +63,9 @@ import { Abilities } from "./data/enums/abilities";
import ArenaFlyout from "./ui/arena-flyout";
import { EaseType } from "./ui/enums/ease-type";
import { ExpNotification } from "./enums/exp-notification";
import MysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounter";
import { MysteryEncounterFlags } from "./data/mystery-encounter-flags";
import { allMysteryEncounters } from "./data/mystery-encounters/mystery-encounters";

export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";

Expand Down Expand Up @@ -198,6 +201,7 @@ export default class BattleScene extends SceneBase {
public money: integer;
public pokemonInfoContainer: PokemonInfoContainer;
private party: PlayerPokemon[];
public mysteryEncounterFlags: MysteryEncounterFlags;
/** Combined Biome and Wave count text */
private biomeWaveText: Phaser.GameObjects.Text;
private moneyText: Phaser.GameObjects.Text;
Expand Down Expand Up @@ -546,6 +550,10 @@ export default class BattleScene extends SceneBase {
this.playTimeTimer.destroy();
}

if (Utils.isNullOrUndefined(this.mysteryEncounterFlags)) {
this.mysteryEncounterFlags = new MysteryEncounterFlags(null);
}

this.playTimeTimer = this.time.addEvent({
delay: Utils.fixedInt(1000),
repeat: -1,
Expand Down Expand Up @@ -768,6 +776,17 @@ export default class BattleScene extends SceneBase {
return pokemon;
}

removePokemonFromPlayerParty(pokemon: PlayerPokemon) {
if (!pokemon) {
return;
}

const partyIndex = this.party.indexOf(pokemon);
this.field.remove(pokemon, true);
this.party.splice(partyIndex, 1);
pokemon.destroy();
}

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);

Expand Down Expand Up @@ -955,7 +974,7 @@ export default class BattleScene extends SceneBase {
}
}

newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean): Battle {
newBattle(waveIndex?: integer, battleType?: BattleType, trainerData?: TrainerData, double?: boolean, mysteryEncounter?: MysteryEncounter): Battle {
const _startingWave = Overrides.STARTING_WAVE_OVERRIDE || startingWave;
const newWaveIndex = waveIndex || ((this.currentBattle?.waveIndex || (_startingWave - 1)) + 1);
let newDouble: boolean;
Expand Down Expand Up @@ -999,6 +1018,19 @@ export default class BattleScene extends SceneBase {
newTrainer = trainerData !== undefined ? trainerData.toTrainer(this) : new Trainer(this, trainerType, doubleTrainer ? TrainerVariant.DOUBLE : Utils.randSeedInt(2) ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT);
this.field.add(newTrainer);
}

// Check for mystery encounter
// Can only occur in place of a standard wild battle
// They will also never be found outside of waves 3-180 in classic mode
if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(newWaveIndex) && !(this.gameMode.isClassic && (newWaveIndex > 180 || newWaveIndex < 3))) {
// Roll for mystery encounter instead of wild battle (25% chance)
const roll = Utils.randSeedInt(64);
const successRate = Utils.isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE) ? 16 : Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE;

if (roll < successRate) {
newBattleType = BattleType.MYSTERY_ENCOUNTER;
}
}
}

if (double === undefined && newWaveIndex > 1) {
Expand Down Expand Up @@ -1037,6 +1069,27 @@ export default class BattleScene extends SceneBase {
}, newWaveIndex << 3, this.waveSeed);
this.currentBattle.incrementTurn(this);

if (newBattleType === BattleType.MYSTERY_ENCOUNTER) {
// Disable double battle on mystery encounters (it may be re-enabled as part of encounter)
this.currentBattle.double = false;

// Load or generate a mystery encounter
const newEncounter = this.getMysteryEncounter(mysteryEncounter);
this.currentBattle.mysteryEncounter = newEncounter;

// If Encounter has an onInit() function, call it
// Usually used for calculating rand data before initializing anything visual
if (newEncounter.onInit) {
newEncounter.onInit(this);
}

// Add intro visuals for mystery encounter
newEncounter.initIntroVisuals(this);
this.field.add(newEncounter.introVisuals);

// this.currentBattle.mysteryEncounter = newEncounter;
}

//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) {
Expand All @@ -1061,19 +1114,25 @@ export default class BattleScene extends SceneBase {
isNewBiome = !Utils.randSeedInt(6 - biomeWaves);
}, lastBattle.waveIndex << 4);
}
const resetArenaState = isNewBiome || this.currentBattle.battleType === BattleType.TRAINER || this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS;


const resetArenaState = isNewBiome || this.currentBattle.battleType === BattleType.TRAINER || this.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER || 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.removeAllTags();
playerField.forEach((_, p) => this.unshiftPhase(new ReturnPhase(this, p)));

for (const pokemon of this.getParty()) {
if (pokemon.hasAbility(Abilities.ICE_FACE)) {
pokemon.formIndex = 0;
// If last battle was mystery encounter and no battle occurred, skip return phase
if (lastBattle?.mysteryEncounter?.encounterVariant !== MysteryEncounterVariant.NO_BATTLE) {
playerField.forEach((_, p) => this.unshiftPhase(new ReturnPhase(this, p)));

for (const pokemon of this.getParty()) {
if (pokemon.hasAbility(Abilities.ICE_FACE)) {
pokemon.formIndex = 0;
}
}
}
this.unshiftPhase(new ShowTrainerPhase(this));
Expand All @@ -1087,6 +1146,7 @@ export default class BattleScene extends SceneBase {
this.triggerPokemonFormChange(pokemon, SpeciesFormChangeTimeOfDayTrigger);
}
}

if (!this.gameMode.hasRandomBiomes && !isNewBiome) {
this.pushPhase(new NextEncounterPhase(this));
} else {
Expand Down Expand Up @@ -2404,4 +2464,44 @@ export default class BattleScene extends SceneBase {
};
(window as any).gameInfo = gameInfo;
}

/**
* Loads or generates a mystery encounter
* @param override - used to load session encounter when restarting game, etc.
* @returns
*/
getMysteryEncounter(override: MysteryEncounter): MysteryEncounter {
// Loading override or session encounter
let encounter: MysteryEncounter;
if (!Utils.isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_OVERRIDE) && Overrides.MYSTERY_ENCOUNTER_OVERRIDE < allMysteryEncounters.length) {
encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE];
} else {
encounter = override?.encounterType >= 0 ? allMysteryEncounters[override?.encounterType] : null;
}

// Generate new encounter if no overrides
if (!encounter) {
const tierValue = Utils.randSeedInt(64);
let tier = tierValue > 32 ? MysteryEncounterTier.COMMON : tierValue > 16 ? MysteryEncounterTier.UNCOMMON : tierValue > 6 ? MysteryEncounterTier.RARE : MysteryEncounterTier.SUPER_RARE;
let availableEncounters = [];

// If no valid encounters exist at tier, checks next tier down, continuing until there are some encounters available
while (availableEncounters.length === 0 && tier >= 0) {
availableEncounters = allMysteryEncounters.filter((encounter) => encounter?.meetsRequirements(this) && encounter.encounterTier === tier);
tier--;
}

// If absolutely no encounters are available, spawn 0th encounter (mysterious trainers)
if (availableEncounters.length === 0) {
return allMysteryEncounters[0];
}

encounter = availableEncounters[Utils.randSeedInt(availableEncounters.length)];
}

// New encounter object to not dirty flags
encounter = new MysteryEncounter(encounter);

return encounter;
}
}
Loading

0 comments on commit 6185274

Please sign in to comment.