rebase encounters branch with pokerogue/main
This commit is contained in:
parent
ffbc922e09
commit
860e875587
|
@ -0,0 +1,198 @@
|
|||
# 📝 Most immediate things to-do list
|
||||
|
||||
- ### High priority
|
||||
- 🐛 Intimidate and other ETB abilities proc twice at the start of wild MEs (fight or flight, dark deal)
|
||||
- ⚙️ Add a tag system so MEs don't show where they shouldn't and bricking Challenge runs:
|
||||
- noChallenge (cant be spawned in challenge runs)
|
||||
- allChallenge (can spawn in all challenge modes)
|
||||
- (typespecific)Challenge:
|
||||
- Example: fireOnly (can only spawn in fire related challenges)
|
||||
|
||||
- ### Medium priority
|
||||
|
||||
- ### Low priority
|
||||
- 🐛 Mysterious Challengers can spawn two trainers (or three) of the same type [Dev comment: not a bug]
|
||||
- 🐛 Fight or Flight intro visuals may show different gender from the actual spawned pokemon
|
||||
|
||||
# 📝 Things to be done before Mystery Encounters ("MEs/Events") MVP is finished:
|
||||
All the things on this list should be done before an MVP (Minimum Viable Product) can be playtested.
|
||||
|
||||
- ## Bugless implementation of the MVP MEs
|
||||
- Establish placeholder waves for MEs to happen ✔️
|
||||
- ⚪ Bug-free implementation of Common ME 1 ('Mysterious Chest')✔️
|
||||
- ⚪ Bug-free implementation of Common ME 2 ('Fight or Flight')✔️
|
||||
- 🔵 Bug-free implementation of Rare ME 1 ('Mysterious Challenger')✔️
|
||||
- 🔵 Bug-free implementation of Rare ME 2 ('Sleeping Snorlax') 🛠️
|
||||
- 🟣 Bug-free implementation of Epic ME 1 ('Training Session') 🛠️
|
||||
- 🟡 Bug-free implementation of Legendary ME 1 ('Dark Deal') ✔️
|
||||
|
||||
- ## First round of playtesting (Alpha)
|
||||
- First round of feedback on bugs for more slippery bugs 🛠️
|
||||
- First round of balance feedback on odds and power-level 🛠️
|
||||
- Tweak difficulty/rewards balance in MEs 🛠️
|
||||
|
||||
## Translation of MEs after playtest/balance
|
||||
- First round of translators feedback to avoid potential issues 🛠️
|
||||
- EN localisation 🛠️
|
||||
- ES localisation 🛠️
|
||||
|
||||
# 📝 Things to be done before Mystery Encounters ("MEs/Events") goes __live__:
|
||||
All the things on this list should be done before the merge to main.
|
||||
|
||||
- ## Bugless implementation of the MVP MEs
|
||||
- Bugless implementation of about 55-60 MEs
|
||||
- 20 non-biome-dependant:
|
||||
- ⚪ 9 Common Events
|
||||
- 🔵 5 Rare Events
|
||||
- 🟣 4 Epic Events
|
||||
- 🟡 2 Legendary Events
|
||||
- 35-40 biome-dependant Events, at least one for each biome
|
||||
|
||||
- ## Second round of playtesting (Beta)
|
||||
- Second round of feedback for bugs ❌
|
||||
- Second round of balance feedback ❌
|
||||
- Final decisions on balance, powerlevel, odds and design choices before live feedback ❌
|
||||
|
||||
## Translation of MEs after playtest/balance
|
||||
- de localisation 🛠️
|
||||
- en localisation 🛠️
|
||||
- es-ES localisation 🛠️
|
||||
- es-MX localisation 🛠️
|
||||
- fr localisation 🛠️
|
||||
- it localisation 🛠️
|
||||
- ko localisation 🛠️
|
||||
- pt-BR localisation 🛠️
|
||||
- zh-CN localisation 🛠️
|
||||
- zh-TW localisation 🛠️
|
||||
|
||||
|
||||
# 🧬 Deep dive into Events and what has been done so far
|
||||
|
||||
Events (referred to as 'Mysterious Encounters, MEs' in the code) 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 RogueLike 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!
|
||||
|
||||
## An Event __**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**__
|
||||
- 🐛 Picking up certain items in Fight or Flight is still broken. Workaround is leave encounter.
|
||||
- 🐛 Modifiers that are applied to pokemon get skipped in Fight or Flight.
|
||||
|
||||
- ## 🟡 __**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
|
||||
- 🐛 Wave seed generates different encounter data if you roll to a new wave, see the spawned stuff, and refresh the app
|
||||
- 🐛 Type-buffing items (like Silk Scarf) get swapped around when offered as a reward in Fight or Flight
|
||||
|
||||
- ## 🟢 __**Non-game breaking**__
|
||||
- Both of these bugs seem to have in common that they don't "forget" their last passed string:
|
||||
- 🐛 Scientist will remember the first PKMN it "did the thing on" and never ever forget it, even in future runs. Only affects dialogue.
|
||||
- 🐛 Tooltip bug in Events. When showing the tooltip of the 2nd or later Event you've found, the tooltip for the first option will match whatever option you selected in the previous Event. This wrong tooltip gets overriden once you move the cursor.
|
||||
|
||||
# 🗿 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")
|
||||
|
||||
|
||||
# Log Documentation
|
||||
|
||||
## 12th-13th June
|
||||
- The 🐛 "Opening the chest simply moves you to a wild battle against nothingness, which you can escape after you get bored of it." is fixed.
|
||||
- The 🐛 "PKMN Sprites and their HP/lvl bar doesn't get properly recalled when finding an ME or when meeting Rival." is fixed.
|
||||
- The 🐛 "Weaker trainers from Mysterious Challenger crashes the game when the reward screen should come out" is fixed.
|
||||
- The 🐛 "If a ME spawns on the first floor of a new biome (NewBiomeEncounterPhase), intro visuals do not spawn properly" is fixed.
|
||||
- The 🐛 "Any ME that procs at wave (?)(?)(1) has its sprite removed. Only the sprite is affected." is fixed.
|
||||
- The 🐛 "Picking a double battle trainer (ie Twins) as your challenge results in a game over, including loss of save." should be fixed.
|
||||
- Allowed catch in "Fight or Flight" -- it was counterintuitive to not allow it as it __is__ a wild PKMN fight.
|
||||
- More minor 🐛 squashed.
|
||||
- Pushed Dark Deal ME to a higher wave requirement (+30) as it seems to be functioning (mostly) bugless.
|
||||
|
||||
## 27-29th June
|
||||
- The 🐛 "Picking up certain items in Fight or Flight works poorly" has been squashed.
|
||||
- The 🐛 "Modifiers that are applied to pokemon get skipped in Fight or Flight" has been squashed.
|
||||
- ⚙️ Added "Omniboost" functionality (Fight or Flight ME)
|
||||
- The 🐛 "Wave seed generates different encounter data if you roll to a new wave, see the spawned stuff, and refresh the app" has been squashed.
|
||||
- The 🐛 "Type-buffing items (like Silk Scarf) get swapped around when offered as a reward in Fight or Flight" has been squashed.
|
||||
- ⚖️ Adjusted Dark Deal odds to show 6-7 cost PKMNs at a much higher rate (70%) than 8-cost (20%) or 9-cost (10%), to avoid box legendaries being overly present.
|
||||
- The 🐛 about "Tooltips being remembered from the previous ME choice until you hovered a different option" is squashed.
|
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"textures": [
|
||||
{
|
||||
"image": "training_gear.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 76,
|
||||
"h": 57
|
||||
},
|
||||
"scale": 1,
|
||||
"frames": [
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 76,
|
||||
"h": 57
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 10,
|
||||
"y": 3,
|
||||
"w": 56,
|
||||
"h": 54
|
||||
},
|
||||
"frame": {
|
||||
"x": 8,
|
||||
"y": 0,
|
||||
"w": 56,
|
||||
"h": 54
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"textures": [
|
||||
{
|
||||
"image": "training_gear.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 76,
|
||||
"h": 57
|
||||
},
|
||||
"scale": 1,
|
||||
"frames": [
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 76,
|
||||
"h": 57
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 10,
|
||||
"y": 3,
|
||||
"w": 56,
|
||||
"h": 54
|
||||
},
|
||||
"frame": {
|
||||
"x": 8,
|
||||
"y": 0,
|
||||
"w": 56,
|
||||
"h": 54
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 2.7 KiB |
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"textures": [
|
||||
{
|
||||
"image": "dark_deal_porygon.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 36,
|
||||
"h": 45
|
||||
},
|
||||
"scale": 1,
|
||||
"frames": [
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 36,
|
||||
"h": 45
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 44,
|
||||
"h": 44
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 36,
|
||||
"h": 45
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
|
@ -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$"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 14 KiB |
|
@ -0,0 +1,41 @@
|
|||
{
|
||||
"textures": [
|
||||
{
|
||||
"image": "training_gear.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 76,
|
||||
"h": 57
|
||||
},
|
||||
"scale": 1,
|
||||
"frames": [
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"rotated": false,
|
||||
"trimmed": false,
|
||||
"sourceSize": {
|
||||
"w": 76,
|
||||
"h": 57
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 10,
|
||||
"y": 3,
|
||||
"w": 56,
|
||||
"h": 54
|
||||
},
|
||||
"frame": {
|
||||
"x": 8,
|
||||
"y": 0,
|
||||
"w": 56,
|
||||
"h": 54
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$"
|
||||
}
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 1.5 KiB |
|
@ -3,9 +3,25 @@ import UI from "./ui/ui";
|
|||
import { NextEncounterPhase, NewBiomeEncounterPhase, SelectBiomePhase, MessagePhase, TurnInitPhase, ReturnPhase, LevelCapPhase, ShowTrainerPhase, LoginPhase, MovePhase, TitlePhase, SwitchPhase } from "./phases";
|
||||
import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon";
|
||||
import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species";
|
||||
import { Constructor } from "#app/utils";
|
||||
import {Constructor, isNullOrUndefined} 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 {
|
||||
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";
|
||||
|
@ -14,7 +30,16 @@ 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, getPartyLuckValue } from "./modifier/modifier-type";
|
||||
import {
|
||||
ModifierPoolType,
|
||||
getDefaultModifierTypeForTier,
|
||||
getEnemyModifierTypesForWave,
|
||||
getLuckString,
|
||||
getLuckTextTint,
|
||||
getModifierPoolForType,
|
||||
getPartyLuckValue,
|
||||
PokemonHeldItemModifierType
|
||||
} 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";
|
||||
|
@ -67,6 +92,10 @@ import { Species } from "#enums/species";
|
|||
import { UiTheme } from "#enums/ui-theme";
|
||||
import { TimedEventManager } from "#app/timed-event-manager.js";
|
||||
import i18next from "i18next";
|
||||
import MysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounter";
|
||||
import {mysteryEncountersByBiome, allMysteryEncounters, BASE_MYSTYERY_ENCOUNTER_WEIGHT} from "./data/mystery-encounters/mystery-encounters";
|
||||
import {MysteryEncounterFlags} from "#app/data/mystery-encounter-flags";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
|
||||
export const bypassLogin = import.meta.env.VITE_BYPASS_LOGIN === "1";
|
||||
|
||||
|
@ -211,6 +240,8 @@ export default class BattleScene extends SceneBase {
|
|||
public money: integer;
|
||||
public pokemonInfoContainer: PokemonInfoContainer;
|
||||
private party: PlayerPokemon[];
|
||||
public mysteryEncounterFlags: MysteryEncounterFlags = new MysteryEncounterFlags(null);
|
||||
public lastMysteryEncounter: MysteryEncounter;
|
||||
/** Combined Biome and Wave count text */
|
||||
private biomeWaveText: Phaser.GameObjects.Text;
|
||||
private moneyText: Phaser.GameObjects.Text;
|
||||
|
@ -816,6 +847,20 @@ export default class BattleScene extends SceneBase {
|
|||
return pokemon;
|
||||
}
|
||||
|
||||
removePokemonFromPlayerParty(pokemon: PlayerPokemon, destroy: boolean = true) {
|
||||
if (!pokemon) {
|
||||
return;
|
||||
}
|
||||
|
||||
const partyIndex = this.party.indexOf(pokemon);
|
||||
this.party.splice(partyIndex, 1);
|
||||
if (destroy) {
|
||||
this.field.remove(pokemon, true);
|
||||
pokemon.destroy();
|
||||
}
|
||||
this.updateModifiers(true);
|
||||
}
|
||||
|
||||
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`);
|
||||
|
@ -1006,7 +1051,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;
|
||||
|
@ -1050,6 +1095,32 @@ 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, waves 10-180
|
||||
if (this.gameMode.hasMysteryEncounters && newBattleType === BattleType.WILD && !this.gameMode.isBoss(newWaveIndex) && !(this.gameMode.isClassic && (newWaveIndex > 180 || newWaveIndex < 10))) {
|
||||
const roll = Utils.randSeedInt(256);
|
||||
|
||||
// Base spawn weight is 3/256, and increases by 1/256 for each missed attempt at spawning an encounter on a valid floor
|
||||
const sessionEncounterRate = !isNullOrUndefined(this.mysteryEncounterFlags?.encounterSpawnChance) ? this.mysteryEncounterFlags.encounterSpawnChance : BASE_MYSTYERY_ENCOUNTER_WEIGHT;
|
||||
|
||||
// If total number of encounters is lower than expected for the run, slightly favor a new encounter spawn
|
||||
// Do the reverse as well
|
||||
// Reduces occurrence of runs with very few (<6) and a ton (>10) of encounters
|
||||
const expectedEncountersByFloor = 8 / (180 - 10) * newWaveIndex;
|
||||
const currentRunDiffFromAvg = expectedEncountersByFloor - (this.mysteryEncounterFlags?.encounteredEvents?.length || 0);
|
||||
const favoredEncounterRate = sessionEncounterRate + currentRunDiffFromAvg * 5;
|
||||
|
||||
const successRate = Utils.isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE) ? favoredEncounterRate : Overrides.MYSTERY_ENCOUNTER_RATE_OVERRIDE;
|
||||
|
||||
if (roll < successRate) {
|
||||
newBattleType = BattleType.MYSTERY_ENCOUNTER;
|
||||
// Reset base spawn weight
|
||||
this.mysteryEncounterFlags.encounterSpawnChance = BASE_MYSTYERY_ENCOUNTER_WEIGHT;
|
||||
} else {
|
||||
this.mysteryEncounterFlags.encounterSpawnChance = sessionEncounterRate + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (double === undefined && newWaveIndex > 1) {
|
||||
|
@ -1082,12 +1153,19 @@ export default class BattleScene extends SceneBase {
|
|||
const maxExpLevel = this.getMaxExpLevel();
|
||||
|
||||
this.lastEnemyTrainer = lastBattle?.trainer ?? null;
|
||||
this.lastMysteryEncounter = lastBattle?.mysteryEncounter ?? null;
|
||||
|
||||
this.executeWithSeedOffset(() => {
|
||||
this.currentBattle = new Battle(this.gameMode, newWaveIndex, newBattleType, newTrainer, newDouble);
|
||||
}, 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;
|
||||
this.currentBattle.mysteryEncounter = this.getMysteryEncounter(mysteryEncounter);
|
||||
}
|
||||
|
||||
//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) {
|
||||
|
@ -1112,7 +1190,9 @@ 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) {
|
||||
|
@ -1120,20 +1200,24 @@ export default class BattleScene extends SceneBase {
|
|||
}
|
||||
if (resetArenaState) {
|
||||
this.arena.removeAllTags();
|
||||
playerField.forEach((_, p) => this.unshiftPhase(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);
|
||||
// If last battle was mystery encounter and no battle occurred, skip return phases
|
||||
if (lastBattle?.mysteryEncounter?.encounterVariant !== MysteryEncounterVariant.NO_BATTLE) {
|
||||
playerField.forEach((_, p) => this.unshiftPhase(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);
|
||||
}
|
||||
|
||||
pokemon.resetBattleData();
|
||||
applyPostBattleInitAbAttrs(PostBattleInitAbAttr, pokemon);
|
||||
this.unshiftPhase(new ShowTrainerPhase(this));
|
||||
}
|
||||
|
||||
this.unshiftPhase(new ShowTrainerPhase(this));
|
||||
}
|
||||
|
||||
for (const pokemon of this.getParty()) {
|
||||
|
@ -2276,7 +2360,7 @@ export default class BattleScene extends SceneBase {
|
|||
});
|
||||
}
|
||||
|
||||
generateEnemyModifiers(): Promise<void> {
|
||||
generateEnemyModifiers(customHeldModifiers?: PokemonHeldItemModifierType[][]): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
if (this.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
|
||||
return resolve();
|
||||
|
@ -2298,29 +2382,33 @@ export default class BattleScene extends SceneBase {
|
|||
}
|
||||
|
||||
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);
|
||||
let pokemonModifierChance = modifierChance;
|
||||
if (this.currentBattle.battleType === BattleType.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 (customHeldModifiers && i < customHeldModifiers.length && customHeldModifiers[i].length > 0) {
|
||||
customHeldModifiers[i].forEach(mt => mt.newModifier(enemyPokemon).add(this.enemyModifiers, false, this));
|
||||
} else {
|
||||
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);
|
||||
let pokemonModifierChance = modifierChance;
|
||||
if (this.currentBattle.battleType === BattleType.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));
|
||||
}
|
||||
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());
|
||||
|
@ -2542,4 +2630,88 @@ 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) && allMysteryEncounters.hasOwnProperty(Overrides.MYSTERY_ENCOUNTER_OVERRIDE)) {
|
||||
encounter = allMysteryEncounters[Overrides.MYSTERY_ENCOUNTER_OVERRIDE];
|
||||
} else {
|
||||
encounter = override?.encounterType >= 0 ? allMysteryEncounters[override?.encounterType] : null;
|
||||
}
|
||||
|
||||
// Check for queued encounters first
|
||||
if (!encounter && this.mysteryEncounterFlags?.nextEncounterQueue?.length > 0) {
|
||||
let i = 0;
|
||||
while (i < this.mysteryEncounterFlags.nextEncounterQueue.length && !!encounter) {
|
||||
const candidate = this.mysteryEncounterFlags.nextEncounterQueue[i];
|
||||
const forcedChance = candidate[1];
|
||||
if (Utils.randSeedInt(100) < forcedChance) {
|
||||
encounter = allMysteryEncounters[candidate[0]];
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (encounter) {
|
||||
encounter = new MysteryEncounter(encounter);
|
||||
encounter.meetsRequirements(this);
|
||||
return encounter;
|
||||
}
|
||||
|
||||
// Common / Uncommon / Rare / Super Rare
|
||||
const tierWeights = [61, 40, 21, 6];
|
||||
|
||||
// Adjust tier weights by previously encountered events to lower odds of only common/uncommons in run
|
||||
this.mysteryEncounterFlags.encounteredEvents.forEach(val => {
|
||||
const tier = val[1];
|
||||
if (tier === MysteryEncounterTier.COMMON) {
|
||||
tierWeights[0] = tierWeights[0] - 6;
|
||||
} else if (tier === MysteryEncounterTier.UNCOMMON) {
|
||||
tierWeights[1] = tierWeights[1] - 4;
|
||||
}
|
||||
});
|
||||
|
||||
const totalWeight = tierWeights.reduce((a, b) => a + b);
|
||||
const tierValue = Utils.randSeedInt(totalWeight);
|
||||
const commonThreshold = totalWeight - tierWeights[0];
|
||||
const uncommonThreshold = totalWeight - tierWeights[0] - tierWeights[1];
|
||||
const rareThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2];
|
||||
let tier = tierValue > commonThreshold ? MysteryEncounterTier.COMMON : tierValue > uncommonThreshold ? MysteryEncounterTier.UNCOMMON : tierValue > rareThreshold ? MysteryEncounterTier.RARE : MysteryEncounterTier.SUPER_RARE;
|
||||
|
||||
if (!Utils.isNullOrUndefined(Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE)) {
|
||||
tier = Overrides.MYSTERY_ENCOUNTER_TIER_OVERRIDE;
|
||||
}
|
||||
|
||||
let availableEncounters = [];
|
||||
// New encounter will never be the same as the most recent encounter
|
||||
const previousEncounter = this.mysteryEncounterFlags.encounteredEvents?.length > 0 ? this.mysteryEncounterFlags.encounteredEvents[this.mysteryEncounterFlags.encounteredEvents.length - 1][0] : null;
|
||||
const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType);
|
||||
// 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 = biomeMysteryEncounters
|
||||
.filter((encounterType) =>
|
||||
allMysteryEncounters[encounterType]?.meetsRequirements(this) &&
|
||||
allMysteryEncounters[encounterType].encounterTier === tier &&
|
||||
(isNullOrUndefined(previousEncounter) || encounterType !== previousEncounter))
|
||||
.map((m) => (allMysteryEncounters[m]));
|
||||
tier--;
|
||||
}
|
||||
|
||||
// If absolutely no encounters are available, spawn 0th encounter
|
||||
if (availableEncounters.length === 0) {
|
||||
return allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHALLENGERS];
|
||||
}
|
||||
encounter = availableEncounters[Utils.randSeedInt(availableEncounters.length)];
|
||||
// New encounter object to not dirty flags
|
||||
encounter = new MysteryEncounter(encounter);
|
||||
encounter.meetsRequirements(this);
|
||||
return encounter;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,32 +14,34 @@ import { PlayerGender } from "#enums/player-gender";
|
|||
import { Species } from "#enums/species";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import i18next from "#app/plugins/i18n";
|
||||
import MysteryEncounter, { MysteryEncounterVariant } from "./data/mystery-encounter";
|
||||
|
||||
export enum BattleType {
|
||||
WILD,
|
||||
TRAINER,
|
||||
CLEAR
|
||||
WILD,
|
||||
TRAINER,
|
||||
CLEAR,
|
||||
MYSTERY_ENCOUNTER
|
||||
}
|
||||
|
||||
export enum BattlerIndex {
|
||||
ATTACKER = -1,
|
||||
PLAYER,
|
||||
PLAYER_2,
|
||||
ENEMY,
|
||||
ENEMY_2
|
||||
ATTACKER = -1,
|
||||
PLAYER,
|
||||
PLAYER_2,
|
||||
ENEMY,
|
||||
ENEMY_2
|
||||
}
|
||||
|
||||
export interface TurnCommand {
|
||||
command: Command;
|
||||
cursor?: integer;
|
||||
move?: QueuedMove;
|
||||
targets?: BattlerIndex[];
|
||||
skip?: boolean;
|
||||
args?: any[];
|
||||
command: Command;
|
||||
cursor?: integer;
|
||||
move?: QueuedMove;
|
||||
targets?: BattlerIndex[];
|
||||
skip?: boolean;
|
||||
args?: any[];
|
||||
}
|
||||
|
||||
interface TurnCommands {
|
||||
[key: integer]: TurnCommand
|
||||
[key: integer]: TurnCommand
|
||||
}
|
||||
|
||||
export default class Battle {
|
||||
|
@ -67,6 +69,7 @@ export default class Battle {
|
|||
public lastUsedPokeball: PokeballType;
|
||||
public playerFaints: number; // The amount of times pokemon on the players side have fainted
|
||||
public enemyFaints: number; // The amount of times pokemon on the enemies side have fainted
|
||||
public mysteryEncounter: MysteryEncounter;
|
||||
|
||||
private rngCounter: integer = 0;
|
||||
|
||||
|
@ -105,7 +108,7 @@ export default class Battle {
|
|||
this.battleSpec = spec;
|
||||
}
|
||||
|
||||
private getLevelForWave(): integer {
|
||||
public getLevelForWave(): integer {
|
||||
const levelWaveIndex = this.gameMode.getWaveForDifficulty(this.waveIndex);
|
||||
const baseLevel = 1 + levelWaveIndex / 2 + Math.pow(levelWaveIndex / 25, 2);
|
||||
const bossMultiplier = 1.2;
|
||||
|
@ -202,7 +205,7 @@ export default class Battle {
|
|||
|
||||
getBgmOverride(scene: BattleScene): string {
|
||||
const battlers = this.enemyParty.slice(0, this.getBattlerCount());
|
||||
if (this.battleType === BattleType.TRAINER) {
|
||||
if (this.battleType === BattleType.TRAINER || this.mysteryEncounter?.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) {
|
||||
if (!this.started && this.trainer.config.encounterBgm && this.trainer.getEncounterMessages()?.length) {
|
||||
return `encounter_${this.trainer.getEncounterBgm()}`;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@ import { StatusEffect } from "./status-effect";
|
|||
import * as Utils from "../utils";
|
||||
import { ChargeAttr, MoveFlags, allMoves } from "./move";
|
||||
import { Type } from "./type";
|
||||
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs } from "./ability";
|
||||
import { BlockNonDirectDamageAbAttr, FlinchEffectAbAttr, ReverseDrainAbAttr, applyAbAttrs, ProtectStatAbAttr } from "./ability";
|
||||
import { TerrainType } from "./terrain";
|
||||
import { WeatherType } from "./weather";
|
||||
import { BattleStat } from "./battle-stat";
|
||||
|
@ -1537,6 +1537,38 @@ export class IceFaceTag extends BattlerTag {
|
|||
}
|
||||
}
|
||||
|
||||
export class MysteryEncounterPostSummonTag extends BattlerTag {
|
||||
constructor(sourceMove: Moves) {
|
||||
super(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON, BattlerTagLapseType.CUSTOM, 1, sourceMove);
|
||||
}
|
||||
|
||||
onAdd(pokemon: Pokemon): void {
|
||||
super.onAdd(pokemon);
|
||||
}
|
||||
|
||||
lapse(pokemon: Pokemon, lapseType: BattlerTagLapseType): boolean {
|
||||
const ret = super.lapse(pokemon, lapseType);
|
||||
|
||||
if (lapseType === BattlerTagLapseType.CUSTOM) {
|
||||
// Give pokemon +1 stats for battle
|
||||
const cancelled = new Utils.BooleanHolder(false);
|
||||
applyAbAttrs(ProtectStatAbAttr, pokemon, cancelled);
|
||||
if (!cancelled.value) {
|
||||
const mysteryEncounterBattleEffects = pokemon.summonData.mysteryEncounterBattleEffects;
|
||||
if (mysteryEncounterBattleEffects) {
|
||||
mysteryEncounterBattleEffects(pokemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
onRemove(pokemon: Pokemon): void {
|
||||
super.onRemove(pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourceMove: Moves, sourceId: integer): BattlerTag {
|
||||
switch (tagType) {
|
||||
case BattlerTagType.RECHARGING:
|
||||
|
@ -1654,6 +1686,8 @@ export function getBattlerTag(tagType: BattlerTagType, turnCount: integer, sourc
|
|||
return new DestinyBondTag(sourceMove, sourceId);
|
||||
case BattlerTagType.ICE_FACE:
|
||||
return new IceFaceTag(sourceMove);
|
||||
case BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON:
|
||||
return new MysteryEncounterPostSummonTag(sourceMove);
|
||||
case BattlerTagType.NONE:
|
||||
default:
|
||||
return new BattlerTag(tagType, BattlerTagLapseType.CUSTOM, turnCount, sourceMove, sourceId);
|
||||
|
|
|
@ -0,0 +1,16 @@
|
|||
import * as Utils from "../utils";
|
||||
import {MysteryEncounterTier} from "#app/data/mystery-encounter";
|
||||
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
|
||||
import {BASE_MYSTYERY_ENCOUNTER_WEIGHT} from "#app/data/mystery-encounters/mystery-encounters";
|
||||
|
||||
export class MysteryEncounterFlags {
|
||||
encounteredEvents: [MysteryEncounterType, MysteryEncounterTier][] = [];
|
||||
encounterSpawnChance: number = BASE_MYSTYERY_ENCOUNTER_WEIGHT;
|
||||
nextEncounterQueue: [MysteryEncounterType, integer][] = [];
|
||||
|
||||
constructor(flags: MysteryEncounterFlags) {
|
||||
if (!Utils.isNullOrUndefined(flags)) {
|
||||
Object.assign(this, flags);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,158 @@
|
|||
import { PlayerPokemon } from "#app/field/pokemon";
|
||||
import * as Utils from "../utils";
|
||||
import BattleScene from "../battle-scene";
|
||||
import { EncounterPokemonRequirement, EncounterSceneRequirement } from "./mystery-encounter-requirements";
|
||||
import {OptionTextDisplay} from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
|
||||
export default interface MysteryEncounterOption {
|
||||
requirements?: EncounterSceneRequirement[];
|
||||
primaryPokemonRequirements?: EncounterPokemonRequirement[];
|
||||
secondaryPokemonRequirements ?: EncounterPokemonRequirement[];
|
||||
primaryPokemon?: PlayerPokemon;
|
||||
secondaryPokemon?: PlayerPokemon[];
|
||||
excludePrimaryFromSecondaryRequirements?: boolean;
|
||||
|
||||
/**
|
||||
* Dialogue object containing all the dialogue, messages, tooltips, etc. for this option
|
||||
* Will be populated on MysteryEncounter initialization
|
||||
*/
|
||||
dialogue?: OptionTextDisplay;
|
||||
|
||||
// Executes before any following dialogue or business logic from option. Usually this will be for calculating dialogueTokens or performing scene/data updates
|
||||
onPreOptionPhase?: (scene: BattleScene) => Promise<void | boolean>;
|
||||
// Business logic for option
|
||||
onOptionPhase?: (scene: BattleScene) => Promise<void | boolean>;
|
||||
// Executes after the encounter is over. Usually this will be for calculating dialogueTokens or performing data updates
|
||||
onPostOptionPhase?: (scene: BattleScene) => Promise<void | boolean>;
|
||||
}
|
||||
|
||||
export default class MysteryEncounterOption implements MysteryEncounterOption {
|
||||
constructor(option: MysteryEncounterOption) {
|
||||
Object.assign(this, option);
|
||||
this.requirements = this.requirements ? this.requirements : [];
|
||||
}
|
||||
|
||||
meetsRequirements?(scene: BattleScene) {
|
||||
return !this.requirements.some(requirement => !requirement.meetsRequirement(scene)) &&
|
||||
this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene) &&
|
||||
this.meetsSupportingRequirementAndSupportingPokemonSelected(scene);
|
||||
}
|
||||
meetsPrimaryRequirementAndPrimaryPokemonSelected?(scene: BattleScene) {
|
||||
if (!this.primaryPokemonRequirements) {
|
||||
return true;
|
||||
}
|
||||
let qualified:PlayerPokemon[] = scene.getParty();
|
||||
for (const req of this.primaryPokemonRequirements) {
|
||||
console.log(req);
|
||||
if (req.meetsRequirement(scene)) {
|
||||
if (req instanceof EncounterPokemonRequirement) {
|
||||
qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn));
|
||||
}
|
||||
} else {
|
||||
this.primaryPokemon = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (qualified.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.excludePrimaryFromSecondaryRequirements && this.secondaryPokemon) {
|
||||
const truePrimaryPool = [];
|
||||
const overlap = [];
|
||||
for (const qp of qualified) {
|
||||
if (!this.secondaryPokemon.includes(qp)) {
|
||||
truePrimaryPool.push(qp);
|
||||
} else {
|
||||
overlap.push(qp);
|
||||
}
|
||||
|
||||
}
|
||||
if (truePrimaryPool.length > 0) {
|
||||
// always choose from the non-overlapping pokemon first
|
||||
this.primaryPokemon = truePrimaryPool[Utils.randSeedInt(truePrimaryPool.length, 0)];
|
||||
return true;
|
||||
} else {
|
||||
// if there are multiple overlapping pokemon, we're okay - just choose one and take it out of the supporting pokemon pool
|
||||
if (overlap.length > 1 || (this.secondaryPokemon.length - overlap.length >= 1)) {
|
||||
// is this working?
|
||||
this.primaryPokemon = overlap[Utils.randSeedInt(overlap.length, 0)];
|
||||
this.secondaryPokemon = this.secondaryPokemon.filter((supp)=> supp !== this.primaryPokemon);
|
||||
return true;
|
||||
}
|
||||
console.log("Mystery Encounter Edge Case: Requirement not met due to primay pokemon overlapping with support pokemon. There's no valid primary pokemon left.");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// this means we CAN have the same pokemon be a primary and secondary pokemon, so just choose any qualifying one randomly.
|
||||
this.primaryPokemon = qualified[Utils.randSeedInt(qualified.length, 0)];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
meetsSupportingRequirementAndSupportingPokemonSelected?(scene: BattleScene) {
|
||||
if (!this.secondaryPokemonRequirements) {
|
||||
this.secondaryPokemon = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
let qualified:PlayerPokemon[] = scene.getParty();
|
||||
for (const req of this.secondaryPokemonRequirements) {
|
||||
if (req.meetsRequirement(scene)) {
|
||||
if (req instanceof EncounterPokemonRequirement) {
|
||||
qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn));
|
||||
|
||||
}
|
||||
} else {
|
||||
this.secondaryPokemon = [];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.secondaryPokemon = qualified;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class MysteryEncounterOptionBuilder implements Partial<MysteryEncounterOption> {
|
||||
requirements?: EncounterSceneRequirement[] = [];
|
||||
primaryPokemonRequirements?: EncounterPokemonRequirement[] = [];
|
||||
secondaryPokemonRequirements ?: EncounterPokemonRequirement[] = [];
|
||||
excludePrimaryFromSecondaryRequirements?: boolean;
|
||||
onPreOptionPhase?: (scene: BattleScene) => Promise<void | boolean>;
|
||||
onOptionPhase?: (scene: BattleScene) => Promise<void | boolean>;
|
||||
onPostOptionPhase?: (scene: BattleScene) => Promise<void | boolean>;
|
||||
|
||||
withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<MysteryEncounterOption, "requirements">> {
|
||||
this.requirements.push(requirement);
|
||||
return Object.assign(this, { requirements: this.requirements });
|
||||
}
|
||||
|
||||
withPreOptionPhase(onPreOptionPhase: (scene: BattleScene) => Promise<void | boolean>): this & Required<Pick<MysteryEncounterOption, "onPreOptionPhase">> {
|
||||
return Object.assign(this, { onPreOptionPhase: onPreOptionPhase });
|
||||
}
|
||||
|
||||
withOptionPhase(onOptionPhase: (scene: BattleScene) => Promise<void | boolean>): this & Required<Pick<MysteryEncounterOption, "onOptionPhase">> {
|
||||
return Object.assign(this, { onOptionPhase: onOptionPhase });
|
||||
}
|
||||
|
||||
withPostOptionPhase(onPostOptionPhase: (scene: BattleScene) => Promise<void | boolean>): this & Required<Pick<MysteryEncounterOption, "onPostOptionPhase">> {
|
||||
return Object.assign(this, { onPostOptionPhase: onPostOptionPhase });
|
||||
}
|
||||
|
||||
build(this: MysteryEncounterOption) {
|
||||
return new MysteryEncounterOption(this);
|
||||
}
|
||||
|
||||
withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required<Pick<MysteryEncounterOption, "primaryPokemonRequirements">> {
|
||||
this.primaryPokemonRequirements.push(requirement);
|
||||
return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements });
|
||||
}
|
||||
|
||||
withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements?: boolean): this & Required<Pick<MysteryEncounterOption, "secondaryPokemonRequirements">> {
|
||||
this.secondaryPokemonRequirements.push(requirement);
|
||||
this.excludePrimaryFromSecondaryRequirements = excludePrimaryFromSecondaryRequirements;
|
||||
return Object.assign(this, { secondaryPokemonRequirements: this.secondaryPokemonRequirements });
|
||||
}
|
||||
}
|
|
@ -0,0 +1,913 @@
|
|||
import { PlayerPokemon } from "#app/field/pokemon";
|
||||
import { ModifierType, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
|
||||
import BattleScene from "../battle-scene";
|
||||
import { isNullOrUndefined } from "../utils";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { TimeOfDay } from "#enums/time-of-day";
|
||||
import { Nature } from "./nature";
|
||||
import { EvolutionItem, pokemonEvolutions } from "./pokemon-evolutions";
|
||||
import { FormChangeItem, SpeciesFormChangeItemTrigger, pokemonFormChanges } from "./pokemon-forms";
|
||||
import { SpeciesFormKey } from "./pokemon-species";
|
||||
import { StatusEffect } from "./status-effect";
|
||||
import { Type } from "./type";
|
||||
import { WeatherType } from "./weather";
|
||||
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
|
||||
|
||||
export interface EncounterRequirement {
|
||||
meetsRequirement(scene: BattleScene): boolean; // Boolean to see if a requirement is met
|
||||
}
|
||||
|
||||
export abstract class EncounterSceneRequirement implements EncounterRequirement {
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(scene:BattleScene): [RegExp, string] {
|
||||
return [(/@/gi), ""];
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class EncounterPokemonRequirement implements EncounterRequirement {
|
||||
minNumberOfPokemon: number;
|
||||
invertQuery: boolean;
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
throw new Error("Method not implemented.");
|
||||
}
|
||||
|
||||
// Returns all party members that are compatible with this requirement. For non pokemon related requirements, the entire party is returned..
|
||||
queryParty(partyPokemon: PlayerPokemon[]) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Doesn't require the "@ec" as prefix, just the string; populates the token with the attribute
|
||||
// ex. @ec{primarySpecies} if strPrefix is simply "primary"
|
||||
getMatchingDialogueToken(strPrefix:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
return [(/@/gi), ""];
|
||||
}
|
||||
}
|
||||
|
||||
export class PreviousEncounterRequirement extends EncounterSceneRequirement {
|
||||
previousEncounterRequirement: MysteryEncounterType;
|
||||
|
||||
/**
|
||||
* Used for specifying an encounter that must be seen before this encounter can spawn
|
||||
* @param previousEncounterRequirement
|
||||
*/
|
||||
constructor(previousEncounterRequirement) {
|
||||
super();
|
||||
this.previousEncounterRequirement = previousEncounterRequirement;
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
return scene.mysteryEncounterFlags.encounteredEvents.some(e => e[0] === this.previousEncounterRequirement);
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(scene:BattleScene): [RegExp, string] {
|
||||
return [new RegExp("@ec\{previousEncounter\\}", "gi"), scene.mysteryEncounterFlags.encounteredEvents.find(e => e[0] === this.previousEncounterRequirement)[0].toString()];
|
||||
}
|
||||
}
|
||||
|
||||
export class WaveCountRequirement extends EncounterSceneRequirement {
|
||||
waveRange: [number, number];
|
||||
|
||||
/**
|
||||
* Used for specifying a unique wave or wave range requirement
|
||||
* If minWaveIndex and maxWaveIndex are equivalent, will check for exact wave number
|
||||
* @param waveRange - [min, max]
|
||||
*/
|
||||
constructor(waveRange: [number, number]) {
|
||||
super();
|
||||
this.waveRange = waveRange;
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
if (!isNullOrUndefined(this?.waveRange) && this.waveRange?.[0] <= this.waveRange?.[1]) {
|
||||
const waveIndex = scene.currentBattle.waveIndex;
|
||||
if (waveIndex >= 0 && (this?.waveRange?.[0] >= 0 && this.waveRange?.[0] > waveIndex) || (this?.waveRange?.[1] >= 0 && this.waveRange?.[1] < waveIndex)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(scene:BattleScene): [RegExp, string] {
|
||||
return [new RegExp("@ec\{waveCount\\}", "gi"), scene.currentBattle.waveIndex.toString()];
|
||||
}
|
||||
}
|
||||
|
||||
export class TimeOfDayRequirement extends EncounterSceneRequirement {
|
||||
requiredTimeOfDay?: TimeOfDay[];
|
||||
|
||||
constructor(timeOfDay: TimeOfDay | TimeOfDay[]) {
|
||||
super();
|
||||
if (timeOfDay instanceof Array) {
|
||||
this.requiredTimeOfDay = timeOfDay;
|
||||
} else {
|
||||
this.requiredTimeOfDay = [];
|
||||
this.requiredTimeOfDay.push(timeOfDay);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const timeOfDay = scene.arena?.getTimeOfDay();
|
||||
if (!isNullOrUndefined(timeOfDay) && this?.requiredTimeOfDay?.length > 0 && !this.requiredTimeOfDay.includes(timeOfDay)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(scene:BattleScene): [RegExp, string] {
|
||||
return [new RegExp("@ec\{timeOfDay\\}", "gi"), TimeOfDay[scene.arena.getTimeOfDay()].toLocaleLowerCase()];
|
||||
}
|
||||
}
|
||||
|
||||
export class WeatherRequirement extends EncounterSceneRequirement {
|
||||
requiredWeather?: WeatherType[];
|
||||
|
||||
constructor(weather: WeatherType | WeatherType[]) {
|
||||
super();
|
||||
if (weather instanceof Array) {
|
||||
this.requiredWeather = weather;
|
||||
} else {
|
||||
this.requiredWeather = [];
|
||||
this.requiredWeather.push(weather);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const currentWeather = scene.arena?.weather?.weatherType;
|
||||
if (!isNullOrUndefined(currentWeather) && this?.requiredWeather?.length > 0 && !this.requiredWeather.includes(currentWeather)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
getMatchingDialogueToken(scene:BattleScene): [RegExp, string] {
|
||||
return [new RegExp("@ec\{weather\\}", "gi"), WeatherType[scene.arena?.weather?.weatherType].replace("_", " ").toLocaleLowerCase()];
|
||||
}
|
||||
}
|
||||
|
||||
export class PartySizeRequirement extends EncounterSceneRequirement {
|
||||
partySizeRange: [number, number];
|
||||
|
||||
/**
|
||||
* Used for specifying a party size requirement
|
||||
* If min and max are equivalent, will check for exact size
|
||||
* @param partySizeRange - [min, max]
|
||||
*/
|
||||
constructor(partySizeRange: [number, number]) {
|
||||
super();
|
||||
this.partySizeRange = partySizeRange;
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
if (!isNullOrUndefined(this?.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) {
|
||||
const partySize = scene.getParty().length;
|
||||
if (partySize >= 0 && (this?.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this?.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(scene:BattleScene): [RegExp, string] {
|
||||
return [new RegExp("@ec\{partySize\\}", "gi"), scene.getParty().length.toString()];
|
||||
}
|
||||
}
|
||||
|
||||
export class PersistentModifierRequirement extends EncounterSceneRequirement {
|
||||
requiredItems?: ModifierType[]; // TODO: not implemented
|
||||
constructor(item: ModifierType | ModifierType[]) {
|
||||
super();
|
||||
if (item instanceof Array) {
|
||||
this.requiredItems = item;
|
||||
} else {
|
||||
this.requiredItems = [];
|
||||
this.requiredItems.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const items = scene.modifiers;
|
||||
|
||||
if (!isNullOrUndefined(items) && this?.requiredItems.length > 0 && this.requiredItems.filter((searchingMod) =>
|
||||
items.filter((itemInScene) => itemInScene.type.id === searchingMod.id).length > 0).length === 0) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(scene:BattleScene): [RegExp, string] {
|
||||
const requiredItemsInInventory = this.requiredItems.filter((a) => {
|
||||
scene.modifiers.filter((itemInScene) => itemInScene.type.id === a.id).length > 0;
|
||||
});
|
||||
if (requiredItemsInInventory.length > 0) {
|
||||
return [new RegExp("@ec\{requiredItem\\}", "gi"), requiredItemsInInventory[0].name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class MoneyRequirement extends EncounterSceneRequirement {
|
||||
requiredMoney: number;
|
||||
|
||||
constructor(requiredMoney: number) {
|
||||
super();
|
||||
this.requiredMoney = requiredMoney;
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const money = scene.money;
|
||||
if (!isNullOrUndefined(money) && this?.requiredMoney > 0 && this.requiredMoney > money) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(scene:BattleScene): [RegExp, string] {
|
||||
return [new RegExp("@ec\{money\\}", "gi"), "₽" + scene.money.toString()];
|
||||
}
|
||||
}
|
||||
|
||||
export class SpeciesRequirement extends EncounterPokemonRequirement {
|
||||
requiredSpecies: Species[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(species: Species | Species[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (species instanceof Array) {
|
||||
this.requiredSpecies = species;
|
||||
} else {
|
||||
this.requiredSpecies = [];
|
||||
this.requiredSpecies.push(species);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredSpecies?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredSpecies.filter((species) => pokemon.species.speciesId === species).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed speciess
|
||||
return partyPokemon.filter((pokemon) => this.requiredSpecies.filter((species) => pokemon.species.speciesId === species).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
if (this.requiredSpecies.includes(pokemon.species.speciesId)) {
|
||||
return [new RegExp("@ec\{" + str + "Species\\}", "gi"), Species[pokemon.species.speciesId]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class NatureRequirement extends EncounterPokemonRequirement {
|
||||
requiredNature: Nature[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(nature: Nature | Nature[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (nature instanceof Array) {
|
||||
this.requiredNature = nature;
|
||||
} else {
|
||||
this.requiredNature = [];
|
||||
this.requiredNature.push(nature);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredNature?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredNature.filter((nature) => pokemon.nature === nature).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed natures
|
||||
return partyPokemon.filter((pokemon) => this.requiredNature.filter((nature) => pokemon.nature === nature).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
if (this.requiredNature.includes(pokemon.nature)) {
|
||||
return [new RegExp("@ec\{" + str + "Nature\\}", "gi"), Nature[pokemon.nature]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class TypeRequirement extends EncounterPokemonRequirement {
|
||||
requiredType: Type[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(type: Type | Type[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (type instanceof Array) {
|
||||
this.requiredType = type;
|
||||
} else {
|
||||
this.requiredType = [];
|
||||
this.requiredType.push(type);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredType?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredType.filter((type) => pokemon.getTypes().includes(type)).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed types
|
||||
return partyPokemon.filter((pokemon) => this.requiredType.filter((type) => pokemon.getTypes().includes(type)).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
const includedTypes = this.requiredType.filter((ty) => pokemon.getTypes().includes(ty));
|
||||
if (includedTypes.length > 0) {
|
||||
return [new RegExp("@ec\{" + str + "Type\\}", "gi"), Type[includedTypes[0]]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class MoveRequirement extends EncounterPokemonRequirement {
|
||||
requiredMoves: Moves[] = [];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(moves: Moves | Moves[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (moves instanceof Array) {
|
||||
this.requiredMoves = moves;
|
||||
} else {
|
||||
this.requiredMoves = [];
|
||||
this.requiredMoves.push(moves);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move.moveId === reqMove).length > 0).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed moves
|
||||
return partyPokemon.filter((pokemon) => this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move.moveId === reqMove).length === 0).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
const includedMoves = this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move.moveId === reqMove).length > 0);
|
||||
if (includedMoves.length > 0) {
|
||||
return [new RegExp("@ec\{" + str + "Move\\}", "gi"), Moves[includedMoves[0]].replace("_", " ")];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Find out if Pokemon in the party are able to learn one of many specific moves by TM.
|
||||
* NOTE: Egg moves are not included as learnable.
|
||||
* NOTE: If the Pokemon already knows the move, this requirement will fail, since it's not technically learnable.
|
||||
*/
|
||||
export class CompatibleMoveRequirement extends EncounterPokemonRequirement {
|
||||
requiredMoves: Moves[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(learnableMove: Moves | Moves[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (learnableMove instanceof Array) {
|
||||
this.requiredMoves = learnableMove;
|
||||
} else {
|
||||
this.requiredMoves = [];
|
||||
this.requiredMoves.push(learnableMove);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredMoves?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredMoves.filter((learnableMove) => pokemon.compatibleTms.filter(tm => !pokemon.moveset.find(m => m.moveId === tm)).includes(learnableMove)).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed learnableMoves
|
||||
return partyPokemon.filter((pokemon) => this.requiredMoves.filter((learnableMove) => pokemon.compatibleTms.filter(tm => !pokemon.moveset.find(m => m.moveId === tm)).includes(learnableMove)).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
const includedCompatMoves = this.requiredMoves.filter((reqMove) => pokemon.compatibleTms.filter((tm) => !pokemon.moveset.find(m => m.moveId === tm)).includes(reqMove));
|
||||
if (includedCompatMoves.length > 0) {
|
||||
return [new RegExp("@ec\{" + str + "CompatibleMove\\}", "gi"), Moves[includedCompatMoves[0]]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/*
|
||||
export class EvolutionTargetSpeciesRequirement extends EncounterPokemonRequirement {
|
||||
requiredEvolutionTargetSpecies: Species[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(evolutionTargetSpecies: Species | Species[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (evolutionTargetSpecies instanceof Array) {
|
||||
this.requiredEvolutionTargetSpecies = evolutionTargetSpecies;
|
||||
} else {
|
||||
this.requiredEvolutionTargetSpecies = [];
|
||||
this.requiredEvolutionTargetSpecies.push(evolutionTargetSpecies);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredEvolutionTargetSpecies?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredEvolutionTargetSpecies.filter((evolutionTargetSpecies) => pokemon.getEvolution()?.speciesId === evolutionTargetSpecies).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed evolutionTargetSpeciess
|
||||
return partyPokemon.filter((pokemon) => this.requiredEvolutionTargetSpecies.filter((evolutionTargetSpecies) => pokemon.getEvolution()?.speciesId === evolutionTargetSpecies).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
const evos = this.requiredEvolutionTargetSpecies.filter((evolutionTargetSpecies) => pokemon.getEvolution().speciesId === evolutionTargetSpecies);
|
||||
if (evos.length > 0) {
|
||||
return [new RegExp("@ec\{" + str + "Evolution\\}", "gi"), Species[evos[0]]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}*/
|
||||
|
||||
export class AbilityRequirement extends EncounterPokemonRequirement {
|
||||
requiredAbilities: Abilities[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(abilities: Abilities | Abilities[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (abilities instanceof Array) {
|
||||
this.requiredAbilities = abilities;
|
||||
} else {
|
||||
this.requiredAbilities = [];
|
||||
this.requiredAbilities.push(abilities);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredAbilities?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredAbilities.filter((abilities) => pokemon.hasAbility(abilities)).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed abilitiess
|
||||
return partyPokemon.filter((pokemon) => this.requiredAbilities.filter((abilities) => pokemon.hasAbility(abilities)).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
const reqAbilities = this.requiredAbilities.filter((a) => {
|
||||
pokemon.hasAbility(a);
|
||||
});
|
||||
if (reqAbilities.length > 0) {
|
||||
return [new RegExp("@ec\{" + str + "Ability\\}", "gi"), Abilities[reqAbilities[0]]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class StatusEffectRequirement extends EncounterPokemonRequirement {
|
||||
requiredStatusEffect: StatusEffect[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(StatusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (StatusEffect instanceof Array) {
|
||||
this.requiredStatusEffect = StatusEffect;
|
||||
} else {
|
||||
this.requiredStatusEffect = [];
|
||||
this.requiredStatusEffect.push(StatusEffect);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredStatusEffect?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
const x = this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
console.log(x);
|
||||
return x;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((StatusEffect) => pokemon.status?.effect === StatusEffect).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed StatusEffects
|
||||
return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((StatusEffect) => pokemon.status?.effect === StatusEffect).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
const reqStatus = this.requiredStatusEffect.filter((a) => {
|
||||
pokemon.status?.effect ===(a);
|
||||
});
|
||||
if (reqStatus.length > 0) {
|
||||
return [new RegExp("@ec\{" + str + "Status\\}", "gi"), StatusEffect[reqStatus[0]]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds if there are pokemon that can form change with a given item.
|
||||
* Notice that we mean specific items, like Charizardite, not the Mega Bracelet.
|
||||
* If you want to trigger the event based on the form change enabler, use PersistentModifierRequirement.
|
||||
*/
|
||||
export class CanFormChangeWithItemRequirement extends EncounterPokemonRequirement {
|
||||
requiredFormChangeItem: FormChangeItem[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(formChangeItem: FormChangeItem | FormChangeItem[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (formChangeItem instanceof Array) {
|
||||
this.requiredFormChangeItem = formChangeItem;
|
||||
} else {
|
||||
this.requiredFormChangeItem = [];
|
||||
this.requiredFormChangeItem.push(formChangeItem);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredFormChangeItem?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
filterByForm(pokemon, formChangeItem) {
|
||||
if (pokemonFormChanges.hasOwnProperty(pokemon.species.speciesId)
|
||||
// Get all form changes for this species with an item trigger, including any compound triggers
|
||||
&& pokemonFormChanges[pokemon.species.speciesId].filter(fc => fc.trigger.hasTriggerType(SpeciesFormChangeItemTrigger))
|
||||
// Returns true if any form changes match this item
|
||||
.map(fc => fc.findTrigger(SpeciesFormChangeItemTrigger) as SpeciesFormChangeItemTrigger)
|
||||
.flat().flatMap(fc => fc.item).includes(formChangeItem)) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredFormChangeItem.filter((formChangeItem) => this.filterByForm(pokemon, formChangeItem)).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed formChangeItems
|
||||
return partyPokemon.filter((pokemon) => this.requiredFormChangeItem.filter((formChangeItem) => this.filterByForm(pokemon, formChangeItem)).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
const requiredItems = this.requiredFormChangeItem.filter((formChangeItem) => this.filterByForm(pokemon, formChangeItem));
|
||||
if (requiredItems.length > 0) {
|
||||
return [new RegExp("@ec\{" + str + "FormChangeItem\\}", "gi"), FormChangeItem[requiredItems[0]]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export class CanEvolveWithItemRequirement extends EncounterPokemonRequirement {
|
||||
requiredEvolutionItem: EvolutionItem[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(evolutionItems: EvolutionItem | EvolutionItem[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (evolutionItems instanceof Array) {
|
||||
this.requiredEvolutionItem = evolutionItems;
|
||||
} else {
|
||||
this.requiredEvolutionItem = [];
|
||||
this.requiredEvolutionItem.push(evolutionItems);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredEvolutionItem?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
|
||||
filterByEvo(pokemon, evolutionItem) {
|
||||
if (pokemonEvolutions.hasOwnProperty(pokemon.species.speciesId) && pokemonEvolutions[pokemon.species.speciesId].filter(e => e.item === evolutionItem
|
||||
&& (!e.condition || e.condition.predicate(pokemon))).length && (pokemon.getFormKey() !== SpeciesFormKey.GIGANTAMAX)) {
|
||||
return true;
|
||||
} else if (pokemon.isFusion() && pokemonEvolutions.hasOwnProperty(pokemon.fusionSpecies.speciesId) && pokemonEvolutions[pokemon.fusionSpecies.speciesId].filter(e => e.item === evolutionItem
|
||||
&& (!e.condition || e.condition.predicate(pokemon))).length && (pokemon.getFusionFormKey() !== SpeciesFormKey.GIGANTAMAX)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredEvolutionItem.filter((evolutionItem) => this.filterByEvo(pokemon, evolutionItem)).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed evolutionItemss
|
||||
return partyPokemon.filter((pokemon) => this.requiredEvolutionItem.filter((evolutionItems) => this.filterByEvo(pokemon, evolutionItems)).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
const requiredItems = this.requiredEvolutionItem.filter((evoItem) => this.filterByEvo(pokemon, evoItem));
|
||||
if (requiredItems.length > 0) {
|
||||
return [new RegExp("@ec\{" + str + "EvolutionItem\\}", "gi"), EvolutionItem[requiredItems[0]]];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export class HeldItemRequirement extends EncounterPokemonRequirement {
|
||||
requiredHeldItemModifier: PokemonHeldItemModifierType[];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(heldItem: PokemonHeldItemModifierType | PokemonHeldItemModifierType[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
if (heldItem instanceof Array) {
|
||||
this.requiredHeldItemModifier = heldItem;
|
||||
} else {
|
||||
this.requiredHeldItemModifier = [];
|
||||
this.requiredHeldItemModifier.push(heldItem);
|
||||
}
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
const partyPokemon = scene.getParty();
|
||||
if (isNullOrUndefined(partyPokemon) || this?.requiredHeldItemModifier?.length < 0) {
|
||||
return false;
|
||||
}
|
||||
return this.queryParty(partyPokemon).length >= this.minNumberOfPokemon;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => this.requiredHeldItemModifier.filter((heldItem) => pokemon.getHeldItems().filter((it) => it.type.id === heldItem.id).length > 0).length > 0);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed heldItems
|
||||
return partyPokemon.filter((pokemon) => this.requiredHeldItemModifier.filter((heldItem) => pokemon.getHeldItems().filter((it) => it.type.id === heldItem.id).length === 0).length === 0);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
const requiredItems = this.requiredHeldItemModifier.filter((a) => {
|
||||
pokemon.getHeldItems().filter((it) => it.type.id === a.id ).length > 0;
|
||||
});
|
||||
if (requiredItems.length > 0) {
|
||||
return [new RegExp("@ec\{" + str + "HeldItem\\}", "gi"), requiredItems[0].name];
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
export class LevelRequirement extends EncounterPokemonRequirement {
|
||||
requiredLevelRange?: [number, number];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(requiredLevelRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
this.requiredLevelRange = requiredLevelRange;
|
||||
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
// Party Pokemon inside required level range
|
||||
if (!isNullOrUndefined(this?.requiredLevelRange) && this.requiredLevelRange?.[0] <= this.requiredLevelRange?.[1]) {
|
||||
const partyPokemon = scene.getParty();
|
||||
const pokemonInRange = this.queryParty(partyPokemon);
|
||||
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => pokemon.level >= this.requiredLevelRange[0] && pokemon.level <= this.requiredLevelRange[1]);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredLevelRanges
|
||||
return partyPokemon.filter((pokemon) => pokemon.level < this.requiredLevelRange[0] || pokemon.level > this.requiredLevelRange[1]);
|
||||
}
|
||||
}
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
return [new RegExp("@ec\{" + str + "Level\\}", "gi"), pokemon.level.toString()];
|
||||
}
|
||||
}
|
||||
|
||||
export class FriendshipRequirement extends EncounterPokemonRequirement {
|
||||
requiredFriendshipRange?: [number, number];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(requiredFriendshipRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
this.requiredFriendshipRange = requiredFriendshipRange;
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
// Party Pokemon inside required friendship range
|
||||
if (!isNullOrUndefined(this?.requiredFriendshipRange) && this.requiredFriendshipRange?.[0] <= this.requiredFriendshipRange?.[1]) {
|
||||
const partyPokemon = scene.getParty();
|
||||
const pokemonInRange = this.queryParty(partyPokemon);
|
||||
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => pokemon.friendship >= this.requiredFriendshipRange[0] && pokemon.friendship <= this.requiredFriendshipRange[1]);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredFriendshipRanges
|
||||
return partyPokemon.filter((pokemon) => pokemon.friendship < this.requiredFriendshipRange[0] || pokemon.friendship > this.requiredFriendshipRange[1]);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
return [new RegExp("@ec\{" + str + "Friendship\\}", "gi"), pokemon.friendship.toString()];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* .1 -> 10% hp
|
||||
* .5 -> 50% hp
|
||||
* 1 -> 100% hp
|
||||
*/
|
||||
export class HealthRatioRequirement extends EncounterPokemonRequirement {
|
||||
requiredHealthRange?: [number, number];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(requiredHealthRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
this.requiredHealthRange = requiredHealthRange;
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
// Party Pokemon inside required level range
|
||||
if (!isNullOrUndefined(this?.requiredHealthRange) && this.requiredHealthRange?.[0] <= this.requiredHealthRange?.[1]) {
|
||||
const partyPokemon = scene.getParty();
|
||||
const pokemonInRange = this.queryParty(partyPokemon);
|
||||
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => pokemon.getHpRatio() >= this.requiredHealthRange[0] && pokemon.getHpRatio() <= this.requiredHealthRange[1]);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredHealthRanges
|
||||
return partyPokemon.filter((pokemon) => pokemon.getHpRatio() < this.requiredHealthRange[0] || pokemon.getHpRatio() > this.requiredHealthRange[1]);
|
||||
}
|
||||
}
|
||||
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
return [new RegExp("@ec\{" + str + "HealthRatio\\}", "gi"), Math.floor(pokemon.getHpRatio()*100).toString() + "%"];
|
||||
}
|
||||
}
|
||||
|
||||
export class WeightRequirement extends EncounterPokemonRequirement {
|
||||
requiredWeightRange?: [number, number];
|
||||
minNumberOfPokemon:number;
|
||||
invertQuery:boolean;
|
||||
|
||||
constructor(requiredWeightRange: [number, number], minNumberOfPokemon: number = 1, invertQuery: boolean = false) {
|
||||
super();
|
||||
this.minNumberOfPokemon = minNumberOfPokemon;
|
||||
this.invertQuery = invertQuery;
|
||||
this.requiredWeightRange = requiredWeightRange;
|
||||
}
|
||||
|
||||
meetsRequirement(scene: BattleScene): boolean {
|
||||
// Party Pokemon inside required friendship range
|
||||
if (!isNullOrUndefined(this?.requiredWeightRange) && this.requiredWeightRange?.[0] <= this.requiredWeightRange?.[1]) {
|
||||
const partyPokemon = scene.getParty();
|
||||
const pokemonInRange = this.queryParty(partyPokemon);
|
||||
if (pokemonInRange.length < this.minNumberOfPokemon) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
if (!this.invertQuery) {
|
||||
return partyPokemon.filter((pokemon) => pokemon.getWeight() >= this.requiredWeightRange[0] && pokemon.getWeight() <= this.requiredWeightRange[1]);
|
||||
} else {
|
||||
// for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredWeightRanges
|
||||
return partyPokemon.filter((pokemon) => pokemon.getWeight() < this.requiredWeightRange[0] || pokemon.getWeight() > this.requiredWeightRange[1]);
|
||||
}
|
||||
}
|
||||
getMatchingDialogueToken(str:string, pokemon: PlayerPokemon): [RegExp, string] {
|
||||
return [new RegExp("@ec\{" + str + "Weight\\}", "gi"), pokemon.getWeight().toString()];
|
||||
}
|
||||
}
|
|
@ -0,0 +1,481 @@
|
|||
import BattleScene from "../battle-scene";
|
||||
import MysteryEncounterIntroVisuals, { MysteryEncounterSpriteConfig } from "../field/mystery-encounter-intro";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import MysteryEncounterDialogue, {
|
||||
allMysteryEncounterDialogue
|
||||
} from "./mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
import MysteryEncounterOption from "./mystery-encounter-option";
|
||||
import { EncounterPokemonRequirement, EncounterSceneRequirement } from "./mystery-encounter-requirements";
|
||||
import * as Utils from "../utils";
|
||||
import {EnemyPartyConfig} from "#app/data/mystery-encounters/mystery-encounter-utils";
|
||||
import { PlayerPokemon } from "#app/field/pokemon";
|
||||
import {isNullOrUndefined} from "../utils";
|
||||
|
||||
export enum MysteryEncounterVariant {
|
||||
DEFAULT,
|
||||
TRAINER_BATTLE,
|
||||
WILD_BATTLE,
|
||||
BOSS_BATTLE,
|
||||
NO_BATTLE
|
||||
}
|
||||
|
||||
export enum MysteryEncounterTier {
|
||||
COMMON,
|
||||
UNCOMMON,
|
||||
RARE,
|
||||
SUPER_RARE,
|
||||
ULTRA_RARE // Not currently used
|
||||
}
|
||||
|
||||
export default interface MysteryEncounter {
|
||||
/**
|
||||
* Required params
|
||||
*/
|
||||
encounterType: MysteryEncounterType;
|
||||
options: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]];
|
||||
spriteConfigs: MysteryEncounterSpriteConfig[];
|
||||
/**
|
||||
* Optional params
|
||||
*/
|
||||
encounterTier?: MysteryEncounterTier;
|
||||
hideBattleIntroMessage?: boolean;
|
||||
hideIntroVisuals?: boolean;
|
||||
catchAllowed?: boolean;
|
||||
doEncounterRewards?: (scene: BattleScene) => boolean;
|
||||
onInit?: (scene: BattleScene) => boolean;
|
||||
|
||||
/**
|
||||
* Requirements
|
||||
*/
|
||||
requirements?: EncounterSceneRequirement[];
|
||||
primaryPokemonRequirements?: EncounterPokemonRequirement[];
|
||||
secondaryPokemonRequirements ?: EncounterPokemonRequirement[]; // A list of requirements that must ALL be met by a subset of pokemon to trigger the event
|
||||
excludePrimaryFromSupportRequirements?: boolean;
|
||||
// Primary Pokemon is a single pokemon randomly selected from a set of pokemon that meet ALL primary pokemon requirements
|
||||
primaryPokemon?: PlayerPokemon;
|
||||
// Support Pokemon are pokemon that meet ALL support pokemon requirements.
|
||||
// Note that an individual requirement may require multiple pokemon, but the resulting pokemon after all secondary requirements are met may be lower than expected
|
||||
// If the primary pokemon and supporting pokemon are the same and ExcludePrimaryFromSupportRequirements flag is true, primary pokemon may be promoted from secondary pool
|
||||
secondaryPokemon?: PlayerPokemon[];
|
||||
|
||||
/**
|
||||
* Post-construct / Auto-populated params
|
||||
*/
|
||||
|
||||
/**
|
||||
* Dialogue object containing all the dialogue, messages, tooltips, etc. for an encounter
|
||||
*/
|
||||
dialogue?: MysteryEncounterDialogue;
|
||||
/**
|
||||
* Data used for setting up/initializing enemy party in battles
|
||||
* Can store multiple configs so that one can be chosen based on option selected
|
||||
*/
|
||||
enemyPartyConfigs?: EnemyPartyConfig[];
|
||||
/**
|
||||
* Object instance containing sprite data for an encounter when it is being spawned
|
||||
* Otherwise, will be undefined
|
||||
* You probably shouldn't do anything with this unless you have a very specific need
|
||||
*/
|
||||
introVisuals?: MysteryEncounterIntroVisuals;
|
||||
|
||||
/**
|
||||
* Flags
|
||||
*/
|
||||
|
||||
/**
|
||||
* Can be set for uses programatic dialogue during an encounter (storing the name of one of the party's pokemon, etc.)
|
||||
* Example use: see MYSTERIOUS_CHEST
|
||||
*/
|
||||
dialogueTokens?: Map<RegExp, string>;
|
||||
/**
|
||||
* Should be set depending upon option selected as part of an encounter
|
||||
* For example, if there is no battle as part of the encounter/selected option, should be set to NO_BATTLE
|
||||
* Defaults to DEFAULT
|
||||
*/
|
||||
encounterVariant?: MysteryEncounterVariant;
|
||||
/**
|
||||
* Flag for checking if it's the first time a shop is being shown for an encounter.
|
||||
* Defaults to true so that the first shop does not override the specified rewards.
|
||||
* Will be set to false after a shop is shown (so can't reroll same rarity items for free)
|
||||
*/
|
||||
lockEncounterRewardTiers?: boolean;
|
||||
/**
|
||||
* Will be set by option select handlers automatically, and can be used to refer to which option was chosen by later phases
|
||||
*/
|
||||
selectedOption?: MysteryEncounterOption;
|
||||
/**
|
||||
* Can be set higher or lower based on the type of battle or exp gained for an option/encounter
|
||||
* Defaults to 1
|
||||
*/
|
||||
expMultiplier?: number;
|
||||
|
||||
/**
|
||||
* Generic property to set any custom data required for the encounter
|
||||
*/
|
||||
misc?: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* MysteryEncounter class that defines the logic for a single encounter
|
||||
* These objects will be saved as part of session data any time the player is on a floor with an encounter
|
||||
* Unless you know what you're doing, you should use MysteryEncounterBuilder to create an instance for this class
|
||||
*/
|
||||
export default class MysteryEncounter implements MysteryEncounter {
|
||||
constructor(encounter: MysteryEncounter) {
|
||||
if (!Utils.isNullOrUndefined(encounter)) {
|
||||
Object.assign(this, encounter);
|
||||
}
|
||||
this.encounterTier = this.encounterTier ? this.encounterTier : MysteryEncounterTier.COMMON;
|
||||
this.dialogue = allMysteryEncounterDialogue[this.encounterType];
|
||||
this.encounterVariant = MysteryEncounterVariant.DEFAULT;
|
||||
this.requirements = this.requirements ? this.requirements : [];
|
||||
this.hideBattleIntroMessage = !isNullOrUndefined(this.hideBattleIntroMessage) ? this.hideBattleIntroMessage : false;
|
||||
this.hideIntroVisuals = !isNullOrUndefined(this.hideIntroVisuals) ? this.hideIntroVisuals : true;
|
||||
|
||||
// Populate options with respective dialogue
|
||||
if (this.dialogue) {
|
||||
this.options.forEach((o, i) => o.dialogue = this.dialogue.encounterOptionsDialogue.options[i]);
|
||||
}
|
||||
|
||||
// Reset any dirty flags or encounter data
|
||||
this.lockEncounterRewardTiers = true;
|
||||
this.dialogueTokens = new Map<RegExp, string>;
|
||||
this.enemyPartyConfigs = [];
|
||||
this.introVisuals = null;
|
||||
this.misc = null;
|
||||
this.expMultiplier = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current scene state meets the requirements for the MysteryEncounter to spawn
|
||||
* This is used to filter the pool of encounters down to only the ones with all requirements met
|
||||
* @param scene
|
||||
* @returns
|
||||
*/
|
||||
meetsRequirements?(scene: BattleScene) {
|
||||
const sceneReq = !this.requirements.some(requirement => !requirement.meetsRequirement(scene));
|
||||
const secReqs = this.meetsSecondaryRequirementAndSecondaryPokemonSelected(scene); // secondary is checked first to handle cases of primary overlapping with secondary
|
||||
const priReqs = this.meetsPrimaryRequirementAndPrimaryPokemonSelected(scene);
|
||||
|
||||
console.log("-------" + MysteryEncounterType[this.encounterType] + " Encounter Check -------");
|
||||
console.log(this);
|
||||
console.log( "sceneCheck: " + sceneReq);
|
||||
console.log( "primaryCheck: " + priReqs);
|
||||
console.log( "secondaryCheck: " + secReqs);
|
||||
console.log(MysteryEncounterTier[this.encounterTier]);
|
||||
|
||||
return sceneReq && secReqs && priReqs;
|
||||
}
|
||||
|
||||
private meetsPrimaryRequirementAndPrimaryPokemonSelected?(scene: BattleScene) {
|
||||
if (this.primaryPokemonRequirements.length === 0) {
|
||||
const activeMon = scene.getParty().filter(p => p.isActive(true));
|
||||
if (activeMon.length > 0) {
|
||||
this.primaryPokemon = activeMon[0];
|
||||
} else {
|
||||
this.primaryPokemon = scene.getParty().filter(p => !p.isFainted())[0];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
let qualified:PlayerPokemon[] = scene.getParty();
|
||||
for (const req of this.primaryPokemonRequirements) {
|
||||
console.log(req);
|
||||
if (req.meetsRequirement(scene)) {
|
||||
if (req instanceof EncounterPokemonRequirement) {
|
||||
qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn));
|
||||
}
|
||||
} else {
|
||||
this.primaryPokemon = null;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (qualified.length === 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.excludePrimaryFromSupportRequirements && this.secondaryPokemon) {
|
||||
const truePrimaryPool = [];
|
||||
const overlap = [];
|
||||
for (const qp of qualified) {
|
||||
if (!this.secondaryPokemon.includes(qp)) {
|
||||
truePrimaryPool.push(qp);
|
||||
} else {
|
||||
overlap.push(qp);
|
||||
}
|
||||
|
||||
}
|
||||
if (truePrimaryPool.length > 0) {
|
||||
// always choose from the non-overlapping pokemon first
|
||||
this.primaryPokemon = truePrimaryPool[Utils.randSeedInt(truePrimaryPool.length, 0)];
|
||||
return true;
|
||||
} else {
|
||||
// if there are multiple overlapping pokemon, we're okay - just choose one and take it out of the primary pokemon pool
|
||||
if (overlap.length > 1 || (this.secondaryPokemon.length - overlap.length >= 1)) {
|
||||
// is this working?
|
||||
this.primaryPokemon = overlap[Utils.randSeedInt(overlap.length, 0)];
|
||||
this.secondaryPokemon = this.secondaryPokemon.filter((supp)=> supp !== this.primaryPokemon);
|
||||
return true;
|
||||
}
|
||||
console.log("Mystery Encounter Edge Case: Requirement not met due to primary pokemon overlapping with secondary pokemon. There's no valid primary pokemon left.");
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
// this means we CAN have the same pokemon be a primary and secondary pokemon, so just choose any qualifying one randomly.
|
||||
this.primaryPokemon = qualified[Utils.randSeedInt(qualified.length, 0)];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private meetsSecondaryRequirementAndSecondaryPokemonSelected?(scene: BattleScene) {
|
||||
if (!this.secondaryPokemonRequirements) {
|
||||
this.secondaryPokemon = [];
|
||||
return true;
|
||||
}
|
||||
|
||||
let qualified:PlayerPokemon[] = scene.getParty();
|
||||
for (const req of this.secondaryPokemonRequirements) {
|
||||
if (req.meetsRequirement(scene)) {
|
||||
if (req instanceof EncounterPokemonRequirement) {
|
||||
qualified = qualified.filter(pkmn => req.queryParty(scene.getParty()).includes(pkmn));
|
||||
|
||||
}
|
||||
} else {
|
||||
this.secondaryPokemon = [];
|
||||
return false;
|
||||
}
|
||||
}
|
||||
this.secondaryPokemon = qualified;
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes encounter intro sprites based on the sprite configs defined in spriteConfigs
|
||||
* @param scene
|
||||
*/
|
||||
initIntroVisuals?(scene: BattleScene) {
|
||||
this.introVisuals = new MysteryEncounterIntroVisuals(scene, this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Auto-pushes dialogue tokens from the encounter (and option) requirements.
|
||||
* Will use the first support pokemon in list
|
||||
* For multiple support pokemon in the dialogue token, it will have to be overridden.
|
||||
*/
|
||||
populateDialogueTokensFromRequirements?() {
|
||||
if (this.primaryPokemon?.length > 0) {
|
||||
this.dialogueTokens.set(/@ec\{primaryName\}/gi, this.primaryPokemon.name);
|
||||
for (const req of this.primaryPokemonRequirements) {
|
||||
if (!req.invertQuery) {
|
||||
const entry: [RegExp, string] = req.getMatchingDialogueToken("primary", this.primaryPokemon);
|
||||
this.dialogueTokens.set(entry[0], entry[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (this.secondaryPokemonRequirements?.length > 0 && this.secondaryPokemon?.length > 0) {
|
||||
this.dialogueTokens.set(/@ec\{secondaryName\}/gi, this.secondaryPokemon[0].name);
|
||||
for (const req of this.secondaryPokemonRequirements) {
|
||||
if (!req.invertQuery) {
|
||||
const entry: [RegExp, string] = req.getMatchingDialogueToken("secondary", this.secondaryPokemon[0]);
|
||||
this.dialogueTokens.set(entry[0], entry[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < this.options.length; i++) {
|
||||
const opt = this.options[i];
|
||||
const j = i + 1;
|
||||
if (opt.primaryPokemonRequirements?.length > 0 && opt.primaryPokemon?.length > 0) {
|
||||
this.dialogueTokens.set(new RegExp("@ec\{option" + j + "PrimaryName\\}", "gi"), opt.primaryPokemon.name);
|
||||
for (const req of opt.primaryPokemonRequirements) {
|
||||
if (!req.invertQuery) {
|
||||
const entry: [RegExp, string] = req.getMatchingDialogueToken("option" + j + "Primary", opt.primaryPokemon);
|
||||
this.dialogueTokens.set(entry[0], entry[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (opt.secondaryPokemonRequirements?.length > 0 && opt.secondaryPokemon?.length > 0) {
|
||||
this.dialogueTokens.set(new RegExp("@ec\{option" + j + "SecondaryName\\}", "gi"), opt.secondaryPokemon[0].name);
|
||||
for (const req of opt.secondaryPokemonRequirements) {
|
||||
if (!req.invertQuery) {
|
||||
const entry: [RegExp, string] = req.getMatchingDialogueToken("option" + j + "Secondary", opt.secondaryPokemon[0]);
|
||||
this.dialogueTokens.set(entry[0], entry[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export class MysteryEncounterBuilder implements Partial<MysteryEncounter> {
|
||||
encounterType?: MysteryEncounterType;
|
||||
options?: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]] = [null, null];
|
||||
spriteConfigs?: MysteryEncounterSpriteConfig[];
|
||||
|
||||
dialogue?: MysteryEncounterDialogue;
|
||||
encounterTier?: MysteryEncounterTier;
|
||||
requirements?: EncounterSceneRequirement[] = [];
|
||||
primaryPokemonRequirements?: EncounterPokemonRequirement[] = [];
|
||||
secondaryPokemonRequirements ?: EncounterPokemonRequirement[] = [];
|
||||
excludePrimaryFromSupportRequirements?: boolean;
|
||||
dialogueTokens?: Map<RegExp, string>;
|
||||
doEncounterRewards?: (scene: BattleScene) => boolean;
|
||||
onInit?: (scene: BattleScene) => boolean;
|
||||
hideBattleIntroMessage?: boolean;
|
||||
hideIntroVisuals?: boolean;
|
||||
enemyPartyConfigs?: EnemyPartyConfig[] = [];
|
||||
|
||||
/**
|
||||
* REQUIRED
|
||||
*/
|
||||
|
||||
/**
|
||||
* Defines the type of encounter which is used as an identifier, should be tied to a unique MysteryEncounterType
|
||||
* @param encounterType
|
||||
* @returns this
|
||||
*/
|
||||
withEncounterType(encounterType: MysteryEncounterType): this & Pick<MysteryEncounter, "encounterType"> {
|
||||
return Object.assign(this, { encounterType: encounterType });
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines an option for the encounter
|
||||
* There should be at least 2 options defined and no more than 4
|
||||
* @param option - MysteryEncounterOption to add, can use MysteryEncounterOptionBuilder to create instance
|
||||
* @returns
|
||||
*/
|
||||
withOption(option: MysteryEncounterOption): this & Pick<MysteryEncounter, "options"> {
|
||||
if (this.options[0] === null) {
|
||||
return Object.assign(this, { options: [ option, this.options[0] ] });
|
||||
} else if (this.options[1] === null) {
|
||||
return Object.assign(this, { options: [this.options[0], option ] });
|
||||
} else {
|
||||
this.options.push(option);
|
||||
return Object.assign(this, { options: this.options });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines the sprites that will be shown on the enemy field when the encounter spawns
|
||||
* Can be one or more sprites, recommended not to exceed 4
|
||||
* @param spriteConfigs
|
||||
* @returns
|
||||
*/
|
||||
withIntroSpriteConfigs(spriteConfigs: MysteryEncounterSpriteConfig[]): this & Pick<MysteryEncounter, "spriteConfigs"> {
|
||||
return Object.assign(this, { spriteConfigs: spriteConfigs });
|
||||
}
|
||||
|
||||
/**
|
||||
* OPTIONAL
|
||||
*/
|
||||
|
||||
/**
|
||||
* Sets the rarity tier for an encounter
|
||||
* If not specified, defaults to COMMON
|
||||
* Tiers are:
|
||||
* COMMON 32/64 odds
|
||||
* UNCOMMON 16/64 odds
|
||||
* RARE 10/64 odds
|
||||
* SUPER_RARE 6/64 odds
|
||||
* ULTRA_RARE Not currently used
|
||||
* @param encounterTier
|
||||
* @returns
|
||||
*/
|
||||
withEncounterTier(encounterTier: MysteryEncounterTier): this & Required<Pick<MysteryEncounter, "encounterTier">> {
|
||||
return Object.assign(this, { encounterTier: encounterTier });
|
||||
}
|
||||
|
||||
/**
|
||||
* Specifies a requirement for an encounter
|
||||
* For example, passing requirement as "new WaveCountRequirement([2, 180])" would create a requirement that the encounter can only be spawned between waves 2 and 180
|
||||
* Existing Requirement objects are defined in mystery-encounter-requirements.ts, and more can always be created to meet a requirement need
|
||||
* @param requirement
|
||||
* @returns
|
||||
*/
|
||||
withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<MysteryEncounter, "requirements">> {
|
||||
if (requirement instanceof EncounterPokemonRequirement) {
|
||||
Error("Incorrectly added pokemon requirement as scene requirement.");
|
||||
}
|
||||
this.requirements.push(requirement);
|
||||
return Object.assign(this, { requirements: this.requirements });
|
||||
}
|
||||
|
||||
withPrimaryPokemonRequirement(requirement: EncounterPokemonRequirement): this & Required<Pick<MysteryEncounter, "primaryPokemonRequirements">> {
|
||||
this.primaryPokemonRequirements.push(requirement);
|
||||
return Object.assign(this, { primaryPokemonRequirements: this.primaryPokemonRequirements });
|
||||
}
|
||||
|
||||
// TODO: Maybe add an optional parameter for excluding primary pokemon from the support cast?
|
||||
// ex. if your only grass type pokemon, a snivy, is chosen as primary, if the support pokemon requires a grass type, the event won't trigger because
|
||||
// it's already been
|
||||
withSecondaryPokemonRequirement(requirement: EncounterPokemonRequirement, excludePrimaryFromSecondaryRequirements:boolean = false): this & Required<Pick<MysteryEncounter, "secondaryPokemonRequirements">> {
|
||||
this.secondaryPokemonRequirements.push(requirement);
|
||||
this.excludePrimaryFromSupportRequirements = excludePrimaryFromSecondaryRequirements;
|
||||
return Object.assign(this, { excludePrimaryFromSecondaryRequirements: this.excludePrimaryFromSupportRequirements, secondaryPokemonRequirements: this.secondaryPokemonRequirements });
|
||||
}
|
||||
|
||||
/**
|
||||
* Can set custom encounter rewards via this callback function
|
||||
* If rewards are always deterministic for an encounter, this is a good way to set them
|
||||
*
|
||||
* NOTE: If rewards are dependent on options selected, runtime data, etc.,
|
||||
* It may be better to programmatically set doEncounterRewards elsewhere.
|
||||
* For instance, doEncounterRewards could instead be set inside the onOptionPhase() callback function for a MysteryEncounterOption
|
||||
* Check other existing mystery encounters for examples on how to use this
|
||||
* @param doEncounterRewards - synchronous callback function to perform during rewards phase of the encounter
|
||||
* @returns
|
||||
*/
|
||||
withRewards(doEncounterRewards: (scene: BattleScene) => boolean): this & Required<Pick<MysteryEncounter, "doEncounterRewards">> {
|
||||
return Object.assign(this, { doEncounterRewards: doEncounterRewards });
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to perform init logic before intro visuals are shown and before the MysteryEncounterPhase begins
|
||||
* Useful for performing things like procedural generation of intro sprites, etc.
|
||||
*
|
||||
* @param onInit - synchronous callback function to perform as soon as the encounter is selected for the next phase
|
||||
* @returns
|
||||
*/
|
||||
withOnInit(onInit: (scene: BattleScene) => boolean): this & Required<Pick<MysteryEncounter, "onInit">> {
|
||||
return Object.assign(this, { onInit: onInit });
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines any enemies to use for a battle from the mystery encounter
|
||||
* @param enemyPartyConfig
|
||||
* @returns
|
||||
*/
|
||||
withEnemyPartyConfig(enemyPartyConfig: EnemyPartyConfig): this & Required<Pick<MysteryEncounter, "enemyPartyConfigs">> {
|
||||
this.enemyPartyConfigs.push(enemyPartyConfig);
|
||||
return Object.assign(this, { enemyPartyConfigs: this.enemyPartyConfigs });
|
||||
}
|
||||
|
||||
/**
|
||||
* Can set whether catching is allowed or not on the encounter
|
||||
* This flag can also be programmatically set inside option event functions or elsewhere
|
||||
* @param catchAllowed - if true, allows enemy pokemon to be caught during the encounter
|
||||
* @returns
|
||||
*/
|
||||
withCatchAllowed(catchAllowed: boolean): this & Required<Pick<MysteryEncounter, "catchAllowed">> {
|
||||
return Object.assign(this, { catchAllowed: catchAllowed });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hideBattleIntroMessage - if true, will not show the trainerAppeared/wildAppeared/bossAppeared message for an encounter
|
||||
* @returns
|
||||
*/
|
||||
withHideWildIntroMessage(hideBattleIntroMessage: boolean): this & Required<Pick<MysteryEncounter, "hideBattleIntroMessage">> {
|
||||
return Object.assign(this, { hideBattleIntroMessage: hideBattleIntroMessage });
|
||||
}
|
||||
|
||||
/**
|
||||
* @param hideIntroVisuals - if false, will not hide the intro visuals that are displayed at the beginning of encounter
|
||||
* @returns
|
||||
*/
|
||||
withHideIntroVisuals(hideIntroVisuals: boolean): this & Required<Pick<MysteryEncounter, "hideIntroVisuals">> {
|
||||
return Object.assign(this, { hideIntroVisuals: hideIntroVisuals });
|
||||
}
|
||||
|
||||
build(this: MysteryEncounter) {
|
||||
return new MysteryEncounter(this);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,134 @@
|
|||
import BattleScene from "../../battle-scene";
|
||||
import {AddPokeballModifierType} from "../../modifier/modifier-type";
|
||||
import {
|
||||
EnemyPartyConfig, EnemyPokemonConfig,
|
||||
getRandomPlayerPokemon,
|
||||
getRandomSpeciesByStarterTier,
|
||||
initBattleWithEnemyConfig,
|
||||
leaveEncounterWithoutBattle
|
||||
} from "./mystery-encounter-utils";
|
||||
import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter";
|
||||
import {ModifierRewardPhase} from "#app/phases";
|
||||
import {getPokemonSpecies} from "../pokemon-species";
|
||||
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
|
||||
import {PokeballType} from "../pokeball";
|
||||
import {PartySizeRequirement, WaveCountRequirement} from "../mystery-encounter-requirements";
|
||||
import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option";
|
||||
import {Type} from "#app/data/type";
|
||||
import {Species} from "#enums/species";
|
||||
import {isNullOrUndefined, randSeedInt} from "#app/utils";
|
||||
|
||||
// Exclude Ultra Beasts, Paradox, Necrozma, Eternatus, and egg-locked mythicals
|
||||
const excludedBosses = [
|
||||
Species.NECROZMA,
|
||||
Species.ETERNATUS,
|
||||
Species.NIHILEGO,
|
||||
Species.BUZZWOLE,
|
||||
Species.PHEROMOSA,
|
||||
Species.XURKITREE,
|
||||
Species.CELESTEELA,
|
||||
Species.KARTANA,
|
||||
Species.GUZZLORD,
|
||||
Species.POIPOLE,
|
||||
Species.NAGANADEL,
|
||||
Species.STAKATAKA,
|
||||
Species.BLACEPHALON,
|
||||
Species.GREAT_TUSK,
|
||||
Species.SCREAM_TAIL,
|
||||
Species.BRUTE_BONNET,
|
||||
Species.FLUTTER_MANE,
|
||||
Species.SLITHER_WING,
|
||||
Species.SANDY_SHOCKS,
|
||||
Species.ROARING_MOON,
|
||||
Species.KORAIDON,
|
||||
Species.WALKING_WAKE,
|
||||
Species.GOUGING_FIRE,
|
||||
Species.RAGING_BOLT,
|
||||
Species.IRON_TREADS,
|
||||
Species.IRON_BUNDLE,
|
||||
Species.IRON_HANDS,
|
||||
Species.IRON_JUGULIS,
|
||||
Species.IRON_MOTH,
|
||||
Species.IRON_THORNS,
|
||||
Species.IRON_VALIANT,
|
||||
Species.MIRAIDON,
|
||||
Species.IRON_LEAVES,
|
||||
Species.IRON_BOULDER,
|
||||
Species.IRON_CROWN,
|
||||
Species.MEW,
|
||||
Species.CELEBI,
|
||||
Species.DEOXYS,
|
||||
Species.JIRACHI,
|
||||
Species.PHIONE,
|
||||
Species.MANAPHY,
|
||||
Species.ARCEUS,
|
||||
Species.VICTINI,
|
||||
Species.MELTAN,
|
||||
Species.PECHARUNT
|
||||
];
|
||||
|
||||
export const DarkDealEncounter: MysteryEncounter = new MysteryEncounterBuilder()
|
||||
.withEncounterType(MysteryEncounterType.DARK_DEAL)
|
||||
.withEncounterTier(MysteryEncounterTier.ULTRA_RARE)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "mad_scientist_m",
|
||||
fileRoot: "mystery-encounters",
|
||||
hasShadow: true
|
||||
},
|
||||
{
|
||||
spriteKey: "dark_deal_porygon",
|
||||
fileRoot: "mystery-encounters",
|
||||
hasShadow: true,
|
||||
repeat: true
|
||||
}
|
||||
])
|
||||
.withSceneRequirement(new WaveCountRequirement([30, 180])) // waves 30 to 180
|
||||
.withSceneRequirement(new PartySizeRequirement([2, 6])) // Must have at least 2 pokemon in party
|
||||
.withCatchAllowed(true)
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withPreOptionPhase(async (scene: BattleScene) => {
|
||||
// Removes random pokemon (including fainted) from party and adds name to dialogue data tokens
|
||||
// Will never return last battle able mon and instead pick fainted/unable to battle
|
||||
const removedPokemon = getRandomPlayerPokemon(scene, false, true);
|
||||
scene.removePokemonFromPlayerParty(removedPokemon);
|
||||
scene.currentBattle.mysteryEncounter.dialogueTokens.set( /@ec\{pokeName\}/gi, removedPokemon.name);
|
||||
|
||||
// Store removed pokemon types
|
||||
scene.currentBattle.mysteryEncounter.misc = [removedPokemon.species.type1];
|
||||
if (removedPokemon.species.type2) {
|
||||
scene.currentBattle.mysteryEncounter.misc.push(removedPokemon.species.type2);
|
||||
}
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Give the player 5 Rogue Balls
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, () => new AddPokeballModifierType("rb", PokeballType.ROGUE_BALL, 5)));
|
||||
|
||||
// Start encounter with random legendary (7-10 starter strength) that has level additive
|
||||
const bossTypes = scene.currentBattle.mysteryEncounter.misc as Type[];
|
||||
// Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+
|
||||
const roll = randSeedInt(100);
|
||||
const starterTier: number | [number, number] = roll > 65 ? 6 : roll > 15 ? 7 : roll > 5 ? 8 : [9, 10];
|
||||
const bossSpecies = getPokemonSpecies(getRandomSpeciesByStarterTier(starterTier, excludedBosses, bossTypes));
|
||||
const pokemonConfig: EnemyPokemonConfig = {
|
||||
species: bossSpecies,
|
||||
isBoss: true
|
||||
};
|
||||
if (!isNullOrUndefined(bossSpecies.forms) && bossSpecies.forms.length > 0) {
|
||||
pokemonConfig.formIndex = 0;
|
||||
}
|
||||
const config: EnemyPartyConfig = {
|
||||
levelAdditiveMultiplier: 0.75,
|
||||
pokemonConfigs: [pokemonConfig]
|
||||
};
|
||||
return initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Leave encounter with no rewards or exp
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
return true;
|
||||
})
|
||||
.build())
|
||||
.build();
|
|
@ -0,0 +1,48 @@
|
|||
import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
|
||||
export const DarkDealDialogue: MysteryEncounterDialogue = {
|
||||
intro: [
|
||||
{
|
||||
text: "mysteryEncounter:dark_deal_intro_message"
|
||||
},
|
||||
{
|
||||
speaker: "mysteryEncounter:dark_deal_speaker",
|
||||
text: "mysteryEncounter:dark_deal_intro_dialogue"
|
||||
}
|
||||
],
|
||||
encounterOptionsDialogue: {
|
||||
title: "mysteryEncounter:dark_deal_title",
|
||||
description: "mysteryEncounter:dark_deal_description",
|
||||
query: "mysteryEncounter:dark_deal_query",
|
||||
options: [
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:dark_deal_option_1_label",
|
||||
buttonTooltip: "mysteryEncounter:dark_deal_option_1_tooltip",
|
||||
selected: [
|
||||
{
|
||||
speaker: "mysteryEncounter:dark_deal_speaker",
|
||||
text: "mysteryEncounter:dark_deal_option_1_selected"
|
||||
},
|
||||
{
|
||||
text: "mysteryEncounter:dark_deal_option_1_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:dark_deal_option_2_label",
|
||||
buttonTooltip: "mysteryEncounter:dark_deal_option_2_tooltip",
|
||||
selected: [
|
||||
{
|
||||
speaker: "mysteryEncounter:dark_deal_speaker",
|
||||
text: "mysteryEncounter:dark_deal_option_2_selected"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
outro: [
|
||||
{
|
||||
text: "mysteryEncounter:dark_deal_outro"
|
||||
}
|
||||
]
|
||||
};
|
|
@ -0,0 +1,38 @@
|
|||
import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
|
||||
export const FightOrFlightDialogue: MysteryEncounterDialogue = {
|
||||
intro: [
|
||||
{
|
||||
text: "mysteryEncounter:fight_or_flight_intro_message"
|
||||
}
|
||||
],
|
||||
encounterOptionsDialogue: {
|
||||
title: "mysteryEncounter:fight_or_flight_title",
|
||||
description: "mysteryEncounter:fight_or_flight_description",
|
||||
query: "mysteryEncounter:fight_or_flight_query",
|
||||
options: [
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:fight_or_flight_option_1_label",
|
||||
buttonTooltip: "mysteryEncounter:fight_or_flight_option_1_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:fight_or_flight_option_1_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:fight_or_flight_option_2_label",
|
||||
buttonTooltip: "mysteryEncounter:fight_or_flight_option_2_tooltip"
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:fight_or_flight_option_3_label",
|
||||
buttonTooltip: "mysteryEncounter:fight_or_flight_option_3_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:fight_or_flight_option_3_selected"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,57 @@
|
|||
import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
|
||||
export const MysteriousChallengersDialogue: MysteryEncounterDialogue = {
|
||||
intro: [
|
||||
{
|
||||
text: "mysteryEncounter:mysterious_challengers_intro_message"
|
||||
}
|
||||
],
|
||||
encounterOptionsDialogue: {
|
||||
title: "mysteryEncounter:mysterious_challengers_title",
|
||||
description: "mysteryEncounter:mysterious_challengers_description",
|
||||
query: "mysteryEncounter:mysterious_challengers_query",
|
||||
options: [
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:mysterious_challengers_option_1_label",
|
||||
buttonTooltip: "mysteryEncounter:mysterious_challengers_option_1_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:mysterious_challengers_option_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:mysterious_challengers_option_2_label",
|
||||
buttonTooltip: "mysteryEncounter:mysterious_challengers_option_2_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:mysterious_challengers_option_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:mysterious_challengers_option_3_label",
|
||||
buttonTooltip: "mysteryEncounter:mysterious_challengers_option_3_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:mysterious_challengers_option_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:mysterious_challengers_option_4_label",
|
||||
buttonTooltip: "mysteryEncounter:mysterious_challengers_option_4_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:mysterious_challengers_option_4_selected_message"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
outro: [
|
||||
{
|
||||
text: "mysteryEncounter:mysterious_challengers_outro_win"
|
||||
}
|
||||
]
|
||||
};
|
|
@ -0,0 +1,34 @@
|
|||
import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
|
||||
export const MysteriousChestDialogue: MysteryEncounterDialogue = {
|
||||
intro: [
|
||||
{
|
||||
text: "mysteryEncounter:mysterious_chest_intro_message"
|
||||
}
|
||||
],
|
||||
encounterOptionsDialogue: {
|
||||
title: "mysteryEncounter:mysterious_chest_title",
|
||||
description: "mysteryEncounter:mysterious_chest_description",
|
||||
query: "mysteryEncounter:mysterious_chest_query",
|
||||
options: [
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:mysterious_chest_option_1_label",
|
||||
buttonTooltip: "mysteryEncounter:mysterious_chest_option_1_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:mysterious_chest_option_1_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:mysterious_chest_option_2_label",
|
||||
buttonTooltip: "mysteryEncounter:mysterious_chest_option_2_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:mysterious_chest_option_2_selected_message"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,94 @@
|
|||
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
|
||||
import {MysteriousChallengersDialogue} from "#app/data/mystery-encounters/dialogue/mysterious-challengers-dialogue";
|
||||
import {MysteriousChestDialogue} from "#app/data/mystery-encounters/dialogue/mysterious-chest-dialogue";
|
||||
import {DarkDealDialogue} from "#app/data/mystery-encounters/dialogue/dark-deal-dialogue";
|
||||
import {FightOrFlightDialogue} from "#app/data/mystery-encounters/dialogue/fight-or-flight-dialogue";
|
||||
import {TrainingSessionDialogue} from "#app/data/mystery-encounters/dialogue/training-session-dialogue";
|
||||
import { SleepingSnorlaxDialogue } from "./sleeping-snorlax-dialogue";
|
||||
|
||||
export class TextDisplay {
|
||||
speaker?: TemplateStringsArray | `mysteryEncounter:${string}`;
|
||||
text: TemplateStringsArray | `mysteryEncounter:${string}`;
|
||||
}
|
||||
|
||||
export class OptionTextDisplay {
|
||||
buttonLabel: TemplateStringsArray | `mysteryEncounter:${string}`;
|
||||
buttonTooltip?: TemplateStringsArray | `mysteryEncounter:${string}`;
|
||||
disabledTooltip?: TemplateStringsArray | `mysteryEncounter:${string}`;
|
||||
secondOptionPrompt?: TemplateStringsArray | `mysteryEncounter:${string}`;
|
||||
selected?: TextDisplay[];
|
||||
}
|
||||
|
||||
export class EncounterOptionsDialogue {
|
||||
title: TemplateStringsArray | `mysteryEncounter:${string}`;
|
||||
description: TemplateStringsArray | `mysteryEncounter:${string}`;
|
||||
query?: TemplateStringsArray | `mysteryEncounter:${string}`;
|
||||
options: [OptionTextDisplay, OptionTextDisplay, ...OptionTextDisplay[]]; // Options array with minimum 2 options
|
||||
}
|
||||
|
||||
export default class MysteryEncounterDialogue {
|
||||
intro?: TextDisplay[];
|
||||
encounterOptionsDialogue: EncounterOptionsDialogue;
|
||||
outro?: TextDisplay[];
|
||||
}
|
||||
|
||||
export interface EncounterTypeDialogue {
|
||||
[key: integer]: MysteryEncounterDialogue
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Example MysteryEncounterDialogue object:
|
||||
*
|
||||
{
|
||||
intro: [
|
||||
{
|
||||
text: "this is a rendered as a message window (no title display)"
|
||||
},
|
||||
{
|
||||
speaker: "John"
|
||||
text: "this is a rendered as a dialogue window (title "John" is displayed above text)"
|
||||
}
|
||||
],
|
||||
encounterOptionsDialogue: {
|
||||
title: "This is the title displayed at top of encounter description box",
|
||||
description: "This is the description in the middle of encounter description box",
|
||||
query: "This is an optional question displayed at the bottom of the description box (keep it short)",
|
||||
options: [
|
||||
{
|
||||
buttonLabel: "Option #1 button label (keep these short)",
|
||||
selected: [ // Optional dialogue windows displayed when specific option is selected and before functional logic for the option is executed
|
||||
{
|
||||
text: "You chose option #1 message"
|
||||
},
|
||||
{
|
||||
speaker: "John"
|
||||
text: "So, you've chosen option #1! It's time to d-d-d-duel!"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "Option #2"
|
||||
}
|
||||
],
|
||||
},
|
||||
outro: [
|
||||
{
|
||||
text: "This message will be displayed at the very end of the encounter (i.e. post battle, post reward, etc.)"
|
||||
}
|
||||
],
|
||||
}
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
export const allMysteryEncounterDialogue: EncounterTypeDialogue = {};
|
||||
|
||||
export function initMysteryEncounterDialogue() {
|
||||
allMysteryEncounterDialogue[MysteryEncounterType.MYSTERIOUS_CHALLENGERS] = MysteriousChallengersDialogue;
|
||||
allMysteryEncounterDialogue[MysteryEncounterType.MYSTERIOUS_CHEST] = MysteriousChestDialogue;
|
||||
allMysteryEncounterDialogue[MysteryEncounterType.DARK_DEAL] = DarkDealDialogue;
|
||||
allMysteryEncounterDialogue[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightDialogue;
|
||||
allMysteryEncounterDialogue[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionDialogue;
|
||||
allMysteryEncounterDialogue[MysteryEncounterType.SLEEPING_SNORLAX] = SleepingSnorlaxDialogue;
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
|
||||
export const SleepingSnorlaxDialogue: MysteryEncounterDialogue = {
|
||||
intro: [
|
||||
{
|
||||
text: "mysteryEncounter:sleeping_snorlax_intro_message"
|
||||
}
|
||||
],
|
||||
encounterOptionsDialogue: {
|
||||
title: "mysteryEncounter:sleeping_snorlax_title",
|
||||
description: "mysteryEncounter:sleeping_snorlax_description",
|
||||
query: "mysteryEncounter:sleeping_snorlax_query",
|
||||
options: [
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:sleeping_snorlax_option_1_label",
|
||||
buttonTooltip: "mysteryEncounter:sleeping_snorlax_option_1_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:sleeping_snorlax_option_1_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:sleeping_snorlax_option_2_label",
|
||||
buttonTooltip: "mysteryEncounter:sleeping_snorlax_option_2_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:sleeping_snorlax_option_2_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:sleeping_snorlax_option_3_label",
|
||||
buttonTooltip: "mysteryEncounter:sleeping_snorlax_option_3_tooltip",
|
||||
disabledTooltip: "mysteryEncounter:sleeping_snorlax_option_3_disabled_tooltip"
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
|
@ -0,0 +1,50 @@
|
|||
import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
|
||||
export const TrainingSessionDialogue: MysteryEncounterDialogue = {
|
||||
intro: [
|
||||
{
|
||||
text: "mysteryEncounter:training_session_intro_message"
|
||||
}
|
||||
],
|
||||
encounterOptionsDialogue: {
|
||||
title: "mysteryEncounter:training_session_title",
|
||||
description: "mysteryEncounter:training_session_description",
|
||||
query: "mysteryEncounter:training_session_query",
|
||||
options: [
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:training_session_option_1_label",
|
||||
buttonTooltip: "mysteryEncounter:training_session_option_1_tooltip",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:training_session_option_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:training_session_option_2_label",
|
||||
buttonTooltip: "mysteryEncounter:training_session_option_2_tooltip",
|
||||
secondOptionPrompt: "mysteryEncounter:training_session_option_2_select_prompt",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:training_session_option_selected_message"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
buttonLabel: "mysteryEncounter:training_session_option_3_label",
|
||||
buttonTooltip: "mysteryEncounter:training_session_option_3_tooltip",
|
||||
secondOptionPrompt: "mysteryEncounter:training_session_option_3_select_prompt",
|
||||
selected: [
|
||||
{
|
||||
text: "mysteryEncounter:training_session_option_selected_message"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
outro: [
|
||||
{
|
||||
text: "mysteryEncounter:training_session_outro_win"
|
||||
}
|
||||
]
|
||||
};
|
|
@ -0,0 +1,116 @@
|
|||
import BattleScene from "../../battle-scene";
|
||||
import {ModifierTier} from "#app/modifier/modifier-tier";
|
||||
import {
|
||||
EnemyPartyConfig,
|
||||
initBattleWithEnemyConfig,
|
||||
leaveEncounterWithoutBattle, queueEncounterMessage,
|
||||
setCustomEncounterRewards,
|
||||
showEncounterText
|
||||
} from "#app/data/mystery-encounters/mystery-encounter-utils";
|
||||
import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter";
|
||||
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
|
||||
import {WaveCountRequirement} from "../mystery-encounter-requirements";
|
||||
import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option";
|
||||
import {
|
||||
getPartyLuckValue,
|
||||
getPlayerModifierTypeOptions,
|
||||
ModifierPoolType,
|
||||
ModifierTypeOption,
|
||||
regenerateModifierPoolThresholds
|
||||
} from "#app/modifier/modifier-type";
|
||||
import {BattlerTagType} from "#enums/battler-tag-type";
|
||||
import {StatChangePhase} from "#app/phases";
|
||||
import {BattleStat} from "#app/data/battle-stat";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import {randSeedInt} from "#app/utils";
|
||||
|
||||
export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuilder()
|
||||
.withEncounterType(MysteryEncounterType.FIGHT_OR_FLIGHT)
|
||||
.withEncounterTier(MysteryEncounterTier.COMMON)
|
||||
.withIntroSpriteConfigs([]) // Set in onInit()
|
||||
.withSceneRequirement(new WaveCountRequirement([10, 180])) // waves 10 to 180
|
||||
.withCatchAllowed(true)
|
||||
.withHideWildIntroMessage(true)
|
||||
.withOnInit((scene: BattleScene) => {
|
||||
const instance = scene.currentBattle.mysteryEncounter;
|
||||
|
||||
// Calculate boss mon
|
||||
const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, scene.currentBattle.waveIndex, 0, getPartyLuckValue(scene.getParty()), true);
|
||||
const config: EnemyPartyConfig = {
|
||||
levelAdditiveMultiplier: 1,
|
||||
pokemonConfigs: [{species: bossSpecies, isBoss: true}]
|
||||
};
|
||||
instance.enemyPartyConfigs = [config];
|
||||
|
||||
// Calculate item
|
||||
// 10-60 GREAT, 60-110 ULTRA, 110-160 ROGUE, 160-180 MASTER
|
||||
const tier = scene.currentBattle.waveIndex > 160 ? ModifierTier.MASTER : scene.currentBattle.waveIndex > 110 ? ModifierTier.ROGUE : scene.currentBattle.waveIndex > 60 ? ModifierTier.ULTRA : ModifierTier.GREAT;
|
||||
regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0); // refresh player item pool
|
||||
const item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [tier]})[0];
|
||||
scene.currentBattle.mysteryEncounter.dialogueTokens.set(/@ec\{itemName\}/gi, item.type.name);
|
||||
scene.currentBattle.mysteryEncounter.misc = item;
|
||||
|
||||
instance.spriteConfigs = [
|
||||
{
|
||||
spriteKey: item.type.iconImage,
|
||||
fileRoot: "items",
|
||||
hasShadow: false,
|
||||
x: 35,
|
||||
y: -5,
|
||||
scale: 0.75,
|
||||
isItem: true
|
||||
},
|
||||
{
|
||||
spriteKey: bossSpecies.speciesId.toString(),
|
||||
fileRoot: "pokemon",
|
||||
hasShadow: true,
|
||||
tint: 0.25,
|
||||
x: -5,
|
||||
repeat: true
|
||||
}
|
||||
];
|
||||
|
||||
return true;
|
||||
})
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Pick battle
|
||||
const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption;
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false});
|
||||
await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]);
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Pick steal
|
||||
const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption;
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false});
|
||||
|
||||
const roll = randSeedInt(16);
|
||||
if (roll > 6) {
|
||||
// Noticed and attacked by boss, gets +1 to all stats at start of fight (62.5%)
|
||||
const config = scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0];
|
||||
config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
|
||||
config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => {
|
||||
pokemon.scene.currentBattle.mysteryEncounter.dialogueTokens.set(/@ec\{enemyPokemon\}/gi, pokemon.name);
|
||||
queueEncounterMessage(pokemon.scene, "mysteryEncounter:fight_or_flight_boss_enraged");
|
||||
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1));
|
||||
};
|
||||
await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_bad_result");
|
||||
await initBattleWithEnemyConfig(scene, config);
|
||||
} else {
|
||||
// Steal item (37.5%)
|
||||
// Display result message then proceed to rewards
|
||||
await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_good_result")
|
||||
.then(() => leaveEncounterWithoutBattle(scene));
|
||||
}
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Leave encounter with no rewards or exp
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
return true;
|
||||
})
|
||||
.build())
|
||||
.build();
|
|
@ -0,0 +1,151 @@
|
|||
import BattleScene from "../../battle-scene";
|
||||
import { ModifierTier } from "#app/modifier/modifier-tier";
|
||||
import {modifierTypes} from "#app/modifier/modifier-type";
|
||||
import { EnemyPartyConfig, initBattleWithEnemyConfig, setCustomEncounterRewards } from "#app/data/mystery-encounters/mystery-encounter-utils";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter";
|
||||
import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
|
||||
import { WaveCountRequirement } from "../mystery-encounter-requirements";
|
||||
import {
|
||||
trainerConfigs,
|
||||
TrainerPartyCompoundTemplate,
|
||||
TrainerPartyTemplate,
|
||||
trainerPartyTemplates
|
||||
} from "#app/data/trainer-config";
|
||||
import * as Utils from "../../utils";
|
||||
import {PartyMemberStrength} from "#enums/party-member-strength";
|
||||
|
||||
export const MysteriousChallengersEncounter: MysteryEncounter = new MysteryEncounterBuilder()
|
||||
.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHALLENGERS)
|
||||
.withEncounterTier(MysteryEncounterTier.UNCOMMON)
|
||||
.withIntroSpriteConfigs([]) // These are set in onInit()
|
||||
.withSceneRequirement(new WaveCountRequirement([10, 180])) // waves 10 to 180
|
||||
.withOnInit((scene: BattleScene) => {
|
||||
const instance = scene.currentBattle.mysteryEncounter;
|
||||
// Calculates what trainers are available for battle in the encounter
|
||||
|
||||
// Normal difficulty trainer is randomly pulled from biome
|
||||
const normalTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex);
|
||||
const normalConfig = trainerConfigs[normalTrainerType].copy();
|
||||
let female = false;
|
||||
if (normalConfig.hasGenders) {
|
||||
female = !!(Utils.randSeedInt(2));
|
||||
}
|
||||
const normalSpriteKey = normalConfig.getSpriteKey(female, normalConfig.doubleOnly);
|
||||
instance.enemyPartyConfigs.push({
|
||||
trainerConfig: normalConfig,
|
||||
female: female
|
||||
});
|
||||
|
||||
// Hard difficulty trainer is another random trainer, but with AVERAGE_BALANCED config
|
||||
// Number of mons is based off wave: 1-20 is 2, 20-40 is 3, etc. capping at 6 after wave 100
|
||||
const hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex);
|
||||
const hardTemplate = new TrainerPartyCompoundTemplate(
|
||||
new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true),
|
||||
new TrainerPartyTemplate(Math.min(Math.ceil(scene.currentBattle.waveIndex / 20), 5), PartyMemberStrength.AVERAGE, false, true));
|
||||
const hardConfig = trainerConfigs[hardTrainerType].copy();
|
||||
hardConfig.setPartyTemplates(hardTemplate);
|
||||
female = false;
|
||||
if (hardConfig.hasGenders) {
|
||||
female = !!(Utils.randSeedInt(2));
|
||||
}
|
||||
const hardSpriteKey = hardConfig.getSpriteKey(female, hardConfig.doubleOnly);
|
||||
instance.enemyPartyConfigs.push({
|
||||
trainerConfig: hardConfig,
|
||||
levelAdditiveMultiplier: 0.5,
|
||||
female: female,
|
||||
});
|
||||
|
||||
// Brutal trainer is pulled from pool of boss trainers (gym leaders) for the biome
|
||||
// They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons
|
||||
const brutalTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex, true);
|
||||
const e4Template = trainerPartyTemplates.ELITE_FOUR;
|
||||
const brutalConfig = trainerConfigs[brutalTrainerType].copy();
|
||||
brutalConfig.setPartyTemplates(e4Template);
|
||||
brutalConfig.partyTemplateFunc = null; // Overrides gym leader party template func
|
||||
female = false;
|
||||
if (brutalConfig.hasGenders) {
|
||||
female = !!(Utils.randSeedInt(2));
|
||||
}
|
||||
const brutalSpriteKey = brutalConfig.getSpriteKey(female, brutalConfig.doubleOnly);
|
||||
instance.enemyPartyConfigs.push({
|
||||
trainerConfig: brutalConfig,
|
||||
levelAdditiveMultiplier: 1.1,
|
||||
female: female
|
||||
});
|
||||
|
||||
instance.spriteConfigs = [
|
||||
{
|
||||
spriteKey: normalSpriteKey,
|
||||
fileRoot: "trainer",
|
||||
hasShadow: true,
|
||||
tint: 1
|
||||
},
|
||||
{
|
||||
spriteKey: hardSpriteKey,
|
||||
fileRoot: "trainer",
|
||||
hasShadow: true,
|
||||
tint: 1
|
||||
},
|
||||
{
|
||||
spriteKey: brutalSpriteKey,
|
||||
fileRoot: "trainer",
|
||||
hasShadow: true,
|
||||
tint: 1
|
||||
}
|
||||
];
|
||||
|
||||
return true;
|
||||
})
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
// Spawn standard trainer battle with memory mushroom reward
|
||||
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
|
||||
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true });
|
||||
|
||||
// Seed offsets to remove possibility of different trainers having exact same teams
|
||||
let ret;
|
||||
scene.executeWithSeedOffset(() => {
|
||||
ret = initBattleWithEnemyConfig(scene, config);
|
||||
}, scene.currentBattle.waveIndex * 10);
|
||||
return ret;
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
// Spawn hard fight with ULTRA/GREAT reward (can improve with luck)
|
||||
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1];
|
||||
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], fillRemaining: true });
|
||||
|
||||
// Seed offsets to remove possibility of different trainers having exact same teams
|
||||
let ret;
|
||||
scene.executeWithSeedOffset(() => {
|
||||
ret = initBattleWithEnemyConfig(scene, config);
|
||||
}, scene.currentBattle.waveIndex * 100);
|
||||
return ret;
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
// Spawn brutal fight with ROGUE/ULTRA/GREAT reward (can improve with luck)
|
||||
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[2];
|
||||
|
||||
// To avoid player level snowballing from picking this option
|
||||
encounter.expMultiplier = 0.9;
|
||||
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true });
|
||||
|
||||
// Seed offsets to remove possibility of different trainers having exact same teams
|
||||
let ret;
|
||||
scene.executeWithSeedOffset(() => {
|
||||
ret = initBattleWithEnemyConfig(scene, config);
|
||||
}, scene.currentBattle.waveIndex * 1000);
|
||||
return ret;
|
||||
})
|
||||
.build())
|
||||
.build();
|
|
@ -0,0 +1,95 @@
|
|||
import BattleScene from "../../battle-scene";
|
||||
import { ModifierTier } from "#app/modifier/modifier-tier";
|
||||
import {
|
||||
getHighestLevelPlayerPokemon,
|
||||
koPlayerPokemon,
|
||||
leaveEncounterWithoutBattle,
|
||||
queueEncounterMessage,
|
||||
setCustomEncounterRewards,
|
||||
showEncounterText
|
||||
} from "#app/data/mystery-encounters/mystery-encounter-utils";
|
||||
import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import {WaveCountRequirement} from "../mystery-encounter-requirements";
|
||||
import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
|
||||
import {GameOverPhase} from "#app/phases";
|
||||
import {randSeedInt} from "#app/utils";
|
||||
|
||||
export const MysteriousChestEncounter: MysteryEncounter = new MysteryEncounterBuilder()
|
||||
.withEncounterType(MysteryEncounterType.MYSTERIOUS_CHEST)
|
||||
.withEncounterTier(MysteryEncounterTier.COMMON)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "chest_blue",
|
||||
fileRoot: "mystery-encounters",
|
||||
hasShadow: true,
|
||||
x: 4,
|
||||
y: 8,
|
||||
disableAnimation: true // Re-enabled after option select
|
||||
}
|
||||
])
|
||||
.withHideIntroVisuals(false)
|
||||
.withSceneRequirement(new WaveCountRequirement([10, 180])) // waves 2 to 180
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withPreOptionPhase(async (scene: BattleScene) => {
|
||||
// Play animation
|
||||
const introVisuals = scene.currentBattle.mysteryEncounter.introVisuals;
|
||||
introVisuals.spriteConfigs[0].disableAnimation = false;
|
||||
introVisuals.playAnim();
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Open the chest
|
||||
const roll = randSeedInt(100);
|
||||
if (roll > 60) {
|
||||
// Choose between 2 COMMON / 2 GREAT tier items (40%)
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.GREAT]});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_normal_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else if (roll > 40) {
|
||||
// Choose between 3 ULTRA tier items (20%)
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.ULTRA]});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_good_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else if (roll > 36) {
|
||||
// Choose between 2 ROGUE tier items (4%)
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE]});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_great_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else if (roll > 35) {
|
||||
// Choose 1 MASTER tier item (1%)
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.MASTER]});
|
||||
// Display result message then proceed to rewards
|
||||
queueEncounterMessage(scene, "mysteryEncounter:mysterious_chest_option_1_amazing_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else {
|
||||
// Your highest level unfainted Pok<6F>mon gets OHKO. Progress with no rewards (35%)
|
||||
const highestLevelPokemon = getHighestLevelPlayerPokemon(scene, true);
|
||||
koPlayerPokemon(highestLevelPokemon);
|
||||
|
||||
scene.currentBattle.mysteryEncounter.dialogueTokens.set(/@ec\{pokeName\}/gi, highestLevelPokemon.name);
|
||||
// Show which Pokemon was KOed, then leave encounter with no rewards
|
||||
// Does this synchronously so that game over doesn't happen over result message
|
||||
await showEncounterText(scene, "mysteryEncounter:mysterious_chest_option_1_bad_result")
|
||||
.then(() => {
|
||||
if (scene.getParty().filter(p => p.isAllowedInBattle()).length === 0) {
|
||||
// All pokemon fainted, game over
|
||||
scene.clearPhaseQueue();
|
||||
scene.unshiftPhase(new GameOverPhase(scene));
|
||||
} else {
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
}
|
||||
});
|
||||
}
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Leave encounter with no rewards or exp
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
return true;
|
||||
})
|
||||
.build())
|
||||
.build();
|
|
@ -0,0 +1,701 @@
|
|||
import i18next from "i18next";
|
||||
import {BattleType} from "#app/battle";
|
||||
import BattleScene from "../../battle-scene";
|
||||
import PokemonSpecies, {getPokemonSpecies, speciesStarters} from "../pokemon-species";
|
||||
import {MysteryEncounterVariant} from "../mystery-encounter";
|
||||
import {Status, StatusEffect} from "../status-effect";
|
||||
import {TrainerConfig, trainerConfigs, TrainerSlot} from "../trainer-config";
|
||||
import Pokemon, {FieldPosition, PlayerPokemon} from "#app/field/pokemon";
|
||||
import Trainer, {TrainerVariant} from "../../field/trainer";
|
||||
import {PokemonExpBoosterModifier} from "#app/modifier/modifier";
|
||||
import {
|
||||
CustomModifierSettings,
|
||||
ModifierPoolType,
|
||||
ModifierTypeFunc,
|
||||
PokemonHeldItemModifierType,
|
||||
regenerateModifierPoolThresholds
|
||||
} from "#app/modifier/modifier-type";
|
||||
import {BattleEndPhase, EggLapsePhase, ModifierRewardPhase, TrainerVictoryPhase} from "#app/phases";
|
||||
import {MysteryEncounterBattlePhase, MysteryEncounterRewardsPhase} from "#app/phases/mystery-encounter-phase";
|
||||
import * as Utils from "../../utils";
|
||||
import {isNullOrUndefined} from "#app/utils";
|
||||
import {SelectModifierPhase} from "#app/phases/select-modifier-phase";
|
||||
import {TrainerType} from "#enums/trainer-type";
|
||||
import {Species} from "#enums/species";
|
||||
import {Type} from "#app/data/type";
|
||||
import {BattlerTagType} from "#enums/battler-tag-type";
|
||||
import PokemonData from "#app/system/pokemon-data";
|
||||
import {Biome} from "#enums/biome";
|
||||
import {biomeLinks} from "#app/data/biomes";
|
||||
import {EncounterSceneRequirement} from "#app/data/mystery-encounter-requirements";
|
||||
import {Mode} from "#app/ui/ui";
|
||||
import {PartyOption, PartyUiMode} from "#app/ui/party-ui-handler";
|
||||
import {OptionSelectConfig, OptionSelectItem} from "#app/ui/abstact-option-select-ui-handler";
|
||||
|
||||
/**
|
||||
*
|
||||
* Will never remove the player's last non-fainted Pokemon (if they only have 1)
|
||||
* Otherwise, picks a Pokemon completely at random and removes from the party
|
||||
* @param scene
|
||||
* @param isAllowedInBattle - default false. If true, only picks from unfainted mons. If there is only 1 unfainted mon left and doNotReturnLastAbleMon is also true, will return fainted mon
|
||||
* @param doNotReturnLastAbleMon - If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted)
|
||||
* @returns
|
||||
*/
|
||||
export function getRandomPlayerPokemon(scene: BattleScene, isAllowedInBattle: boolean = false, doNotReturnLastAbleMon: boolean = false): PlayerPokemon {
|
||||
const party = scene.getParty();
|
||||
let chosenIndex: number;
|
||||
let chosenPokemon: PlayerPokemon;
|
||||
const unfaintedMons = party.filter(p => p.isAllowedInBattle());
|
||||
const faintedMons = party.filter(p => !p.isAllowedInBattle());
|
||||
|
||||
if (doNotReturnLastAbleMon && unfaintedMons.length === 1) {
|
||||
chosenIndex = Utils.randSeedInt(faintedMons.length);
|
||||
chosenPokemon = faintedMons.at(chosenIndex);
|
||||
} else if (isAllowedInBattle) {
|
||||
chosenIndex = Utils.randSeedInt(unfaintedMons.length);
|
||||
chosenPokemon = unfaintedMons.at(chosenIndex);
|
||||
} else {
|
||||
chosenIndex = Utils.randSeedInt(party.length);
|
||||
chosenPokemon = party.at(chosenIndex);
|
||||
}
|
||||
|
||||
return chosenPokemon;
|
||||
}
|
||||
|
||||
|
||||
export function getTokensFromScene(scene: BattleScene, reqs: EncounterSceneRequirement[]): Array<[RegExp, String]> {
|
||||
const arr = [];
|
||||
if (scene) {
|
||||
for (const req of reqs) {
|
||||
req.getMatchingDialogueToken(scene);
|
||||
}
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ties are broken by whatever mon is closer to the front of the party
|
||||
* @param scene
|
||||
* @param unfainted - default false. If true, only picks from unfainted mons.
|
||||
* @returns
|
||||
*/
|
||||
export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon {
|
||||
const party = scene.getParty();
|
||||
let pokemon: PlayerPokemon;
|
||||
party.every(p => {
|
||||
if (unfainted && p.isFainted()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
pokemon = pokemon ? pokemon?.level < p?.level ? p : pokemon : p;
|
||||
return true;
|
||||
});
|
||||
|
||||
return pokemon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ties are broken by whatever mon is closer to the front of the party
|
||||
* @param scene
|
||||
* @param unfainted - default false. If true, only picks from unfainted mons.
|
||||
* @returns
|
||||
*/
|
||||
export function getLowestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon {
|
||||
const party = scene.getParty();
|
||||
let pokemon: PlayerPokemon;
|
||||
party.every(p => {
|
||||
if (unfainted && p.isFainted()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
pokemon = pokemon ? pokemon?.level > p?.level ? p : pokemon : p;
|
||||
return true;
|
||||
});
|
||||
|
||||
return pokemon;
|
||||
}
|
||||
|
||||
export function koPlayerPokemon(pokemon: PlayerPokemon) {
|
||||
pokemon.hp = 0;
|
||||
pokemon.trySetStatus(StatusEffect.FAINT);
|
||||
pokemon.updateInfo();
|
||||
}
|
||||
|
||||
export function getTextWithEncounterDialogueTokens(scene: BattleScene, textKey: TemplateStringsArray | `mysteryEncounter:${string}`): string {
|
||||
if (isNullOrUndefined(textKey)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let textString: string = i18next.t(textKey);
|
||||
|
||||
const dialogueTokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens;
|
||||
|
||||
if (dialogueTokens) {
|
||||
dialogueTokens.forEach((value, key) => {
|
||||
textString = textString.replace(key, value);
|
||||
});
|
||||
}
|
||||
|
||||
return textString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Will queue a message in UI with injected encounter data tokens
|
||||
* @param scene
|
||||
* @param contentKey
|
||||
*/
|
||||
export function queueEncounterMessage(scene: BattleScene, contentKey: TemplateStringsArray | `mysteryEncounter:${string}`): void {
|
||||
const text: string = getTextWithEncounterDialogueTokens(scene, contentKey);
|
||||
scene.queueMessage(text, null, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Will display a message in UI with injected encounter data tokens
|
||||
* @param scene
|
||||
* @param contentKey
|
||||
*/
|
||||
export function showEncounterText(scene: BattleScene, contentKey: TemplateStringsArray | `mysteryEncounter:${string}`): Promise<void> {
|
||||
return new Promise<void>(resolve => {
|
||||
const text: string = getTextWithEncounterDialogueTokens(scene, contentKey);
|
||||
scene.ui.showText(text, null, () => resolve(), 0, true);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Will display a dialogue (with speaker title) in UI with injected encounter data tokens
|
||||
* @param scene
|
||||
* @param textContentKey
|
||||
* @param speakerContentKey
|
||||
* @param callback
|
||||
*/
|
||||
export function showEncounterDialogue(scene: BattleScene, textContentKey: TemplateStringsArray | `mysteryEncounter:${string}`, speakerContentKey: TemplateStringsArray | `mysteryEncounter:${string}`, callback?: Function) {
|
||||
const text: string = getTextWithEncounterDialogueTokens(scene, textContentKey);
|
||||
const speaker: string = getTextWithEncounterDialogueTokens(scene, speakerContentKey);
|
||||
scene.ui.showDialogue(text, speaker, null, callback, 0, 0);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* NOTE: This returns ANY random species, including those locked behind eggs, etc.
|
||||
* @param starterTiers
|
||||
* @param excludedSpecies
|
||||
* @param types
|
||||
* @returns
|
||||
*/
|
||||
export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[]): Species {
|
||||
let min = starterTiers instanceof Array ? starterTiers[0] : starterTiers;
|
||||
let max = starterTiers instanceof Array ? starterTiers[1] : starterTiers;
|
||||
|
||||
let filteredSpecies = Object.entries(speciesStarters)
|
||||
.map(s => parseInt(s[0]))
|
||||
.filter(s => getPokemonSpecies(s) && !excludedSpecies.includes(s));
|
||||
|
||||
if (!isNullOrUndefined(types) && types.length > 0) {
|
||||
filteredSpecies = filteredSpecies.filter(s => {
|
||||
const species = getPokemonSpecies(s);
|
||||
return types.includes(species.type1) || types.includes(species.type2);
|
||||
});
|
||||
}
|
||||
|
||||
// If no filtered mons exist at specified starter tiers, will expand starter search range until there are
|
||||
// Starts by decrementing starter tier min until it is 0, then increments tier max up to 10
|
||||
let tryFilterStarterTiers = filteredSpecies.filter(s => s[1] >= min && s[1] <= max);
|
||||
while (tryFilterStarterTiers.length === 0 || !(min === 0 && max === 10)) {
|
||||
if (min > 0) {
|
||||
min--;
|
||||
} else {
|
||||
max++;
|
||||
}
|
||||
|
||||
tryFilterStarterTiers = filteredSpecies.filter(s => s[1] >= min && s[1] <= max);
|
||||
}
|
||||
|
||||
if (tryFilterStarterTiers.length > 0) {
|
||||
const index = Utils.randSeedInt(tryFilterStarterTiers.length);
|
||||
return Phaser.Math.RND.shuffle(tryFilterStarterTiers)[index];
|
||||
}
|
||||
|
||||
return Species.BULBASAUR;
|
||||
}
|
||||
|
||||
export class EnemyPokemonConfig {
|
||||
species: PokemonSpecies;
|
||||
isBoss: boolean = false;
|
||||
bossSegments?: number;
|
||||
bossSegmentModifier?: number; // Additive to the determined segment number
|
||||
formIndex?: number;
|
||||
level?: number;
|
||||
modifierTypes?: PokemonHeldItemModifierType[];
|
||||
dataSource?: PokemonData;
|
||||
tags?: BattlerTagType[];
|
||||
mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
|
||||
status?: StatusEffect;
|
||||
}
|
||||
|
||||
export class EnemyPartyConfig {
|
||||
levelAdditiveMultiplier?: number = 0; // Formula for enemy: level += waveIndex / 10 * levelAdditive
|
||||
doubleBattle?: boolean = false;
|
||||
trainerType?: TrainerType; // Generates trainer battle solely off trainer type
|
||||
trainerConfig?: TrainerConfig; // More customizable option for configuring trainer battle
|
||||
pokemonConfigs?: EnemyPokemonConfig[];
|
||||
female?: boolean; // True for female trainer, false for male
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates an enemy party for a mystery encounter battle
|
||||
* This will override and replace any standard encounter generation logic
|
||||
* Useful for tailoring specific battles to mystery encounters
|
||||
* @param scene - Battle Scene
|
||||
* @param partyConfig - Can pass various customizable attributes for the enemy party, see EnemyPartyConfig
|
||||
*/
|
||||
export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: EnemyPartyConfig): Promise<void> {
|
||||
const loaded = false;
|
||||
const loadEnemyAssets = [];
|
||||
|
||||
const battle = scene.currentBattle;
|
||||
|
||||
let doubleBattle = partyConfig?.doubleBattle;
|
||||
|
||||
// Trainer
|
||||
const trainerType = partyConfig?.trainerType;
|
||||
let trainerConfig = partyConfig?.trainerConfig;
|
||||
if (trainerType || trainerConfig) {
|
||||
scene.currentBattle.mysteryEncounter.encounterVariant = MysteryEncounterVariant.TRAINER_BATTLE;
|
||||
if (scene.currentBattle.trainer) {
|
||||
scene.currentBattle.trainer.setVisible(false);
|
||||
scene.currentBattle.trainer.destroy();
|
||||
}
|
||||
|
||||
trainerConfig = partyConfig?.trainerConfig ? partyConfig?.trainerConfig : trainerConfigs[trainerType];
|
||||
|
||||
const doubleTrainer = trainerConfig.doubleOnly || (trainerConfig.hasDouble && partyConfig.doubleBattle);
|
||||
doubleBattle = doubleTrainer;
|
||||
const trainerFemale = isNullOrUndefined(partyConfig.female) ? !!(Utils.randSeedInt(2)) : partyConfig.female;
|
||||
const newTrainer = new Trainer(scene, trainerConfig.trainerType, doubleTrainer ? TrainerVariant.DOUBLE : trainerFemale ? TrainerVariant.FEMALE : TrainerVariant.DEFAULT, null, null, null, trainerConfig);
|
||||
newTrainer.x += 300;
|
||||
newTrainer.setVisible(false);
|
||||
scene.field.add(newTrainer);
|
||||
scene.currentBattle.trainer = newTrainer;
|
||||
loadEnemyAssets.push(newTrainer.loadAssets());
|
||||
|
||||
battle.enemyLevels = scene.currentBattle.trainer.getPartyLevels(scene.currentBattle.waveIndex);
|
||||
} else {
|
||||
// Wild
|
||||
scene.currentBattle.mysteryEncounter.encounterVariant = MysteryEncounterVariant.WILD_BATTLE;
|
||||
battle.enemyLevels = new Array(partyConfig?.pokemonConfigs?.length > 0 ? partyConfig?.pokemonConfigs?.length : doubleBattle ? 2 : 1).fill(null).map(() => scene.currentBattle.getLevelForWave());
|
||||
}
|
||||
|
||||
scene.getEnemyParty().forEach(enemyPokemon => enemyPokemon.destroy());
|
||||
battle.enemyParty = [];
|
||||
battle.double = doubleBattle;
|
||||
|
||||
// ME levels are modified by an additive value that scales with wave index
|
||||
// Base scaling: Every 10 waves, modifier gets +1 level
|
||||
// This can be amplified or counteracted by setting levelAdditiveMultiplier in config
|
||||
// levelAdditiveMultiplier value of 0.5 will halve the modifier scaling, 2 will double it, etc.
|
||||
// Leaving null/undefined will disable level scaling
|
||||
const mult = !isNullOrUndefined(partyConfig.levelAdditiveMultiplier) ? partyConfig.levelAdditiveMultiplier : 0;
|
||||
const additive = Math.max(Math.round((scene.currentBattle.waveIndex / 10) * mult), 0);
|
||||
battle.enemyLevels = battle.enemyLevels.map(level => level + additive);
|
||||
|
||||
battle.enemyLevels.forEach((level, e) => {
|
||||
let enemySpecies;
|
||||
let dataSource;
|
||||
let isBoss = false;
|
||||
if (!loaded) {
|
||||
if (trainerType || trainerConfig) {
|
||||
battle.enemyParty[e] = battle.trainer.genPartyMember(e);
|
||||
} else {
|
||||
if (e < partyConfig?.pokemonConfigs?.length) {
|
||||
const config = partyConfig?.pokemonConfigs?.[e];
|
||||
level = config.level ? config.level : level;
|
||||
dataSource = config.dataSource;
|
||||
enemySpecies = config.species;
|
||||
isBoss = config.isBoss;
|
||||
if (isBoss) {
|
||||
scene.currentBattle.mysteryEncounter.encounterVariant = MysteryEncounterVariant.BOSS_BATTLE;
|
||||
}
|
||||
} else {
|
||||
enemySpecies = scene.randomSpecies(battle.waveIndex, level, true);
|
||||
}
|
||||
|
||||
battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, isBoss, dataSource);
|
||||
}
|
||||
}
|
||||
|
||||
const enemyPokemon = scene.getEnemyParty()[e];
|
||||
|
||||
if (e < (doubleBattle ? 2 : 1)) {
|
||||
enemyPokemon.setX(-66 + enemyPokemon.getFieldPositionOffset()[0]);
|
||||
enemyPokemon.resetSummonData();
|
||||
}
|
||||
|
||||
if (!loaded) {
|
||||
scene.gameData.setPokemonSeen(enemyPokemon, true, !!(trainerType || trainerConfig));
|
||||
}
|
||||
|
||||
if (e < partyConfig?.pokemonConfigs?.length) {
|
||||
const config = partyConfig?.pokemonConfigs?.[e];
|
||||
|
||||
// Generate new id in case using data source
|
||||
if (config.dataSource) {
|
||||
enemyPokemon.id = Utils.randSeedInt(4294967296);
|
||||
}
|
||||
|
||||
// Set form
|
||||
if (!isNullOrUndefined(config.formIndex)) {
|
||||
enemyPokemon.formIndex = config.formIndex;
|
||||
}
|
||||
|
||||
// Set Boss
|
||||
if (config.isBoss) {
|
||||
let segments = !isNullOrUndefined(config.bossSegments) ? config.bossSegments : scene.getEncounterBossSegments(scene.currentBattle.waveIndex, level, enemySpecies, true);
|
||||
if (!isNullOrUndefined(config.bossSegmentModifier)) {
|
||||
segments += config.bossSegmentModifier;
|
||||
}
|
||||
enemyPokemon.setBoss(true, segments);
|
||||
}
|
||||
|
||||
// Set Status
|
||||
if (partyConfig.pokemonConfigs[e].status) {
|
||||
// Default to cureturn 3 for sleep
|
||||
const cureTurn = partyConfig.pokemonConfigs[e].status === StatusEffect.SLEEP ? 3: null;
|
||||
enemyPokemon.status = new Status(partyConfig.pokemonConfigs[e].status, 0, cureTurn);
|
||||
}
|
||||
|
||||
// Set tags
|
||||
if (config.tags?.length > 0) {
|
||||
const tags = config.tags;
|
||||
tags.forEach(tag => enemyPokemon.addTag(tag));
|
||||
// mysteryEncounterBattleEffects can be used IFF MYSTERY_ENCOUNTER_POST_SUMMON tag is applied
|
||||
enemyPokemon.summonData.mysteryEncounterBattleEffects = config.mysteryEncounterBattleEffects;
|
||||
|
||||
// Requires re-priming summon data so that tags are not cleared on SummonPhase
|
||||
enemyPokemon.primeSummonData(enemyPokemon.summonData);
|
||||
}
|
||||
|
||||
enemyPokemon.initBattleInfo();
|
||||
}
|
||||
|
||||
loadEnemyAssets.push(enemyPokemon.loadAssets());
|
||||
|
||||
console.log(enemyPokemon.name, enemyPokemon.species.speciesId, enemyPokemon.stats);
|
||||
});
|
||||
|
||||
scene.pushPhase(new MysteryEncounterBattlePhase(scene));
|
||||
|
||||
await Promise.all(loadEnemyAssets);
|
||||
battle.enemyParty.forEach((enemyPokemon_2, e_1) => {
|
||||
if (e_1 < (doubleBattle ? 2 : 1)) {
|
||||
enemyPokemon_2.setVisible(false);
|
||||
if (battle.double) {
|
||||
enemyPokemon_2.setFieldPosition(e_1 ? FieldPosition.RIGHT : FieldPosition.LEFT);
|
||||
}
|
||||
// Spawns at current visible field instead of on "next encounter" field (off screen to the left)
|
||||
enemyPokemon_2.x += 300;
|
||||
}
|
||||
});
|
||||
if (!loaded) {
|
||||
regenerateModifierPoolThresholds(scene.getEnemyField(), battle.battleType === BattleType.TRAINER ? ModifierPoolType.TRAINER : ModifierPoolType.WILD);
|
||||
const customModifiers = partyConfig?.pokemonConfigs?.map(config => config?.modifierTypes);
|
||||
scene.generateEnemyModifiers(customModifiers);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will initialize reward phases to follow the mystery encounter
|
||||
* Can have shop displayed or skipped
|
||||
* @param scene - Battle Scene
|
||||
* @param customShopRewards - adds a shop phase with the specified rewards / reward tiers
|
||||
* @param nonShopRewards - will add a non-shop reward phase for each specified item/modifier (can happen in addition to a shop)
|
||||
* @param preRewardsCallback - can execute an arbitrary callback before the new phases if necessary (useful for updating items/party/injecting new phases before MysteryEncounterRewardsPhase)
|
||||
*/
|
||||
export function setCustomEncounterRewards(scene: BattleScene, customShopRewards?: CustomModifierSettings, nonShopRewards?: ModifierTypeFunc[], preRewardsCallback?: Function) {
|
||||
scene.currentBattle.mysteryEncounter.doEncounterRewards = (scene: BattleScene) => {
|
||||
if (preRewardsCallback) {
|
||||
preRewardsCallback();
|
||||
}
|
||||
|
||||
if (customShopRewards) {
|
||||
scene.unshiftPhase(new SelectModifierPhase(scene, 0, null, customShopRewards));
|
||||
} else {
|
||||
scene.tryRemovePhase(p => p instanceof SelectModifierPhase);
|
||||
}
|
||||
|
||||
if (nonShopRewards?.length > 0) {
|
||||
nonShopRewards.forEach((reward) => {
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, reward));
|
||||
});
|
||||
} else {
|
||||
while (!Utils.isNullOrUndefined(scene.findPhase(p => p instanceof ModifierRewardPhase))) {
|
||||
scene.tryRemovePhase(p => p instanceof ModifierRewardPhase);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param scene
|
||||
* @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen
|
||||
* If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object
|
||||
* @param onPokemonNotSelected - Any logic that needs to be performed if no Pokemon is chosen
|
||||
*/
|
||||
export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
// Open party screen to choose pokemon to train
|
||||
scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, option: PartyOption) => {
|
||||
if (slotIndex < scene.getParty().length) {
|
||||
scene.ui.setMode(Mode.MYSTERY_ENCOUNTER).then(() => {
|
||||
const pokemon = scene.getParty()[slotIndex];
|
||||
const secondaryOptions = onPokemonSelected(pokemon);
|
||||
if (!secondaryOptions) {
|
||||
scene.currentBattle.mysteryEncounter.dialogueTokens.set(/@ec\{selectedPokemon\}/gi, pokemon.name);
|
||||
resolve(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// There is a second option to choose after selecting the Pokemon
|
||||
scene.ui.setMode(Mode.MESSAGE).then(() => {
|
||||
const displayOptions = () => {
|
||||
// Always appends a cancel option to bottom of options
|
||||
const fullOptions = secondaryOptions.map(option => {
|
||||
// Update handler to resolve promise
|
||||
const onSelect = option.handler;
|
||||
option.handler = () => {
|
||||
onSelect();
|
||||
scene.currentBattle.mysteryEncounter.dialogueTokens.set(/@ec\{selectedPokemon\}/gi, pokemon.name);
|
||||
resolve(true);
|
||||
return true;
|
||||
};
|
||||
return option;
|
||||
}).concat({
|
||||
label: i18next.t("menu:cancel"),
|
||||
handler: () => {
|
||||
scene.ui.clearText();
|
||||
scene.ui.setMode(Mode.MYSTERY_ENCOUNTER);
|
||||
resolve(false);
|
||||
return true;
|
||||
},
|
||||
onHover: () => {
|
||||
scene.ui.showText("Return to encounter option select.");
|
||||
}
|
||||
});
|
||||
|
||||
const config: OptionSelectConfig = {
|
||||
options: fullOptions,
|
||||
maxOptions: 7,
|
||||
yOffset: 0,
|
||||
supportHover: true
|
||||
};
|
||||
scene.ui.setModeWithoutClear(Mode.OPTION_SELECT, config, null, true);
|
||||
};
|
||||
|
||||
const textPromptKey = scene.currentBattle.mysteryEncounter.selectedOption.dialogue.secondOptionPrompt;
|
||||
if (!textPromptKey) {
|
||||
displayOptions();
|
||||
} else {
|
||||
const secondOptionSelectPrompt = getTextWithEncounterDialogueTokens(scene, textPromptKey);
|
||||
scene.ui.showText(secondOptionSelectPrompt, null, displayOptions, null, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
} else {
|
||||
scene.ui.setMode(Mode.MYSTERY_ENCOUNTER).then(() => {
|
||||
if (onPokemonNotSelected) {
|
||||
onPokemonNotSelected();
|
||||
}
|
||||
resolve(false);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Will initialize exp phases to follow the mystery encounter (in addition to any combat or other exp earned)
|
||||
* Exp earned will be a simple function that linearly scales with wave index, that can be increased or decreased by the expMultiplier
|
||||
* Exp Share will have no effect (so no accounting for what mon is "on the field")
|
||||
* Exp Balance will still function as normal
|
||||
* @param scene - Battle Scene
|
||||
* @param expMultiplier - default is 100, can be increased or decreased as desired
|
||||
*/
|
||||
export function setEncounterExp(scene: BattleScene, expMultiplier: number = 100) {
|
||||
//const expBalanceModifier = scene.findModifier(m => m instanceof ExpBalanceModifier) as ExpBalanceModifier;
|
||||
const expVal = scene.currentBattle.waveIndex * expMultiplier;
|
||||
const pokemonExp = new Utils.NumberHolder(expVal);
|
||||
const partyMemberExp = [];
|
||||
|
||||
const party = scene.getParty();
|
||||
party.forEach(pokemon => {
|
||||
scene.applyModifiers(PokemonExpBoosterModifier, true, pokemon, pokemonExp);
|
||||
partyMemberExp.push(Math.floor(pokemonExp.value));
|
||||
});
|
||||
|
||||
// TODO
|
||||
//if (expBalanceModifier) {
|
||||
// let totalLevel = 0;
|
||||
// let totalExp = 0;
|
||||
// expPartyMembers.forEach((expPartyMember, epm) => {
|
||||
// totalExp += partyMemberExp[epm];
|
||||
// totalLevel += expPartyMember.level;
|
||||
// });
|
||||
|
||||
// const medianLevel = Math.floor(totalLevel / expPartyMembers.length);
|
||||
|
||||
// const recipientExpPartyMemberIndexes = [];
|
||||
// expPartyMembers.forEach((expPartyMember, epm) => {
|
||||
// if (expPartyMember.level <= medianLevel) {
|
||||
// recipientExpPartyMemberIndexes.push(epm);
|
||||
// }
|
||||
// });
|
||||
|
||||
// const splitExp = Math.floor(totalExp / recipientExpPartyMemberIndexes.length);
|
||||
|
||||
// expPartyMembers.forEach((_partyMember, pm) => {
|
||||
// partyMemberExp[pm] = Phaser.Math.Linear(partyMemberExp[pm], recipientExpPartyMemberIndexes.indexOf(pm) > -1 ? splitExp : 0, 0.2 * expBalanceModifier.getStackCount());
|
||||
// });
|
||||
//}
|
||||
}
|
||||
|
||||
/**
|
||||
* Can be used to exit an encounter without any battles or followup
|
||||
* Will skip any shops and rewards, and queue the next encounter phase as normal
|
||||
* @param scene
|
||||
* @param addHealPhase - when true, will add a shop phase to end of encounter with 0 rewards but healing items are available
|
||||
*/
|
||||
export function leaveEncounterWithoutBattle(scene: BattleScene, addHealPhase: boolean = false) {
|
||||
scene.currentBattle.mysteryEncounter.encounterVariant = MysteryEncounterVariant.NO_BATTLE;
|
||||
scene.clearPhaseQueue();
|
||||
scene.clearPhaseQueueSplice();
|
||||
handleMysteryEncounterVictory(scene, addHealPhase);
|
||||
}
|
||||
|
||||
export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase: boolean = false) {
|
||||
if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.NO_BATTLE) {
|
||||
scene.pushPhase(new EggLapsePhase(scene));
|
||||
scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase));
|
||||
} else if (!scene.getEnemyParty().find(p => scene.currentBattle.mysteryEncounter.encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE ? p.isOnField() : !p?.isFainted(true))) {
|
||||
scene.pushPhase(new BattleEndPhase(scene));
|
||||
if (scene.currentBattle.mysteryEncounter.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) {
|
||||
scene.pushPhase(new TrainerVictoryPhase(scene));
|
||||
}
|
||||
if (scene.gameMode.isEndless || !scene.gameMode.isWaveFinal(scene.currentBattle.waveIndex)) {
|
||||
scene.pushPhase(new EggLapsePhase(scene));
|
||||
scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: remove once encounter spawn rate is finalized
|
||||
* Just a helper function to calculate aggregate stats for MEs in a Classic run
|
||||
* @param scene
|
||||
* @param baseSpawnWeight
|
||||
*/
|
||||
export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: number) {
|
||||
const numRuns = 1000;
|
||||
let run = 0;
|
||||
|
||||
const calculateNumEncounters = (): number[] => {
|
||||
let encounterRate = baseSpawnWeight;
|
||||
const numEncounters = [0, 0, 0, 0];
|
||||
let currentBiome = Biome.TOWN;
|
||||
let currentArena = scene.newArena(currentBiome);
|
||||
for (let i = 10; i < 180; i++) {
|
||||
// Boss
|
||||
if (i % 10 === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// New biome
|
||||
if (i % 10 === 1) {
|
||||
if (Array.isArray(biomeLinks[currentBiome])) {
|
||||
let biomes: Biome[];
|
||||
scene.executeWithSeedOffset(() => {
|
||||
biomes = (biomeLinks[currentBiome] as (Biome | [Biome, integer])[])
|
||||
.filter(b => !Array.isArray(b) || !Utils.randSeedInt(b[1]))
|
||||
.map(b => !Array.isArray(b) ? b : b[0]);
|
||||
}, i);
|
||||
currentBiome = biomes[Utils.randSeedInt(biomes.length)];
|
||||
} else if (biomeLinks.hasOwnProperty(currentBiome)) {
|
||||
currentBiome = (biomeLinks[currentBiome] as Biome);
|
||||
} else {
|
||||
if (!(i % 50)) {
|
||||
currentBiome = Biome.END;
|
||||
} else {
|
||||
currentBiome = scene.generateRandomBiome(i);
|
||||
}
|
||||
}
|
||||
|
||||
currentArena = scene.newArena(currentBiome);
|
||||
}
|
||||
|
||||
// Fixed battle
|
||||
if (scene.gameMode.isFixedBattle(i)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Trainer
|
||||
if (scene.gameMode.isWaveTrainer(i, currentArena)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Otherwise, roll encounter
|
||||
|
||||
const roll = Utils.randSeedInt(256);
|
||||
|
||||
// If total number of encounters is lower than expected for the run, slightly favor a new encounter
|
||||
// Do the reverse as well
|
||||
const expectedEncountersByFloor = 8 / (180 - 10) * i;
|
||||
const currentRunDiffFromAvg = expectedEncountersByFloor - numEncounters.reduce((a, b) => a + b);
|
||||
const favoredEncounterRate = encounterRate + currentRunDiffFromAvg * 5;
|
||||
|
||||
if (roll < favoredEncounterRate) {
|
||||
encounterRate = baseSpawnWeight;
|
||||
|
||||
// Calculate encounter rarity
|
||||
// Common / Uncommon / Rare / Super Rare (base is out of 128)
|
||||
const tierWeights = [61, 40, 21, 6];
|
||||
|
||||
// Adjust tier weights by currently encountered events (pity system that lowers odds of multiple common/uncommons)
|
||||
tierWeights[0] = tierWeights[0] - 6 * numEncounters[0];
|
||||
tierWeights[1] = tierWeights[1] - 4 * numEncounters[1];
|
||||
|
||||
const totalWeight = tierWeights.reduce((a, b) => a + b);
|
||||
const tierValue = Utils.randSeedInt(totalWeight);
|
||||
const commonThreshold = totalWeight - tierWeights[0]; // 64 - 32 = 32
|
||||
const uncommonThreshold = totalWeight - tierWeights[0] - tierWeights[1]; // 64 - 32 - 16 = 16
|
||||
const rareThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; // 64 - 32 - 16 - 10 = 6
|
||||
|
||||
tierValue > commonThreshold ? ++numEncounters[0] : tierValue > uncommonThreshold ? ++numEncounters[1] : tierValue > rareThreshold ? ++numEncounters[2] : ++numEncounters[3];
|
||||
} else {
|
||||
encounterRate++;
|
||||
}
|
||||
}
|
||||
|
||||
return numEncounters;
|
||||
};
|
||||
|
||||
const runs = [];
|
||||
while (run < numRuns) {
|
||||
scene.executeWithSeedOffset(() => {
|
||||
const numEncounters = calculateNumEncounters();
|
||||
runs.push(numEncounters);
|
||||
}, 1000 * run);
|
||||
run++;
|
||||
}
|
||||
|
||||
const n = runs.length;
|
||||
const totalEncountersInRun = runs.map(run => run.reduce((a, b) => a + b));
|
||||
const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n;
|
||||
const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n);
|
||||
const commonMean = runs.reduce((a, b) => a + b[0], 0) / n;
|
||||
const uncommonMean = runs.reduce((a, b) => a + b[1], 0) / n;
|
||||
const rareMean = runs.reduce((a, b) => a + b[2], 0) / n;
|
||||
const superRareMean = runs.reduce((a, b) => a + b[3], 0) / n;
|
||||
|
||||
console.log(`Starting weight: ${baseSpawnWeight}\nAverage MEs per run: ${totalMean}\nStandard Deviation: ${totalStd}\nAvg Commons: ${commonMean}\nAvg Uncommons: ${uncommonMean}\nAvg Rares: ${rareMean}\nAvg Super Rares: ${superRareMean}`);
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
import MysteryEncounter from "../mystery-encounter";
|
||||
import {DarkDealEncounter} from "./dark-deal";
|
||||
import {MysteriousChallengersEncounter} from "./mysterious-challengers";
|
||||
import {MysteriousChestEncounter} from "./mysterious-chest";
|
||||
import {FightOrFlightEncounter} from "#app/data/mystery-encounters/fight-or-flight";
|
||||
import {TrainingSessionEncounter} from "#app/data/mystery-encounters/training-session";
|
||||
import { Biome } from "#app/enums/biome";
|
||||
import { SleepingSnorlaxEncounter } from "./sleeping-snorlax";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
|
||||
export const BASE_MYSTYERY_ENCOUNTER_WEIGHT = 3;
|
||||
|
||||
export const allMysteryEncounters : {[encounterType:string]: MysteryEncounter} = {};
|
||||
|
||||
// Add MysteryEncounterType to biomes to enable it exclusively for those biomes
|
||||
// To enable an encounter in all biomes, do not add to this map
|
||||
export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
|
||||
[Biome.TOWN, [
|
||||
]],
|
||||
[Biome.PLAINS,[
|
||||
|
||||
]],
|
||||
[Biome.GRASS, [
|
||||
MysteryEncounterType.SLEEPING_SNORLAX
|
||||
]],
|
||||
[Biome.TALL_GRASS, [
|
||||
|
||||
]],
|
||||
[Biome.METROPOLIS, [
|
||||
|
||||
]],
|
||||
[Biome.FOREST, [
|
||||
MysteryEncounterType.SLEEPING_SNORLAX
|
||||
]],
|
||||
|
||||
[Biome.SEA, [
|
||||
|
||||
]],
|
||||
[Biome.SWAMP, [
|
||||
|
||||
]],
|
||||
[Biome.BEACH, [
|
||||
|
||||
]],
|
||||
[Biome.LAKE, [
|
||||
|
||||
]],
|
||||
[Biome.SEABED, [
|
||||
|
||||
]],
|
||||
[Biome.MOUNTAIN, [
|
||||
MysteryEncounterType.SLEEPING_SNORLAX
|
||||
]],
|
||||
[Biome.BADLANDS, [
|
||||
|
||||
]],
|
||||
[Biome.CAVE, [
|
||||
MysteryEncounterType.SLEEPING_SNORLAX
|
||||
]],
|
||||
[Biome.DESERT, [
|
||||
|
||||
]],
|
||||
[Biome.ICE_CAVE, [
|
||||
|
||||
]],
|
||||
[Biome.MEADOW, [
|
||||
|
||||
]],
|
||||
[Biome.POWER_PLANT, [
|
||||
|
||||
]],
|
||||
[Biome.VOLCANO, [
|
||||
|
||||
]],
|
||||
[Biome.GRAVEYARD, [
|
||||
|
||||
]],
|
||||
[Biome.DOJO, [
|
||||
|
||||
]],
|
||||
[Biome.FACTORY, [
|
||||
|
||||
]],
|
||||
[Biome.RUINS, [
|
||||
|
||||
]],
|
||||
[Biome.WASTELAND, [
|
||||
|
||||
]],
|
||||
[Biome.ABYSS, [
|
||||
|
||||
]],
|
||||
[Biome.SPACE, [
|
||||
|
||||
]],
|
||||
[Biome.CONSTRUCTION_SITE, [
|
||||
|
||||
]],
|
||||
[Biome.JUNGLE, [
|
||||
|
||||
]],
|
||||
[Biome.FAIRY_CAVE, [
|
||||
|
||||
]],
|
||||
[Biome.TEMPLE, [
|
||||
|
||||
]],
|
||||
[Biome.SLUM, [
|
||||
|
||||
]],
|
||||
[Biome.SNOWY_FOREST, [
|
||||
|
||||
]],
|
||||
[Biome.ISLAND, [
|
||||
|
||||
]],
|
||||
[Biome.LABORATORY, [
|
||||
|
||||
]]
|
||||
]);
|
||||
|
||||
// Only add your MysterEncounter here if you want it to be in every biome.
|
||||
// We recommend designing biome-specific encounters for better flavor and variance
|
||||
export function initMysteryEncounters() {
|
||||
allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHALLENGERS] = MysteriousChallengersEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.MYSTERIOUS_CHEST] = MysteriousChestEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.DARK_DEAL] = DarkDealEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionEncounter;
|
||||
allMysteryEncounters[MysteryEncounterType.SLEEPING_SNORLAX] = SleepingSnorlaxEncounter;
|
||||
|
||||
// Append encounters that can occur in any biome to biome map
|
||||
const anyBiomeEncounters: MysteryEncounterType[] = Object.keys(MysteryEncounterType).filter(e => !isNaN(Number(e))).map(k => Number(k) as MysteryEncounterType);
|
||||
mysteryEncountersByBiome.forEach(biomeEncounters => {
|
||||
biomeEncounters.forEach(e => {
|
||||
if (anyBiomeEncounters.includes(e)) {
|
||||
anyBiomeEncounters.splice(anyBiomeEncounters.indexOf(e), 1);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
mysteryEncountersByBiome.forEach(biomeEncounters => biomeEncounters.push(...anyBiomeEncounters));
|
||||
}
|
|
@ -0,0 +1,109 @@
|
|||
import BattleScene from "../../battle-scene";
|
||||
import {
|
||||
EnemyPartyConfig,
|
||||
EnemyPokemonConfig,
|
||||
initBattleWithEnemyConfig,
|
||||
leaveEncounterWithoutBattle, queueEncounterMessage,
|
||||
setCustomEncounterRewards
|
||||
} from "./mystery-encounter-utils";
|
||||
import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter";
|
||||
import * as Utils from "../../utils";
|
||||
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
|
||||
import {MoveRequirement, WaveCountRequirement} from "../mystery-encounter-requirements";
|
||||
import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option";
|
||||
import {
|
||||
ModifierTypeGenerator,
|
||||
ModifierTypeOption,
|
||||
modifierTypes
|
||||
} from "#app/modifier/modifier-type";
|
||||
import { getPokemonSpecies } from "../pokemon-species";
|
||||
import { Species } from "#enums/species";
|
||||
import { Status, StatusEffect } from "../status-effect";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { BerryType } from "#enums/berry-type";
|
||||
|
||||
export const SleepingSnorlaxEncounter: MysteryEncounter = new MysteryEncounterBuilder()
|
||||
.withEncounterType(MysteryEncounterType.SLEEPING_SNORLAX)
|
||||
.withEncounterTier(MysteryEncounterTier.RARE)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: Species.SNORLAX.toString(),
|
||||
fileRoot: "pokemon",
|
||||
hasShadow: true,
|
||||
tint: 0.25,
|
||||
repeat: true
|
||||
}
|
||||
])
|
||||
.withSceneRequirement(new WaveCountRequirement([10, 180])) // waves 10 to 180
|
||||
.withCatchAllowed(true)
|
||||
.withHideWildIntroMessage(true)
|
||||
.withOnInit((scene: BattleScene) => {
|
||||
const instance = scene.currentBattle.mysteryEncounter;
|
||||
console.log(instance);
|
||||
|
||||
// Calculate boss mon
|
||||
const bossSpecies = getPokemonSpecies(Species.SNORLAX);
|
||||
const pokemonConfig: EnemyPokemonConfig = {
|
||||
species: bossSpecies,
|
||||
isBoss: true,
|
||||
status: StatusEffect.SLEEP
|
||||
};
|
||||
const config: EnemyPartyConfig = {
|
||||
levelAdditiveMultiplier: 2,
|
||||
pokemonConfigs: [pokemonConfig]
|
||||
};
|
||||
instance.enemyPartyConfigs = [config];
|
||||
return true;
|
||||
})
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Pick battle
|
||||
// TODO: do we want special rewards for this?
|
||||
// setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: true});
|
||||
await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0]);
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const instance = scene.currentBattle.mysteryEncounter;
|
||||
let roll:integer;
|
||||
scene.executeWithSeedOffset(() => {
|
||||
roll = Utils.randSeedInt(16, 0);
|
||||
}, scene.currentBattle.waveIndex);
|
||||
console.log(roll);
|
||||
if (roll > 4) {
|
||||
// Fall asleep and get a sitrus berry (75%)
|
||||
const p = instance.primaryPokemon;
|
||||
p.status = new Status(StatusEffect.SLEEP, 0, 3);
|
||||
p.updateInfo(true);
|
||||
const sitrus = (modifierTypes.BERRY?.() as ModifierTypeGenerator).generateType(scene.getParty(), [BerryType.SITRUS]);
|
||||
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [new ModifierTypeOption(sitrus, 0)], fillRemaining: false});
|
||||
queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_2_bad_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
} else {
|
||||
// Heal to full (25%)
|
||||
for (const pokemon of scene.getParty()) {
|
||||
pokemon.hp = pokemon.getMaxHp();
|
||||
pokemon.resetStatus();
|
||||
for (const move of pokemon.moveset) {
|
||||
move.ppUsed = 0;
|
||||
}
|
||||
pokemon.updateInfo(true);
|
||||
}
|
||||
|
||||
queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_2_good_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
}
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withPrimaryPokemonRequirement(new MoveRequirement([Moves.PLUCK, Moves.COVET, Moves.KNOCK_OFF, Moves.THIEF, Moves.TRICK, Moves.SWITCHEROO]))
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Leave encounter with no rewards or exp
|
||||
setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.LEFTOVERS], fillRemaining: false});
|
||||
queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_3_good_result");
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
})
|
||||
.build())
|
||||
.build();
|
|
@ -0,0 +1,306 @@
|
|||
import BattleScene from "../../battle-scene";
|
||||
import {
|
||||
EnemyPartyConfig,
|
||||
getTextWithEncounterDialogueTokens,
|
||||
initBattleWithEnemyConfig,
|
||||
selectPokemonForOption,
|
||||
setCustomEncounterRewards
|
||||
} from "#app/data/mystery-encounters/mystery-encounter-utils";
|
||||
import {MysteryEncounterType} from "#enums/mystery-encounter-type";
|
||||
import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter";
|
||||
import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option";
|
||||
import {WaveCountRequirement} from "../mystery-encounter-requirements";
|
||||
import {PlayerPokemon} from "#app/field/pokemon";
|
||||
import PokemonData from "#app/system/pokemon-data";
|
||||
import {randSeedShuffle} from "#app/utils";
|
||||
import {getNatureName, Nature} from "#app/data/nature";
|
||||
import {BattlerTagType} from "#enums/battler-tag-type";
|
||||
import {OptionSelectItem} from "#app/ui/abstact-option-select-ui-handler";
|
||||
import {PokemonHeldItemModifier} from "#app/modifier/modifier";
|
||||
import {PokemonHeldItemModifierType} from "#app/modifier/modifier-type";
|
||||
import {Ability, allAbilities} from "#app/data/ability";
|
||||
import {speciesStarters} from "#app/data/pokemon-species";
|
||||
import {AbilityAttr} from "#app/system/game-data";
|
||||
import {Stat} from "#app/data/pokemon-stat";
|
||||
import {pokemonInfo} from "#app/locales/en/pokemon-info";
|
||||
|
||||
export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBuilder()
|
||||
.withEncounterType(MysteryEncounterType.TRAINING_SESSION)
|
||||
.withEncounterTier(MysteryEncounterTier.RARE)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "training_gear",
|
||||
fileRoot: "mystery-encounters",
|
||||
hasShadow: true,
|
||||
y: 3
|
||||
}
|
||||
])
|
||||
.withSceneRequirement(new WaveCountRequirement([10, 180])) // waves 10 to 180
|
||||
.withHideWildIntroMessage(true)
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
||||
encounter.misc = {
|
||||
playerPokemon: pokemon
|
||||
};
|
||||
};
|
||||
|
||||
return selectPokemonForOption(scene, onPokemonSelected);
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon;
|
||||
|
||||
// Spawn light training session with chosen pokemon
|
||||
// Every 50 waves, add +1 boss segment, capping at 5
|
||||
const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 50), 5);
|
||||
const modifiers = new ModifiersHolder();
|
||||
const config = getEnemyConfig(scene, playerPokemon, segments, modifiers);
|
||||
scene.removePokemonFromPlayerParty(playerPokemon, false);
|
||||
|
||||
const getIvName = (index: number) => {
|
||||
switch (index) {
|
||||
case Stat.HP:
|
||||
return pokemonInfo.Stat["HPshortened"];
|
||||
case Stat.ATK:
|
||||
return pokemonInfo.Stat["ATKshortened"];
|
||||
case Stat.DEF:
|
||||
return pokemonInfo.Stat["DEFshortened"];
|
||||
case Stat.SPATK:
|
||||
return pokemonInfo.Stat["SPATKshortened"];
|
||||
case Stat.SPDEF:
|
||||
return pokemonInfo.Stat["SPDEFshortened"];
|
||||
case Stat.SPD:
|
||||
return pokemonInfo.Stat["SPDshortened"];
|
||||
}
|
||||
};
|
||||
|
||||
const onBeforeRewardsPhase = () => {
|
||||
encounter.dialogueTokens.set(/@ec\{stat1\}/gi, "-");
|
||||
encounter.dialogueTokens.set(/@ec\{stat2\}/gi, "-");
|
||||
// Add the pokemon back to party with IV boost
|
||||
const ivIndexes = [];
|
||||
playerPokemon.ivs.forEach((iv, index) => {
|
||||
if (iv < 31) {
|
||||
ivIndexes.push({iv: iv, index: index});
|
||||
}
|
||||
});
|
||||
|
||||
// Improves 2 random non-maxed IVs
|
||||
// +10 if IV is < 10, +5 if between 10-20, and +3 if > 20
|
||||
// A 0-4 starting IV will cap in 6 encounters (assuming you always rolled that IV)
|
||||
// 5-14 starting IV caps in 5 encounters
|
||||
// 15-19 starting IV caps in 4 encounters
|
||||
// 20-24 starting IV caps in 3 encounters
|
||||
// 25-27 starting IV caps in 2 encounters
|
||||
let improvedCount = 0;
|
||||
while (ivIndexes.length > 0 && improvedCount < 2) {
|
||||
randSeedShuffle(ivIndexes);
|
||||
const ivToChange = ivIndexes.pop();
|
||||
let newVal = ivToChange.iv;
|
||||
if (improvedCount === 0) {
|
||||
encounter.dialogueTokens.set(/@ec\{stat1\}/gi, getIvName(ivToChange.index));
|
||||
} else {
|
||||
encounter.dialogueTokens.set(/@ec\{stat2\}/gi, getIvName(ivToChange.index));
|
||||
}
|
||||
|
||||
// Corrects required encounter breakpoints to be continuous for all IV values
|
||||
if (ivToChange.iv <= 21 && ivToChange.iv - 1 % 5 === 0) {
|
||||
newVal += 1;
|
||||
}
|
||||
|
||||
newVal += ivToChange.iv <= 10 ? 10 : ivToChange.iv <= 20 ? 5 : 3;
|
||||
newVal = Math.min(newVal, 31);
|
||||
playerPokemon.ivs[ivToChange.index] = newVal;
|
||||
improvedCount++;
|
||||
}
|
||||
|
||||
if (improvedCount > 0) {
|
||||
playerPokemon.calculateStats();
|
||||
scene.gameData.updateSpeciesDexIvs(playerPokemon.species.getRootSpeciesId(true), playerPokemon.ivs);
|
||||
scene.gameData.setPokemonCaught(playerPokemon, false);
|
||||
}
|
||||
|
||||
// Add pokemon and mods back
|
||||
scene.getParty().push(playerPokemon);
|
||||
for (const mod of modifiers.value) {
|
||||
scene.addModifier(mod, true, false, false, true);
|
||||
}
|
||||
scene.updateModifiers(true);
|
||||
scene.queueMessage(getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:training_session_battle_finished_1"), null, true);
|
||||
};
|
||||
|
||||
setCustomEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase);
|
||||
|
||||
return initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
|
||||
// Open menu for selecting pokemon and Nature
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
const natures = new Array(25).fill(null).map((val, i) => i as Nature);
|
||||
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
||||
// Return the options for nature selection
|
||||
return natures.map((nature: Nature) => {
|
||||
const option: OptionSelectItem = {
|
||||
label: getNatureName(nature, true, true, true, scene.uiTheme),
|
||||
handler: () => {
|
||||
// Pokemon and second option selected
|
||||
encounter.dialogueTokens.set(/@ec\{nature\}/gi, getNatureName(nature));
|
||||
encounter.misc = {
|
||||
playerPokemon: pokemon,
|
||||
chosenNature: nature
|
||||
};
|
||||
return true;
|
||||
}
|
||||
};
|
||||
return option;
|
||||
});
|
||||
};
|
||||
|
||||
return selectPokemonForOption(scene, onPokemonSelected);
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon;
|
||||
|
||||
// Spawn medium training session with chosen pokemon
|
||||
// Every 40 waves, add +1 boss segment, capping at 6
|
||||
const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 40), 6);
|
||||
const modifiers = new ModifiersHolder();
|
||||
const config = getEnemyConfig(scene, playerPokemon, segments, modifiers);
|
||||
scene.removePokemonFromPlayerParty(playerPokemon, false);
|
||||
|
||||
const onBeforeRewardsPhase = () => {
|
||||
scene.queueMessage(getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:training_session_battle_finished_2"), null, true);
|
||||
// Add the pokemon back to party with Nature change
|
||||
playerPokemon.setNature(encounter.misc.chosenNature);
|
||||
scene.gameData.setPokemonCaught(playerPokemon, false);
|
||||
|
||||
// Add pokemon and mods back
|
||||
scene.getParty().push(playerPokemon);
|
||||
for (const mod of modifiers.value) {
|
||||
scene.addModifier(mod, true, false, false, true);
|
||||
}
|
||||
scene.updateModifiers(true);
|
||||
};
|
||||
|
||||
setCustomEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase);
|
||||
|
||||
return initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.build())
|
||||
.withOption(new MysteryEncounterOptionBuilder()
|
||||
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
|
||||
// Open menu for selecting pokemon and ability to learn
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
||||
// Return the options for ability selection
|
||||
const speciesForm = !!pokemon.getFusionSpeciesForm() ? pokemon.getFusionSpeciesForm() : pokemon.getSpeciesForm();
|
||||
const abilityCount = speciesForm.getAbilityCount();
|
||||
const abilities = new Array(abilityCount).fill(null).map((val, i) => allAbilities[speciesForm.getAbility(i)]);
|
||||
return abilities.map((ability: Ability) => {
|
||||
const option: OptionSelectItem = {
|
||||
label: ability.name,
|
||||
handler: () => {
|
||||
// Pokemon and ability selected
|
||||
encounter.dialogueTokens.set(/@ec\{ability\}/gi, ability.name);
|
||||
encounter.misc = {
|
||||
playerPokemon: pokemon,
|
||||
abilityIndex: ability
|
||||
};
|
||||
return true;
|
||||
},
|
||||
onHover: () => {
|
||||
scene.ui.showText(ability.description);
|
||||
}
|
||||
};
|
||||
return option;
|
||||
});
|
||||
};
|
||||
|
||||
return selectPokemonForOption(scene, onPokemonSelected);
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
const encounter = scene.currentBattle.mysteryEncounter;
|
||||
const playerPokemon: PlayerPokemon = encounter.misc.playerPokemon;
|
||||
|
||||
// Spawn hard training session with chosen pokemon
|
||||
// Every 30 waves, add +1 boss segment, capping at 6
|
||||
// Also starts with +1 to all stats
|
||||
const segments = Math.min(2 + Math.floor(scene.currentBattle.waveIndex / 30), 6);
|
||||
const modifiers = new ModifiersHolder();
|
||||
const config = getEnemyConfig(scene, playerPokemon, segments, modifiers);
|
||||
config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
|
||||
scene.removePokemonFromPlayerParty(playerPokemon, false);
|
||||
|
||||
const onBeforeRewardsPhase = () => {
|
||||
scene.queueMessage(getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:training_session_battle_finished_3"), null, true);
|
||||
// Add the pokemon back to party with ability change
|
||||
const abilityIndex = encounter.misc.abilityIndex;
|
||||
if (!!playerPokemon.getFusionSpeciesForm()) {
|
||||
playerPokemon.fusionAbilityIndex = abilityIndex;
|
||||
if (speciesStarters.hasOwnProperty(playerPokemon.fusionSpecies.speciesId)) {
|
||||
scene.gameData.starterData[playerPokemon.fusionSpecies.speciesId].abilityAttr |= abilityIndex !== 1 || playerPokemon.fusionSpecies.ability2
|
||||
? Math.pow(2, playerPokemon.fusionAbilityIndex)
|
||||
: AbilityAttr.ABILITY_HIDDEN;
|
||||
}
|
||||
} else {
|
||||
playerPokemon.abilityIndex = abilityIndex;
|
||||
if (speciesStarters.hasOwnProperty(playerPokemon.species.speciesId)) {
|
||||
scene.gameData.starterData[playerPokemon.species.speciesId].abilityAttr |= abilityIndex !== 1 || playerPokemon.species.ability2
|
||||
? Math.pow(2, playerPokemon.abilityIndex)
|
||||
: AbilityAttr.ABILITY_HIDDEN;
|
||||
}
|
||||
}
|
||||
|
||||
playerPokemon.getAbility();
|
||||
playerPokemon.calculateStats();
|
||||
scene.gameData.setPokemonCaught(playerPokemon, false);
|
||||
|
||||
// Add pokemon and mods back
|
||||
scene.getParty().push(playerPokemon);
|
||||
for (const mod of modifiers.value) {
|
||||
scene.addModifier(mod, true, false, false, true);
|
||||
}
|
||||
scene.updateModifiers(true);
|
||||
};
|
||||
|
||||
setCustomEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase);
|
||||
|
||||
return initBattleWithEnemyConfig(scene, config);
|
||||
})
|
||||
.build())
|
||||
.build();
|
||||
|
||||
function getEnemyConfig(scene: BattleScene, playerPokemon: PlayerPokemon, segments: number, modifiers: ModifiersHolder): EnemyPartyConfig {
|
||||
playerPokemon.resetSummonData();
|
||||
|
||||
// Passes modifiers by reference
|
||||
modifiers.value = scene.findModifiers(m => m instanceof PokemonHeldItemModifier
|
||||
&& (m as PokemonHeldItemModifier).pokemonId === playerPokemon.id) as PokemonHeldItemModifier[];
|
||||
const modifierTypes = modifiers.value.map(mod => mod.type) as PokemonHeldItemModifierType[];
|
||||
|
||||
const data = new PokemonData(playerPokemon);
|
||||
return {
|
||||
pokemonConfigs: [
|
||||
{
|
||||
species: playerPokemon.species,
|
||||
isBoss: true,
|
||||
bossSegments: segments,
|
||||
formIndex: playerPokemon.formIndex,
|
||||
level: playerPokemon.level,
|
||||
dataSource: data,
|
||||
modifierTypes: modifierTypes
|
||||
}
|
||||
]
|
||||
};
|
||||
}
|
||||
|
||||
class ModifiersHolder {
|
||||
public value: PokemonHeldItemModifier[] = [];
|
||||
|
||||
constructor() {}
|
||||
}
|
|
@ -810,6 +810,63 @@ export class TrainerConfig {
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
copy(): TrainerConfig {
|
||||
let copy = new TrainerConfig(this.trainerType);
|
||||
copy = this.trainerTypeDouble ? copy.setDoubleTrainerType(this.trainerTypeDouble) : copy;
|
||||
copy = this.name ? copy.setName(this.name) : copy;
|
||||
copy = this.hasGenders ? copy.setHasGenders(this.nameFemale, this.femaleEncounterBgm) : copy;
|
||||
copy = this.hasDouble ? copy.setHasDouble(this.nameDouble, this.doubleEncounterBgm) : copy;
|
||||
copy = this.title ? copy.setTitle(this.title) : copy;
|
||||
copy = this.titleDouble ? copy.setDoubleTitle(this.titleDouble) : copy;
|
||||
copy = this.hasCharSprite ? copy.setHasCharSprite() : copy;
|
||||
copy = this.doubleOnly ? copy.setDoubleOnly() : copy;
|
||||
copy = this.moneyMultiplier ? copy.setMoneyMultiplier(this.moneyMultiplier) : copy;
|
||||
copy = this.isBoss ? copy.setBoss() : copy;
|
||||
copy = this.hasStaticParty ? copy.setStaticParty() : copy;
|
||||
copy = this.useSameSeedForAllMembers ? copy.setUseSameSeedForAllMembers() : copy;
|
||||
copy = this.battleBgm ? copy.setBattleBgm(this.battleBgm) : copy;
|
||||
copy = this.encounterBgm ? copy.setEncounterBgm(this.encounterBgm) : copy;
|
||||
copy = this.victoryBgm ? copy.setVictoryBgm(this.victoryBgm) : copy;
|
||||
copy = this.genModifiersFunc ? copy.setGenModifiersFunc(this.genModifiersFunc) : copy;
|
||||
|
||||
if (this.modifierRewardFuncs) {
|
||||
// Clones array instead of passing ref
|
||||
copy.modifierRewardFuncs = this.modifierRewardFuncs.slice(0);
|
||||
}
|
||||
|
||||
if (this.partyTemplates) {
|
||||
copy.partyTemplates = this.partyTemplates.slice(0);
|
||||
}
|
||||
|
||||
copy = this.partyTemplateFunc ? copy.setPartyTemplateFunc(this.partyTemplateFunc) : copy;
|
||||
|
||||
if (this.partyMemberFuncs) {
|
||||
Object.keys(this.partyMemberFuncs).forEach((index) => {
|
||||
copy = copy.setPartyMemberFunc(parseInt(index, 10), this.partyMemberFuncs[index]);
|
||||
});
|
||||
}
|
||||
|
||||
copy = this.speciesPools ? copy.setSpeciesPools(this.speciesPools) : copy;
|
||||
copy = this.speciesFilter ? copy.setSpeciesFilter(this.speciesFilter) : copy;
|
||||
if (this.specialtyTypes) {
|
||||
copy.specialtyTypes = this.specialtyTypes.slice(0);
|
||||
}
|
||||
|
||||
copy.encounterMessages = this.encounterMessages?.slice(0);
|
||||
copy.victoryMessages = this.victoryMessages?.slice(0);
|
||||
copy.defeatMessages = this.defeatMessages?.slice(0);
|
||||
|
||||
copy.femaleEncounterMessages = this.femaleEncounterMessages?.slice(0);
|
||||
copy.femaleVictoryMessages = this.femaleVictoryMessages?.slice(0);
|
||||
copy.femaleDefeatMessages = this.femaleDefeatMessages?.slice(0);
|
||||
|
||||
copy.doubleEncounterMessages = this.doubleEncounterMessages?.slice(0);
|
||||
copy.doubleVictoryMessages = this.doubleVictoryMessages?.slice(0);
|
||||
copy.doubleDefeatMessages = this.doubleDefeatMessages?.slice(0);
|
||||
|
||||
return copy;
|
||||
}
|
||||
}
|
||||
|
||||
let t = 0;
|
||||
|
|
|
@ -59,5 +59,6 @@ export enum BattlerTagType {
|
|||
MINIMIZED = "MINIMIZED",
|
||||
DESTINY_BOND = "DESTINY_BOND",
|
||||
CENTER_OF_ATTENTION = "CENTER_OF_ATTENTION",
|
||||
ICE_FACE = "ICE_FACE"
|
||||
ICE_FACE = "ICE_FACE",
|
||||
MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON" // Provides effects on post-summon for MEs
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
export enum MysteryEncounterType {
|
||||
MYSTERIOUS_CHALLENGERS,
|
||||
MYSTERIOUS_CHEST,
|
||||
DARK_DEAL,
|
||||
FIGHT_OR_FLIGHT,
|
||||
SLEEPING_SNORLAX,
|
||||
TRAINING_SESSION
|
||||
}
|
|
@ -73,21 +73,21 @@ export class Arena {
|
|||
}
|
||||
}
|
||||
|
||||
randomSpecies(waveIndex: integer, level: integer, attempt?: integer, luckValue?: integer): PokemonSpecies {
|
||||
randomSpecies(waveIndex: integer, level: integer, attempt?: integer, luckValue?: integer, isBoss?: boolean): PokemonSpecies {
|
||||
const overrideSpecies = this.scene.gameMode.getOverrideSpecies(waveIndex);
|
||||
if (overrideSpecies) {
|
||||
return overrideSpecies;
|
||||
}
|
||||
const isBoss = !!this.scene.getEncounterBossSegments(waveIndex, level) && !!this.pokemonPool[BiomePoolTier.BOSS].length
|
||||
const isBossSpecies = !!this.scene.getEncounterBossSegments(waveIndex, level) && !!this.pokemonPool[BiomePoolTier.BOSS].length
|
||||
&& (this.biomeType !== Biome.END || this.scene.gameMode.isClassic || this.scene.gameMode.isWaveFinal(waveIndex));
|
||||
const randVal = isBoss ? 64 : 512;
|
||||
const randVal = isBossSpecies ? 64 : 512;
|
||||
// luck influences encounter rarity
|
||||
let luckModifier = 0;
|
||||
if (typeof luckValue !== "undefined") {
|
||||
luckModifier = luckValue * (isBoss ? 0.5 : 2);
|
||||
luckModifier = luckValue * (isBossSpecies ? 0.5 : 2);
|
||||
}
|
||||
const tierValue = Utils.randSeedInt(randVal - luckModifier);
|
||||
let tier = !isBoss
|
||||
let tier = !isBossSpecies
|
||||
? tierValue >= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE
|
||||
: tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE;
|
||||
console.log(BiomePoolTier[tier]);
|
||||
|
@ -154,12 +154,12 @@ export class Arena {
|
|||
return ret;
|
||||
}
|
||||
|
||||
randomTrainerType(waveIndex: integer): TrainerType {
|
||||
const isBoss = !!this.trainerPool[BiomePoolTier.BOSS].length
|
||||
&& this.scene.gameMode.isTrainerBoss(waveIndex, this.biomeType, this.scene.offsetGym);
|
||||
randomTrainerType(waveIndex: integer, isBoss: boolean = false): TrainerType {
|
||||
const isTrainerBoss = !!this.trainerPool[BiomePoolTier.BOSS].length
|
||||
&& (this.scene.gameMode.isTrainerBoss(waveIndex, this.biomeType, this.scene.offsetGym) || isBoss);
|
||||
console.log(isBoss, this.trainerPool);
|
||||
const tierValue = Utils.randSeedInt(!isBoss ? 512 : 64);
|
||||
let tier = !isBoss
|
||||
const tierValue = Utils.randSeedInt(!isTrainerBoss ? 512 : 64);
|
||||
let tier = !isTrainerBoss
|
||||
? tierValue >= 156 ? BiomePoolTier.COMMON : tierValue >= 32 ? BiomePoolTier.UNCOMMON : tierValue >= 6 ? BiomePoolTier.RARE : tierValue >= 1 ? BiomePoolTier.SUPER_RARE : BiomePoolTier.ULTRA_RARE
|
||||
: tierValue >= 20 ? BiomePoolTier.BOSS : tierValue >= 6 ? BiomePoolTier.BOSS_RARE : tierValue >= 1 ? BiomePoolTier.BOSS_SUPER_RARE : BiomePoolTier.BOSS_ULTRA_RARE;
|
||||
console.log(BiomePoolTier[tier]);
|
||||
|
|
|
@ -0,0 +1,311 @@
|
|||
import { GameObjects } from "phaser";
|
||||
import BattleScene from "../battle-scene";
|
||||
import MysteryEncounter from "../data/mystery-encounter";
|
||||
|
||||
export class MysteryEncounterSpriteConfig {
|
||||
spriteKey: string; // e.g. "ace_trainer_f"
|
||||
fileRoot: string; // "trainer" for trainer sprites, "pokemon" for pokemon, etc. Refer to /public/images directory for the folder name
|
||||
hasShadow?: boolean = false; // Spawns shadow underneath sprite
|
||||
disableAnimation?: boolean = false; // Animates frames or not
|
||||
repeat?: boolean = false; // Cycles animation
|
||||
tint?: number;
|
||||
x?: number; // X offset
|
||||
y?: number; // Y offset
|
||||
scale?: number;
|
||||
isItem?: boolean; // For item sprites, set to true
|
||||
}
|
||||
|
||||
/**
|
||||
* When a mystery encounter spawns, there are visuals (mainly sprites) tied to the field for the new encounter to inform the player of the type of encounter
|
||||
* These slide in with the field as part of standard field change cycle, and will typically be hidden after the player has selected an option for the encounter
|
||||
* Note: intro visuals are not "Trainers" or any other specific game object, though they may contain trainer sprites
|
||||
*/
|
||||
export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Container {
|
||||
public encounter: MysteryEncounter;
|
||||
public spriteConfigs: MysteryEncounterSpriteConfig[];
|
||||
|
||||
constructor(scene: BattleScene, encounter: MysteryEncounter) {
|
||||
super(scene, -72, 76);
|
||||
this.encounter = encounter;
|
||||
// Shallow copy configs to allow visual config updates at runtime without dirtying master copy of Encounter
|
||||
this.spriteConfigs = encounter.spriteConfigs.map(config => {
|
||||
return {
|
||||
...config
|
||||
};
|
||||
});
|
||||
if (!this.spriteConfigs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const getSprite = (spriteKey: string, hasShadow?: boolean) => {
|
||||
const ret = this.scene.addFieldSprite(0, 0, spriteKey);
|
||||
ret.setOrigin(0.5, 1);
|
||||
ret.setPipeline(this.scene.spritePipeline, {tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow});
|
||||
return ret;
|
||||
};
|
||||
|
||||
const getItemSprite = (spriteKey: string) => {
|
||||
const icon = this.scene.add.sprite(-19, 2, "items", spriteKey);
|
||||
icon.setOrigin(0.5, 1);
|
||||
return icon;
|
||||
};
|
||||
|
||||
// Depending on number of sprites added, should space them to be on the circular field sprite
|
||||
const minX = -40;
|
||||
const maxX = 40;
|
||||
const origin = 4;
|
||||
let n = 0;
|
||||
// Sprites with custom X or Y defined will not count for normal spacing requirements
|
||||
const spacingValue = Math.round((maxX - minX) / Math.max(this.spriteConfigs.filter(s => !s.x && !s.y).length, 1));
|
||||
|
||||
this.spriteConfigs?.forEach((config) => {
|
||||
let sprite: GameObjects.Sprite;
|
||||
let tintSprite: GameObjects.Sprite;
|
||||
if (!config.isItem) {
|
||||
sprite = getSprite(config.spriteKey, config.hasShadow);
|
||||
tintSprite = getSprite(config.spriteKey);
|
||||
} else {
|
||||
sprite = getItemSprite(config.spriteKey);
|
||||
tintSprite = getItemSprite(config.spriteKey);
|
||||
}
|
||||
|
||||
tintSprite.setVisible(false);
|
||||
|
||||
if (config.scale) {
|
||||
sprite.setScale(config.scale);
|
||||
tintSprite.setScale(config.scale);
|
||||
}
|
||||
|
||||
// Sprite offset from origin
|
||||
if (config.x || config.y) {
|
||||
if (config.x) {
|
||||
sprite.x = origin + config.x;
|
||||
tintSprite.x = origin + config.x;
|
||||
}
|
||||
if (config.y) {
|
||||
sprite.y = origin + config.y;
|
||||
tintSprite.y = origin + config.y;
|
||||
}
|
||||
} else {
|
||||
// Single sprite
|
||||
if (this.spriteConfigs.length === 1) {
|
||||
sprite.x = origin;
|
||||
tintSprite.x = origin;
|
||||
} else {
|
||||
// Do standard sprite spacing (not including offset sprites)
|
||||
sprite.x = minX + (n + 0.5) * spacingValue + origin;
|
||||
tintSprite.x = minX + (n + 0.5) * spacingValue + origin;
|
||||
n++;
|
||||
}
|
||||
}
|
||||
|
||||
this.add(sprite);
|
||||
this.add(tintSprite);
|
||||
});
|
||||
}
|
||||
|
||||
loadAssets(): Promise<void> {
|
||||
return new Promise(resolve => {
|
||||
if (!this.spriteConfigs) {
|
||||
resolve();
|
||||
}
|
||||
|
||||
this.spriteConfigs.forEach((config) => {
|
||||
if (!config.isItem) {
|
||||
this.scene.loadAtlas(config.spriteKey, config.fileRoot);
|
||||
} else {
|
||||
this.scene.loadAtlas("items", "");
|
||||
}
|
||||
});
|
||||
|
||||
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => {
|
||||
this.spriteConfigs.every((config) => {
|
||||
if (config.isItem) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const originalWarn = console.warn;
|
||||
|
||||
// Ignore warnings for missing frames, because there will be a lot
|
||||
console.warn = () => {
|
||||
};
|
||||
const frameNames = this.scene.anims.generateFrameNames(config.spriteKey, { zeroPad: 4, suffix: ".png", start: 1, end: 128 });
|
||||
|
||||
console.warn = originalWarn;
|
||||
if (!(this.scene.anims.exists(config.spriteKey))) {
|
||||
this.scene.anims.create({
|
||||
key: config.spriteKey,
|
||||
frames: frameNames,
|
||||
frameRate: 12,
|
||||
repeat: -1
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
|
||||
resolve();
|
||||
});
|
||||
|
||||
if (!this.scene.load.isLoading()) {
|
||||
this.scene.load.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
initSprite(): void {
|
||||
if (!this.spriteConfigs) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.getSprites().map((sprite, i) => {
|
||||
if (!this.spriteConfigs[i].isItem) {
|
||||
sprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0);
|
||||
}
|
||||
});
|
||||
this.getTintSprites().map((tintSprite, i) => {
|
||||
if (!this.spriteConfigs[i].isItem) {
|
||||
tintSprite.setTexture(this.spriteConfigs[i].spriteKey).setFrame(0);
|
||||
}
|
||||
});
|
||||
|
||||
this.spriteConfigs.every((config, i) => {
|
||||
if (!config.tint) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const tintSprite = this.getAt(i * 2 + 1);
|
||||
this.tint(tintSprite, 0, config.tint);
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempts to animate a given set of {@linkcode Phaser.GameObjects.Sprite}
|
||||
* @see {@linkcode Phaser.GameObjects.Sprite.play}
|
||||
* @param sprite {@linkcode Phaser.GameObjects.Sprite} to animate
|
||||
* @param tintSprite {@linkcode Phaser.GameObjects.Sprite} placed on top of the sprite to add a color tint
|
||||
* @param animConfig {@linkcode Phaser.Types.Animations.PlayAnimationConfig} to pass to {@linkcode Phaser.GameObjects.Sprite.play}
|
||||
* @returns true if the sprite was able to be animated
|
||||
*/
|
||||
tryPlaySprite(sprite: Phaser.GameObjects.Sprite, tintSprite: Phaser.GameObjects.Sprite, animConfig: Phaser.Types.Animations.PlayAnimationConfig): boolean {
|
||||
// Show an error in the console if there isn't a texture loaded
|
||||
if (sprite.texture.key === "__MISSING") {
|
||||
console.error(`No texture found for '${animConfig.key}'!`);
|
||||
|
||||
return false;
|
||||
}
|
||||
// Don't try to play an animation when there isn't one
|
||||
if (sprite.texture.frameTotal <= 1) {
|
||||
console.warn(`No animation found for '${animConfig.key}'. Is this intentional?`);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
sprite.play(animConfig);
|
||||
tintSprite.play(animConfig);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
playAnim(): void {
|
||||
if (!this.spriteConfigs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sprites = this.getSprites();
|
||||
const tintSprites = this.getTintSprites();
|
||||
this.spriteConfigs.forEach((config, i) => {
|
||||
if (!config.disableAnimation) {
|
||||
const trainerAnimConfig = {
|
||||
key: config.spriteKey,
|
||||
repeat: config?.repeat ? -1 : 0,
|
||||
startFrame: 0
|
||||
};
|
||||
|
||||
this.tryPlaySprite(sprites[i], tintSprites[i], trainerAnimConfig);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getSprites(): Phaser.GameObjects.Sprite[] {
|
||||
if (!this.spriteConfigs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ret: Phaser.GameObjects.Sprite[] = [];
|
||||
this.spriteConfigs.forEach((config, i) => {
|
||||
ret.push(this.getAt(i * 2));
|
||||
});
|
||||
return ret;
|
||||
}
|
||||
|
||||
getTintSprites(): Phaser.GameObjects.Sprite[] {
|
||||
if (!this.spriteConfigs) {
|
||||
return;
|
||||
}
|
||||
|
||||
const ret: Phaser.GameObjects.Sprite[] = [];
|
||||
this.spriteConfigs.forEach((config, i) => {
|
||||
ret.push(this.getAt(i * 2 + 1));
|
||||
});
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
tint(sprite, color: number, alpha?: number, duration?: integer, ease?: string): void {
|
||||
// const tintSprites = this.getTintSprites();
|
||||
sprite.setTintFill(color);
|
||||
sprite.setVisible(true);
|
||||
|
||||
if (duration) {
|
||||
sprite.setAlpha(0);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: sprite,
|
||||
alpha: alpha || 1,
|
||||
duration: duration,
|
||||
ease: ease || "Linear"
|
||||
});
|
||||
} else {
|
||||
sprite.setAlpha(alpha);
|
||||
}
|
||||
}
|
||||
|
||||
tintAll(color: number, alpha?: number, duration?: integer, ease?: string): void {
|
||||
const tintSprites = this.getTintSprites();
|
||||
tintSprites.map(tintSprite => {
|
||||
this.tint(tintSprite, color, alpha, duration, ease);
|
||||
});
|
||||
}
|
||||
|
||||
untint(sprite, duration: integer, ease?: string): void {
|
||||
if (duration) {
|
||||
this.scene.tweens.add({
|
||||
targets: sprite,
|
||||
alpha: 0,
|
||||
duration: duration,
|
||||
ease: ease || "Linear",
|
||||
onComplete: () => {
|
||||
sprite.setVisible(false);
|
||||
sprite.setAlpha(1);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
sprite.setVisible(false);
|
||||
sprite.setAlpha(1);
|
||||
}
|
||||
}
|
||||
|
||||
untintAll(duration: integer, ease?: string): void {
|
||||
const tintSprites = this.getTintSprites();
|
||||
tintSprites.map(tintSprite => {
|
||||
this.untint(tintSprite, duration, ease);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export default interface MysteryEncounterIntroVisuals {
|
||||
scene: BattleScene
|
||||
}
|
|
@ -3913,6 +3913,7 @@ export class PokemonSummonData {
|
|||
public moveset: PokemonMove[];
|
||||
// If not initialized this value will not be populated from save data.
|
||||
public types: Type[] = null;
|
||||
public mysteryEncounterBattleEffects: (pokemon: Pokemon) => void = null;
|
||||
}
|
||||
|
||||
export class PokemonBattleData {
|
||||
|
|
|
@ -35,11 +35,16 @@ export default class Trainer extends Phaser.GameObjects.Container {
|
|||
public name: string;
|
||||
public partnerName: string;
|
||||
|
||||
constructor(scene: BattleScene, trainerType: TrainerType, variant: TrainerVariant, partyTemplateIndex?: integer, name?: string, partnerName?: string) {
|
||||
constructor(scene: BattleScene, trainerType: TrainerType, variant: TrainerVariant, partyTemplateIndex?: integer, name?: string, partnerName?: string, trainerConfigOverride?: TrainerConfig) {
|
||||
super(scene, -72, 80);
|
||||
this.config = trainerConfigs.hasOwnProperty(trainerType)
|
||||
? trainerConfigs[trainerType]
|
||||
: trainerConfigs[TrainerType.ACE_TRAINER];
|
||||
|
||||
if (trainerConfigOverride) {
|
||||
this.config = trainerConfigOverride;
|
||||
}
|
||||
|
||||
this.variant = variant;
|
||||
this.partyTemplateIndex = Math.min(partyTemplateIndex !== undefined ? partyTemplateIndex : Utils.randSeedWeightedItem(this.config.partyTemplates.map((_, i) => i)),
|
||||
this.config.partyTemplates.length - 1);
|
||||
|
|
|
@ -8,8 +8,9 @@ import Pokemon, { EnemyPokemon, PlayerPokemon } from "./field/pokemon";
|
|||
import { Mode } from "./ui/ui";
|
||||
import PartyUiHandler from "./ui/party-ui-handler";
|
||||
import { BattleSpec } from "#enums/battle-spec";
|
||||
import { BattlePhase, MovePhase, PokemonHealPhase } from "./phases";
|
||||
import { MovePhase, PokemonHealPhase } from "./phases";
|
||||
import { getTypeRgb } from "./data/type";
|
||||
import { BattlePhase } from "#app/phases/battle-phase";
|
||||
|
||||
export class FormChangePhase extends EvolutionPhase {
|
||||
private formChange: SpeciesFormChange;
|
||||
|
|
|
@ -28,6 +28,7 @@ interface GameModeConfig {
|
|||
hasRandomBosses?: boolean;
|
||||
isSplicedOnly?: boolean;
|
||||
isChallenge?: boolean;
|
||||
hasMysteryEncounters?: boolean;
|
||||
}
|
||||
|
||||
export class GameMode implements GameModeConfig {
|
||||
|
@ -44,6 +45,7 @@ export class GameMode implements GameModeConfig {
|
|||
public isChallenge: boolean;
|
||||
public challenges: Challenge[];
|
||||
public battleConfig: FixedBattleConfigs;
|
||||
public hasMysteryEncounters: boolean;
|
||||
|
||||
constructor(modeId: GameModes, config: GameModeConfig, battleConfig?: FixedBattleConfigs) {
|
||||
this.modeId = modeId;
|
||||
|
@ -316,7 +318,7 @@ export class GameMode implements GameModeConfig {
|
|||
export function getGameMode(gameMode: GameModes): GameMode {
|
||||
switch (gameMode) {
|
||||
case GameModes.CLASSIC:
|
||||
return new GameMode(GameModes.CLASSIC, { isClassic: true, hasTrainers: true }, classicFixedBattles);
|
||||
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:
|
||||
|
@ -324,6 +326,6 @@ export function getGameMode(gameMode: GameModes): GameMode {
|
|||
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 }, classicFixedBattles);
|
||||
return new GameMode(GameModes.CHALLENGE, { isClassic: true, hasTrainers: true, isChallenge: true, hasMysteryEncounters: true }, classicFixedBattles);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -22,6 +22,8 @@ import { initStatsKeys } from "./ui/game-stats-ui-handler";
|
|||
import { initVouchers } from "./system/voucher";
|
||||
import { Biome } from "#enums/biome";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import {initMysteryEncounterDialogue} from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
import {initMysteryEncounters} from "#app/data/mystery-encounters/mystery-encounters";
|
||||
|
||||
export class LoadingScene extends SceneBase {
|
||||
readonly LOAD_EVENTS = Phaser.Loader.Events;
|
||||
|
@ -344,6 +346,8 @@ export class LoadingScene extends SceneBase {
|
|||
initMoves();
|
||||
initAbilities();
|
||||
initChallenges();
|
||||
initMysteryEncounterDialogue();
|
||||
initMysteryEncounters();
|
||||
}
|
||||
|
||||
loadLoadingScreen() {
|
||||
|
|
|
@ -49,6 +49,7 @@ export const battle: SimpleTranslationEntries = {
|
|||
"noPokeballTrainer": "You can't catch\nanother trainer's Pokémon!",
|
||||
"noPokeballMulti": "You can only throw a Poké Ball\nwhen there is one Pokémon remaining!",
|
||||
"noPokeballStrong": "The target Pokémon is too strong to be caught!\nYou need to weaken it first!",
|
||||
"noPokeballMysteryEncounter": "You aren't able to\ncatch this Pokémon!",
|
||||
"noEscapeForce": "An unseen force\nprevents escape.",
|
||||
"noEscapeTrainer": "You can't run\nfrom a trainer battle!",
|
||||
"noEscapePokemon": "{{pokemonName}}'s {{moveName}}\nprevents {{escapeVerb}}!",
|
||||
|
@ -134,5 +135,6 @@ export const battle: SimpleTranslationEntries = {
|
|||
"battlerTagsSaltCuredOnAdd": "{{pokemonNameWithAffix}} is being salt cured!",
|
||||
"battlerTagsSaltCuredLapse": "{{pokemonNameWithAffix}} is hurt by {{moveName}}!",
|
||||
"battlerTagsCursedOnAdd": "{{pokemonNameWithAffix}} cut its own HP and put a curse on the {{pokemonName}}!",
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!"
|
||||
"battlerTagsCursedLapse": "{{pokemonNameWithAffix}} is afflicted by the Curse!",
|
||||
"mysteryEncounterAppeared": "What's this?"
|
||||
} as const;
|
||||
|
|
|
@ -30,6 +30,7 @@ import { menuUiHandler } from "./menu-ui-handler";
|
|||
import { modifier } from "./modifier";
|
||||
import { modifierType } from "./modifier-type";
|
||||
import { move } from "./move";
|
||||
import { mysteryEncounter } from "./mystery-encounter";
|
||||
import { nature } from "./nature";
|
||||
import { partyUiHandler } from "./party-ui-handler";
|
||||
import { pokeball } from "./pokeball";
|
||||
|
@ -77,6 +78,7 @@ export const enConfig = {
|
|||
modifier: modifier,
|
||||
modifierType: modifierType,
|
||||
move: move,
|
||||
mysteryEncounter: mysteryEncounter,
|
||||
nature: nature,
|
||||
pokeball: pokeball,
|
||||
pokemon: pokemon,
|
||||
|
|
|
@ -0,0 +1,125 @@
|
|||
import {SimpleTranslationEntries} from "#app/interfaces/locales";
|
||||
|
||||
export const mysteryEncounter: SimpleTranslationEntries = {
|
||||
|
||||
// Mysterious Encounters -- Common Tier
|
||||
|
||||
"mysterious_chest_intro_message": "You found...@d{32} a chest?",
|
||||
"mysterious_chest_title": "The Mysterious Chest",
|
||||
"mysterious_chest_description": "A beautifully ornamented chest stands on the ground. There must be something good inside... right?",
|
||||
"mysterious_chest_query": "Will you open it?",
|
||||
"mysterious_chest_option_1_label": "Open it",
|
||||
"mysterious_chest_option_1_tooltip": "(35%) Something terrible\n(40%) Okay Rewards\n(20%) Good Rewards\n(4%) Great Rewards\n(1%) Amazing Rewards",
|
||||
"mysterious_chest_option_2_label": "It's too risky, leave",
|
||||
"mysterious_chest_option_2_tooltip": "(-) No Rewards",
|
||||
"mysterious_chest_option_1_selected_message": "You open the chest to find...",
|
||||
"mysterious_chest_option_2_selected_message": "You hurry along your way,\nwith a slight feeling of regret.",
|
||||
"mysterious_chest_option_1_normal_result": "Just some normal tools and items.",
|
||||
"mysterious_chest_option_1_good_result": "Some pretty nice tools and items.",
|
||||
"mysterious_chest_option_1_great_result": "A couple great tools and items!",
|
||||
"mysterious_chest_option_1_amazing_result": "Whoa! An amazing item!",
|
||||
"mysterious_chest_option_1_bad_result": `Oh no!@d{32}\nThe chest was trapped!
|
||||
$Your @ec{pokeName} jumps in front of you\nbut is KOed in the process.`,
|
||||
|
||||
"fight_or_flight_intro_message": "Something shiny is sparkling\non the ground near that Pokémon!",
|
||||
"fight_or_flight_title": "Fight or Flight",
|
||||
"fight_or_flight_description": "It looks like there's a strong Pokémon guarding an item. Battling is the straightforward approach, but this Pokémon looks strong. You could also try to sneak around, though the Pokémon might catch you.",
|
||||
"fight_or_flight_query": "What will you do?",
|
||||
"fight_or_flight_option_1_label": "Battle it",
|
||||
"fight_or_flight_option_1_tooltip": "(+) Hard Battle\n(+) New Item",
|
||||
"fight_or_flight_option_2_label": "Sneak around",
|
||||
"fight_or_flight_option_2_tooltip": "(35%) Steal Item\n(65%) Harder Battle",
|
||||
"fight_or_flight_option_3_label": "Leave",
|
||||
"fight_or_flight_option_3_tooltip": "(-) No Rewards",
|
||||
"fight_or_flight_option_1_selected_message": "You approach the\nPokémon without fear.",
|
||||
"fight_or_flight_option_2_good_result": `.@d{32}.@d{32}.@d{32}
|
||||
$You manage to sneak your way\npast and grab the item!`,
|
||||
"fight_or_flight_option_2_bad_result": `.@d{32}.@d{32}.@d{32}
|
||||
$The Pokémon catches you\nas you try to sneak around!`,
|
||||
"fight_or_flight_boss_enraged": "The opposing @ec{enemyPokemon} has become enraged!",
|
||||
"fight_or_flight_option_3_selected": "You leave the strong Pokémon\nwith its prize and continue on.",
|
||||
|
||||
// Mysterious Encounters -- Uncommon Tier
|
||||
|
||||
"mysterious_challengers_intro_message": "Mysterious challengers have appeared!",
|
||||
"mysterious_challengers_title": "Mysterious Challengers",
|
||||
"mysterious_challengers_description": "If you defeat a challenger, you might impress them enough to receive a boon. But some look tough, are you up to the challenge?",
|
||||
"mysterious_challengers_query": "Who will you battle?",
|
||||
"mysterious_challengers_option_1_label": "A clever, mindful foe",
|
||||
"mysterious_challengers_option_1_tooltip": "(+) Standard Battle\n(+) Move Item Rewards",
|
||||
"mysterious_challengers_option_2_label": "A strong foe",
|
||||
"mysterious_challengers_option_2_tooltip": "(+) Hard Battle\n(+) Good Rewards",
|
||||
"mysterious_challengers_option_3_label": "The mightiest foe",
|
||||
"mysterious_challengers_option_3_tooltip": "(+) Brutal Battle\n(+) Great Rewards",
|
||||
"mysterious_challengers_option_selected_message": "The trainer steps forward...",
|
||||
"mysterious_challengers_outro_win": "The mysterious challenger was defeated!",
|
||||
|
||||
// Mysterious Encounters -- Rare Tier
|
||||
"training_session_intro_message": "You've come across a some\ntraining tools and supplies.",
|
||||
"training_session_title": "Training Session",
|
||||
"training_session_description": "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.",
|
||||
"training_session_query": "How should you train?",
|
||||
"training_session_option_1_label": "Light Training",
|
||||
"training_session_option_1_tooltip": "(-) Light Battle\n(+) Improve 2 Random IVs of Pokémon",
|
||||
"training_session_option_2_label": "Moderate Training",
|
||||
"training_session_option_2_tooltip": "(-) Moderate Battle\n(+) Change Pokémon's Nature",
|
||||
"training_session_option_2_select_prompt": "Select a new nature\nto train your Pokémon in.",
|
||||
"training_session_option_3_label": "Heavy Training",
|
||||
"training_session_option_3_tooltip": "(-) Harsh Battle\n(+) Change Pokémon's Ability",
|
||||
"training_session_option_3_select_prompt": "Select a new ability\nto train your Pokémon in.",
|
||||
"training_session_option_selected_message": "@ec{selectedPokemon} moves across\nthe clearing to face you...",
|
||||
"training_session_battle_finished_1": `@ec{selectedPokemon} returns, feeling\nworn out but accomplished!
|
||||
$Its @ec{stat1} and @ec{stat2}IVs were improved!`,
|
||||
"training_session_battle_finished_2": `@ec{selectedPokemon} returns, feeling\nworn out but accomplished!
|
||||
$Its nature was changed to @ec{nature}!`,
|
||||
"training_session_battle_finished_3": `@ec{selectedPokemon} returns, feeling\nworn out but accomplished!
|
||||
$Its ability was changed to @ec{ability}!`,
|
||||
"training_session_outro_win": "That was a successful training session!",
|
||||
|
||||
// Mysterious Encounters -- Super Rare Tier
|
||||
|
||||
"dark_deal_intro_message": "A strange man in a tattered coat\nstands in your way...",
|
||||
"dark_deal_speaker": "Shady Guy",
|
||||
"dark_deal_intro_dialogue": `Hey, you!
|
||||
$I've been working on a new device\nto bring out a Pokémon's latent power!
|
||||
$It completely rebinds the Pokémon's atoms\nat a molecular level into a far more powerful form.
|
||||
$Hehe...@d{64} I just need some sac-@d{32}\nErr, test subjects, to prove it works.`,
|
||||
"dark_deal_title": "Dark Deal",
|
||||
"dark_deal_description": "The disturbing fellow holds up some Pokéballs.\n\"I'll make it worth your while! You can have these strong Pokéballs as payment, All I need is a Pokémon from your team! Hehe...\"",
|
||||
"dark_deal_query": "What will you do?",
|
||||
"dark_deal_option_1_label": "Accept", // Give player 10 rogue balls. Remove a random Pokémon from player's party. Fight a legendary Pokémon as a boss
|
||||
"dark_deal_option_1_tooltip": "(+) 5 Rogue Balls\n(?) Enhance a Random Pokémon", // Give player 10 rogue balls. Remove a random Pokémon from player's party. Fight a legendary Pokémon as a boss
|
||||
"dark_deal_option_2_label": "Refuse",
|
||||
"dark_deal_option_2_tooltip": "(-) No Rewards",
|
||||
"dark_deal_option_1_selected": `Let's see, that @ec{pokeName} will do nicely!
|
||||
$Remember, I'm not responsible\nif anything bad happens!@d{32} Hehe...`,
|
||||
"dark_deal_option_1_selected_message": `The man hands you 5 Rogue Balls.
|
||||
$@ec{pokeName} hops into the strange machine...
|
||||
$Flashing lights and weird noises\nstart coming from the machine!
|
||||
$...@d{96} Something emerges\nfrom the device, raging wildly!`,
|
||||
"dark_deal_option_2_selected": "Not gonna help a poor fellow out?\nPah!",
|
||||
"dark_deal_outro": "After the harrowing encounter,\nyou collect yourself and depart.",
|
||||
|
||||
"sleeping_snorlax_intro_message": `As you walk down a narrow pathway, you see a towering silhouette blocking your path.
|
||||
$You get closer to see a Snorlax sleeping peacefully.\nIt seems like there's no way around it.`,
|
||||
"sleeping_snorlax_title": "Sleeping Snorlax",
|
||||
"sleeping_snorlax_description": "You could attack it to try and get it to move, or simply wait for it to wake up.",
|
||||
"sleeping_snorlax_query": "What will you do?",
|
||||
"sleeping_snorlax_option_1_label": "Fight it",
|
||||
"sleeping_snorlax_option_1_tooltip": "(+) Fight Sleeping Snorlax",
|
||||
"sleeping_snorlax_option_2_label": "Wait for it to move",
|
||||
"sleeping_snorlax_option_2_tooltip": "(75%) Wait a short time\n(25%) Wait a long time",
|
||||
"sleeping_snorlax_option_3_label": "Steal",
|
||||
"sleeping_snorlax_option_3_tooltip": "(+) Leftovers",
|
||||
"sleeping_snorlax_option_3_disabled_tooltip": "Your Pokémon need to know certain moves to choose this",
|
||||
"sleeping_snorlax_option_1_selected_message": "You approach the\nPokémon without fear.",
|
||||
"sleeping_snorlax_option_2_selected_message": `.@d{32}.@d{32}.@d{32}
|
||||
$You wait for a time, but the Snorlax's yawns make your party sleepy.`,
|
||||
"sleeping_snorlax_option_2_good_result": "When you all awaken, the Snorlax is no where to be found - but your Pokémon are all healed!",
|
||||
"sleeping_snorlax_option_2_bad_result": `Your @ec{primaryName} is still asleep...
|
||||
$But on the bright side, the Snorlax left something behind...
|
||||
$@s{item_fanfare}You gained a Berry!`,
|
||||
"sleeping_snorlax_option_3_good_result": "Your @ec{option3PrimaryName} uses @ec{option3PrimaryMove}! @s{item_fanfare}It steals Leftovers off the sleeping Snorlax and you make out like bandits!",
|
||||
// "sleeping_snorlax_outro_win": "The mysterious challengers were defeated!",
|
||||
|
||||
} as const;
|
|
@ -16,6 +16,7 @@ export const partyUiHandler: SimpleTranslationEntries = {
|
|||
"PASS_BATON": "Pass Baton",
|
||||
"UNPAUSE_EVOLUTION": "Unpause Evolution",
|
||||
"REVIVE": "Revive",
|
||||
"SELECT": "Select",
|
||||
|
||||
"choosePokemon": "Choose a Pokémon.",
|
||||
"doWhatWithThisPokemon": "Do what with this Pokémon?",
|
||||
|
|
|
@ -143,7 +143,7 @@ export interface GeneratedPersistentModifierType {
|
|||
getPregenArgs(): any[];
|
||||
}
|
||||
|
||||
class AddPokeballModifierType extends ModifierType {
|
||||
export class AddPokeballModifierType extends ModifierType {
|
||||
private pokeballType: PokeballType;
|
||||
private count: integer;
|
||||
|
||||
|
@ -1801,21 +1801,74 @@ export function regenerateModifierPoolThresholds(party: Pokemon[], poolType: Mod
|
|||
}
|
||||
}
|
||||
|
||||
export interface CustomModifierSettings {
|
||||
guaranteedModifierTiers?: ModifierTier[];
|
||||
guaranteedModifierTypeOptions?: ModifierTypeOption[];
|
||||
guaranteedModifierTypeFuncs?: ModifierTypeFunc[];
|
||||
fillRemaining?: boolean;
|
||||
rerollMultiplier?: number;
|
||||
}
|
||||
|
||||
export function getModifierTypeFuncById(id: string): ModifierTypeFunc {
|
||||
return modifierTypes[id];
|
||||
}
|
||||
|
||||
export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[]): ModifierTypeOption[] {
|
||||
export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings): ModifierTypeOption[] {
|
||||
const options: ModifierTypeOption[] = [];
|
||||
const retryCount = Math.min(count * 5, 50);
|
||||
new Array(count).fill(0).map((_, i) => {
|
||||
let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, modifierTiers?.length > i ? modifierTiers[i] : undefined);
|
||||
let r = 0;
|
||||
while (options.length && ++r < retryCount && options.filter(o => o.type.name === candidate.type.name || o.type.group === candidate.type.group).length) {
|
||||
candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate.type.tier, candidate.upgradeCount);
|
||||
if (!customModifierSettings) {
|
||||
new Array(count).fill(0).map((_, i) => {
|
||||
options.push(getModifierTypeOptionWithLuckUpgrades(options, retryCount, party, modifierTiers?.length > i ? modifierTiers[i] : undefined));
|
||||
});
|
||||
} else {
|
||||
// Guaranteed mods first
|
||||
if (customModifierSettings?.guaranteedModifierTypeOptions?.length) {
|
||||
customModifierSettings?.guaranteedModifierTypeOptions.forEach((option) => {
|
||||
options.push(option);
|
||||
});
|
||||
}
|
||||
options.push(candidate);
|
||||
});
|
||||
|
||||
// Guaranteed mod funcs second
|
||||
if (customModifierSettings?.guaranteedModifierTypeFuncs?.length) {
|
||||
customModifierSettings?.guaranteedModifierTypeFuncs.forEach((mod, i) => {
|
||||
const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === mod);
|
||||
let guaranteedMod: ModifierType = modifierTypes[modifierId]?.();
|
||||
|
||||
// Gets tier of item by checking player item pool
|
||||
Object.keys(modifierPool).every(modifierTier => {
|
||||
const modType = modifierPool[modifierTier].find(m => {
|
||||
if (m.modifierType.id === modifierId) {
|
||||
return m;
|
||||
}
|
||||
});
|
||||
if (modType) {
|
||||
guaranteedMod = modType.modifierType;
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const modType = guaranteedMod instanceof ModifierTypeGenerator ? guaranteedMod.generateType(party) : guaranteedMod;
|
||||
const option = new ModifierTypeOption(modType, 0);
|
||||
options.push(option);
|
||||
});
|
||||
}
|
||||
|
||||
// Guaranteed tiers third
|
||||
if (customModifierSettings?.guaranteedModifierTiers?.length) {
|
||||
customModifierSettings?.guaranteedModifierTiers.forEach((tier) => {
|
||||
options.push(getModifierTypeOptionWithLuckUpgrades(options, retryCount, party, tier));
|
||||
});
|
||||
}
|
||||
|
||||
// Fill remaining
|
||||
if (options.length < count && customModifierSettings.fillRemaining) {
|
||||
while (options.length < count) {
|
||||
options.push(getModifierTypeOptionWithLuckUpgrades(options, retryCount, party, undefined));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OVERRIDE IF NECESSARY
|
||||
if (Overrides.ITEM_REWARD_OVERRIDE?.length) {
|
||||
options.forEach((mod, i) => {
|
||||
|
@ -1827,6 +1880,15 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
|
|||
return options;
|
||||
}
|
||||
|
||||
function getModifierTypeOptionWithLuckUpgrades(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier): ModifierTypeOption {
|
||||
let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, modifierTiers?.length > i ? modifierTiers[i] : undefined);
|
||||
let r = 0;
|
||||
while (options.length && ++r < retryCount && options.filter(o => o.type.name === candidate.type.name || o.type.group === candidate.type.group).length) {
|
||||
candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate.type.tier, candidate.upgradeCount);
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
export function getPlayerShopModifierTypeOptionsForWave(waveIndex: integer, baseCost: integer): ModifierTypeOption[] {
|
||||
if (!(waveIndex % 10)) {
|
||||
return [];
|
||||
|
|
|
@ -18,6 +18,8 @@ import { Biome } from "#enums/biome";
|
|||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { TimeOfDay } from "#enums/time-of-day";
|
||||
import {MysteryEncounterType} from "#enums/mystery-encounter-type"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
import {MysteryEncounterTier} from "#app/data/mystery-encounter"; // eslint-disable-line @typescript-eslint/no-unused-vars
|
||||
|
||||
/**
|
||||
* Overrides for testing different in game situations
|
||||
|
@ -111,6 +113,15 @@ export const EGG_VARIANT_OVERRIDE: VariantTier = null;
|
|||
export const EGG_FREE_GACHA_PULLS_OVERRIDE: boolean = false;
|
||||
export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0;
|
||||
|
||||
/**
|
||||
* MYSTERY ENCOUNTER OVERRIDES
|
||||
*/
|
||||
|
||||
// 1 to 256, set to null to ignore
|
||||
export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256;
|
||||
export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null;
|
||||
export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.FIGHT_OR_FLIGHT;
|
||||
|
||||
/**
|
||||
* MODIFIER / ITEM OVERRIDES
|
||||
* if count is not provided, it will default to 1
|
||||
|
|
224
src/phases.ts
224
src/phases.ts
|
@ -17,7 +17,7 @@ import { Phase } from "./phase";
|
|||
import { BattleStat, getBattleStatLevelChangeDescription, getBattleStatName } from "./data/battle-stat";
|
||||
import { biomeLinks, getBiomeName } from "./data/biomes";
|
||||
import { ModifierTier } from "./modifier/modifier-tier";
|
||||
import { FusePokemonModifierType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeOption, PokemonModifierType, PokemonMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, RememberMoveModifierType, TmModifierType, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, getPlayerModifierTypeOptions, getPlayerShopModifierTypeOptionsForWave, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type";
|
||||
import { ModifierPoolType, ModifierType, ModifierTypeFunc, getDailyRunStarterModifiers, getEnemyBuffModifierForWave, getModifierType, modifierTypes, regenerateModifierPoolThresholds } from "./modifier/modifier-type";
|
||||
import SoundFade from "phaser3-rex-plugins/plugins/soundfade";
|
||||
import { BattlerTagLapseType, CenterOfAttentionTag, EncoreTag, ProtectedTag, SemiInvulnerableTag, TrappedTag } from "./data/battler-tags";
|
||||
import { getPokemonMessage, getPokemonNameWithAffix } from "./messages";
|
||||
|
@ -65,6 +65,11 @@ import { Moves } from "#enums/moves";
|
|||
import { PlayerGender } from "#enums/player-gender";
|
||||
import { Species } from "#enums/species";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { BattlePhase } from "#app/phases/battle-phase";
|
||||
import { MysteryEncounterVariant } from "#app/data/mystery-encounter";
|
||||
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phase";
|
||||
import { handleMysteryEncounterVictory } from "#app/data/mystery-encounters/mystery-encounter-utils";
|
||||
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||
|
||||
const { t } = i18next;
|
||||
|
||||
|
@ -291,6 +296,14 @@ export class TitlePhase extends Phase {
|
|||
},
|
||||
keepOpen: true
|
||||
},
|
||||
{
|
||||
label: i18next.t("menu:settings"),
|
||||
handler: () => {
|
||||
this.scene.ui.setOverlayMode(Mode.SETTINGS);
|
||||
return true;
|
||||
},
|
||||
keepOpen: true
|
||||
},
|
||||
{
|
||||
label: i18next.t("menu:settings"),
|
||||
handler: () => {
|
||||
|
@ -667,17 +680,6 @@ export class BattlePhase extends Phase {
|
|||
duration: 750
|
||||
});
|
||||
}
|
||||
|
||||
hideEnemyTrainer(): void {
|
||||
this.scene.tweens.add({
|
||||
targets: this.scene.currentBattle.trainer,
|
||||
x: "+=16",
|
||||
y: "-=16",
|
||||
alpha: 0,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 750
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
type PokemonFunc = (pokemon: Pokemon) => void;
|
||||
|
@ -814,6 +816,24 @@ export class EncounterPhase extends BattlePhase {
|
|||
|
||||
const battle = this.scene.currentBattle;
|
||||
|
||||
// Init Mystery Encounter if there is one
|
||||
const mysteryEncounter = battle.mysteryEncounter;
|
||||
if (mysteryEncounter) {
|
||||
// If ME has an onInit() function, call it
|
||||
// Usually used for calculating rand data before initializing anything visual
|
||||
// Also prepopulates any dialogue tokens from encounter/option requirements
|
||||
this.scene.executeWithSeedOffset(() => {
|
||||
if (mysteryEncounter.onInit) {
|
||||
mysteryEncounter.onInit(this.scene);
|
||||
}
|
||||
mysteryEncounter.populateDialogueTokensFromRequirements();
|
||||
}, this.scene.currentBattle.waveIndex);
|
||||
|
||||
// Add intro visuals for mystery encounter
|
||||
mysteryEncounter.initIntroVisuals(this.scene);
|
||||
this.scene.field.add(mysteryEncounter.introVisuals);
|
||||
}
|
||||
|
||||
let totalBst = 0;
|
||||
|
||||
battle.enemyLevels.forEach((level, e) => {
|
||||
|
@ -838,7 +858,7 @@ export class EncounterPhase extends BattlePhase {
|
|||
}
|
||||
|
||||
if (!this.loaded) {
|
||||
this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER);
|
||||
this.scene.gameData.setPokemonSeen(enemyPokemon, true, battle.battleType === BattleType.TRAINER || battle?.mysteryEncounter?.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE);
|
||||
}
|
||||
|
||||
if (enemyPokemon.species.speciesId === Species.ETERNATUS) {
|
||||
|
@ -867,6 +887,12 @@ export class EncounterPhase extends BattlePhase {
|
|||
|
||||
if (battle.battleType === BattleType.TRAINER) {
|
||||
loadEnemyAssets.push(battle.trainer.loadAssets().then(() => battle.trainer.initSprite()));
|
||||
} else if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) {
|
||||
if (!battle.mysteryEncounter) {
|
||||
const newEncounter = this.scene.getMysteryEncounter(mysteryEncounter);
|
||||
battle.mysteryEncounter = newEncounter;
|
||||
}
|
||||
loadEnemyAssets.push(battle.mysteryEncounter.introVisuals.loadAssets().then(() => battle.mysteryEncounter.introVisuals.initSprite()));
|
||||
} else {
|
||||
// This block only applies for double battles to init the boss segments (idk why it's split up like this)
|
||||
if (battle.enemyParty.filter(p => p.isBoss()).length > 1) {
|
||||
|
@ -894,6 +920,9 @@ export class EncounterPhase extends BattlePhase {
|
|||
} else if (battle.battleType === BattleType.TRAINER) {
|
||||
enemyPokemon.setVisible(false);
|
||||
this.scene.currentBattle.trainer.tint(0, 0.5);
|
||||
} else if (battle.battleType === BattleType.MYSTERY_ENCOUNTER) {
|
||||
enemyPokemon.setVisible(false);
|
||||
this.scene.currentBattle?.trainer?.tint(0, 0.5);
|
||||
}
|
||||
if (battle.double) {
|
||||
enemyPokemon.setFieldPosition(e ? FieldPosition.RIGHT : FieldPosition.LEFT);
|
||||
|
@ -945,8 +974,8 @@ export class EncounterPhase extends BattlePhase {
|
|||
|
||||
const enemyField = this.scene.getEnemyField();
|
||||
this.scene.tweens.add({
|
||||
targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.arenaPlayer, this.scene.trainer].flat(),
|
||||
x: (_target, _key, value, fieldIndex: integer) => fieldIndex < 2 + (enemyField.length) ? value + 300 : value - 300,
|
||||
targets: [this.scene.arenaEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.currentBattle?.mysteryEncounter?.introVisuals, this.scene.arenaPlayer, this.scene.trainer].flat(),
|
||||
x: (_target, _key, value, fieldIndex: integer) => fieldIndex < 3 + (enemyField.length) ? value + 300 : value - 300,
|
||||
duration: 2000,
|
||||
onComplete: () => {
|
||||
if (!this.tryOverrideForBattleSpec()) {
|
||||
|
@ -1040,6 +1069,54 @@ export class EncounterPhase extends BattlePhase {
|
|||
showDialogueAndSummon();
|
||||
}
|
||||
}
|
||||
} else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) {
|
||||
const introVisuals = this.scene.currentBattle.mysteryEncounter.introVisuals;
|
||||
introVisuals.playAnim();
|
||||
|
||||
const doEncounter = () => {
|
||||
this.scene.playBgm(undefined);
|
||||
|
||||
const doShowEncounterOptions = () => {
|
||||
this.scene.ui.clearText();
|
||||
this.scene.ui.getMessageHandler().hideNameText();
|
||||
this.scene.unshiftPhase(new MysteryEncounterPhase(this.scene));
|
||||
|
||||
this.end();
|
||||
};
|
||||
|
||||
if (showEncounterMessage) {
|
||||
const introDialogue = this.scene.currentBattle.mysteryEncounter.dialogue.intro;
|
||||
let i = 0;
|
||||
const showNextDialogue = () => {
|
||||
const nextAction = i === introDialogue.length - 1 ? doShowEncounterOptions : showNextDialogue;
|
||||
const dialogue = introDialogue[i];
|
||||
const title = dialogue.speaker;
|
||||
const text = dialogue.text;
|
||||
if (title) {
|
||||
this.scene.ui.showDialogue(i18next.t(text), i18next.t(title), null, nextAction, 0, i === 0 ? 750 : 0);
|
||||
} else {
|
||||
this.scene.ui.showText(i18next.t(text), null, nextAction, i === 0 ? 750 : 0, true);
|
||||
}
|
||||
i++;
|
||||
};
|
||||
|
||||
if (introDialogue.length > 0) {
|
||||
showNextDialogue();
|
||||
}
|
||||
} else {
|
||||
doShowEncounterOptions();
|
||||
}
|
||||
};
|
||||
|
||||
const encounterMessage = i18next.t("battle:mysteryEncounterAppeared");
|
||||
|
||||
if (!encounterMessage) {
|
||||
doEncounter();
|
||||
} else {
|
||||
this.scene.ui.showDialogue(encounterMessage, "???", null, () => {
|
||||
this.scene.charSprite.hide().then(() => this.scene.hideFieldOverlay(250).then(() => doEncounter()));
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1052,7 +1129,7 @@ export class EncounterPhase extends BattlePhase {
|
|||
}
|
||||
});
|
||||
|
||||
if (this.scene.currentBattle.battleType !== BattleType.TRAINER) {
|
||||
if (this.scene.currentBattle.battleType !== BattleType.TRAINER && this.scene.currentBattle.battleType !== BattleType.MYSTERY_ENCOUNTER) {
|
||||
enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => {
|
||||
// if there is not a player party, we can't continue
|
||||
if (!this.scene.getParty()?.length) {
|
||||
|
@ -1146,8 +1223,17 @@ export class NextEncounterPhase extends EncounterPhase {
|
|||
this.scene.arenaNextEnemy.setVisible(true);
|
||||
|
||||
const enemyField = this.scene.getEnemyField();
|
||||
const moveTargets: any[] = [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer];
|
||||
const lastEncounterVisuals = this.scene?.lastMysteryEncounter?.introVisuals;
|
||||
if (lastEncounterVisuals) {
|
||||
moveTargets.push(lastEncounterVisuals);
|
||||
}
|
||||
const nextEncounterVisuals = this.scene.currentBattle?.mysteryEncounter?.introVisuals;
|
||||
if (nextEncounterVisuals) {
|
||||
moveTargets.push(nextEncounterVisuals);
|
||||
}
|
||||
this.scene.tweens.add({
|
||||
targets: [this.scene.arenaEnemy, this.scene.arenaNextEnemy, this.scene.currentBattle.trainer, enemyField, this.scene.lastEnemyTrainer].flat(),
|
||||
targets: moveTargets.flat(),
|
||||
x: "+=300",
|
||||
duration: 2000,
|
||||
onComplete: () => {
|
||||
|
@ -1159,6 +1245,11 @@ export class NextEncounterPhase extends EncounterPhase {
|
|||
if (this.scene.lastEnemyTrainer) {
|
||||
this.scene.lastEnemyTrainer.destroy();
|
||||
}
|
||||
if (lastEncounterVisuals) {
|
||||
this.scene.field.remove(lastEncounterVisuals);
|
||||
lastEncounterVisuals.destroy();
|
||||
this.scene.lastMysteryEncounter.introVisuals = null;
|
||||
}
|
||||
|
||||
if (!this.tryOverrideForBattleSpec()) {
|
||||
this.doEncounterCommon();
|
||||
|
@ -1189,8 +1280,14 @@ export class NewBiomeEncounterPhase extends NextEncounterPhase {
|
|||
}
|
||||
|
||||
const enemyField = this.scene.getEnemyField();
|
||||
const moveTargets: any[] = [this.scene.arenaEnemy, enemyField];
|
||||
const mysteryEncounter = this.scene.currentBattle?.mysteryEncounter?.introVisuals;
|
||||
if (mysteryEncounter) {
|
||||
moveTargets.push(mysteryEncounter);
|
||||
}
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: [this.scene.arenaEnemy, enemyField].flat(),
|
||||
targets: moveTargets.flat(),
|
||||
x: "+=300",
|
||||
duration: 2000,
|
||||
onComplete: () => {
|
||||
|
@ -1216,6 +1313,12 @@ export class PostSummonPhase extends PokemonPhase {
|
|||
pokemon.status.turnCount = 0;
|
||||
}
|
||||
this.scene.arena.applyTags(ArenaTrapTag, pokemon);
|
||||
|
||||
// If this is fight or flight mystery encounter and is enemy pokemon summon phase, add enraged tag
|
||||
if (pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag)) {
|
||||
pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON);
|
||||
}
|
||||
|
||||
applyPostSummonAbAttrs(PostSummonAbAttr, pokemon).then(() => this.end());
|
||||
}
|
||||
}
|
||||
|
@ -1376,7 +1479,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
|||
preSummon(): void {
|
||||
const partyMember = this.getPokemon();
|
||||
// If the Pokemon about to be sent out is fainted or illegal under a challenge, switch to the first non-fainted legal Pokemon
|
||||
if (!partyMember.isAllowedInBattle()) {
|
||||
if (!partyMember.isAllowedInBattle() || (this.player && isNullOrUndefined(this.getParty().find(p => p.id === partyMember.id)))) {
|
||||
console.warn("The Pokemon about to be sent out is fainted or illegal under a challenge. Attempting to resolve...");
|
||||
|
||||
// First check if they're somehow still in play, if so remove them.
|
||||
|
@ -1424,13 +1527,16 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
|||
onComplete: () => this.scene.trainer.setVisible(false)
|
||||
});
|
||||
this.scene.time.delayedCall(750, () => this.summon());
|
||||
} else {
|
||||
} else if (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene?.currentBattle?.mysteryEncounter?.encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) {
|
||||
const trainerName = this.scene.currentBattle.trainer.getName(!(this.fieldIndex % 2) ? TrainerSlot.TRAINER : TrainerSlot.TRAINER_PARTNER);
|
||||
const pokemonName = this.getPokemon().name;
|
||||
const message = i18next.t("battle:trainerSendOut", { trainerName, pokemonName });
|
||||
|
||||
this.scene.pbTrayEnemy.hide();
|
||||
this.scene.ui.showText(message, null, () => this.summon());
|
||||
} else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) {
|
||||
this.scene.pbTrayEnemy.hide();
|
||||
this.summonWild();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1512,6 +1618,57 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
|||
});
|
||||
}
|
||||
|
||||
summonWild(): void {
|
||||
const pokemon = this.getPokemon();
|
||||
|
||||
if (this.fieldIndex === 1) {
|
||||
pokemon.setFieldPosition(FieldPosition.RIGHT, 0);
|
||||
} else {
|
||||
const availablePartyMembers = this.getParty().filter(p => !p.isFainted()).length;
|
||||
pokemon.setFieldPosition(!this.scene.currentBattle.double || availablePartyMembers === 1 ? FieldPosition.CENTER : FieldPosition.LEFT);
|
||||
}
|
||||
|
||||
this.scene.add.existing(pokemon);
|
||||
this.scene.field.add(pokemon);
|
||||
if (!this.player) {
|
||||
const playerPokemon = this.scene.getPlayerPokemon() as Pokemon;
|
||||
if (playerPokemon?.visible) {
|
||||
this.scene.field.moveBelow(pokemon, playerPokemon);
|
||||
}
|
||||
this.scene.currentBattle.seenEnemyPartyMemberIds.add(pokemon.id);
|
||||
}
|
||||
this.scene.updateModifiers(this.player);
|
||||
this.scene.updateFieldScale();
|
||||
pokemon.showInfo();
|
||||
pokemon.playAnim();
|
||||
pokemon.setVisible(true);
|
||||
pokemon.getSprite().setVisible(true);
|
||||
pokemon.setScale(0.75);
|
||||
pokemon.tint(getPokeballTintColor(pokemon.pokeball));
|
||||
pokemon.untint(250, "Sine.easeIn");
|
||||
this.scene.updateFieldScale();
|
||||
pokemon.x += 16;
|
||||
pokemon.y -= 16;
|
||||
pokemon.alpha = 0;
|
||||
|
||||
// Ease pokemon in
|
||||
this.scene.tweens.add({
|
||||
targets: pokemon,
|
||||
x: "-=16",
|
||||
y: "+=16",
|
||||
alpha: 1,
|
||||
duration: 1000,
|
||||
ease: "Sine.easeIn",
|
||||
scale: pokemon.getSpriteScale(),
|
||||
onComplete: () => {
|
||||
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
|
||||
pokemon.getSprite().clearTint();
|
||||
pokemon.resetSummonData();
|
||||
this.scene.time.delayedCall(1000, () => this.end());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onEnd(): void {
|
||||
const pokemon = this.getPokemon();
|
||||
|
||||
|
@ -1521,7 +1678,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
|
|||
|
||||
pokemon.resetTurnData();
|
||||
|
||||
if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || (this.scene.currentBattle.waveIndex % 10) === 1) {
|
||||
if (!this.loaded || this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER || (this.scene.currentBattle.waveIndex % 10) === 1) {
|
||||
this.scene.triggerPokemonFormChange(pokemon, SpeciesFormChangeActiveTrigger, true);
|
||||
this.queuePostSummon();
|
||||
}
|
||||
|
@ -1583,6 +1740,11 @@ export class SwitchSummonPhase extends SummonPhase {
|
|||
(this.player ? this.scene.getEnemyField() : this.scene.getPlayerField()).forEach(enemyPokemon => enemyPokemon.removeTagsBySourceId(pokemon.id));
|
||||
}
|
||||
|
||||
// TODO: maybe remove this? Not sure if still necessary
|
||||
if (!pokemon.isActive() || !pokemon.isOnField()) {
|
||||
this.switchAndSummon();
|
||||
}
|
||||
|
||||
this.scene.ui.showText(this.player ?
|
||||
i18next.t("battle:playerComeBack", { pokemonName: pokemon.name }) :
|
||||
i18next.t("battle:trainerComeBack", {
|
||||
|
@ -2001,6 +2163,13 @@ export class CommandPhase extends FieldPhase {
|
|||
this.scene.ui.showText(null, 0);
|
||||
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
|
||||
}, null, true);
|
||||
} else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER && !this.scene.currentBattle.mysteryEncounter.catchAllowed) {
|
||||
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
this.scene.ui.showText(i18next.t("battle:noPokeballMysteryEncounter"), null, () => {
|
||||
this.scene.ui.showText(null, 0);
|
||||
this.scene.ui.setMode(Mode.COMMAND, this.fieldIndex);
|
||||
}, null, true);
|
||||
} else {
|
||||
const targets = this.scene.getEnemyField().filter(p => p.isActive(true)).map(p => p.getBattlerIndex());
|
||||
if (targets.length > 1) {
|
||||
|
@ -3774,7 +3943,7 @@ export class FaintPhase extends PokemonPhase {
|
|||
}
|
||||
} else {
|
||||
this.scene.unshiftPhase(new VictoryPhase(this.scene, this.battlerIndex));
|
||||
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
|
||||
if (this.scene.currentBattle.battleType === BattleType.TRAINER || this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) {
|
||||
const hasReservePartyMember = !!this.scene.getEnemyParty().filter(p => p.isActive() && !p.isOnField() && p.trainerSlot === (pokemon as EnemyPokemon).trainerSlot).length;
|
||||
if (hasReservePartyMember) {
|
||||
this.scene.pushPhase(new SwitchSummonPhase(this.scene, this.fieldIndex, -1, false, false, false));
|
||||
|
@ -3870,6 +4039,8 @@ export class VictoryPhase extends PokemonPhase {
|
|||
let expValue = this.getPokemon().getExpValue();
|
||||
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
|
||||
expValue = Math.floor(expValue * 1.5);
|
||||
} else if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) {
|
||||
expValue = Math.floor(expValue * this.scene.currentBattle.mysteryEncounter.expMultiplier);
|
||||
}
|
||||
for (const partyMember of nonFaintedPartyMembers) {
|
||||
const pId = partyMember.id;
|
||||
|
@ -3938,7 +4109,13 @@ export class VictoryPhase extends PokemonPhase {
|
|||
}
|
||||
}
|
||||
|
||||
if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType ? !p?.isFainted(true) : p.isOnField())) {
|
||||
if (this.scene.currentBattle.battleType === BattleType.MYSTERY_ENCOUNTER) {
|
||||
handleMysteryEncounterVictory(this.scene);
|
||||
this.end();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.scene.getEnemyParty().find(p => this.scene.currentBattle.battleType === BattleType.WILD ? p.isOnField() : !p?.isFainted(true))) {
|
||||
this.scene.pushPhase(new BattleEndPhase(this.scene));
|
||||
if (this.scene.currentBattle.battleType === BattleType.TRAINER) {
|
||||
this.scene.pushPhase(new TrainerVictoryPhase(this.scene));
|
||||
|
@ -3968,6 +4145,7 @@ export class VictoryPhase extends PokemonPhase {
|
|||
this.scene.pushPhase(new AddEnemyBuffModifierPhase(this.scene));
|
||||
}
|
||||
}
|
||||
|
||||
this.scene.pushPhase(new NewBattlePhase(this.scene));
|
||||
} else {
|
||||
this.scene.currentBattle.battleType = BattleType.CLEAR;
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
import {Phase} from "#app/phase";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import {TrainerSlot} from "#app/data/trainer-config";
|
||||
|
||||
export class BattlePhase extends Phase {
|
||||
constructor(scene: BattleScene) {
|
||||
super(scene);
|
||||
}
|
||||
|
||||
showEnemyTrainer(trainerSlot: TrainerSlot = TrainerSlot.NONE): void {
|
||||
const sprites = this.scene.currentBattle.trainer.getSprites();
|
||||
const tintSprites = this.scene.currentBattle.trainer.getTintSprites();
|
||||
for (let i = 0; i < sprites.length; i++) {
|
||||
const visible = !trainerSlot || !i === (trainerSlot === TrainerSlot.TRAINER) || sprites.length < 2;
|
||||
[sprites[i], tintSprites[i]].map(sprite => {
|
||||
if (visible) {
|
||||
sprite.x = trainerSlot || sprites.length < 2 ? 0 : i ? 16 : -16;
|
||||
}
|
||||
sprite.setVisible(visible);
|
||||
sprite.clearTint();
|
||||
});
|
||||
sprites[i].setVisible(visible);
|
||||
tintSprites[i].setVisible(visible);
|
||||
sprites[i].clearTint();
|
||||
tintSprites[i].clearTint();
|
||||
}
|
||||
this.scene.tweens.add({
|
||||
targets: this.scene.currentBattle.trainer,
|
||||
x: "-=16",
|
||||
y: "+=16",
|
||||
alpha: 1,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 750
|
||||
});
|
||||
}
|
||||
|
||||
hideEnemyTrainer(): void {
|
||||
this.scene.tweens.add({
|
||||
targets: this.scene.currentBattle.trainer,
|
||||
x: "+=16",
|
||||
y: "-=16",
|
||||
alpha: 0,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 750
|
||||
});
|
||||
}
|
||||
}
|
|
@ -0,0 +1,474 @@
|
|||
import i18next from "i18next";
|
||||
import BattleScene from "../battle-scene";
|
||||
import { Phase } from "../phase";
|
||||
import { Mode } from "../ui/ui";
|
||||
import {
|
||||
getTextWithEncounterDialogueTokens
|
||||
} from "../data/mystery-encounters/mystery-encounter-utils";
|
||||
import { CheckSwitchPhase, NewBattlePhase, PostSummonPhase, ReturnPhase, ScanIvsPhase, SummonPhase, ToggleDoublePositionPhase } from "../phases";
|
||||
import MysteryEncounterOption from "../data/mystery-encounter-option";
|
||||
import { MysteryEncounterVariant } from "../data/mystery-encounter";
|
||||
import { getCharVariantFromDialogue } from "../data/dialogue";
|
||||
import { TrainerSlot } from "../data/trainer-config";
|
||||
import { BattleSpec } from "../enums/battle-spec";
|
||||
import { Tutorial, handleTutorial } from "../tutorial";
|
||||
import { IvScannerModifier } from "../modifier/modifier";
|
||||
import * as Utils from "../utils";
|
||||
import {SelectModifierPhase} from "#app/phases/select-modifier-phase";
|
||||
import {isNullOrUndefined} from "../utils";
|
||||
|
||||
/**
|
||||
* Will handle (in order):
|
||||
* - Clearing of phase queues to enter the Mystery Encounter game state
|
||||
* - Management of session data related to MEs
|
||||
* - Initialization of ME option select menu and UI
|
||||
* - Execute onPreOptionPhase() logic if it exists for the selected option
|
||||
* - Display any OptionTextDisplay.selected type dialogue that is set in the MysteryEncounterDialogue dialogue tree for selected option
|
||||
* - Queuing of the MysteryEncounterOptionSelectedPhase
|
||||
*/
|
||||
export class MysteryEncounterPhase extends Phase {
|
||||
constructor(scene: BattleScene) {
|
||||
super(scene);
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
// Clears out queued phases that are part of standard battle
|
||||
this.scene.ui.clearText();
|
||||
this.scene.clearPhaseQueue();
|
||||
this.scene.clearPhaseQueueSplice();
|
||||
|
||||
// Sets flag that ME was encountered
|
||||
// Can be used in later MEs to check for requirements to spawn, etc.
|
||||
this.scene.mysteryEncounterFlags.encounteredEvents.push([this.scene.currentBattle.mysteryEncounter.encounterType, this.scene.currentBattle.mysteryEncounter.encounterTier]);
|
||||
|
||||
// Initiates encounter dialogue window and option select
|
||||
this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER);
|
||||
}
|
||||
|
||||
handleOptionSelect(option: MysteryEncounterOption, index: number): boolean {
|
||||
// Set option selected flag
|
||||
this.scene.currentBattle.mysteryEncounter.selectedOption = option;
|
||||
|
||||
if (!option.onOptionPhase) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Populate dialogue tokens for option requirements
|
||||
this.scene.currentBattle.mysteryEncounter.populateDialogueTokensFromRequirements();
|
||||
|
||||
if (option.onPreOptionPhase) {
|
||||
this.scene.executeWithSeedOffset(async () => {
|
||||
return await option.onPreOptionPhase(this.scene)
|
||||
.then((result) => {
|
||||
if (isNullOrUndefined(result) || result) {
|
||||
this.continueEncounter(index);
|
||||
}
|
||||
});
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
} else {
|
||||
this.continueEncounter(index);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
continueEncounter(optionIndex: number) {
|
||||
const endDialogueAndContinueEncounter = () => {
|
||||
this.scene.pushPhase(new MysteryEncounterOptionSelectedPhase(this.scene));
|
||||
this.end();
|
||||
};
|
||||
|
||||
const optionSelectDialogue = this.scene.currentBattle?.mysteryEncounter?.dialogue?.encounterOptionsDialogue?.options?.[optionIndex];
|
||||
if (optionSelectDialogue?.selected?.length > 0) {
|
||||
// Handle intermediate dialogue (between player selection event and the onOptionSelect logic)
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
const selectedDialogue = optionSelectDialogue.selected;
|
||||
let i = 0;
|
||||
const showNextDialogue = () => {
|
||||
const nextAction = i === selectedDialogue.length - 1 ? endDialogueAndContinueEncounter : showNextDialogue;
|
||||
const dialogue = selectedDialogue[i];
|
||||
let title: string = null;
|
||||
const text: string = getTextWithEncounterDialogueTokens(this.scene, dialogue.text);
|
||||
if (dialogue.speaker) {
|
||||
title = getTextWithEncounterDialogueTokens(this.scene, dialogue.speaker);
|
||||
}
|
||||
|
||||
if (title) {
|
||||
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 750 : 0);
|
||||
} else {
|
||||
this.scene.ui.showText(text, null, nextAction, i === 0 ? 750 : 0, true);
|
||||
}
|
||||
i++;
|
||||
};
|
||||
|
||||
showNextDialogue();
|
||||
} else {
|
||||
endDialogueAndContinueEncounter();
|
||||
}
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.end();
|
||||
}
|
||||
|
||||
end() {
|
||||
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will handle (in order):
|
||||
* - Execute onOptionSelect() logic if it exists for the selected option
|
||||
*
|
||||
* It is important to point out that no phases are directly queued by any logic within this phase
|
||||
* Any phase that is meant to follow this one MUST be queued via the onOptionSelect() logic of the selected option
|
||||
*/
|
||||
export class MysteryEncounterOptionSelectedPhase extends Phase {
|
||||
onOptionSelect: (scene: BattleScene) => Promise<boolean | void>;
|
||||
|
||||
constructor(scene: BattleScene) {
|
||||
super(scene);
|
||||
this.onOptionSelect = this.scene.currentBattle.mysteryEncounter.selectedOption.onOptionPhase;
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
if (this.scene.currentBattle.mysteryEncounter.hideIntroVisuals) {
|
||||
this.hideMysteryEncounterIntroVisuals().then(() => {
|
||||
this.scene.executeWithSeedOffset(() => {
|
||||
this.onOptionSelect(this.scene).finally(() => {
|
||||
this.end();
|
||||
});
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
});
|
||||
} else {
|
||||
this.scene.executeWithSeedOffset(() => {
|
||||
this.onOptionSelect(this.scene).finally(() => {
|
||||
this.end();
|
||||
});
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
hideMysteryEncounterIntroVisuals(): Promise<boolean> {
|
||||
return new Promise(resolve => {
|
||||
const introVisuals = this.scene.currentBattle.mysteryEncounter.introVisuals;
|
||||
if (introVisuals) {
|
||||
// Hide
|
||||
this.scene.tweens.add({
|
||||
targets: introVisuals,
|
||||
x: "+=16",
|
||||
y: "-=16",
|
||||
alpha: 0,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 750,
|
||||
onComplete: () => {
|
||||
this.scene.field.remove(introVisuals);
|
||||
introVisuals.setVisible(false);
|
||||
introVisuals.destroy();
|
||||
this.scene.currentBattle.mysteryEncounter.introVisuals = null;
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will handle (in order):
|
||||
* - Setting BGM
|
||||
* - Showing intro dialogue for an enemy trainer or wild Pokemon
|
||||
* - Sliding in the visuals for enemy trainer or wild Pokemon, as well as handling summoning animations
|
||||
* - Queue the SummonPhases, PostSummonPhases, etc., required to initialize the phase queue for a battle
|
||||
*/
|
||||
export class MysteryEncounterBattlePhase extends Phase {
|
||||
constructor(scene: BattleScene) {
|
||||
super(scene);
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
this.doMysteryEncounterBattle(this.scene);
|
||||
}
|
||||
|
||||
getBattleMessage(scene: BattleScene): string {
|
||||
const enemyField = scene.getEnemyField();
|
||||
const encounterVariant = scene.currentBattle.mysteryEncounter.encounterVariant;
|
||||
|
||||
if (scene.currentBattle.battleSpec === BattleSpec.FINAL_BOSS) {
|
||||
return i18next.t("battle:bossAppeared", { bossName: enemyField[0].name });
|
||||
}
|
||||
|
||||
if (encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) {
|
||||
if (scene.currentBattle.double) {
|
||||
return i18next.t("battle:trainerAppearedDouble", { trainerName: scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) });
|
||||
|
||||
} else {
|
||||
return i18next.t("battle:trainerAppeared", { trainerName: scene.currentBattle.trainer.getName(TrainerSlot.NONE, true) });
|
||||
}
|
||||
}
|
||||
|
||||
return enemyField.length === 1
|
||||
? i18next.t("battle:singleWildAppeared", { pokemonName: enemyField[0].name })
|
||||
: i18next.t("battle:multiWildAppeared", { pokemonName1: enemyField[0].name, pokemonName2: enemyField[1].name });
|
||||
}
|
||||
|
||||
doMysteryEncounterBattle(scene: BattleScene) {
|
||||
const encounterVariant = scene.currentBattle.mysteryEncounter.encounterVariant;
|
||||
if (encounterVariant === MysteryEncounterVariant.WILD_BATTLE || encounterVariant === MysteryEncounterVariant.BOSS_BATTLE) {
|
||||
// Summons the wild/boss Pokemon
|
||||
if (encounterVariant === MysteryEncounterVariant.BOSS_BATTLE) {
|
||||
scene.playBgm(undefined);
|
||||
}
|
||||
const availablePartyMembers = scene.getEnemyParty().filter(p => !p.isFainted()).length;
|
||||
scene.unshiftPhase(new SummonPhase(scene, 0, false));
|
||||
if (scene.currentBattle.double && availablePartyMembers > 1) {
|
||||
scene.unshiftPhase(new SummonPhase(scene, 1, false));
|
||||
}
|
||||
|
||||
if (!scene.currentBattle.mysteryEncounter.hideBattleIntroMessage) {
|
||||
scene.ui.showText(this.getBattleMessage(scene), null, () => this.endBattleSetup(scene), 1500);
|
||||
} else {
|
||||
this.endBattleSetup(scene);
|
||||
}
|
||||
} else if (encounterVariant === MysteryEncounterVariant.TRAINER_BATTLE) {
|
||||
this.showEnemyTrainer();
|
||||
const doSummon = () => {
|
||||
scene.currentBattle.started = true;
|
||||
scene.playBgm(undefined);
|
||||
scene.pbTray.showPbTray(scene.getParty());
|
||||
scene.pbTrayEnemy.showPbTray(scene.getEnemyParty());
|
||||
const doTrainerSummon = () => {
|
||||
this.hideEnemyTrainer();
|
||||
const availablePartyMembers = scene.getEnemyParty().filter(p => !p.isFainted()).length;
|
||||
scene.unshiftPhase(new SummonPhase(scene, 0, false));
|
||||
if (scene.currentBattle.double && availablePartyMembers > 1) {
|
||||
scene.unshiftPhase(new SummonPhase(scene, 1, false));
|
||||
}
|
||||
this.endBattleSetup(scene);
|
||||
};
|
||||
if (!scene.currentBattle.mysteryEncounter.hideBattleIntroMessage) {
|
||||
scene.ui.showText(this.getBattleMessage(scene), null, doTrainerSummon, 1500, true);
|
||||
} else {
|
||||
doTrainerSummon();
|
||||
}
|
||||
};
|
||||
|
||||
const encounterMessages = scene.currentBattle.trainer.getEncounterMessages();
|
||||
|
||||
if (!encounterMessages?.length) {
|
||||
doSummon();
|
||||
} else {
|
||||
const trainer = this.scene.currentBattle.trainer;
|
||||
let message: string;
|
||||
scene.executeWithSeedOffset(() => message = Utils.randSeedItem(encounterMessages), scene.currentBattle.waveIndex * 1000);
|
||||
|
||||
const showDialogueAndSummon = () => {
|
||||
scene.ui.showDialogue(message, trainer.getName(TrainerSlot.NONE, true), null, () => {
|
||||
scene.charSprite.hide().then(() => scene.hideFieldOverlay(250).then(() => doSummon()));
|
||||
});
|
||||
};
|
||||
if (scene.currentBattle.trainer.config.hasCharSprite && !scene.ui.shouldSkipDialogue(message)) {
|
||||
scene.showFieldOverlay(500).then(() => scene.charSprite.showCharacter(trainer.getKey(), getCharVariantFromDialogue(encounterMessages[0])).then(() => showDialogueAndSummon()));
|
||||
} else {
|
||||
showDialogueAndSummon();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
endBattleSetup(scene: BattleScene) {
|
||||
const enemyField = scene.getEnemyField();
|
||||
const encounterVariant = scene.currentBattle.mysteryEncounter.encounterVariant;
|
||||
|
||||
if (encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE) {
|
||||
enemyField.map(p => this.scene.pushConditionalPhase(new PostSummonPhase(this.scene, p.getBattlerIndex()), () => {
|
||||
// if there is not a player party, we can't continue
|
||||
if (!this.scene.getParty()?.length) {
|
||||
return false;
|
||||
}
|
||||
// how many player pokemon are on the field ?
|
||||
const pokemonsOnFieldCount = this.scene.getParty().filter(p => p.isOnField()).length;
|
||||
// if it's a 2vs1, there will never be a 2nd pokemon on our field even
|
||||
const requiredPokemonsOnField = Math.min(this.scene.getParty().filter((p) => !p.isFainted()).length, 2);
|
||||
// if it's a double, there should be 2, otherwise 1
|
||||
if (this.scene.currentBattle.double) {
|
||||
return pokemonsOnFieldCount === requiredPokemonsOnField;
|
||||
}
|
||||
return pokemonsOnFieldCount === 1;
|
||||
}));
|
||||
const ivScannerModifier = this.scene.findModifier(m => m instanceof IvScannerModifier);
|
||||
if (ivScannerModifier) {
|
||||
enemyField.map(p => this.scene.pushPhase(new ScanIvsPhase(this.scene, p.getBattlerIndex(), Math.min(ivScannerModifier.getStackCount() * 2, 6))));
|
||||
}
|
||||
}
|
||||
|
||||
const availablePartyMembers = scene.getParty().filter(p => !p.isFainted());
|
||||
|
||||
if (!availablePartyMembers[0].isOnField()) {
|
||||
scene.pushPhase(new SummonPhase(scene, 0));
|
||||
}
|
||||
|
||||
if (scene.currentBattle.double) {
|
||||
if (availablePartyMembers.length > 1) {
|
||||
scene.pushPhase(new ToggleDoublePositionPhase(scene, true));
|
||||
if (!availablePartyMembers[1].isOnField()) {
|
||||
scene.pushPhase(new SummonPhase(scene, 1));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (availablePartyMembers.length > 1 && availablePartyMembers[1].isOnField()) {
|
||||
scene.pushPhase(new ReturnPhase(scene, 1));
|
||||
}
|
||||
scene.pushPhase(new ToggleDoublePositionPhase(scene, false));
|
||||
}
|
||||
|
||||
if (encounterVariant !== MysteryEncounterVariant.TRAINER_BATTLE && (scene.currentBattle.waveIndex > 1 || !scene.gameMode.isDaily)) {
|
||||
const minPartySize = scene.currentBattle.double ? 2 : 1;
|
||||
if (availablePartyMembers.length > minPartySize) {
|
||||
scene.pushPhase(new CheckSwitchPhase(scene, 0, scene.currentBattle.double));
|
||||
if (scene.currentBattle.double) {
|
||||
scene.pushPhase(new CheckSwitchPhase(scene, 1, scene.currentBattle.double));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: remove?
|
||||
handleTutorial(this.scene, Tutorial.Access_Menu).then(() => super.end());
|
||||
}
|
||||
|
||||
showEnemyTrainer(): void {
|
||||
// Show enemy trainer
|
||||
const trainer = this.scene.currentBattle.trainer;
|
||||
trainer.alpha = 0;
|
||||
trainer.x += 16;
|
||||
trainer.y -= 16;
|
||||
trainer.setVisible(true);
|
||||
this.scene.tweens.add({
|
||||
targets: trainer,
|
||||
x: "-=16",
|
||||
y: "+=16",
|
||||
alpha: 1,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 750,
|
||||
onComplete: () => {
|
||||
trainer.untint(100, "Sine.easeOut");
|
||||
trainer.playAnim();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
hideEnemyTrainer(): void {
|
||||
this.scene.tweens.add({
|
||||
targets: this.scene.currentBattle.trainer,
|
||||
x: "+=16",
|
||||
y: "-=16",
|
||||
alpha: 0,
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 750
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will handle (in order):
|
||||
* - Any encounter reward logic that is set within MysteryEncounter doEncounterRewards
|
||||
* - Otherwise, can add a no-reward-item shop with only Potions, etc. if addHealPhase is true
|
||||
* - Queuing of the PostMysteryEncounterPhase
|
||||
*/
|
||||
export class MysteryEncounterRewardsPhase extends Phase {
|
||||
addHealPhase: boolean;
|
||||
|
||||
constructor(scene: BattleScene, addHealPhase: boolean = false) {
|
||||
super(scene);
|
||||
this.addHealPhase = addHealPhase;
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
this.scene.executeWithSeedOffset(() => {
|
||||
if (this.scene.currentBattle.mysteryEncounter.doEncounterRewards) {
|
||||
this.scene.currentBattle.mysteryEncounter.doEncounterRewards(this.scene);
|
||||
} else if (this.addHealPhase) {
|
||||
this.scene.tryRemovePhase(p => p instanceof SelectModifierPhase);
|
||||
this.scene.unshiftPhase(new SelectModifierPhase(this.scene, 0, null, { fillRemaining: false, rerollMultiplier: 0}));
|
||||
}
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
|
||||
this.scene.pushPhase(new PostMysteryEncounterPhase(this.scene));
|
||||
this.end();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Will handle (in order):
|
||||
* - onPostOptionSelect logic (based on an option that was selected)
|
||||
* - Showing any outro dialogue messages
|
||||
* - Cleanup of any leftover intro visuals
|
||||
* - Queuing of the next wave
|
||||
*/
|
||||
export class PostMysteryEncounterPhase extends Phase {
|
||||
onPostOptionSelect: (scene: BattleScene) => Promise<void | boolean>;
|
||||
|
||||
constructor(scene: BattleScene) {
|
||||
super(scene);
|
||||
this.onPostOptionSelect = this.scene.currentBattle.mysteryEncounter.selectedOption.onPostOptionPhase;
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
if (this.onPostOptionSelect) {
|
||||
this.scene.executeWithSeedOffset(async () => {
|
||||
return await this.onPostOptionSelect(this.scene)
|
||||
.then((result) => {
|
||||
if (isNullOrUndefined(result) || result) {
|
||||
this.continueEncounter();
|
||||
}
|
||||
});
|
||||
}, this.scene.currentBattle.waveIndex * 1000);
|
||||
} else {
|
||||
this.continueEncounter();
|
||||
}
|
||||
}
|
||||
|
||||
continueEncounter() {
|
||||
const endPhase = () => {
|
||||
this.scene.pushPhase(new NewBattlePhase(this.scene));
|
||||
this.end();
|
||||
};
|
||||
|
||||
const outroDialogue = this.scene.currentBattle?.mysteryEncounter?.dialogue?.outro;
|
||||
if (outroDialogue?.length > 0) {
|
||||
let i = 0;
|
||||
const showNextDialogue = () => {
|
||||
const nextAction = i === outroDialogue.length - 1 ? endPhase : showNextDialogue;
|
||||
const dialogue = outroDialogue[i];
|
||||
let title: string = null;
|
||||
const text: string = getTextWithEncounterDialogueTokens(this.scene, dialogue.text);
|
||||
if (dialogue.speaker) {
|
||||
title = getTextWithEncounterDialogueTokens(this.scene, dialogue.speaker);
|
||||
}
|
||||
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
if (title) {
|
||||
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 750 : 0);
|
||||
} else {
|
||||
this.scene.ui.showText(text, null, nextAction, i === 0 ? 750 : 0, true);
|
||||
}
|
||||
i++;
|
||||
};
|
||||
|
||||
showNextDialogue();
|
||||
} else {
|
||||
endPhase();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,259 @@
|
|||
import {ModifierTier} from "#app/modifier/modifier-tier";
|
||||
import {
|
||||
CustomModifierSettings,
|
||||
FusePokemonModifierType, getPlayerModifierTypeOptions,
|
||||
getPlayerShopModifierTypeOptionsForWave, ModifierPoolType,
|
||||
ModifierType,
|
||||
ModifierTypeOption,
|
||||
PokemonModifierType,
|
||||
PokemonMoveModifierType,
|
||||
PokemonPpRestoreModifierType,
|
||||
PokemonPpUpModifierType,
|
||||
regenerateModifierPoolThresholds,
|
||||
RememberMoveModifierType,
|
||||
TmModifierType
|
||||
} from "#app/modifier/modifier-type";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import * as Utils from "#app/utils";
|
||||
import {ExtraModifierModifier, Modifier, PokemonHeldItemModifier} from "#app/modifier/modifier";
|
||||
import i18next from "#app/plugins/i18n";
|
||||
import {Mode} from "#app/ui/ui";
|
||||
import PartyUiHandler, {PartyOption, PartyUiMode} from "#app/ui/party-ui-handler";
|
||||
import ModifierSelectUiHandler, {SHOP_OPTIONS_ROW_LIMIT} from "#app/ui/modifier-select-ui-handler";
|
||||
import {BattlePhase} from "#app/phases/battle-phase";
|
||||
import {isNullOrUndefined} from "#app/utils";
|
||||
|
||||
export class SelectModifierPhase extends BattlePhase {
|
||||
private rerollCount: integer;
|
||||
private modifierTiers: ModifierTier[];
|
||||
private customModifierSettings: CustomModifierSettings;
|
||||
|
||||
constructor(scene: BattleScene, rerollCount: integer = 0, modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings) {
|
||||
super(scene);
|
||||
|
||||
this.rerollCount = rerollCount;
|
||||
this.modifierTiers = modifierTiers;
|
||||
this.customModifierSettings = customModifierSettings;
|
||||
}
|
||||
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
if (!this.rerollCount) {
|
||||
this.updateSeed();
|
||||
} else {
|
||||
this.scene.reroll = false;
|
||||
}
|
||||
|
||||
const party = this.scene.getParty();
|
||||
regenerateModifierPoolThresholds(party, this.getPoolType(), this.rerollCount);
|
||||
const modifierCount = new Utils.IntegerHolder(3);
|
||||
if (this.isPlayer()) {
|
||||
this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount);
|
||||
}
|
||||
|
||||
// If custom modifiers are specified, overrides default item count
|
||||
if (!!this.customModifierSettings) {
|
||||
const newItemCount = (this.customModifierSettings.guaranteedModifierTiers?.length || 0) +
|
||||
(this.customModifierSettings.guaranteedModifierTypeOptions?.length || 0) +
|
||||
(this.customModifierSettings.guaranteedModifierTypeFuncs?.length || 0);
|
||||
if (this.customModifierSettings.fillRemaining) {
|
||||
const originalCount = modifierCount.value;
|
||||
modifierCount.value = originalCount > newItemCount ? originalCount : newItemCount;
|
||||
} else {
|
||||
modifierCount.value = newItemCount;
|
||||
}
|
||||
}
|
||||
|
||||
const typeOptions: ModifierTypeOption[] = this.getModifierTypeOptions(modifierCount.value);
|
||||
|
||||
const modifierSelectCallback = (rowCursor: integer, cursor: integer) => {
|
||||
if (rowCursor < 0 || cursor < 0) {
|
||||
this.scene.ui.showText(i18next.t("battle:skipItemQuestion"), null, () => {
|
||||
this.scene.ui.setOverlayMode(Mode.CONFIRM, () => {
|
||||
this.scene.ui.revertMode();
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
super.end();
|
||||
}, () => this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers)));
|
||||
});
|
||||
return false;
|
||||
}
|
||||
let modifierType: ModifierType;
|
||||
let cost: integer;
|
||||
switch (rowCursor) {
|
||||
case 0:
|
||||
switch (cursor) {
|
||||
case 0:
|
||||
const rerollCost = this.getRerollCost(typeOptions, this.scene.lockModifierTiers);
|
||||
if (rerollCost === 0 || this.scene.money < rerollCost) {
|
||||
this.scene.ui.playError();
|
||||
return false;
|
||||
} else {
|
||||
this.scene.reroll = true;
|
||||
this.scene.unshiftPhase(new SelectModifierPhase(this.scene, this.rerollCount + 1, typeOptions.map(o => o.type.tier)));
|
||||
this.scene.ui.clearText();
|
||||
this.scene.ui.setMode(Mode.MESSAGE).then(() => super.end());
|
||||
this.scene.money -= rerollCost;
|
||||
this.scene.updateMoneyText();
|
||||
this.scene.animateMoneyChanged(false);
|
||||
this.scene.playSound("buy");
|
||||
}
|
||||
break;
|
||||
case 1:
|
||||
this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.MODIFIER_TRANSFER, -1, (fromSlotIndex: integer, itemIndex: integer, itemQuantity: integer, toSlotIndex: integer) => {
|
||||
if (toSlotIndex !== undefined && fromSlotIndex < 6 && toSlotIndex < 6 && fromSlotIndex !== toSlotIndex && itemIndex > -1) {
|
||||
const itemModifiers = this.scene.findModifiers(m => m instanceof PokemonHeldItemModifier
|
||||
&& (m as PokemonHeldItemModifier).getTransferrable(true) && (m as PokemonHeldItemModifier).pokemonId === party[fromSlotIndex].id) as PokemonHeldItemModifier[];
|
||||
const itemModifier = itemModifiers[itemIndex];
|
||||
this.scene.tryTransferHeldItemModifier(itemModifier, party[toSlotIndex], true, itemQuantity);
|
||||
} else {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
}
|
||||
}, PartyUiHandler.FilterItemMaxStacks);
|
||||
break;
|
||||
case 2:
|
||||
this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.CHECK, -1, () => {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
});
|
||||
break;
|
||||
case 3:
|
||||
this.scene.lockModifierTiers = !this.scene.lockModifierTiers;
|
||||
const uiHandler = this.scene.ui.getHandler() as ModifierSelectUiHandler;
|
||||
uiHandler.setRerollCost(this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
uiHandler.updateLockRaritiesText();
|
||||
uiHandler.updateRerollCostText();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
case 1:
|
||||
if (typeOptions.length === 0) {
|
||||
this.scene.ui.revertMode();
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
super.end();
|
||||
}
|
||||
modifierType = typeOptions[cursor].type;
|
||||
break;
|
||||
default:
|
||||
const shopOptions = getPlayerShopModifierTypeOptionsForWave(this.scene.currentBattle.waveIndex, this.scene.getWaveMoneyAmount(1));
|
||||
const shopOption = shopOptions[rowCursor > 2 || shopOptions.length <= SHOP_OPTIONS_ROW_LIMIT ? cursor : cursor + SHOP_OPTIONS_ROW_LIMIT];
|
||||
modifierType = shopOption.type;
|
||||
cost = shopOption.cost;
|
||||
break;
|
||||
}
|
||||
|
||||
if (cost && this.scene.money < cost) {
|
||||
this.scene.ui.playError();
|
||||
return false;
|
||||
}
|
||||
|
||||
const applyModifier = (modifier: Modifier, playSound: boolean = false) => {
|
||||
const result = this.scene.addModifier(modifier, false, playSound);
|
||||
if (cost) {
|
||||
result.then(success => {
|
||||
if (success) {
|
||||
this.scene.money -= cost;
|
||||
this.scene.updateMoneyText();
|
||||
this.scene.animateMoneyChanged(false);
|
||||
this.scene.playSound("buy");
|
||||
(this.scene.ui.getHandler() as ModifierSelectUiHandler).updateCostText();
|
||||
} else {
|
||||
this.scene.ui.playError();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
const doEnd = () => {
|
||||
this.scene.ui.clearText();
|
||||
this.scene.ui.setMode(Mode.MESSAGE);
|
||||
super.end();
|
||||
};
|
||||
if (result instanceof Promise) {
|
||||
result.then(() => doEnd());
|
||||
} else {
|
||||
doEnd();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (modifierType instanceof PokemonModifierType) {
|
||||
if (modifierType instanceof FusePokemonModifierType) {
|
||||
this.scene.ui.setModeWithoutClear(Mode.PARTY, PartyUiMode.SPLICE, -1, (fromSlotIndex: integer, spliceSlotIndex: integer) => {
|
||||
if (spliceSlotIndex !== undefined && fromSlotIndex < 6 && spliceSlotIndex < 6 && fromSlotIndex !== spliceSlotIndex) {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer()).then(() => {
|
||||
const modifier = modifierType.newModifier(party[fromSlotIndex], party[spliceSlotIndex]);
|
||||
applyModifier(modifier, true);
|
||||
});
|
||||
} else {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
}
|
||||
}, modifierType.selectFilter);
|
||||
} else {
|
||||
const pokemonModifierType = modifierType as PokemonModifierType;
|
||||
const isMoveModifier = modifierType instanceof PokemonMoveModifierType;
|
||||
const isTmModifier = modifierType instanceof TmModifierType;
|
||||
const isRememberMoveModifier = modifierType instanceof RememberMoveModifierType;
|
||||
const isPpRestoreModifier = (modifierType instanceof PokemonPpRestoreModifierType || modifierType instanceof PokemonPpUpModifierType);
|
||||
const partyUiMode = isMoveModifier ? PartyUiMode.MOVE_MODIFIER
|
||||
: isTmModifier ? PartyUiMode.TM_MODIFIER
|
||||
: isRememberMoveModifier ? PartyUiMode.REMEMBER_MOVE_MODIFIER
|
||||
: PartyUiMode.MODIFIER;
|
||||
const tmMoveId = isTmModifier
|
||||
? (modifierType as TmModifierType).moveId
|
||||
: undefined;
|
||||
this.scene.ui.setModeWithoutClear(Mode.PARTY, partyUiMode, -1, (slotIndex: integer, option: PartyOption) => {
|
||||
if (slotIndex < 6) {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer()).then(() => {
|
||||
const modifier = !isMoveModifier
|
||||
? !isRememberMoveModifier
|
||||
? modifierType.newModifier(party[slotIndex])
|
||||
: modifierType.newModifier(party[slotIndex], option as integer)
|
||||
: modifierType.newModifier(party[slotIndex], option - PartyOption.MOVE_1);
|
||||
applyModifier(modifier, true);
|
||||
});
|
||||
} else {
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
}
|
||||
}, pokemonModifierType.selectFilter, modifierType instanceof PokemonMoveModifierType ? (modifierType as PokemonMoveModifierType).moveSelectFilter : undefined, tmMoveId, isPpRestoreModifier);
|
||||
}
|
||||
} else {
|
||||
applyModifier(modifierType.newModifier());
|
||||
}
|
||||
|
||||
return !cost;
|
||||
};
|
||||
this.scene.ui.setMode(Mode.MODIFIER_SELECT, this.isPlayer(), typeOptions, modifierSelectCallback, this.getRerollCost(typeOptions, this.scene.lockModifierTiers));
|
||||
}
|
||||
|
||||
updateSeed(): void {
|
||||
this.scene.resetSeed();
|
||||
}
|
||||
|
||||
isPlayer(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
||||
getRerollCost(typeOptions: ModifierTypeOption[], lockRarities: boolean): integer {
|
||||
let baseValue = 0;
|
||||
if (lockRarities) {
|
||||
const tierValues = [50, 125, 300, 750, 2000];
|
||||
for (const opt of typeOptions) {
|
||||
baseValue += tierValues[opt.type.tier];
|
||||
}
|
||||
} else {
|
||||
baseValue = 250;
|
||||
}
|
||||
const multiplier = !isNullOrUndefined(this.customModifierSettings?.rerollMultiplier) ? this.customModifierSettings.rerollMultiplier : 1;
|
||||
return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER);
|
||||
}
|
||||
|
||||
getPoolType(): ModifierPoolType {
|
||||
return ModifierPoolType.PLAYER;
|
||||
}
|
||||
|
||||
getModifierTypeOptions(modifierCount: integer): ModifierTypeOption[] {
|
||||
return getPlayerModifierTypeOptions(modifierCount, this.scene.getParty(), this.scene.lockModifierTiers ? this.modifierTiers : undefined, this.customModifierSettings);
|
||||
}
|
||||
|
||||
addModifier(modifier: Modifier): Promise<boolean> {
|
||||
return this.scene.addModifier(modifier, false, true);
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@ import Pokemon from "../field/pokemon";
|
|||
import Trainer from "../field/trainer";
|
||||
import FieldSpritePipeline from "./field-sprite";
|
||||
import * as Utils from "../utils";
|
||||
import MysteryEncounterIntroVisuals from "../field/mystery-encounter-intro";
|
||||
|
||||
const spriteFragShader = `
|
||||
#ifdef GL_FRAGMENT_PRECISION_HIGH
|
||||
|
@ -353,7 +354,7 @@ export default class SpritePipeline extends FieldSpritePipeline {
|
|||
const ignoreFieldPos = data["ignoreFieldPos"] as boolean;
|
||||
const ignoreOverride = data["ignoreOverride"] as boolean;
|
||||
|
||||
const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer;
|
||||
const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals;
|
||||
const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer;
|
||||
const position = isEntityObj
|
||||
? [ sprite.parentContainer.x, sprite.parentContainer.y ]
|
||||
|
@ -448,7 +449,7 @@ export default class SpritePipeline extends FieldSpritePipeline {
|
|||
|
||||
const hasShadow = sprite.pipelineData["hasShadow"] as boolean;
|
||||
if (hasShadow) {
|
||||
const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer;
|
||||
const isEntityObj = sprite.parentContainer instanceof Pokemon || sprite.parentContainer instanceof Trainer || sprite.parentContainer instanceof MysteryEncounterIntroVisuals;
|
||||
const field = isEntityObj ? sprite.parentContainer.parentContainer : sprite.parentContainer;
|
||||
const fieldScaleRatio = field.scale / 6;
|
||||
const baseY = (isEntityObj
|
||||
|
|
|
@ -40,6 +40,8 @@ import { GameDataType } from "#enums/game-data-type";
|
|||
import { Moves } from "#enums/moves";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
import { Species } from "#enums/species";
|
||||
import { MysteryEncounterFlags } from "../data/mystery-encounter-flags";
|
||||
import MysteryEncounter from "../data/mystery-encounter";
|
||||
|
||||
export const defaultStarterSpecies: Species[] = [
|
||||
Species.BULBASAUR, Species.CHARMANDER, Species.SQUIRTLE,
|
||||
|
@ -76,13 +78,13 @@ export function getDataTypeKey(dataType: GameDataType, slotId: integer = 0): str
|
|||
|
||||
export function encrypt(data: string, bypassLogin: boolean): string {
|
||||
return (bypassLogin
|
||||
? (data: string) => btoa(data)
|
||||
? (data: string) => btoa(encodeURIComponent(data))
|
||||
: (data: string) => AES.encrypt(data, saveKey))(data);
|
||||
}
|
||||
|
||||
export function decrypt(data: string, bypassLogin: boolean): string {
|
||||
return (bypassLogin
|
||||
? (data: string) => atob(data)
|
||||
? (data: string) => decodeURIComponent(atob(data))
|
||||
: (data: string) => AES.decrypt(data, saveKey).toString(enc.Utf8))(data);
|
||||
}
|
||||
|
||||
|
@ -122,6 +124,8 @@ export interface SessionSaveData {
|
|||
gameVersion: string;
|
||||
timestamp: integer;
|
||||
challenges: ChallengeData[];
|
||||
mysteryEncounter: MysteryEncounter;
|
||||
mysteryEncounterFlags: MysteryEncounterFlags;
|
||||
}
|
||||
|
||||
interface Unlocks {
|
||||
|
@ -836,7 +840,9 @@ export class GameData {
|
|||
trainer: scene.currentBattle.battleType === BattleType.TRAINER ? new TrainerData(scene.currentBattle.trainer) : null,
|
||||
gameVersion: scene.game.config.gameVersion,
|
||||
timestamp: new Date().getTime(),
|
||||
challenges: scene.gameMode.challenges.map(c => new ChallengeData(c))
|
||||
challenges: scene.gameMode.challenges.map(c => new ChallengeData(c)),
|
||||
mysteryEncounter: scene.currentBattle.mysteryEncounter,
|
||||
mysteryEncounterFlags: scene.mysteryEncounterFlags
|
||||
} as SessionSaveData;
|
||||
}
|
||||
|
||||
|
@ -927,11 +933,14 @@ export class GameData {
|
|||
scene.score = sessionData.score;
|
||||
scene.updateScoreText();
|
||||
|
||||
scene.mysteryEncounterFlags = sessionData?.mysteryEncounterFlags ? sessionData?.mysteryEncounterFlags : new MysteryEncounterFlags(null);
|
||||
|
||||
scene.newArena(sessionData.arena.biome);
|
||||
|
||||
const battleType = sessionData.battleType || 0;
|
||||
const trainerConfig = sessionData.trainer ? trainerConfigs[sessionData.trainer.trainerType] : null;
|
||||
const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1);
|
||||
const mysteryEncounterConfig = sessionData?.mysteryEncounter;
|
||||
const battle = scene.newBattle(sessionData.waveIndex, battleType, sessionData.trainer, battleType === BattleType.TRAINER ? trainerConfig?.doubleOnly || sessionData.trainer?.variant === TrainerVariant.DOUBLE : sessionData.enemyParty.length > 1, mysteryEncounterConfig);
|
||||
battle.enemyLevels = sessionData.enemyParty.map(p => p.level);
|
||||
|
||||
scene.arena.init();
|
||||
|
@ -1145,6 +1154,14 @@ export class GameData {
|
|||
return ret;
|
||||
}
|
||||
|
||||
if (k === "mysteryEncounter") {
|
||||
return new MysteryEncounter(v);
|
||||
}
|
||||
|
||||
if (k === "mysteryEncounterFlags") {
|
||||
return new MysteryEncounterFlags(v);
|
||||
}
|
||||
|
||||
return v;
|
||||
}) as SessionSaveData;
|
||||
}
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
EnemyCommandPhase,
|
||||
LoginPhase,
|
||||
SelectGenderPhase,
|
||||
SelectModifierPhase,
|
||||
SelectStarterPhase,
|
||||
SummonPhase,
|
||||
TitlePhase,
|
||||
|
@ -24,6 +23,7 @@ import { Abilities } from "#enums/abilities";
|
|||
import { Moves } from "#enums/moves";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
import { Species } from "#enums/species";
|
||||
import {SelectModifierPhase} from "#app/phases/select-modifier-phase";
|
||||
|
||||
describe("Test Battle Phase", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
|
|
|
@ -0,0 +1,208 @@
|
|||
import {beforeAll, describe, expect, it} from "vitest";
|
||||
import { getBattleStatName, getBattleStatLevelChangeDescription } from "#app/data/battle-stat.js";
|
||||
import { BattleStat } from "#app/data/battle-stat.js";
|
||||
import { pokemonInfo as enPokemonInfo } from "#app/locales/en/pokemon-info.js";
|
||||
import { battle as enBattleStat } from "#app/locales/en/battle.js";
|
||||
import { pokemonInfo as dePokemonInfo } from "#app/locales/de/pokemon-info.js";
|
||||
import { battle as deBattleStat } from "#app/locales/de/battle.js";
|
||||
import { pokemonInfo as esPokemonInfo } from "#app/locales/es/pokemon-info.js";
|
||||
import { battle as esBattleStat } from "#app/locales/es/battle.js";
|
||||
import { pokemonInfo as frPokemonInfo } from "#app/locales/fr/pokemon-info.js";
|
||||
import { battle as frBattleStat } from "#app/locales/fr/battle.js";
|
||||
import { pokemonInfo as itPokemonInfo } from "#app/locales/it/pokemon-info.js";
|
||||
import { battle as itBattleStat } from "#app/locales/it/battle.js";
|
||||
import { pokemonInfo as koPokemonInfo } from "#app/locales/ko/pokemon-info.js";
|
||||
import { battle as koBattleStat } from "#app/locales/ko/battle.js";
|
||||
import { pokemonInfo as ptBrPokemonInfo } from "#app/locales/pt_BR/pokemon-info.js";
|
||||
import { battle as ptBrBattleStat } from "#app/locales/pt_BR/battle.js";
|
||||
import { pokemonInfo as zhCnPokemonInfo } from "#app/locales/zh_CN/pokemon-info.js";
|
||||
import { battle as zhCnBattleStat } from "#app/locales/zh_CN/battle.js";
|
||||
import { pokemonInfo as zhTwPokemonInfo } from "#app/locales/zh_TW/pokemon-info.js";
|
||||
import { battle as zhTwBattleStat } from "#app/locales/zh_TW/battle.js";
|
||||
|
||||
import i18next, {initI18n} from "#app/plugins/i18n";
|
||||
import { KoreanPostpositionProcessor } from "i18next-korean-postposition-processor";
|
||||
|
||||
interface BattleStatTestUnit {
|
||||
stat: BattleStat,
|
||||
key: string
|
||||
}
|
||||
|
||||
interface BattleStatLevelTestUnit {
|
||||
levels: integer,
|
||||
up: boolean,
|
||||
key: string
|
||||
}
|
||||
|
||||
function testBattleStatName(stat: BattleStat, expectMessage: string) {
|
||||
const message = getBattleStatName(stat);
|
||||
console.log(`message ${message}, expected ${expectMessage}`);
|
||||
expect(message).toBe(expectMessage);
|
||||
}
|
||||
|
||||
function testBattleStatLevelChangeDescription(levels: integer, up: boolean, expectMessage: string) {
|
||||
const message = getBattleStatLevelChangeDescription("{{pokemonNameWithAffix}}", "{{stats}}", levels, up);
|
||||
console.log(`message ${message}, expected ${expectMessage}`);
|
||||
expect(message).toBe(expectMessage);
|
||||
}
|
||||
|
||||
describe("Test for BattleStat Localization", () => {
|
||||
const battleStatUnits: BattleStatTestUnit[] = [];
|
||||
const battleStatLevelUnits: BattleStatLevelTestUnit[] = [];
|
||||
|
||||
beforeAll(() => {
|
||||
initI18n();
|
||||
|
||||
battleStatUnits.push({ stat: BattleStat.ATK, key: "Stat.ATK" });
|
||||
battleStatUnits.push({ stat: BattleStat.DEF, key: "Stat.DEF" });
|
||||
battleStatUnits.push({ stat: BattleStat.SPATK, key: "Stat.SPATK" });
|
||||
battleStatUnits.push({ stat: BattleStat.SPDEF, key: "Stat.SPDEF" });
|
||||
battleStatUnits.push({ stat: BattleStat.SPD, key: "Stat.SPD" });
|
||||
battleStatUnits.push({ stat: BattleStat.ACC, key: "Stat.ACC" });
|
||||
battleStatUnits.push({ stat: BattleStat.EVA, key: "Stat.EVA" });
|
||||
|
||||
battleStatLevelUnits.push({ levels: 1, up: true, key: "statRose" });
|
||||
battleStatLevelUnits.push({ levels: 2, up: true, key: "statSharplyRose" });
|
||||
battleStatLevelUnits.push({ levels: 3, up: true, key: "statRoseDrastically" });
|
||||
battleStatLevelUnits.push({ levels: 4, up: true, key: "statRoseDrastically" });
|
||||
battleStatLevelUnits.push({ levels: 5, up: true, key: "statRoseDrastically" });
|
||||
battleStatLevelUnits.push({ levels: 6, up: true, key: "statRoseDrastically" });
|
||||
battleStatLevelUnits.push({ levels: 7, up: true, key: "statWontGoAnyHigher" });
|
||||
battleStatLevelUnits.push({ levels: 1, up: false, key: "statFell" });
|
||||
battleStatLevelUnits.push({ levels: 2, up: false, key: "statHarshlyFell" });
|
||||
battleStatLevelUnits.push({ levels: 3, up: false, key: "statSeverelyFell" });
|
||||
battleStatLevelUnits.push({ levels: 4, up: false, key: "statSeverelyFell" });
|
||||
battleStatLevelUnits.push({ levels: 5, up: false, key: "statSeverelyFell" });
|
||||
battleStatLevelUnits.push({ levels: 6, up: false, key: "statSeverelyFell" });
|
||||
battleStatLevelUnits.push({ levels: 7, up: false, key: "statWontGoAnyLower" });
|
||||
});
|
||||
|
||||
it("Test getBattleStatName() in English", async () => {
|
||||
i18next.changeLanguage("en");
|
||||
battleStatUnits.forEach(unit => {
|
||||
testBattleStatName(unit.stat, enPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatLevelChangeDescription() in English", async () => {
|
||||
i18next.changeLanguage("en");
|
||||
battleStatLevelUnits.forEach(unit => {
|
||||
testBattleStatLevelChangeDescription(unit.levels, unit.up, enBattleStat[unit.key]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatName() in Español", async () => {
|
||||
i18next.changeLanguage("es");
|
||||
battleStatUnits.forEach(unit => {
|
||||
testBattleStatName(unit.stat, esPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatLevelChangeDescription() in Español", async () => {
|
||||
i18next.changeLanguage("es");
|
||||
battleStatLevelUnits.forEach(unit => {
|
||||
testBattleStatLevelChangeDescription(unit.levels, unit.up, esBattleStat[unit.key]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatName() in Italiano", async () => {
|
||||
i18next.changeLanguage("it");
|
||||
battleStatUnits.forEach(unit => {
|
||||
testBattleStatName(unit.stat, itPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatLevelChangeDescription() in Italiano", async () => {
|
||||
i18next.changeLanguage("it");
|
||||
battleStatLevelUnits.forEach(unit => {
|
||||
testBattleStatLevelChangeDescription(unit.levels, unit.up, itBattleStat[unit.key]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatName() in Français", async () => {
|
||||
i18next.changeLanguage("fr");
|
||||
battleStatUnits.forEach(unit => {
|
||||
testBattleStatName(unit.stat, frPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatLevelChangeDescription() in Français", async () => {
|
||||
i18next.changeLanguage("fr");
|
||||
battleStatLevelUnits.forEach(unit => {
|
||||
testBattleStatLevelChangeDescription(unit.levels, unit.up, frBattleStat[unit.key]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatName() in Deutsch", async () => {
|
||||
i18next.changeLanguage("de");
|
||||
battleStatUnits.forEach(unit => {
|
||||
testBattleStatName(unit.stat, dePokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatLevelChangeDescription() in Deutsch", async () => {
|
||||
i18next.changeLanguage("de");
|
||||
battleStatLevelUnits.forEach(unit => {
|
||||
testBattleStatLevelChangeDescription(unit.levels, unit.up, deBattleStat[unit.key]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatName() in Português (BR)", async () => {
|
||||
i18next.changeLanguage("pt-BR");
|
||||
battleStatUnits.forEach(unit => {
|
||||
testBattleStatName(unit.stat, ptBrPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatLevelChangeDescription() in Português (BR)", async () => {
|
||||
i18next.changeLanguage("pt-BR");
|
||||
battleStatLevelUnits.forEach(unit => {
|
||||
testBattleStatLevelChangeDescription(unit.levels, unit.up, ptBrBattleStat[unit.key]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatName() in 简体中文", async () => {
|
||||
i18next.changeLanguage("zh-CN");
|
||||
battleStatUnits.forEach(unit => {
|
||||
testBattleStatName(unit.stat, zhCnPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatLevelChangeDescription() in 简体中文", async () => {
|
||||
i18next.changeLanguage("zh-CN");
|
||||
battleStatLevelUnits.forEach(unit => {
|
||||
testBattleStatLevelChangeDescription(unit.levels, unit.up, zhCnBattleStat[unit.key]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatName() in 繁體中文", async () => {
|
||||
i18next.changeLanguage("zh-TW");
|
||||
battleStatUnits.forEach(unit => {
|
||||
testBattleStatName(unit.stat, zhTwPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatLevelChangeDescription() in 繁體中文", async () => {
|
||||
i18next.changeLanguage("zh-TW");
|
||||
battleStatLevelUnits.forEach(unit => {
|
||||
testBattleStatLevelChangeDescription(unit.levels, unit.up, zhTwBattleStat[unit.key]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatName() in 한국어", async () => {
|
||||
await i18next.changeLanguage("ko");
|
||||
battleStatUnits.forEach(unit => {
|
||||
testBattleStatName(unit.stat, koPokemonInfo[unit.key.split(".")[0]][unit.key.split(".")[1]]);
|
||||
});
|
||||
});
|
||||
|
||||
it("Test getBattleStatLevelChangeDescription() in 한국어", async () => {
|
||||
i18next.changeLanguage("ko", () => {
|
||||
battleStatLevelUnits.forEach(unit => {
|
||||
const processor = new KoreanPostpositionProcessor();
|
||||
const message = processor.process(koBattleStat[unit.key]);
|
||||
testBattleStatLevelChangeDescription(unit.levels, unit.up, message);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,42 @@
|
|||
import {afterEach, beforeAll, describe, expect, it} from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import {Species} from "#enums/species";
|
||||
import i18next from "i18next";
|
||||
import {initI18n} from "#app/plugins/i18n";
|
||||
|
||||
describe("Lokalization - french", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
initI18n();
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
it("test bulbasaur name english", async () => {
|
||||
game = new GameManager(phaserGame);
|
||||
await game.startBattle([
|
||||
Species.BULBASAUR,
|
||||
]);
|
||||
expect(game.scene.getParty()[0].name).toBe("Bulbasaur");
|
||||
}, 20000);
|
||||
|
||||
it("test bulbasaure name french", async () => {
|
||||
const locale = "fr";
|
||||
i18next.changeLanguage(locale);
|
||||
localStorage.setItem("prLang", locale);
|
||||
game = new GameManager(phaserGame);
|
||||
|
||||
await game.startBattle([
|
||||
Species.BULBASAUR,
|
||||
]);
|
||||
expect(game.scene.getParty()[0].name).toBe("Bulbizarre");
|
||||
}, 20000);
|
||||
});
|
|
@ -0,0 +1,300 @@
|
|||
import { beforeAll, describe, afterEach, expect, it, vi } from "vitest";
|
||||
import {
|
||||
StatusEffect,
|
||||
getStatusEffectActivationText,
|
||||
getStatusEffectDescriptor,
|
||||
getStatusEffectHealText,
|
||||
getStatusEffectObtainText,
|
||||
getStatusEffectOverlapText,
|
||||
} from "#app/data/status-effect";
|
||||
import i18next from "i18next";
|
||||
import { mockI18next } from "../utils/testUtils";
|
||||
|
||||
const pokemonName = "PKM";
|
||||
const sourceText = "SOURCE";
|
||||
|
||||
describe("status-effect", () => {
|
||||
beforeAll(() => {
|
||||
i18next.init();
|
||||
});
|
||||
|
||||
describe("NONE", () => {
|
||||
const statusEffect = StatusEffect.NONE;
|
||||
|
||||
it("should return the obtain text", () => {
|
||||
mockI18next();
|
||||
|
||||
const text = getStatusEffectObtainText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:none.obtain");
|
||||
|
||||
const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, "");
|
||||
expect(emptySourceText).toBe("statusEffect:none.obtain");
|
||||
});
|
||||
|
||||
it("should return the source-obtain text", () => {
|
||||
mockI18next();
|
||||
|
||||
const text = getStatusEffectObtainText(statusEffect, pokemonName, sourceText);
|
||||
expect(text).toBe("statusEffect:none.obtainSource");
|
||||
|
||||
const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, "");
|
||||
expect(emptySourceText).not.toBe("statusEffect:none.obtainSource");
|
||||
});
|
||||
|
||||
it("should return the activation text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectActivationText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:none.activation");
|
||||
});
|
||||
|
||||
it("should return the overlap text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectOverlapText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:none.overlap");
|
||||
});
|
||||
|
||||
it("should return the heal text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectHealText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:none.heal");
|
||||
});
|
||||
|
||||
it("should return the descriptor", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectDescriptor(statusEffect);
|
||||
expect(text).toBe("statusEffect:none.description");
|
||||
});
|
||||
});
|
||||
|
||||
describe("POISON", () => {
|
||||
const statusEffect = StatusEffect.POISON;
|
||||
|
||||
it("should return the obtain text", () => {
|
||||
mockI18next();
|
||||
|
||||
const text = getStatusEffectObtainText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:poison.obtain");
|
||||
|
||||
const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, "");
|
||||
expect(emptySourceText).toBe("statusEffect:poison.obtain");
|
||||
});
|
||||
|
||||
it("should return the activation text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectActivationText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:poison.activation");
|
||||
});
|
||||
|
||||
it("should return the descriptor", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectDescriptor(statusEffect);
|
||||
expect(text).toBe("statusEffect:poison.description");
|
||||
});
|
||||
|
||||
it("should return the heal text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectHealText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:poison.heal");
|
||||
});
|
||||
|
||||
it("should return the overlap text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectOverlapText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:poison.overlap");
|
||||
});
|
||||
});
|
||||
|
||||
describe("TOXIC", () => {
|
||||
const statusEffect = StatusEffect.TOXIC;
|
||||
|
||||
it("should return the obtain text", () => {
|
||||
mockI18next();
|
||||
|
||||
const text = getStatusEffectObtainText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:toxic.obtain");
|
||||
|
||||
const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, "");
|
||||
expect(emptySourceText).toBe("statusEffect:toxic.obtain");
|
||||
});
|
||||
|
||||
it("should return the activation text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectActivationText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:toxic.activation");
|
||||
});
|
||||
|
||||
it("should return the descriptor", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectDescriptor(statusEffect);
|
||||
expect(text).toBe("statusEffect:toxic.description");
|
||||
});
|
||||
|
||||
it("should return the heal text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectHealText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:toxic.heal");
|
||||
});
|
||||
|
||||
it("should return the overlap text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectOverlapText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:toxic.overlap");
|
||||
});
|
||||
});
|
||||
|
||||
describe("PARALYSIS", () => {
|
||||
const statusEffect = StatusEffect.PARALYSIS;
|
||||
|
||||
it("should return the obtain text", () => {
|
||||
mockI18next();
|
||||
|
||||
const text = getStatusEffectObtainText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:paralysis.obtain");
|
||||
|
||||
const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, "");
|
||||
expect(emptySourceText).toBe("statusEffect:paralysis.obtain");
|
||||
});
|
||||
|
||||
it("should return the activation text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectActivationText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:paralysis.activation");
|
||||
});
|
||||
|
||||
it("should return the descriptor", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectDescriptor(statusEffect);
|
||||
expect(text).toBe("statusEffect:paralysis.description");
|
||||
});
|
||||
|
||||
it("should return the heal text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectHealText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:paralysis.heal");
|
||||
});
|
||||
|
||||
it("should return the overlap text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectOverlapText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:paralysis.overlap");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SLEEP", () => {
|
||||
const statusEffect = StatusEffect.SLEEP;
|
||||
|
||||
it("should return the obtain text", () => {
|
||||
mockI18next();
|
||||
|
||||
const text = getStatusEffectObtainText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:sleep.obtain");
|
||||
|
||||
const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, "");
|
||||
expect(emptySourceText).toBe("statusEffect:sleep.obtain");
|
||||
});
|
||||
|
||||
it("should return the activation text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectActivationText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:sleep.activation");
|
||||
});
|
||||
|
||||
it("should return the descriptor", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectDescriptor(statusEffect);
|
||||
expect(text).toBe("statusEffect:sleep.description");
|
||||
});
|
||||
|
||||
it("should return the heal text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectHealText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:sleep.heal");
|
||||
});
|
||||
|
||||
it("should return the overlap text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectOverlapText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:sleep.overlap");
|
||||
});
|
||||
});
|
||||
|
||||
describe("FREEZE", () => {
|
||||
const statusEffect = StatusEffect.FREEZE;
|
||||
|
||||
it("should return the obtain text", () => {
|
||||
mockI18next();
|
||||
|
||||
const text = getStatusEffectObtainText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:freeze.obtain");
|
||||
|
||||
const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, "");
|
||||
expect(emptySourceText).toBe("statusEffect:freeze.obtain");
|
||||
});
|
||||
|
||||
it("should return the activation text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectActivationText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:freeze.activation");
|
||||
});
|
||||
|
||||
it("should return the descriptor", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectDescriptor(statusEffect);
|
||||
expect(text).toBe("statusEffect:freeze.description");
|
||||
});
|
||||
|
||||
it("should return the heal text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectHealText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:freeze.heal");
|
||||
});
|
||||
|
||||
it("should return the overlap text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectOverlapText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:freeze.overlap");
|
||||
});
|
||||
});
|
||||
|
||||
describe("BURN", () => {
|
||||
const statusEffect = StatusEffect.BURN;
|
||||
|
||||
it("should return the obtain text", () => {
|
||||
mockI18next();
|
||||
|
||||
const text = getStatusEffectObtainText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:burn.obtain");
|
||||
|
||||
const emptySourceText = getStatusEffectObtainText(statusEffect, pokemonName, "");
|
||||
expect(emptySourceText).toBe("statusEffect:burn.obtain");
|
||||
});
|
||||
|
||||
it("should return the activation text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectActivationText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:burn.activation");
|
||||
});
|
||||
|
||||
it("should return the descriptor", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectDescriptor(statusEffect);
|
||||
expect(text).toBe("statusEffect:burn.description");
|
||||
});
|
||||
|
||||
it("should return the heal text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectHealText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:burn.heal");
|
||||
});
|
||||
|
||||
it("should return the overlap text", () => {
|
||||
mockI18next();
|
||||
const text = getStatusEffectOverlapText(statusEffect, pokemonName);
|
||||
expect(text).toBe("statusEffect:burn.overlap");
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,46 @@
|
|||
import {afterEach, beforeAll, beforeEach, describe, it, vi} from "vitest";
|
||||
import * as overrides from "../../overrides";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import Phaser from "phaser";
|
||||
|
||||
describe("Mystery Encounter", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
vi.spyOn(overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(64);
|
||||
vi.spyOn(overrides, "STARTING_WAVE_OVERRIDE", "get").mockReturnValue(3);
|
||||
});
|
||||
|
||||
it("spawns a mystery encounter", async() => {
|
||||
// await game.runToSummon([
|
||||
// Species.CHARIZARD,
|
||||
// Species.VOLCARONA
|
||||
// ]);
|
||||
// expect(game.scene.getCurrentPhase().constructor.name).toBe(EncounterPhase.name);
|
||||
}, 100000);
|
||||
|
||||
it("spawns mysterious challengers encounter", async() => {
|
||||
});
|
||||
|
||||
it("spawns mysterious chest encounter", async() => {
|
||||
});
|
||||
|
||||
it("spawns dark deal encounter", async() => {
|
||||
});
|
||||
|
||||
it("spawns fight or flight encounter", async() => {
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,224 @@
|
|||
import {afterEach, beforeAll, beforeEach, describe, expect, it, vi} from "vitest";
|
||||
import Phaser from "phaser";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import {initSceneWithoutEncounterPhase} from "#app/test/utils/gameManagerUtils";
|
||||
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
|
||||
import {ModifierTier} from "#app/modifier/modifier-tier";
|
||||
import * as Utils from "#app/utils";
|
||||
import {CustomModifierSettings, ModifierTypeOption, modifierTypes} from "#app/modifier/modifier-type";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import {SelectModifierPhase} from "#app/phases/select-modifier-phase";
|
||||
import {Species} from "#enums/species";
|
||||
import {Mode} from "#app/ui/ui";
|
||||
import {PlayerPokemon} from "#app/field/pokemon";
|
||||
import {getPokemonSpecies} from "#app/data/pokemon-species";
|
||||
|
||||
describe("SelectModifierPhase", () => {
|
||||
let phaserGame: Phaser.Game;
|
||||
let game: GameManager;
|
||||
let scene: BattleScene;
|
||||
|
||||
beforeAll(() => {
|
||||
phaserGame = new Phaser.Game({
|
||||
type: Phaser.HEADLESS,
|
||||
});
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
game = new GameManager(phaserGame);
|
||||
scene = game.scene;
|
||||
|
||||
vi.spyOn(scene, "resetSeed").mockImplementation(() => {
|
||||
scene.waveSeed = "test";
|
||||
Phaser.Math.RND.sow([ scene.waveSeed ]);
|
||||
scene.rngCounter = 0;
|
||||
});
|
||||
|
||||
initSceneWithoutEncounterPhase(scene, [Species.ABRA, Species.VOLCARONA]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
game.phaseInterceptor.restoreOg();
|
||||
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should start a select modifier phase", async () => {
|
||||
const selectModifierPhase = new SelectModifierPhase(scene);
|
||||
scene.pushPhase(selectModifierPhase);
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
}, 20000);
|
||||
|
||||
it("should generate random modifiers", async () => {
|
||||
const selectModifierPhase = new SelectModifierPhase(scene);
|
||||
scene.pushPhase(selectModifierPhase);
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
|
||||
expect(modifierSelectHandler.options.length).toEqual(3);
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("TEMP_STAT_BOOSTER");
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("POKEBALL");
|
||||
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("BERRY");
|
||||
}, 5000);
|
||||
|
||||
it("should modify reroll cost", async () => {
|
||||
const options = [
|
||||
new ModifierTypeOption(modifierTypes.POTION(), 0, 100),
|
||||
new ModifierTypeOption(modifierTypes.ETHER(), 0, 400),
|
||||
new ModifierTypeOption(modifierTypes.REVIVE(), 0, 1000)
|
||||
];
|
||||
|
||||
const selectModifierPhase1 = new SelectModifierPhase(scene);
|
||||
const selectModifierPhase2 = new SelectModifierPhase(scene, 0, null, { rerollMultiplier: 2});
|
||||
|
||||
const cost1 = selectModifierPhase1.getRerollCost(options, false);
|
||||
const cost2 = selectModifierPhase2.getRerollCost(options, false);
|
||||
expect(cost2).toEqual(cost1 * 2);
|
||||
}, 5000);
|
||||
|
||||
it("should generate random modifiers from reroll", async () => {
|
||||
let selectModifierPhase = new SelectModifierPhase(scene);
|
||||
scene.pushPhase(selectModifierPhase);
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
|
||||
expect(modifierSelectHandler.options.length).toEqual(3);
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("TEMP_STAT_BOOSTER");
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("POKEBALL");
|
||||
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("BERRY");
|
||||
|
||||
// Simulate selecting reroll
|
||||
selectModifierPhase = new SelectModifierPhase(scene, 1, [ModifierTier.COMMON, ModifierTier.COMMON, ModifierTier.COMMON]);
|
||||
scene.unshiftPhase(selectModifierPhase);
|
||||
scene.ui.setMode(Mode.MESSAGE).then(() => game.endPhase());
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
expect(modifierSelectHandler.options.length).toEqual(3);
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("TM_COMMON");
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("LURE");
|
||||
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("PP_UP");
|
||||
}, 5000);
|
||||
|
||||
it("should generate random modifiers of same tier for reroll with reroll lock", async () => {
|
||||
// Just use fully random seed for this test
|
||||
vi.spyOn(scene, "resetSeed").mockImplementation(() => {
|
||||
scene.waveSeed = Utils.shiftCharCodes(scene.seed, 5);
|
||||
Phaser.Math.RND.sow([ scene.waveSeed ]);
|
||||
console.log("Wave Seed:", scene.waveSeed, 5);
|
||||
scene.rngCounter = 0;
|
||||
});
|
||||
|
||||
let selectModifierPhase = new SelectModifierPhase(scene);
|
||||
scene.pushPhase(selectModifierPhase);
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
|
||||
expect(modifierSelectHandler.options.length).toEqual(3);
|
||||
const firstRollTiers: ModifierTier[] = modifierSelectHandler.options.map(o => o.modifierTypeOption.type.tier);
|
||||
|
||||
// Simulate selecting reroll with lock
|
||||
scene.lockModifierTiers = true;
|
||||
scene.reroll = true;
|
||||
selectModifierPhase = new SelectModifierPhase(scene, 1, firstRollTiers);
|
||||
scene.unshiftPhase(selectModifierPhase);
|
||||
scene.ui.setMode(Mode.MESSAGE).then(() => game.endPhase());
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
expect(modifierSelectHandler.options.length).toEqual(3);
|
||||
// Reroll with lock can still upgrade
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(firstRollTiers[0]);
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(firstRollTiers[1]);
|
||||
expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(firstRollTiers[2]);
|
||||
}, 5000);
|
||||
|
||||
it("should generate custom modifiers", async () => {
|
||||
const customModifiers: CustomModifierSettings = {
|
||||
guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.TM_ULTRA, modifierTypes.LEFTOVERS, modifierTypes.AMULET_COIN, modifierTypes.GOLDEN_PUNCH]
|
||||
};
|
||||
const selectModifierPhase = new SelectModifierPhase(scene, 0, null, customModifiers);
|
||||
scene.pushPhase(selectModifierPhase);
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
|
||||
expect(modifierSelectHandler.options.length).toEqual(5);
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM");
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("TM_ULTRA");
|
||||
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("LEFTOVERS");
|
||||
expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("AMULET_COIN");
|
||||
expect(modifierSelectHandler.options[4].modifierTypeOption.type.id).toEqual("GOLDEN_PUNCH");
|
||||
}, 5000);
|
||||
|
||||
it("should generate custom modifier tiers that can upgrade from luck", async () => {
|
||||
const customModifiers: CustomModifierSettings = {
|
||||
guaranteedModifierTiers: [ModifierTier.COMMON, ModifierTier.GREAT, ModifierTier.ULTRA, ModifierTier.ROGUE, ModifierTier.MASTER]
|
||||
};
|
||||
const pokemon = new PlayerPokemon(scene, getPokemonSpecies(Species.BULBASAUR), 10, undefined, 0, undefined, true, 2, undefined, undefined, undefined);
|
||||
|
||||
// Fill party with max shinies
|
||||
while (scene.getParty().length > 0) {
|
||||
scene.getParty().pop();
|
||||
}
|
||||
scene.getParty().push(pokemon, pokemon, pokemon, pokemon, pokemon, pokemon);
|
||||
|
||||
const selectModifierPhase = new SelectModifierPhase(scene, 0, null, customModifiers);
|
||||
scene.pushPhase(selectModifierPhase);
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
|
||||
expect(modifierSelectHandler.options.length).toEqual(5);
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(ModifierTier.COMMON);
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT);
|
||||
expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ULTRA);
|
||||
expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE);
|
||||
expect(modifierSelectHandler.options[4].modifierTypeOption.type.tier - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount).toEqual(ModifierTier.MASTER);
|
||||
}, 5000);
|
||||
|
||||
it("should generate custom modifiers and modifier tiers together", async () => {
|
||||
const customModifiers: CustomModifierSettings = {
|
||||
guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.TM_COMMON],
|
||||
guaranteedModifierTiers: [ModifierTier.MASTER, ModifierTier.MASTER]
|
||||
};
|
||||
const selectModifierPhase = new SelectModifierPhase(scene, 0, null, customModifiers);
|
||||
scene.pushPhase(selectModifierPhase);
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
|
||||
expect(modifierSelectHandler.options.length).toEqual(4);
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM");
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("TM_COMMON");
|
||||
expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER);
|
||||
expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER);
|
||||
}, 5000);
|
||||
|
||||
it("should fill remaining modifiers if fillRemaining is true with custom modifiers", async () => {
|
||||
const customModifiers: CustomModifierSettings = {
|
||||
guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM],
|
||||
guaranteedModifierTiers: [ModifierTier.MASTER],
|
||||
fillRemaining: true
|
||||
};
|
||||
const selectModifierPhase = new SelectModifierPhase(scene, 0, null, customModifiers);
|
||||
scene.pushPhase(selectModifierPhase);
|
||||
await game.phaseInterceptor.run(SelectModifierPhase);
|
||||
|
||||
|
||||
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
|
||||
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
|
||||
expect(modifierSelectHandler.options.length).toEqual(3);
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM");
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier).toEqual(ModifierTier.MASTER);
|
||||
}, 5000);
|
||||
});
|
|
@ -6,6 +6,7 @@ import {Starter} from "#app/ui/starter-select-ui-handler";
|
|||
import {GameModes, getGameMode} from "#app/game-mode";
|
||||
import {getPokemonSpecies, getPokemonSpeciesForm} from "#app/data/pokemon-species";
|
||||
import {PlayerPokemon} from "#app/field/pokemon";
|
||||
import Battle, {BattleType} from "#app/battle";
|
||||
|
||||
export function blobToString(blob) {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
@ -85,3 +86,23 @@ export function getMovePosition(scene, pokemonIndex, moveIndex) {
|
|||
const index = moveSet.findIndex((move) => move.moveId === moveIndex);
|
||||
return index;
|
||||
}
|
||||
|
||||
/**
|
||||
* Useful for populating party, wave index, etc. without having to spin up and run through an entire EncounterPhase
|
||||
* @param scene
|
||||
* @param species
|
||||
*/
|
||||
export function initSceneWithoutEncounterPhase(scene, species?: Species[]) {
|
||||
const starters = generateStarter(scene, species);
|
||||
starters.forEach((starter) => {
|
||||
const starterProps = scene.gameData.getSpeciesDexAttrProps(starter.species, starter.dexAttr);
|
||||
const starterFormIndex = Math.min(starterProps.formIndex, Math.max(starter.species.forms.length - 1, 0));
|
||||
const starterGender = Gender.MALE;
|
||||
const starterIvs = scene.gameData.dexData[starter.species.speciesId].ivs.slice(0);
|
||||
const starterPokemon = scene.addPlayerPokemon(starter.species, scene.gameMode.getStartingLevel(), starter.abilityIndex, starterFormIndex, starterGender, starterProps.shiny, starterProps.variant, starterIvs, starter.nature);
|
||||
starterPokemon.tryPopulateMoveset(starter.moveset);
|
||||
scene.getParty().push(starterPokemon);
|
||||
});
|
||||
|
||||
scene.currentBattle = new Battle(getGameMode(GameModes.CLASSIC), 5, BattleType.WILD, null, false);
|
||||
}
|
||||
|
|
|
@ -17,7 +17,6 @@ import {
|
|||
NextEncounterPhase,
|
||||
PostSummonPhase,
|
||||
SelectGenderPhase,
|
||||
SelectModifierPhase,
|
||||
SelectStarterPhase,
|
||||
SelectTargetPhase,
|
||||
ShinySparklePhase,
|
||||
|
@ -38,6 +37,13 @@ import UI, {Mode} from "#app/ui/ui";
|
|||
import {Phase} from "#app/phase";
|
||||
import ErrorInterceptor from "#app/test/utils/errorInterceptor";
|
||||
import {QuietFormChangePhase} from "#app/form-change-phase";
|
||||
import {SelectModifierPhase} from "#app/phases/select-modifier-phase";
|
||||
import {
|
||||
MysteryEncounterBattlePhase,
|
||||
MysteryEncounterOptionSelectedPhase,
|
||||
MysteryEncounterPhase,
|
||||
PostMysteryEncounterPhase
|
||||
} from "#app/phases/mystery-encounter-phase";
|
||||
|
||||
export default class PhaseInterceptor {
|
||||
public scene;
|
||||
|
@ -92,10 +98,14 @@ export default class PhaseInterceptor {
|
|||
[QuietFormChangePhase, this.startPhase],
|
||||
[SwitchPhase, this.startPhase],
|
||||
[SwitchSummonPhase, this.startPhase],
|
||||
[MysteryEncounterPhase, this.startPhase],
|
||||
[MysteryEncounterOptionSelectedPhase, this.startPhase],
|
||||
[MysteryEncounterBattlePhase, this.startPhase],
|
||||
[PostMysteryEncounterPhase, this.startPhase]
|
||||
];
|
||||
|
||||
private endBySetMode = [
|
||||
TitlePhase, SelectGenderPhase, CommandPhase
|
||||
TitlePhase, SelectGenderPhase, CommandPhase, SelectModifierPhase
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
@ -12,11 +12,11 @@ import { initSpecies } from "#app/data/pokemon-species";
|
|||
import { initAchievements } from "#app/system/achv.js";
|
||||
import { initVouchers } from "#app/system/voucher.js";
|
||||
import { initStatsKeys } from "#app/ui/game-stats-ui-handler";
|
||||
import { beforeAll, beforeEach, vi } from "vitest";
|
||||
import * as overrides from "#app/overrides";
|
||||
import {initMysteryEncounterDialogue} from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue";
|
||||
import {initMysteryEncounters} from "#app/data/mystery-encounters/mystery-encounters";
|
||||
|
||||
import { beforeAll } from "vitest";
|
||||
|
||||
initVouchers();
|
||||
initAchievements();
|
||||
initStatsKeys();
|
||||
initPokemonPrevolutions();
|
||||
initBiomes();
|
||||
|
@ -26,6 +26,8 @@ initSpecies();
|
|||
initMoves();
|
||||
initAbilities();
|
||||
initLoggedInUser();
|
||||
initMysteryEncounterDialogue();
|
||||
initMysteryEncounters();
|
||||
|
||||
global.testFailed = false;
|
||||
|
||||
|
@ -37,3 +39,8 @@ beforeAll(() => {
|
|||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Disables Mystery Encounters on all tests (can be overridden at test level)
|
||||
beforeEach( () => {
|
||||
vi.spyOn(overrides, "MYSTERY_ENCOUNTER_RATE_OVERRIDE", "get").mockReturnValue(0);
|
||||
});
|
||||
|
|
|
@ -20,6 +20,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||
private lockRarityButtonContainer: Phaser.GameObjects.Container;
|
||||
private transferButtonContainer: Phaser.GameObjects.Container;
|
||||
private checkButtonContainer: Phaser.GameObjects.Container;
|
||||
private continueButtonContainer: Phaser.GameObjects.Container;
|
||||
private rerollCostText: Phaser.GameObjects.Text;
|
||||
private lockRarityButtonText: Phaser.GameObjects.Text;
|
||||
private moveInfoOverlay : MoveInfoOverlay;
|
||||
|
@ -100,6 +101,10 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||
this.lockRarityButtonText.setOrigin(0, 0);
|
||||
this.lockRarityButtonContainer.add(this.lockRarityButtonText);
|
||||
|
||||
this.continueButtonContainer = this.scene.add.container((this.scene.game.canvas.width / 12), -(this.scene.game.canvas.height / 12));
|
||||
this.continueButtonContainer.setVisible(false);
|
||||
ui.add(this.continueButtonContainer);
|
||||
|
||||
// prepare move overlay
|
||||
const overlayScale = 1;
|
||||
this.moveInfoOverlay = new MoveInfoOverlay(this.scene, {
|
||||
|
@ -126,7 +131,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||
return false;
|
||||
}
|
||||
|
||||
if (args.length !== 4 || !(args[1] instanceof Array) || !args[1].length || !(args[2] instanceof Function)) {
|
||||
if (args.length !== 4 || !(args[1] instanceof Array) || !(args[2] instanceof Function)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -151,6 +156,9 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||
this.lockRarityButtonContainer.setVisible(false);
|
||||
this.lockRarityButtonContainer.setAlpha(0);
|
||||
|
||||
this.continueButtonContainer.setVisible(false);
|
||||
this.continueButtonContainer.setAlpha(0);
|
||||
|
||||
this.rerollButtonContainer.setPositionRelative(this.lockRarityButtonContainer, 0, canLockRarities ? -12 : 0);
|
||||
|
||||
this.rerollCost = args[3] as integer;
|
||||
|
@ -172,6 +180,13 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||
this.options.push(option);
|
||||
}
|
||||
|
||||
// Add continue button
|
||||
if (this.options.length === 0) {
|
||||
const continueButtonText = addTextObject(this.scene, -24, optionsYOffset - 5, "Continue", TextStyle.MESSAGE);
|
||||
continueButtonText.setName("text-continue-btn");
|
||||
this.continueButtonContainer.add(continueButtonText);
|
||||
}
|
||||
|
||||
for (let m = 0; m < shopTypeOptions.length; m++) {
|
||||
const row = m < SHOP_OPTIONS_ROW_LIMIT ? 0 : 1;
|
||||
const col = m < SHOP_OPTIONS_ROW_LIMIT ? m : m - SHOP_OPTIONS_ROW_LIMIT;
|
||||
|
@ -235,16 +250,24 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||
this.rerollButtonContainer.setAlpha(0);
|
||||
this.checkButtonContainer.setAlpha(0);
|
||||
this.lockRarityButtonContainer.setAlpha(0);
|
||||
this.continueButtonContainer.setAlpha(0);
|
||||
this.rerollButtonContainer.setVisible(true);
|
||||
this.checkButtonContainer.setVisible(true);
|
||||
this.continueButtonContainer.setVisible(this.rerollCost === 0);
|
||||
this.lockRarityButtonContainer.setVisible(canLockRarities);
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: [ this.rerollButtonContainer, this.lockRarityButtonContainer, this.checkButtonContainer ],
|
||||
targets: [ this.lockRarityButtonContainer, this.checkButtonContainer, this.continueButtonContainer ],
|
||||
alpha: 1,
|
||||
duration: 250
|
||||
});
|
||||
|
||||
this.scene.tweens.add({
|
||||
targets: [this.rerollButtonContainer],
|
||||
alpha: this.rerollCost === 0 ? 0.5 : 1,
|
||||
duration: 250
|
||||
});
|
||||
|
||||
this.setCursor(0);
|
||||
this.setRowCursor(1);
|
||||
|
||||
|
@ -385,6 +408,14 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||
// the modifier selection has been updated, always hide the overlay
|
||||
this.moveInfoOverlay.clear();
|
||||
if (this.rowCursor) {
|
||||
if (this.rowCursor === 1 && options.length === 0) {
|
||||
// Continue button when no shop items
|
||||
this.cursorObj.setScale(1.25);
|
||||
this.cursorObj.setPosition((this.scene.game.canvas.width / 18) + 23, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? 6 : 22));
|
||||
ui.showText("Continue to the next wave.");
|
||||
return ret;
|
||||
}
|
||||
|
||||
const sliceWidth = (this.scene.game.canvas.width / 6) / (options.length + 2);
|
||||
if (this.rowCursor < 2) {
|
||||
this.cursorObj.setPosition(sliceWidth * (cursor + 1) + (sliceWidth * 0.5) - 20, (-this.scene.game.canvas.height / 12) - (this.shopOptionsRows.length > 1 ? 6 : 22));
|
||||
|
@ -421,7 +452,14 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||
if (rowCursor !== lastRowCursor) {
|
||||
this.rowCursor = rowCursor;
|
||||
let newCursor = Math.round(this.cursor / Math.max(this.getRowItems(lastRowCursor) - 1, 1) * (this.getRowItems(rowCursor) - 1));
|
||||
if (rowCursor === 1 && this.options.length === 0) {
|
||||
// Handle empty shop
|
||||
newCursor = 0;
|
||||
}
|
||||
if (rowCursor === 0) {
|
||||
if (this.options.length === 0) {
|
||||
newCursor = 1;
|
||||
}
|
||||
if (newCursor === 0 && !this.rerollButtonContainer.visible) {
|
||||
newCursor = 1;
|
||||
}
|
||||
|
@ -506,7 +544,7 @@ export default class ModifierSelectUiHandler extends AwaitableUiHandler {
|
|||
onComplete: () => options.forEach(o => o.destroy())
|
||||
});
|
||||
|
||||
[ this.rerollButtonContainer, this.checkButtonContainer, this.transferButtonContainer, this.lockRarityButtonContainer ].forEach(container => {
|
||||
[ this.rerollButtonContainer, this.checkButtonContainer, this.transferButtonContainer, this.lockRarityButtonContainer, this.continueButtonContainer ].forEach(container => {
|
||||
if (container.visible) {
|
||||
this.scene.tweens.add({
|
||||
targets: container,
|
||||
|
|
|
@ -0,0 +1,475 @@
|
|||
import BattleScene from "../battle-scene";
|
||||
import { addTextObject, TextStyle } from "./text";
|
||||
import { Mode } from "./ui";
|
||||
import UiHandler from "./ui-handler";
|
||||
import { Button } from "#enums/buttons";
|
||||
import { addWindow, WindowVariant } from "./ui-theme";
|
||||
import i18next from "i18next";
|
||||
import { MysteryEncounterPhase } from "../phases/mystery-encounter-phase";
|
||||
import { PartyUiMode } from "./party-ui-handler";
|
||||
import MysteryEncounterOption from "../data/mystery-encounter-option";
|
||||
import * as Utils from "../utils";
|
||||
import { getPokeballAtlasKey } from "../data/pokeball";
|
||||
|
||||
export default class MysteryEncounterUiHandler extends UiHandler {
|
||||
private cursorContainer: Phaser.GameObjects.Container;
|
||||
private cursorObj: Phaser.GameObjects.Image;
|
||||
|
||||
private optionsContainer: Phaser.GameObjects.Container;
|
||||
|
||||
private tooltipWindow: Phaser.GameObjects.NineSlice;
|
||||
private tooltipContainer: Phaser.GameObjects.Container;
|
||||
private tooltipScrollTween: Phaser.Tweens.Tween;
|
||||
|
||||
private descriptionWindow: Phaser.GameObjects.NineSlice;
|
||||
private descriptionContainer: Phaser.GameObjects.Container;
|
||||
private descriptionScrollTween: Phaser.Tweens.Tween;
|
||||
private rarityBall: Phaser.GameObjects.Sprite;
|
||||
|
||||
private filteredEncounterOptions: MysteryEncounterOption[] = [];
|
||||
private optionsMeetsReqs: boolean[];
|
||||
|
||||
protected viewPartyIndex: integer = 0;
|
||||
|
||||
protected blockInput: boolean = true;
|
||||
|
||||
constructor(scene: BattleScene) {
|
||||
super(scene, Mode.MYSTERY_ENCOUNTER);
|
||||
}
|
||||
|
||||
setup() {
|
||||
const ui = this.getUi();
|
||||
|
||||
this.cursorContainer = this.scene.add.container(18, -38.7);
|
||||
this.cursorContainer.setVisible(false);
|
||||
ui.add(this.cursorContainer);
|
||||
this.optionsContainer = this.scene.add.container(12, -38.7);
|
||||
this.optionsContainer.setVisible(false);
|
||||
ui.add(this.optionsContainer);
|
||||
this.descriptionContainer = this.scene.add.container(0, -152);
|
||||
this.descriptionContainer.setVisible(false);
|
||||
ui.add(this.descriptionContainer);
|
||||
this.tooltipContainer = this.scene.add.container(210, -48);
|
||||
this.tooltipContainer.setVisible(false);
|
||||
ui.add(this.tooltipContainer);
|
||||
|
||||
this.setCursor(this.getCursor());
|
||||
|
||||
this.descriptionWindow = addWindow(this.scene, 0, 0, 150, 105, false, false, 0, 0, WindowVariant.THIN);
|
||||
this.descriptionContainer.add(this.descriptionWindow);
|
||||
|
||||
this.tooltipWindow = addWindow(this.scene, 0, 0, 110, 48, false, false, 0, 0, WindowVariant.THIN);
|
||||
this.tooltipContainer.add(this.tooltipWindow);
|
||||
|
||||
this.rarityBall = this.scene.add.sprite(141, 9, "pb");
|
||||
this.rarityBall.setScale(0.75);
|
||||
this.descriptionContainer.add(this.rarityBall);
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
super.show(args);
|
||||
|
||||
this.cursorContainer.setVisible(true);
|
||||
this.descriptionContainer.setVisible(true);
|
||||
this.optionsContainer.setVisible(true);
|
||||
this.displayEncounterOptions(!(args[0] as boolean || false));
|
||||
const cursor = this.getCursor();
|
||||
if (cursor === (this?.optionsContainer?.length || 0) - 1) {
|
||||
// Always resets cursor on view party button if it was last there
|
||||
this.setCursor(cursor);
|
||||
} else {
|
||||
this.setCursor(0);
|
||||
}
|
||||
if (this.blockInput) {
|
||||
setTimeout(() => {
|
||||
this.unblockInput();
|
||||
}, 1500);
|
||||
}
|
||||
this.displayOptionTooltip();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
const ui = this.getUi();
|
||||
|
||||
let success = false;
|
||||
|
||||
const cursor = this.getCursor();
|
||||
|
||||
if (button === Button.CANCEL || button === Button.ACTION) {
|
||||
if (button === Button.ACTION) {
|
||||
if (cursor === this.viewPartyIndex) {
|
||||
// Handle view party
|
||||
success = true;
|
||||
this.clear();
|
||||
this.scene.ui.setMode(Mode.PARTY, PartyUiMode.CHECK, -1, () => {
|
||||
this.scene.ui.setMode(Mode.MYSTERY_ENCOUNTER, true);
|
||||
setTimeout(() => {
|
||||
this.setCursor(this.viewPartyIndex);
|
||||
this.unblockInput();
|
||||
}, 300);
|
||||
});
|
||||
} else if (this.blockInput || !this.optionsMeetsReqs[cursor]) {
|
||||
success = false;
|
||||
} else {
|
||||
const selected = this.filteredEncounterOptions[cursor];
|
||||
if ((this.scene.getCurrentPhase() as MysteryEncounterPhase).handleOptionSelect(selected, cursor)) {
|
||||
this.clear();
|
||||
success = true;
|
||||
} else {
|
||||
ui.playError();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// TODO: If we need to handle cancel option? Maybe default logic to leave/run from encounter idk
|
||||
}
|
||||
} else {
|
||||
switch (this.optionsContainer.length) {
|
||||
case 3:
|
||||
success = this.handleTwoOptionMoveInput(button);
|
||||
break;
|
||||
case 4:
|
||||
success = this.handleThreeOptionMoveInput(button);
|
||||
break;
|
||||
case 5:
|
||||
success = this.handleFourOptionMoveInput(button);
|
||||
break;
|
||||
}
|
||||
|
||||
this.displayOptionTooltip();
|
||||
}
|
||||
|
||||
if (success) {
|
||||
ui.playSelect();
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
handleTwoOptionMoveInput(button: Button): boolean {
|
||||
let success = false;
|
||||
const cursor = this.getCursor();
|
||||
switch (button) {
|
||||
case Button.UP:
|
||||
if (cursor < this.viewPartyIndex) {
|
||||
success = this.setCursor(this.viewPartyIndex);
|
||||
}
|
||||
break;
|
||||
case Button.DOWN:
|
||||
if (cursor === this.viewPartyIndex) {
|
||||
success = this.setCursor(1);
|
||||
}
|
||||
break;
|
||||
case Button.LEFT:
|
||||
if (cursor > 0) {
|
||||
success = this.setCursor(cursor - 1);
|
||||
}
|
||||
break;
|
||||
case Button.RIGHT:
|
||||
if (cursor < this.viewPartyIndex) {
|
||||
success = this.setCursor(cursor + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
handleThreeOptionMoveInput(button: Button): boolean {
|
||||
let success = false;
|
||||
const cursor = this.getCursor();
|
||||
switch (button) {
|
||||
case Button.UP:
|
||||
if (cursor === 2) {
|
||||
success = this.setCursor(cursor - 2);
|
||||
} else {
|
||||
success = this.setCursor(this.viewPartyIndex);
|
||||
}
|
||||
break;
|
||||
case Button.DOWN:
|
||||
if (cursor === this.viewPartyIndex) {
|
||||
success = this.setCursor(1);
|
||||
} else {
|
||||
success = this.setCursor(2);
|
||||
}
|
||||
break;
|
||||
case Button.LEFT:
|
||||
if (cursor === this.viewPartyIndex) {
|
||||
success = this.setCursor(1);
|
||||
} else if (cursor === 1) {
|
||||
success = this.setCursor(cursor - 1);
|
||||
}
|
||||
break;
|
||||
case Button.RIGHT:
|
||||
if (cursor === 1) {
|
||||
success = this.setCursor(this.viewPartyIndex);
|
||||
} else if (cursor < 1) {
|
||||
success = this.setCursor(cursor + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
handleFourOptionMoveInput(button: Button): boolean {
|
||||
let success = false;
|
||||
const cursor = this.getCursor();
|
||||
switch (button) {
|
||||
case Button.UP:
|
||||
if (cursor >= 2 && cursor !== this.viewPartyIndex) {
|
||||
success = this.setCursor(cursor - 2);
|
||||
} else {
|
||||
success = this.setCursor(this.viewPartyIndex);
|
||||
}
|
||||
break;
|
||||
case Button.DOWN:
|
||||
if (cursor <= 1) {
|
||||
success = this.setCursor(cursor + 2);
|
||||
} else if (cursor === this.viewPartyIndex) {
|
||||
success = this.setCursor(1);
|
||||
}
|
||||
break;
|
||||
case Button.LEFT:
|
||||
if (cursor === this.viewPartyIndex) {
|
||||
success = this.setCursor(1);
|
||||
} else if (cursor % 2 === 1) {
|
||||
success = this.setCursor(cursor - 1);
|
||||
}
|
||||
break;
|
||||
case Button.RIGHT:
|
||||
if (cursor === 1) {
|
||||
success = this.setCursor(this.viewPartyIndex);
|
||||
} else if (cursor % 2 === 0 && cursor !== this.viewPartyIndex) {
|
||||
success = this.setCursor(cursor + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
unblockInput() {
|
||||
if (this.blockInput) {
|
||||
this.blockInput = false;
|
||||
for (let i = 0; i < this.optionsContainer.length - 1; i++) {
|
||||
if (!this.optionsMeetsReqs[i]) {
|
||||
continue;
|
||||
}
|
||||
(this.optionsContainer.getAt(i) as Phaser.GameObjects.Text).setAlpha(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
getCursor(): integer {
|
||||
return this.cursor ? this.cursor : 0;
|
||||
}
|
||||
|
||||
setCursor(cursor: integer): boolean {
|
||||
const prevCursor = this.getCursor();
|
||||
const changed = prevCursor !== cursor;
|
||||
if (changed) {
|
||||
this.cursor = cursor;
|
||||
}
|
||||
|
||||
this.viewPartyIndex = this.optionsContainer.length - 1;
|
||||
|
||||
if (!this.cursorObj) {
|
||||
this.cursorObj = this.scene.add.image(0, 0, "cursor");
|
||||
this.cursorContainer.add(this.cursorObj);
|
||||
}
|
||||
|
||||
if (cursor === this.viewPartyIndex) {
|
||||
this.cursorObj.setPosition(246, -17);
|
||||
} else if (this.optionsContainer.length === 3) { // 2 Options
|
||||
this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 15);
|
||||
} else if (this.optionsContainer.length === 4) { // 3 Options
|
||||
this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0));
|
||||
} else if (this.optionsContainer.length === 5) { // 4 Options
|
||||
this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0));
|
||||
}
|
||||
|
||||
return changed;
|
||||
}
|
||||
|
||||
displayEncounterOptions(slideInDescription: boolean = true): void {
|
||||
const mysteryEncounter = this.scene.currentBattle.mysteryEncounter;
|
||||
this.filteredEncounterOptions = mysteryEncounter.options;
|
||||
this.optionsMeetsReqs = [];
|
||||
|
||||
const titleText: string = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.title);
|
||||
const descriptionText: string = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.description);
|
||||
const queryText: string = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.query);
|
||||
|
||||
// Clear options container (except cursor)
|
||||
this.optionsContainer.removeAll();
|
||||
|
||||
// Options Window
|
||||
for (let i = 0; i < this.filteredEncounterOptions.length; i++) {
|
||||
let optionText;
|
||||
switch (this.filteredEncounterOptions.length) {
|
||||
case 2:
|
||||
optionText = addTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
|
||||
break;
|
||||
case 3:
|
||||
optionText = addTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
|
||||
break;
|
||||
case 4:
|
||||
optionText = addTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 });
|
||||
break;
|
||||
}
|
||||
const text = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.options[i].buttonLabel);
|
||||
if (text) {
|
||||
optionText.setText(text);
|
||||
}
|
||||
|
||||
this.optionsMeetsReqs.push(this.filteredEncounterOptions[i].meetsRequirements(this.scene));
|
||||
|
||||
if (!this.optionsMeetsReqs[i]) {
|
||||
optionText.setAlpha(0.5);
|
||||
}
|
||||
if (this.blockInput) {
|
||||
optionText.setAlpha(0.5);
|
||||
}
|
||||
this.optionsContainer.add(optionText);
|
||||
}
|
||||
|
||||
// View Party Button
|
||||
const viewPartyText = addTextObject(this.scene, 256, -24, "View Party", TextStyle.PARTY);
|
||||
this.optionsContainer.add(viewPartyText);
|
||||
|
||||
// Description Window
|
||||
const titleTextObject = addTextObject(this.scene, 0, 0, titleText, TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 });
|
||||
this.descriptionContainer.add(titleTextObject);
|
||||
titleTextObject.setPosition(72 - titleTextObject.displayWidth / 2, 5.5);
|
||||
|
||||
// Rarity of encounter
|
||||
const ballType = getPokeballAtlasKey(mysteryEncounter.encounterTier as number);
|
||||
this.rarityBall.setTexture("pb", ballType);
|
||||
|
||||
const descriptionTextObject = addTextObject(this.scene, 6, 25, descriptionText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });
|
||||
|
||||
// Sets up the mask that hides the description text to give an illusion of scrolling
|
||||
const descriptionTextMaskRect = this.scene.make.graphics({});
|
||||
descriptionTextMaskRect.setScale(6);
|
||||
descriptionTextMaskRect.fillStyle(0xFFFFFF);
|
||||
descriptionTextMaskRect.beginPath();
|
||||
descriptionTextMaskRect.fillRect(6, 54, 206, 60);
|
||||
|
||||
const abilityDescriptionTextMask = descriptionTextMaskRect.createGeometryMask();
|
||||
|
||||
descriptionTextObject.setMask(abilityDescriptionTextMask);
|
||||
|
||||
const descriptionLineCount = Math.floor(descriptionTextObject.displayHeight / 10);
|
||||
|
||||
if (this.descriptionScrollTween) {
|
||||
this.descriptionScrollTween.remove();
|
||||
this.descriptionScrollTween = null;
|
||||
}
|
||||
|
||||
// Animates the description text moving upwards
|
||||
if (descriptionLineCount > 6) {
|
||||
this.descriptionScrollTween = this.scene.tweens.add({
|
||||
targets: descriptionTextObject,
|
||||
delay: Utils.fixedInt(2000),
|
||||
loop: -1,
|
||||
hold: Utils.fixedInt(2000),
|
||||
duration: Utils.fixedInt((descriptionLineCount - 6) * 2000),
|
||||
y: `-=${10 * (descriptionLineCount - 6)}`
|
||||
});
|
||||
}
|
||||
|
||||
this.descriptionContainer.add(descriptionTextObject);
|
||||
|
||||
const queryTextObject = addTextObject(this.scene, 65 - (queryText.length), 90, queryText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });
|
||||
this.descriptionContainer.add(queryTextObject);
|
||||
|
||||
// Slide in description container
|
||||
if (slideInDescription) {
|
||||
this.descriptionContainer.x -= 150;
|
||||
this.scene.tweens.add({
|
||||
targets: this.descriptionContainer,
|
||||
x: "+=150",
|
||||
ease: "Sine.easeInOut",
|
||||
duration: 1000
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
displayOptionTooltip() {
|
||||
const cursor = this.getCursor();
|
||||
// Clear tooltip box
|
||||
if (this.tooltipContainer.length > 1) {
|
||||
this.tooltipContainer.removeBetween(1, this.tooltipContainer.length, true);
|
||||
}
|
||||
this.tooltipContainer.setVisible(true);
|
||||
|
||||
if (Utils.isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) {
|
||||
// Ignore hovers on view party button
|
||||
return;
|
||||
}
|
||||
|
||||
const mysteryEncounter = this.scene.currentBattle.mysteryEncounter;
|
||||
let text;
|
||||
if (!this.optionsMeetsReqs[cursor] && mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor].disabledTooltip) {
|
||||
text = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor].disabledTooltip);
|
||||
} else {
|
||||
text = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor].buttonTooltip);
|
||||
}
|
||||
|
||||
|
||||
if (text) {
|
||||
const tooltipTextObject = addTextObject(this.scene, 6, 7, text, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 600 }, fontSize: "72px" });
|
||||
this.tooltipContainer.add(tooltipTextObject);
|
||||
|
||||
// Sets up the mask that hides the description text to give an illusion of scrolling
|
||||
const tooltipTextMaskRect = this.scene.make.graphics({});
|
||||
tooltipTextMaskRect.setScale(6);
|
||||
tooltipTextMaskRect.fillStyle(0xFFFFFF);
|
||||
tooltipTextMaskRect.beginPath();
|
||||
tooltipTextMaskRect.fillRect(this.tooltipContainer.x, this.tooltipContainer.y + 188.5, 150, 32);
|
||||
|
||||
const textMask = tooltipTextMaskRect.createGeometryMask();
|
||||
tooltipTextObject.setMask(textMask);
|
||||
|
||||
const tooltipLineCount = Math.floor(tooltipTextObject.displayHeight / 11.2);
|
||||
|
||||
if (this.tooltipScrollTween) {
|
||||
this.tooltipScrollTween.remove();
|
||||
this.tooltipScrollTween = null;
|
||||
}
|
||||
|
||||
// Animates the tooltip text moving upwards
|
||||
if (tooltipLineCount > 3) {
|
||||
this.tooltipScrollTween = this.scene.tweens.add({
|
||||
targets: tooltipTextObject,
|
||||
delay: Utils.fixedInt(1200),
|
||||
loop: -1,
|
||||
hold: Utils.fixedInt(1200),
|
||||
duration: Utils.fixedInt((tooltipLineCount - 3) * 1200),
|
||||
y: `-=${11.2 * (tooltipLineCount - 3)}`
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
super.clear();
|
||||
this.optionsContainer.setVisible(false);
|
||||
this.optionsContainer.removeAll(true);
|
||||
this.descriptionContainer.setVisible(false);
|
||||
this.tooltipContainer.setVisible(false);
|
||||
// Keeps container background and pokeball
|
||||
this.descriptionContainer.removeBetween(2, this.descriptionContainer.length, true);
|
||||
this.getUi().getMessageHandler().clearText();
|
||||
this.eraseCursor();
|
||||
}
|
||||
|
||||
eraseCursor(): void {
|
||||
if (this.cursorObj) {
|
||||
this.cursorObj.destroy();
|
||||
}
|
||||
this.cursorObj = null;
|
||||
}
|
||||
}
|
|
@ -21,6 +21,7 @@ import MoveInfoOverlay from "./move-info-overlay";
|
|||
import i18next from "i18next";
|
||||
import BBCodeText from "phaser3-rex-plugins/plugins/bbcodetext";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||
|
||||
const defaultMessage = i18next.t("partyUiHandler:choosePokemon");
|
||||
|
||||
|
@ -36,7 +37,8 @@ export enum PartyUiMode {
|
|||
MODIFIER_TRANSFER,
|
||||
SPLICE,
|
||||
RELEASE,
|
||||
CHECK
|
||||
CHECK,
|
||||
SELECT
|
||||
}
|
||||
|
||||
export enum PartyOption {
|
||||
|
@ -52,6 +54,7 @@ export enum PartyOption {
|
|||
SPLICE,
|
||||
UNSPLICE,
|
||||
RELEASE,
|
||||
SELECT,
|
||||
SCROLL_UP = 1000,
|
||||
SCROLL_DOWN = 1001,
|
||||
FORM_CHANGE_ITEM = 2000,
|
||||
|
@ -153,7 +156,7 @@ export default class PartyUiHandler extends MessageUiHandler {
|
|||
|
||||
public static NoEffectMessage = i18next.t("partyUiHandler:anyEffect");
|
||||
|
||||
private localizedOptions = [PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON];
|
||||
private localizedOptions = [PartyOption.SEND_OUT, PartyOption.SUMMARY, PartyOption.CANCEL, PartyOption.APPLY, PartyOption.RELEASE, PartyOption.TEACH, PartyOption.SPLICE, PartyOption.UNSPLICE, PartyOption.REVIVE, PartyOption.TRANSFER, PartyOption.UNPAUSE_EVOLUTION, PartyOption.PASS_BATON, PartyOption.SELECT];
|
||||
|
||||
constructor(scene: BattleScene) {
|
||||
super(scene, Mode.PARTY);
|
||||
|
@ -418,6 +421,10 @@ export default class PartyUiHandler extends MessageUiHandler {
|
|||
return true;
|
||||
} else if (option === PartyOption.CANCEL) {
|
||||
return this.processInput(Button.CANCEL);
|
||||
} else if (option === PartyOption.SELECT) {
|
||||
ui.playSelect();
|
||||
// ui.setModeWithoutClear(Mode.SUMMARY, pokemon).then(() => this.clearOptions());
|
||||
return true;
|
||||
}
|
||||
} else if (button === Button.CANCEL) {
|
||||
this.clearOptions();
|
||||
|
@ -759,6 +766,9 @@ export default class PartyUiHandler extends MessageUiHandler {
|
|||
}
|
||||
}
|
||||
break;
|
||||
case PartyUiMode.SELECT:
|
||||
this.options.push(PartyOption.SELECT);
|
||||
break;
|
||||
}
|
||||
|
||||
this.options.push(PartyOption.SUMMARY);
|
||||
|
|
|
@ -46,6 +46,7 @@ import SettingsDisplayUiHandler from "./settings/settings-display-ui-handler";
|
|||
import SettingsAudioUiHandler from "./settings/settings-audio-ui-handler";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
import BgmBar from "#app/ui/bgm-bar";
|
||||
import MysteryEncounterUiHandler from "./mystery-encounter-ui-handler";
|
||||
|
||||
export enum Mode {
|
||||
MESSAGE,
|
||||
|
@ -83,7 +84,8 @@ export enum Mode {
|
|||
SESSION_RELOAD,
|
||||
UNAVAILABLE,
|
||||
OUTDATED,
|
||||
CHALLENGE_SELECT
|
||||
CHALLENGE_SELECT,
|
||||
MYSTERY_ENCOUNTER
|
||||
}
|
||||
|
||||
const transitionModes = [
|
||||
|
@ -180,7 +182,8 @@ export default class UI extends Phaser.GameObjects.Container {
|
|||
new SessionReloadModalUiHandler(scene),
|
||||
new UnavailableModalUiHandler(scene),
|
||||
new OutdatedModalUiHandler(scene),
|
||||
new GameChallengesUiHandler(scene)
|
||||
new GameChallengesUiHandler(scene),
|
||||
new MysteryEncounterUiHandler(scene)
|
||||
];
|
||||
}
|
||||
|
||||
|
|
|
@ -527,4 +527,6 @@ export function reverseValueToKeySetting(input) {
|
|||
return capitalizedWords.join("_");
|
||||
}
|
||||
|
||||
|
||||
export function isNullOrUndefined(object: any): boolean {
|
||||
return null === object || undefined === object;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue