Merge branch 'beta' into pr-illusion
Before Width: | Height: | Size: 57 KiB After Width: | Height: | Size: 57 KiB |
BIN
public/images/items/pb_silver.png
Normal file
After Width: | Height: | Size: 556 B |
41
public/images/trainer/future_self_f.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"textures": [
|
||||
{
|
||||
"image": "future_self_f.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 29,
|
||||
"h": 69
|
||||
},
|
||||
"scale": 1,
|
||||
"frames": [
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 69,
|
||||
"h": 69
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 21,
|
||||
"y": 0,
|
||||
"w": 29,
|
||||
"h": 69
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 29,
|
||||
"h": 69
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:4eb16332c2e77886e4e621b62269f05e:26f1bc53c853efdbe228d67604b95b54:d25525a5db42bd57d2afe4b6e3081ee1$"
|
||||
}
|
||||
}
|
BIN
public/images/trainer/future_self_f.png
Normal file
After Width: | Height: | Size: 664 B |
41
public/images/trainer/future_self_m.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"textures": [
|
||||
{
|
||||
"image": "future_self_m.png",
|
||||
"format": "RGBA8888",
|
||||
"size": {
|
||||
"w": 36,
|
||||
"h": 73
|
||||
},
|
||||
"scale": 1,
|
||||
"frames": [
|
||||
{
|
||||
"filename": "0001.png",
|
||||
"rotated": false,
|
||||
"trimmed": true,
|
||||
"sourceSize": {
|
||||
"w": 73,
|
||||
"h": 73
|
||||
},
|
||||
"spriteSourceSize": {
|
||||
"x": 18,
|
||||
"y": 0,
|
||||
"w": 36,
|
||||
"h": 73
|
||||
},
|
||||
"frame": {
|
||||
"x": 0,
|
||||
"y": 0,
|
||||
"w": 36,
|
||||
"h": 73
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"meta": {
|
||||
"app": "https://www.codeandweb.com/texturepacker",
|
||||
"version": "3.0",
|
||||
"smartupdate": "$TexturePacker:SmartUpdate:0d8d68d06ae75bc93d72b54183a9df02:d3d509801da9ff5c0bd4793a05ece391:83c4b8c2ed25ea7d9795bec5c40c8602$"
|
||||
}
|
||||
}
|
BIN
public/images/trainer/future_self_m.png
Normal file
After Width: | Height: | Size: 695 B |
BIN
public/images/ui/legacy/link_icon.png
Normal file
After Width: | Height: | Size: 209 B |
BIN
public/images/ui/legacy/unlink_icon.png
Normal file
After Width: | Height: | Size: 219 B |
BIN
public/images/ui/link_icon.png
Normal file
After Width: | Height: | Size: 209 B |
BIN
public/images/ui/unlink_icon.png
Normal file
After Width: | Height: | Size: 219 B |
@ -1 +1 @@
|
||||
Subproject commit 3ccef8472dd7cc7c362538489954cb8fdad27e5f
|
||||
Subproject commit 87615556d8a2bd7eef7abac818f84423a8a13b03
|
@ -1,7 +1,6 @@
|
||||
import { Gender } from "#app/data/gender";
|
||||
import { PokeballType } from "#app/data/pokeball";
|
||||
import Pokemon from "#app/field/pokemon";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { Type } from "#app/data/type";
|
||||
import * as Utils from "#app/utils";
|
||||
import { WeatherType } from "#app/data/weather";
|
||||
@ -10,7 +9,7 @@ import { Biome } from "#enums/biome";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { Species } from "#enums/species";
|
||||
import { TimeOfDay } from "#enums/time-of-day";
|
||||
import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier } from "#app/modifier/modifier";
|
||||
import { DamageMoneyRewardModifier, ExtraModifierModifier, MoneyMultiplierModifier, TempExtraModifierModifier } from "#app/modifier/modifier";
|
||||
import { SpeciesFormKey } from "#enums/species-form-key";
|
||||
|
||||
|
||||
@ -271,9 +270,21 @@ export const pokemonEvolutions: PokemonEvolutions = {
|
||||
new SpeciesEvolution(Species.MAROWAK, 28, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DAWN || p.scene.arena.getTimeOfDay() === TimeOfDay.DAY))
|
||||
],
|
||||
[Species.TYROGUE]: [
|
||||
new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] > p.stats[Stat.DEF])),
|
||||
new SpeciesEvolution(Species.HITMONCHAN, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] < p.stats[Stat.DEF])),
|
||||
new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p => p.stats[Stat.ATK] === p.stats[Stat.DEF]))
|
||||
/**
|
||||
* Custom: Evolves into Hitmonlee, Hitmonchan or Hitmontop at level 20
|
||||
* if it knows Low Sweep, Mach Punch, or Rapid Spin, respectively.
|
||||
* If Tyrogue knows multiple of these moves, its evolution is based on
|
||||
* the first qualifying move in its moveset.
|
||||
*/
|
||||
new SpeciesEvolution(Species.HITMONLEE, 20, null, new SpeciesEvolutionCondition(p =>
|
||||
p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.LOW_SWEEP
|
||||
)),
|
||||
new SpeciesEvolution(Species.HITMONCHAN, 20, null, new SpeciesEvolutionCondition(p =>
|
||||
p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.MACH_PUNCH
|
||||
)),
|
||||
new SpeciesEvolution(Species.HITMONTOP, 20, null, new SpeciesEvolutionCondition(p =>
|
||||
p.getMoveset(true).find(move => move && [ Moves.LOW_SWEEP, Moves.MACH_PUNCH, Moves.RAPID_SPIN ].includes(move?.moveId))?.moveId === Moves.RAPID_SPIN
|
||||
)),
|
||||
],
|
||||
[Species.KOFFING]: [
|
||||
new SpeciesEvolution(Species.GALAR_WEEZING, 35, null, new SpeciesEvolutionCondition(p => p.scene.arena.getTimeOfDay() === TimeOfDay.DUSK || p.scene.arena.getTimeOfDay() === TimeOfDay.NIGHT)),
|
||||
@ -1652,11 +1663,11 @@ export const pokemonEvolutions: PokemonEvolutions = {
|
||||
new SpeciesFormEvolution(Species.GHOLDENGO, "chest", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
|
||||
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
|
||||
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier
|
||||
|| m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG),
|
||||
|| m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG),
|
||||
new SpeciesFormEvolution(Species.GHOLDENGO, "roaming", "", 1, null, new SpeciesEvolutionCondition(p => p.evoCounter
|
||||
+ p.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
|
||||
+ p.scene.findModifiers(m => m instanceof MoneyMultiplierModifier
|
||||
|| m instanceof ExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG)
|
||||
|| m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length > 9), SpeciesWildEvolutionDelay.VERY_LONG)
|
||||
]
|
||||
};
|
||||
|
||||
|
@ -1835,6 +1835,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = {
|
||||
[ 1, Moves.LOW_SWEEP ],
|
||||
[ 1, Moves.JUMP_KICK ],
|
||||
[ 1, Moves.ROLLING_KICK ],
|
||||
[ 1, Moves.MACH_PUNCH ], // Previous Stage Move, Custom
|
||||
[ 1, Moves.RAPID_SPIN ], // Previous Stage Move, Custom
|
||||
[ 4, Moves.DOUBLE_KICK ],
|
||||
[ 8, Moves.LOW_KICK ],
|
||||
[ 12, Moves.ENDURE ],
|
||||
@ -1857,6 +1859,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = {
|
||||
[ 1, Moves.FEINT ],
|
||||
[ 1, Moves.PURSUIT ],
|
||||
[ 1, Moves.COMET_PUNCH ],
|
||||
[ 1, Moves.LOW_SWEEP ], // Previous Stage Move, Custom
|
||||
[ 1, Moves.RAPID_SPIN ], // Previous Stage Move, Custom
|
||||
[ 4, Moves.MACH_PUNCH ],
|
||||
[ 8, Moves.VACUUM_WAVE ],
|
||||
[ 12, Moves.DETECT ],
|
||||
@ -4148,6 +4152,9 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = {
|
||||
[ 1, Moves.FOCUS_ENERGY ],
|
||||
[ 1, Moves.FAKE_OUT ],
|
||||
[ 1, Moves.HELPING_HAND ],
|
||||
[ 10, Moves.LOW_SWEEP ], // Custom
|
||||
[ 10, Moves.MACH_PUNCH ], // Custom
|
||||
[ 10, Moves.RAPID_SPIN ], // Custom
|
||||
],
|
||||
[Species.HITMONTOP]: [
|
||||
[ EVOLVE_MOVE, Moves.TRIPLE_KICK ],
|
||||
@ -4159,6 +4166,8 @@ export const pokemonSpeciesLevelMoves: PokemonSpeciesLevelMoves = {
|
||||
[ 1, Moves.FEINT ],
|
||||
[ 1, Moves.PURSUIT ],
|
||||
[ 1, Moves.ROLLING_KICK ],
|
||||
[ 1, Moves.LOW_SWEEP ], // Previous Stage Move, Custom
|
||||
[ 1, Moves.MACH_PUNCH ], // Previous Stage Move, Custom
|
||||
[ 4, Moves.QUICK_ATTACK ],
|
||||
[ 8, Moves.GYRO_BALL ],
|
||||
[ 12, Moves.DETECT ],
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import { generateModifierType, leaveEncounterWithoutBattle, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import { Species } from "#enums/species";
|
||||
@ -14,6 +14,7 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
|
||||
import i18next from "i18next";
|
||||
|
||||
/** the i18n namespace for this encounter */
|
||||
const namespace = "mysteryEncounters/anOfferYouCantRefuse";
|
||||
@ -98,6 +99,8 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
|
||||
}
|
||||
}
|
||||
|
||||
const shinyCharm = generateModifierType(scene, modifierTypes.SHINY_CHARM);
|
||||
encounter.setDialogueToken("itemName", shinyCharm?.name ?? i18next.t("modifierType:ModifierType.SHINY_CHARM.name"));
|
||||
encounter.setDialogueToken("liepardName", getPokemonSpecies(Species.LIEPARD).getName());
|
||||
|
||||
return true;
|
||||
@ -123,7 +126,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
|
||||
return true;
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Give the player a Shiny charm
|
||||
// Give the player a Shiny Charm
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.SHINY_CHARM));
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
@ -132,9 +135,11 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
|
||||
.withOption(
|
||||
MysteryEncounterOptionBuilder
|
||||
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
|
||||
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement(
|
||||
.withPrimaryPokemonRequirement(
|
||||
CombinationPokemonRequirement.Some(
|
||||
new MoveRequirement(EXTORTION_MOVES, true),
|
||||
new AbilityRequirement(EXTORTION_ABILITIES, true))
|
||||
new AbilityRequirement(EXTORTION_ABILITIES, true)
|
||||
)
|
||||
)
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}:option.2.label`,
|
||||
|
@ -193,12 +193,14 @@ const WAVE_LEVEL_BREAKPOINTS = [ 30, 50, 70, 100, 120, 140, 160 ];
|
||||
export const BugTypeSuperfanEncounter: MysteryEncounter =
|
||||
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.BUG_TYPE_SUPERFAN)
|
||||
.withEncounterTier(MysteryEncounterTier.GREAT)
|
||||
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement(
|
||||
.withPrimaryPokemonRequirement(
|
||||
CombinationPokemonRequirement.Some(
|
||||
// Must have at least 1 Bug type on team, OR have a bug item somewhere on the team
|
||||
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
|
||||
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1),
|
||||
new TypeRequirement(Type.BUG, false, 1)
|
||||
))
|
||||
)
|
||||
)
|
||||
.withMaxAllowedEncounters(1)
|
||||
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
||||
.withIntroSpriteConfigs([]) // These are set in onInit()
|
||||
@ -405,11 +407,13 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
|
||||
.build())
|
||||
.withOption(MysteryEncounterOptionBuilder
|
||||
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_DEFAULT)
|
||||
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement(
|
||||
.withPrimaryPokemonRequirement(
|
||||
CombinationPokemonRequirement.Some(
|
||||
// Meets one or both of the below reqs
|
||||
new HeldItemRequirement([ "BypassSpeedChanceModifier", "ContactHeldItemTransferChanceModifier" ], 1),
|
||||
new AttackTypeBoosterHeldItemTypeRequirement(Type.BUG, 1)
|
||||
))
|
||||
)
|
||||
)
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}:option.3.label`,
|
||||
buttonTooltip: `${namespace}:option.3.tooltip`,
|
||||
|
@ -11,7 +11,7 @@ import { Species } from "#enums/species";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { applyAbilityOverrideToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { Type } from "#app/data/type";
|
||||
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
@ -425,17 +425,8 @@ function onYesAbilitySwap(scene: BattleScene, resolve) {
|
||||
const onPokemonSelected = (pokemon: PlayerPokemon) => {
|
||||
// Do ability swap
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
if (pokemon.isFusion()) {
|
||||
if (!pokemon.fusionCustomPokemonData) {
|
||||
pokemon.fusionCustomPokemonData = new CustomPokemonData();
|
||||
}
|
||||
pokemon.fusionCustomPokemonData.ability = encounter.misc.ability;
|
||||
} else {
|
||||
if (!pokemon.customPokemonData) {
|
||||
pokemon.customPokemonData = new CustomPokemonData();
|
||||
}
|
||||
pokemon.customPokemonData.ability = encounter.misc.ability;
|
||||
}
|
||||
|
||||
applyAbilityOverrideToPokemon(pokemon, encounter.misc.ability);
|
||||
encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender());
|
||||
scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true));
|
||||
};
|
||||
|
@ -14,6 +14,7 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
|
||||
import { ModifierRewardPhase } from "#app/phases/modifier-reward-phase";
|
||||
import { PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
|
||||
/** i18n namespace for encounter */
|
||||
const namespace = "mysteryEncounters/darkDeal";
|
||||
@ -141,6 +142,7 @@ export const DarkDealEncounter: MysteryEncounter =
|
||||
// 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, true, false, true);
|
||||
|
||||
// Get all the pokemon's held items
|
||||
const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
|
||||
scene.removePokemonFromPlayerParty(removedPokemon);
|
||||
@ -160,7 +162,13 @@ export const DarkDealEncounter: MysteryEncounter =
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.ROGUE_BALL));
|
||||
|
||||
// Start encounter with random legendary (7-10 starter strength) that has level additive
|
||||
const bossTypes: Type[] = encounter.misc.removedTypes;
|
||||
// If this is a mono-type challenge, always ensure the required type is filtered for
|
||||
let bossTypes: Type[] = encounter.misc.removedTypes;
|
||||
const singleTypeChallenges = scene.gameMode.challenges.filter(c => c.value && c.id === Challenges.SINGLE_TYPE);
|
||||
if (scene.gameMode.isChallenge && singleTypeChallenges.length > 0) {
|
||||
bossTypes = singleTypeChallenges.map(c => (c.value - 1) as Type);
|
||||
}
|
||||
|
||||
const bossModifiers: PokemonHeldItemModifier[] = encounter.misc.modifiers;
|
||||
// Starter egg tier, 35/50/10/5 %odds for tiers 6/7/8/9+
|
||||
const roll = randSeedInt(100);
|
||||
|
@ -45,10 +45,13 @@ export const DelibirdyEncounter: MysteryEncounter =
|
||||
.withEncounterTier(MysteryEncounterTier.GREAT)
|
||||
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
||||
.withSceneRequirement(new MoneyRequirement(0, DELIBIRDY_MONEY_PRICE_MULTIPLIER)) // Must have enough money for it to spawn at the very least
|
||||
.withPrimaryPokemonRequirement(new CombinationPokemonRequirement( // Must also have either option 2 or 3 available to spawn
|
||||
.withPrimaryPokemonRequirement(
|
||||
CombinationPokemonRequirement.Some(
|
||||
// Must also have either option 2 or 3 available to spawn
|
||||
new HeldItemRequirement(OPTION_2_ALLOWED_MODIFIERS),
|
||||
new HeldItemRequirement(OPTION_3_DISALLOWED_MODIFIERS, 1, true)
|
||||
))
|
||||
)
|
||||
)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "",
|
||||
@ -196,7 +199,7 @@ export const DelibirdyEncounter: MysteryEncounter =
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
const modifier: BerryModifier | HealingBoosterModifier = encounter.misc.chosenModifier;
|
||||
|
||||
// Give the player a Candy Jar if they gave a Berry, and a Healing Charm for Reviver Seed
|
||||
// Give the player a Candy Jar if they gave a Berry, and a Berry Pouch for Reviver Seed
|
||||
if (modifier instanceof BerryModifier) {
|
||||
// Check if the player has max stacks of that Candy Jar already
|
||||
const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier;
|
||||
@ -211,8 +214,8 @@ export const DelibirdyEncounter: MysteryEncounter =
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.CANDY_JAR));
|
||||
}
|
||||
} else {
|
||||
// Check if the player has max stacks of that Healing Charm already
|
||||
const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
|
||||
// Check if the player has max stacks of that Berry Pouch already
|
||||
const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier;
|
||||
|
||||
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
|
||||
// At max stacks, give the first party pokemon a Shell Bell instead
|
||||
@ -221,7 +224,7 @@ export const DelibirdyEncounter: MysteryEncounter =
|
||||
scene.playSound("item_fanfare");
|
||||
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
|
||||
} else {
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM));
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH));
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,8 +293,8 @@ export const DelibirdyEncounter: MysteryEncounter =
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
const modifier = encounter.misc.chosenModifier;
|
||||
|
||||
// Check if the player has max stacks of Berry Pouch already
|
||||
const existing = scene.findModifier(m => m instanceof PreserveBerryModifier) as PreserveBerryModifier;
|
||||
// Check if the player has max stacks of Healing Charm already
|
||||
const existing = scene.findModifier(m => m instanceof HealingBoosterModifier) as HealingBoosterModifier;
|
||||
|
||||
if (existing && existing.getStackCount() >= existing.getMaxStackCount(scene)) {
|
||||
// At max stacks, give the first party pokemon a Shell Bell instead
|
||||
@ -300,7 +303,7 @@ export const DelibirdyEncounter: MysteryEncounter =
|
||||
scene.playSound("item_fanfare");
|
||||
await showEncounterText(scene, i18next.t("battle:rewardGain", { modifierName: shellBell.name }), null, undefined, true);
|
||||
} else {
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.BERRY_POUCH));
|
||||
scene.unshiftPhase(new ModifierRewardPhase(scene, modifierTypes.HEALING_CHARM));
|
||||
}
|
||||
|
||||
// Remove the modifier if its stacks go to 0
|
||||
|
@ -4,24 +4,30 @@ import { AttackTypeBoosterModifierType, modifierTypes, } from "#app/modifier/mod
|
||||
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
|
||||
import { TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
|
||||
import { AbilityRequirement, CombinationPokemonRequirement, TypeRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
|
||||
import { Species } from "#enums/species";
|
||||
import { getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import { Gender } from "#app/data/gender";
|
||||
import { Type } from "#app/data/type";
|
||||
import { BattlerIndex } from "#app/battle";
|
||||
import { PokemonMove } from "#app/field/pokemon";
|
||||
import Pokemon, { PokemonMove } from "#app/field/pokemon";
|
||||
import { Moves } from "#enums/moves";
|
||||
import { EncounterBattleAnim } from "#app/data/battle-anims";
|
||||
import { WeatherType } from "#app/data/weather";
|
||||
import { isNullOrUndefined, randSeedInt } from "#app/utils";
|
||||
import { StatusEffect } from "#app/data/status-effect";
|
||||
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { applyAbilityOverrideToPokemon, applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
import { EncounterAnim } from "#enums/encounter-anims";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { Ability } from "#app/data/ability";
|
||||
import { FIRE_RESISTANT_ABILITIES } from "#app/data/mystery-encounters/requirements/requirement-groups";
|
||||
|
||||
/** the i18n namespace for the encounter */
|
||||
const namespace = "mysteryEncounters/fieryFallout";
|
||||
@ -62,16 +68,24 @@ export const FieryFalloutEncounter: MysteryEncounter =
|
||||
{
|
||||
species: volcaronaSpecies,
|
||||
isBoss: false,
|
||||
gender: Gender.MALE
|
||||
gender: Gender.MALE,
|
||||
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
|
||||
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1));
|
||||
}
|
||||
},
|
||||
{
|
||||
species: volcaronaSpecies,
|
||||
isBoss: false,
|
||||
gender: Gender.FEMALE
|
||||
gender: Gender.FEMALE,
|
||||
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
|
||||
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
|
||||
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [ Stat.SPDEF, Stat.SPD ], 1));
|
||||
}
|
||||
}
|
||||
],
|
||||
doubleBattle: true,
|
||||
disableSwitch: true
|
||||
disableSwitch: true,
|
||||
};
|
||||
encounter.enemyPartyConfigs = [ config ];
|
||||
|
||||
@ -139,7 +153,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
|
||||
async (scene: BattleScene) => {
|
||||
// Pick battle
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonCharcoal(scene));
|
||||
setEncounterRewards(scene, { fillRemaining: true }, undefined, () => giveLeadPokemonAttackTypeBoostItem(scene));
|
||||
|
||||
encounter.startOfBattleEffects.push(
|
||||
{
|
||||
@ -153,18 +167,6 @@ export const FieryFalloutEncounter: MysteryEncounter =
|
||||
targets: [ BattlerIndex.PLAYER_2 ],
|
||||
move: new PokemonMove(Moves.FIRE_SPIN),
|
||||
ignorePp: true
|
||||
},
|
||||
{
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY,
|
||||
targets: [ BattlerIndex.ENEMY ],
|
||||
move: new PokemonMove(Moves.QUIVER_DANCE),
|
||||
ignorePp: true
|
||||
},
|
||||
{
|
||||
sourceBattlerIndex: BattlerIndex.ENEMY_2,
|
||||
targets: [ BattlerIndex.ENEMY_2 ],
|
||||
move: new PokemonMove(Moves.QUIVER_DANCE),
|
||||
ignorePp: true
|
||||
});
|
||||
await initBattleWithEnemyConfig(scene, scene.currentBattle.mysteryEncounter!.enemyPartyConfigs[0]);
|
||||
}
|
||||
@ -180,7 +182,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
|
||||
],
|
||||
},
|
||||
async (scene: BattleScene) => {
|
||||
// Damage non-fire types and burn 1 random non-fire type member
|
||||
// Damage non-fire types and burn 1 random non-fire type member + give it Heatproof
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
const nonFireTypes = scene.getParty().filter((p) => p.isAllowedInBattle() && !p.getTypes().includes(Type.FIRE));
|
||||
|
||||
@ -198,7 +200,11 @@ export const FieryFalloutEncounter: MysteryEncounter =
|
||||
if (chosenPokemon.trySetStatus(StatusEffect.BURN)) {
|
||||
// Burn applied
|
||||
encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender());
|
||||
encounter.setDialogueToken("abilityName", new Ability(Abilities.HEATPROOF, 3).name);
|
||||
queueEncounterMessage(scene, `${namespace}:option.2.target_burned`);
|
||||
|
||||
// Also permanently change the burned Pokemon's ability to Heatproof
|
||||
applyAbilityOverrideToPokemon(chosenPokemon, Abilities.HEATPROOF);
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,8 +215,12 @@ export const FieryFalloutEncounter: MysteryEncounter =
|
||||
.withOption(
|
||||
MysteryEncounterOptionBuilder
|
||||
.newOptionWithMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
|
||||
.withPrimaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3PrimaryName dialogue token automatically
|
||||
.withSecondaryPokemonRequirement(new TypeRequirement(Type.FIRE, true, 1)) // Will set option3SecondaryName dialogue token automatically
|
||||
.withPrimaryPokemonRequirement(
|
||||
CombinationPokemonRequirement.Some(
|
||||
new TypeRequirement(Type.FIRE, true, 1),
|
||||
new AbilityRequirement(FIRE_RESISTANT_ABILITIES, true)
|
||||
)
|
||||
) // Will set option3PrimaryName dialogue token automatically
|
||||
.withDialogue({
|
||||
buttonLabel: `${namespace}:option.3.label`,
|
||||
buttonTooltip: `${namespace}:option.3.tooltip`,
|
||||
@ -233,26 +243,32 @@ export const FieryFalloutEncounter: MysteryEncounter =
|
||||
{ fillRemaining: true },
|
||||
undefined,
|
||||
() => {
|
||||
giveLeadPokemonCharcoal(scene);
|
||||
giveLeadPokemonAttackTypeBoostItem(scene);
|
||||
});
|
||||
|
||||
const primary = encounter.options[2].primaryPokemon!;
|
||||
const secondary = encounter.options[2].secondaryPokemon![0];
|
||||
|
||||
setEncounterExp(scene, [ primary.id, secondary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2);
|
||||
setEncounterExp(scene, [ primary.id ], getPokemonSpecies(Species.VOLCARONA).baseExp * 2);
|
||||
leaveEncounterWithoutBattle(scene);
|
||||
})
|
||||
.build()
|
||||
)
|
||||
.build();
|
||||
|
||||
function giveLeadPokemonCharcoal(scene: BattleScene) {
|
||||
// Give first party pokemon Charcoal for free at end of battle
|
||||
function giveLeadPokemonAttackTypeBoostItem(scene: BattleScene) {
|
||||
// Give first party pokemon attack type boost item for free at end of battle
|
||||
const leadPokemon = scene.getParty()?.[0];
|
||||
if (leadPokemon) {
|
||||
const charcoal = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType;
|
||||
applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal);
|
||||
scene.currentBattle.mysteryEncounter!.setDialogueToken("leadPokemon", leadPokemon.getNameToRender());
|
||||
queueEncounterMessage(scene, `${namespace}:found_charcoal`);
|
||||
// Generate type booster held item, default to Charcoal if item fails to generate
|
||||
let boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER) as AttackTypeBoosterModifierType;
|
||||
if (!boosterModifierType) {
|
||||
boosterModifierType = generateModifierType(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [ Type.FIRE ]) as AttackTypeBoosterModifierType;
|
||||
}
|
||||
applyModifierTypeToPlayerPokemon(scene, leadPokemon, boosterModifierType);
|
||||
|
||||
const encounter = scene.currentBattle.mysteryEncounter!;
|
||||
encounter.setDialogueToken("itemName", boosterModifierType.name);
|
||||
encounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender());
|
||||
queueEncounterMessage(scene, `${namespace}:found_item`);
|
||||
}
|
||||
}
|
||||
|
@ -56,7 +56,13 @@ export const MysteriousChallengersEncounter: MysteryEncounter =
|
||||
|
||||
// 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);
|
||||
let retries = 0;
|
||||
let hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex);
|
||||
while (retries < 5 && hardTrainerType === normalTrainerType) {
|
||||
// Will try to use a different trainer from the normal trainer type
|
||||
hardTrainerType = scene.arena.randomTrainerType(scene.currentBattle.waveIndex);
|
||||
retries++;
|
||||
}
|
||||
const hardTemplate = new TrainerPartyCompoundTemplate(
|
||||
new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true),
|
||||
new TrainerPartyTemplate(
|
||||
|
@ -21,7 +21,7 @@ import i18next from "i18next";
|
||||
const namespace = "mysteryEncounters/shadyVitaminDealer";
|
||||
|
||||
const VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER = 1.5;
|
||||
const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 3.5;
|
||||
const VITAMIN_DEALER_EXPENSIVE_PRICE_MULTIPLIER = 5;
|
||||
|
||||
/**
|
||||
* Shady Vitamin Dealer encounter.
|
||||
|
@ -222,7 +222,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
|
||||
encounter.misc.chosenPokemon = pokemon1;
|
||||
encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender());
|
||||
const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs);
|
||||
setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
|
||||
setEncounterRewards(scene,
|
||||
{ guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true },
|
||||
eggOptions,
|
||||
() => doPostEncounterCleanup(scene));
|
||||
|
||||
// Remove all Pokemon from the party except the chosen Pokemon
|
||||
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon1);
|
||||
@ -271,7 +274,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
|
||||
encounter.misc.chosenPokemon = pokemon2;
|
||||
encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender());
|
||||
const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs);
|
||||
setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
|
||||
setEncounterRewards(scene,
|
||||
{ guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true },
|
||||
eggOptions,
|
||||
() => doPostEncounterCleanup(scene));
|
||||
|
||||
// Remove all Pokemon from the party except the chosen Pokemon
|
||||
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon2);
|
||||
@ -320,7 +326,10 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
|
||||
encounter.misc.chosenPokemon = pokemon3;
|
||||
encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender());
|
||||
const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs);
|
||||
setEncounterRewards(scene, { fillRemaining: true }, eggOptions, () => doPostEncounterCleanup(scene));
|
||||
setEncounterRewards(scene,
|
||||
{ guaranteedModifierTypeFuncs: [ modifierTypes.SOOTHE_BELL ], fillRemaining: true },
|
||||
eggOptions,
|
||||
() => doPostEncounterCleanup(scene));
|
||||
|
||||
// Remove all Pokemon from the party except the chosen Pokemon
|
||||
removePokemonFromPartyAndStoreHeldItems(scene, encounter, pokemon3);
|
||||
@ -454,12 +463,16 @@ function calculateEggRewardsForPokemon(pokemon: PlayerPokemon): [number, number]
|
||||
}
|
||||
|
||||
// Maximum of 30 points
|
||||
const totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30);
|
||||
let totalPoints = Math.min(pointsFromStarterTier + pointsFromBst, 30);
|
||||
|
||||
// 1 Rare egg for every 6 points
|
||||
const numRares = Math.floor(totalPoints / 6);
|
||||
// First 5 points go to Common eggs
|
||||
let numCommons = Math.min(totalPoints, 5);
|
||||
totalPoints -= numCommons;
|
||||
|
||||
// Then, 1 Rare egg for every 4 points
|
||||
const numRares = Math.floor(totalPoints / 4);
|
||||
// 1 Common egg for every point leftover
|
||||
const numCommons = totalPoints % 6;
|
||||
numCommons += totalPoints % 4;
|
||||
|
||||
return [ numCommons, numRares ];
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
|
||||
.withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party
|
||||
.withFleeAllowed(false)
|
||||
.withHideWildIntroMessage(true)
|
||||
.withPreventGameStatsUpdates(true) // Do not count the Pokemon as seen or defeated since it is ours
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "training_session_gear",
|
||||
|
@ -71,7 +71,7 @@ export const TrashToTreasureEncounter: MysteryEncounter =
|
||||
moveSet: [ Moves.PAYBACK, Moves.GUNK_SHOT, Moves.STOMPING_TANTRUM, Moves.DRAIN_PUNCH ]
|
||||
};
|
||||
const config: EnemyPartyConfig = {
|
||||
levelAdditiveModifier: 1,
|
||||
levelAdditiveModifier: 0.5,
|
||||
pokemonConfigs: [ pokemonConfig ],
|
||||
disableSwitch: true
|
||||
};
|
||||
|
@ -4,23 +4,30 @@ import { Species } from "#enums/species";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
|
||||
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
|
||||
import { leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils";
|
||||
import { EnemyPartyConfig, EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils";
|
||||
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
|
||||
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
|
||||
import { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
|
||||
import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
|
||||
import { IntegerHolder, isNullOrUndefined, randSeedInt, randSeedShuffle } from "#app/utils";
|
||||
import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
|
||||
import { HiddenAbilityRateBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
|
||||
import { achvs } from "#app/system/achv";
|
||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
|
||||
import { modifierTypes } from "#app/modifier/modifier-type";
|
||||
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
|
||||
import i18next from "#app/plugins/i18n";
|
||||
import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
|
||||
import { getLevelTotalExp } from "#app/data/exp";
|
||||
import { Stat } from "#enums/stat";
|
||||
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
|
||||
import { Challenges } from "#enums/challenges";
|
||||
import { ModifierTier } from "#app/modifier/modifier-tier";
|
||||
import { PlayerGender } from "#enums/player-gender";
|
||||
import { TrainerType } from "#enums/trainer-type";
|
||||
import PokemonData from "#app/system/pokemon-data";
|
||||
import { Nature } from "#enums/nature";
|
||||
import HeldModifierConfig from "#app/interfaces/held-modifier-config";
|
||||
import { trainerConfigs, TrainerPartyTemplate } from "#app/data/trainer-config";
|
||||
import { PartyMemberStrength } from "#enums/party-member-strength";
|
||||
|
||||
/** i18n namespace for encounter */
|
||||
const namespace = "mysteryEncounters/weirdDream";
|
||||
@ -80,10 +87,11 @@ const EXCLUDED_TRANSFORMATION_SPECIES = [
|
||||
|
||||
const SUPER_LEGENDARY_BST_THRESHOLD = 600;
|
||||
const NON_LEGENDARY_BST_THRESHOLD = 570;
|
||||
const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450;
|
||||
|
||||
const OLD_GATEAU_STATS_UP = 20;
|
||||
|
||||
/** 0-100 */
|
||||
const PERCENT_LEVEL_LOSS_ON_REFUSE = 12.5;
|
||||
const PERCENT_LEVEL_LOSS_ON_REFUSE = 10;
|
||||
|
||||
/**
|
||||
* Value ranges of the resulting species BST transformations after adding values to original species
|
||||
@ -105,7 +113,8 @@ export const WeirdDreamEncounter: MysteryEncounter =
|
||||
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM)
|
||||
.withEncounterTier(MysteryEncounterTier.ROGUE)
|
||||
.withDisallowedChallenges(Challenges.SINGLE_TYPE, Challenges.SINGLE_GENERATION)
|
||||
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
|
||||
// TODO: should reset minimum wave to 10 when there are more Rogue tiers in pool. Matching Dark Deal minimum for now.
|
||||
.withSceneWaveRangeRequirement(30, 140)
|
||||
.withIntroSpriteConfigs([
|
||||
{
|
||||
spriteKey: "weird_dream_woman",
|
||||
@ -131,6 +140,15 @@ export const WeirdDreamEncounter: MysteryEncounter =
|
||||
.withQuery(`${namespace}:query`)
|
||||
.withOnInit((scene: BattleScene) => {
|
||||
scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3");
|
||||
|
||||
// Calculate all the newly transformed Pokemon and begin asset load
|
||||
const teamTransformations = getTeamTransformations(scene);
|
||||
const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets());
|
||||
scene.currentBattle.mysteryEncounter!.misc = {
|
||||
teamTransformations,
|
||||
loadAssets
|
||||
};
|
||||
|
||||
return true;
|
||||
})
|
||||
.withOnVisualsStart((scene: BattleScene) => {
|
||||
@ -156,13 +174,10 @@ export const WeirdDreamEncounter: MysteryEncounter =
|
||||
doShowDreamBackground(scene);
|
||||
});
|
||||
|
||||
// Calculate all the newly transformed Pokemon and begin asset load
|
||||
const teamTransformations = getTeamTransformations(scene);
|
||||
const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets());
|
||||
scene.currentBattle.mysteryEncounter!.misc = {
|
||||
teamTransformations,
|
||||
loadAssets
|
||||
};
|
||||
for (const transformation of scene.currentBattle.mysteryEncounter!.misc.teamTransformations) {
|
||||
scene.removePokemonFromPlayerParty(transformation.previousPokemon, false);
|
||||
scene.getParty().push(transformation.newPokemon);
|
||||
}
|
||||
})
|
||||
.withOptionPhase(async (scene: BattleScene) => {
|
||||
// Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue
|
||||
@ -193,7 +208,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
|
||||
await showEncounterText(scene, `${namespace}:option.1.dream_complete`);
|
||||
|
||||
await doNewTeamPostProcess(scene, transformations);
|
||||
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT ]});
|
||||
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [ modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT, modifierTypes.MINT ], fillRemaining: false });
|
||||
leaveEncounterWithoutBattle(scene, true);
|
||||
})
|
||||
.build()
|
||||
@ -209,7 +224,88 @@ export const WeirdDreamEncounter: MysteryEncounter =
|
||||
],
|
||||
},
|
||||
async (scene: BattleScene) => {
|
||||
// Reduce party levels by 20%
|
||||
// Battle your "future" team for some item rewards
|
||||
const transformations: PokemonTransformation[] = scene.currentBattle.mysteryEncounter!.misc.teamTransformations;
|
||||
|
||||
// Uses the pokemon that player's party would have transformed into
|
||||
const enemyPokemonConfigs: EnemyPokemonConfig[] = [];
|
||||
for (const transformation of transformations) {
|
||||
const newPokemon = transformation.newPokemon;
|
||||
const previousPokemon = transformation.previousPokemon;
|
||||
|
||||
await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, newPokemon.species.getRootSpeciesId(), true);
|
||||
|
||||
const dataSource = new PokemonData(newPokemon);
|
||||
dataSource.player = false;
|
||||
|
||||
// Copy held items to new pokemon
|
||||
const newPokemonHeldItemConfigs: HeldModifierConfig[] = [];
|
||||
for (const item of transformation.heldItems) {
|
||||
newPokemonHeldItemConfigs.push({
|
||||
modifier: item.clone() as PokemonHeldItemModifier,
|
||||
stackCount: item.getStackCount(),
|
||||
isTransferable: false
|
||||
});
|
||||
}
|
||||
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
|
||||
if (shouldGetOldGateau(newPokemon)) {
|
||||
const stats = getOldGateauBoostedStats(newPokemon);
|
||||
newPokemonHeldItemConfigs.push({
|
||||
modifier: generateModifierType(scene, modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU, [ OLD_GATEAU_STATS_UP, stats ]) as PokemonHeldItemModifierType,
|
||||
stackCount: 1,
|
||||
isTransferable: false
|
||||
});
|
||||
}
|
||||
|
||||
const enemyConfig: EnemyPokemonConfig = {
|
||||
species: transformation.newSpecies,
|
||||
isBoss: newPokemon.getSpeciesForm().getBaseStatTotal() > NON_LEGENDARY_BST_THRESHOLD,
|
||||
level: previousPokemon.level,
|
||||
dataSource: dataSource,
|
||||
modifierConfigs: newPokemonHeldItemConfigs
|
||||
};
|
||||
|
||||
enemyPokemonConfigs.push(enemyConfig);
|
||||
}
|
||||
|
||||
const genderIndex = scene.gameData.gender ?? PlayerGender.UNSET;
|
||||
const trainerConfig = trainerConfigs[genderIndex === PlayerGender.FEMALE ? TrainerType.FUTURE_SELF_F : TrainerType.FUTURE_SELF_M].clone();
|
||||
trainerConfig.setPartyTemplates(new TrainerPartyTemplate(transformations.length, PartyMemberStrength.STRONG));
|
||||
const enemyPartyConfig: EnemyPartyConfig = {
|
||||
trainerConfig: trainerConfig,
|
||||
pokemonConfigs: enemyPokemonConfigs,
|
||||
female: genderIndex === PlayerGender.FEMALE
|
||||
};
|
||||
|
||||
const onBeforeRewards = () => {
|
||||
// Before battle rewards, unlock the passive on a pokemon in the player's team for the rest of the run (not permanently)
|
||||
// One random pokemon will get its passive unlocked
|
||||
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive);
|
||||
if (passiveDisabledPokemon?.length > 0) {
|
||||
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
|
||||
enablePassiveMon.passive = true;
|
||||
enablePassiveMon.updateInfo(true);
|
||||
}
|
||||
};
|
||||
|
||||
setEncounterRewards(scene, { guaranteedModifierTiers: [ ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT ], fillRemaining: false }, undefined, onBeforeRewards);
|
||||
|
||||
await showEncounterText(scene, `${namespace}:option.2.selected_2`, null, undefined, true);
|
||||
await initBattleWithEnemyConfig(scene, enemyPartyConfig);
|
||||
}
|
||||
)
|
||||
.withSimpleOption(
|
||||
{
|
||||
buttonLabel: `${namespace}:option.3.label`,
|
||||
buttonTooltip: `${namespace}:option.3.tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: `${namespace}:option.3.selected`,
|
||||
},
|
||||
],
|
||||
},
|
||||
async (scene: BattleScene) => {
|
||||
// Leave, reduce party levels by 10%
|
||||
for (const pokemon of scene.getParty()) {
|
||||
pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1);
|
||||
pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate);
|
||||
@ -235,7 +331,7 @@ interface PokemonTransformation {
|
||||
function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
|
||||
const party = scene.getParty();
|
||||
// Removes all pokemon from the party
|
||||
const alreadyUsedSpecies: PokemonSpecies[] = [];
|
||||
const alreadyUsedSpecies: PokemonSpecies[] = party.map(p => p.species);
|
||||
const pokemonTransformations: PokemonTransformation[] = party.map(p => {
|
||||
return {
|
||||
previousPokemon: p
|
||||
@ -250,11 +346,11 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
|
||||
// First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference
|
||||
// Then, roll the remainder of the party members at a +40 to +50 BST difference
|
||||
const numPokemon = party.length;
|
||||
const removedPokemon = randSeedShuffle(party.slice(0));
|
||||
for (let i = 0; i < numPokemon; i++) {
|
||||
const removed = party[randSeedInt(party.length)];
|
||||
const removed = removedPokemon[i];
|
||||
const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id);
|
||||
pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
|
||||
scene.removePokemonFromPlayerParty(removed, false);
|
||||
|
||||
const bst = removed.calculateBaseStats().reduce((a, b) => a + b, 0);
|
||||
let newBstRange: [number, number];
|
||||
@ -276,14 +372,13 @@ function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
|
||||
|
||||
|
||||
pokemonTransformations[index].newSpecies = newSpecies;
|
||||
console.log("New species: " + JSON.stringify(newSpecies));
|
||||
alreadyUsedSpecies.push(newSpecies);
|
||||
}
|
||||
|
||||
for (const transformation of pokemonTransformations) {
|
||||
const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount());
|
||||
const newPlayerPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined);
|
||||
transformation.newPokemon = newPlayerPokemon;
|
||||
scene.getParty().push(newPlayerPokemon);
|
||||
transformation.newPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined);
|
||||
}
|
||||
|
||||
return pokemonTransformations;
|
||||
@ -296,6 +391,56 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
|
||||
const newPokemon = transformation.newPokemon;
|
||||
const speciesRootForm = newPokemon.species.getRootSpeciesId();
|
||||
|
||||
if (await postProcessTransformedPokemon(scene, previousPokemon, newPokemon, speciesRootForm)) {
|
||||
atLeastOneNewStarter = true;
|
||||
}
|
||||
|
||||
// Copy old items to new pokemon
|
||||
for (const item of transformation.heldItems) {
|
||||
item.pokemonId = newPokemon.id;
|
||||
await scene.addModifier(item, false, false, false, true);
|
||||
}
|
||||
// Any pokemon that is below 570 BST gets +20 permanent BST to 3 stats
|
||||
if (shouldGetOldGateau(newPokemon)) {
|
||||
const stats = getOldGateauBoostedStats(newPokemon);
|
||||
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU()
|
||||
.generateType(scene.getParty(), [ OLD_GATEAU_STATS_UP, stats ])
|
||||
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
|
||||
const modifier = modType?.newModifier(newPokemon);
|
||||
if (modifier) {
|
||||
await scene.addModifier(modifier, false, false, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
newPokemon.calculateStats();
|
||||
await newPokemon.updateInfo();
|
||||
}
|
||||
|
||||
// One random pokemon will get its passive unlocked
|
||||
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive);
|
||||
if (passiveDisabledPokemon?.length > 0) {
|
||||
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
|
||||
enablePassiveMon.passive = true;
|
||||
await enablePassiveMon.updateInfo(true);
|
||||
}
|
||||
|
||||
// If at least one new starter was unlocked, play 1 fanfare
|
||||
if (atLeastOneNewStarter) {
|
||||
scene.playSound("level_up_fanfare");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies special changes to the newly transformed pokemon, such as passing previous moves, gaining egg moves, etc.
|
||||
* Returns whether the transformed pokemon unlocks a new starter for the player.
|
||||
* @param scene
|
||||
* @param previousPokemon
|
||||
* @param newPokemon
|
||||
* @param speciesRootForm
|
||||
* @param forBattle Default `false`. If false, will perform achievements and dex unlocks for the player.
|
||||
*/
|
||||
async function postProcessTransformedPokemon(scene: BattleScene, previousPokemon: PlayerPokemon, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise<boolean> {
|
||||
let isNewStarter = false;
|
||||
// Roll HA a second time
|
||||
if (newPokemon.species.abilityHidden) {
|
||||
const hiddenIndex = newPokemon.species.ability2 ? 2 : 1;
|
||||
@ -317,8 +462,11 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
|
||||
return newValue > iv ? newValue : iv;
|
||||
});
|
||||
|
||||
// Roll a neutral nature
|
||||
newPokemon.nature = [ Nature.HARDY, Nature.DOCILE, Nature.BASHFUL, Nature.QUIRKY, Nature.SERIOUS ][randSeedInt(5)];
|
||||
|
||||
// For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it
|
||||
if (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny()) {
|
||||
if (!forBattle && (newPokemon.getSpeciesForm().getBaseStatTotal() <= NON_LEGENDARY_BST_THRESHOLD || newPokemon.isShiny())) {
|
||||
if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) {
|
||||
scene.validateAchv(achvs.HIDDEN_ABILITY);
|
||||
}
|
||||
@ -338,7 +486,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
|
||||
scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs);
|
||||
const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false);
|
||||
if (newStarterUnlocked) {
|
||||
atLeastOneNewStarter = true;
|
||||
isNewStarter = true;
|
||||
await showEncounterText(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() }));
|
||||
}
|
||||
}
|
||||
@ -355,7 +503,7 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
|
||||
});
|
||||
|
||||
// For pokemon that the player owns (including ones just caught), gain a candy
|
||||
if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) {
|
||||
if (!forBattle && !!scene.gameData.dexData[speciesRootForm].caughtAttr) {
|
||||
scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1);
|
||||
}
|
||||
|
||||
@ -364,9 +512,9 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
|
||||
// Store a copy of a "standard" generated moveset for the new pokemon, will be used later for finding a favored move
|
||||
const newPokemonGeneratedMoveset = newPokemon.moveset;
|
||||
|
||||
newPokemon.moveset = previousPokemon.moveset;
|
||||
newPokemon.moveset = previousPokemon.moveset.slice(0);
|
||||
|
||||
const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm);
|
||||
const newEggMoveIndex = await addEggMoveToNewPokemonMoveset(scene, newPokemon, speciesRootForm, forBattle);
|
||||
|
||||
// Try to add a favored STAB move (might fail if Pokemon already knows a bunch of moves from newPokemonGeneratedMoveset)
|
||||
addFavoredMoveToNewPokemonMoveset(newPokemon, newPokemonGeneratedMoveset, newEggMoveIndex);
|
||||
@ -384,49 +532,36 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
|
||||
}
|
||||
newPokemon.customPokemonData.types = newTypes;
|
||||
|
||||
for (const item of transformation.heldItems) {
|
||||
item.pokemonId = newPokemon.id;
|
||||
await scene.addModifier(item, false, false, false, true);
|
||||
}
|
||||
// Enable passive if previous had it
|
||||
newPokemon.passive = previousPokemon.passive;
|
||||
|
||||
// Any pokemon that is at or below 450 BST gets +20 permanent BST to 3 stats: HP (halved, +10), lowest of Atk/SpAtk, and lowest of Def/SpDef
|
||||
if (newPokemon.getSpeciesForm().getBaseStatTotal() <= GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD) {
|
||||
const stats: Stat[] = [ Stat.HP ];
|
||||
const baseStats = newPokemon.getSpeciesForm().baseStats.slice(0);
|
||||
return isNewStarter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns `true` if a given Pokemon has valid BST to be given an Old Gateau
|
||||
*/
|
||||
function shouldGetOldGateau(pokemon: Pokemon): boolean {
|
||||
return pokemon.getSpeciesForm().getBaseStatTotal() < NON_LEGENDARY_BST_THRESHOLD;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the lowest of HP/Spd, lowest of Atk/SpAtk, and lowest of Def/SpDef
|
||||
* @returns Array of 3 {@linkcode Stat}s to boost
|
||||
*/
|
||||
function getOldGateauBoostedStats(pokemon: Pokemon): Stat[] {
|
||||
const stats: Stat[] = [];
|
||||
const baseStats = pokemon.getSpeciesForm().baseStats.slice(0);
|
||||
// HP or Speed
|
||||
stats.push(baseStats[Stat.HP] < baseStats[Stat.SPD] ? Stat.HP : Stat.SPD);
|
||||
// Attack or SpAtk
|
||||
stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK);
|
||||
// Def or SpDef
|
||||
stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF);
|
||||
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU()
|
||||
.generateType(scene.getParty(), [ 20, stats ])
|
||||
?.withIdFromFunc(modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU);
|
||||
const modifier = modType?.newModifier(newPokemon);
|
||||
if (modifier) {
|
||||
await scene.addModifier(modifier, false, false, false, true);
|
||||
}
|
||||
}
|
||||
|
||||
// Enable passive if previous had it
|
||||
newPokemon.passive = previousPokemon.passive;
|
||||
|
||||
newPokemon.calculateStats();
|
||||
await newPokemon.updateInfo();
|
||||
}
|
||||
|
||||
// One random pokemon will get its passive unlocked
|
||||
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive);
|
||||
if (passiveDisabledPokemon?.length > 0) {
|
||||
const enablePassiveMon = passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)];
|
||||
enablePassiveMon.passive = true;
|
||||
await enablePassiveMon.updateInfo(true);
|
||||
}
|
||||
|
||||
// If at least one new starter was unlocked, play 1 fanfare
|
||||
if (atLeastOneNewStarter) {
|
||||
scene.playSound("level_up_fanfare");
|
||||
}
|
||||
return stats;
|
||||
}
|
||||
|
||||
|
||||
function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies {
|
||||
let newSpecies: PokemonSpecies | undefined;
|
||||
while (isNullOrUndefined(newSpecies)) {
|
||||
@ -550,7 +685,7 @@ function doSideBySideTransformations(scene: BattleScene, transformations: Pokemo
|
||||
* @param newPokemon
|
||||
* @param speciesRootForm
|
||||
*/
|
||||
async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species): Promise<number | null> {
|
||||
async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: PlayerPokemon, speciesRootForm: Species, forBattle: boolean = false): Promise<number | null> {
|
||||
let eggMoveIndex: null | number = null;
|
||||
const eggMoves = newPokemon.getEggMoves()?.slice(0);
|
||||
if (eggMoves) {
|
||||
@ -576,7 +711,7 @@ async function addEggMoveToNewPokemonMoveset(scene: BattleScene, newPokemon: Pla
|
||||
}
|
||||
|
||||
// For pokemon that the player owns (including ones just caught), unlock the egg move
|
||||
if (!isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) {
|
||||
if (!forBattle && !isNullOrUndefined(randomEggMoveIndex) && !!scene.gameData.dexData[speciesRootForm].caughtAttr) {
|
||||
await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true);
|
||||
}
|
||||
}
|
||||
|
@ -37,31 +37,58 @@ export abstract class EncounterSceneRequirement implements EncounterRequirement
|
||||
abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string];
|
||||
}
|
||||
|
||||
/**
|
||||
* Combination of multiple {@linkcode EncounterSceneRequirement | EncounterSceneRequirements} (OR/AND possible. See {@linkcode isAnd})
|
||||
*/
|
||||
export class CombinationSceneRequirement extends EncounterSceneRequirement {
|
||||
orRequirements: EncounterSceneRequirement[];
|
||||
/** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */
|
||||
private isAnd: boolean;
|
||||
requirements: EncounterSceneRequirement[];
|
||||
|
||||
constructor(... orRequirements: EncounterSceneRequirement[]) {
|
||||
public static Some(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement {
|
||||
return new CombinationSceneRequirement(false, ...requirements);
|
||||
}
|
||||
|
||||
public static Every(...requirements: EncounterSceneRequirement[]): CombinationSceneRequirement {
|
||||
return new CombinationSceneRequirement(true, ...requirements);
|
||||
}
|
||||
|
||||
private constructor(isAnd: boolean, ...requirements: EncounterSceneRequirement[]) {
|
||||
super();
|
||||
this.orRequirements = orRequirements;
|
||||
this.isAnd = isAnd;
|
||||
this.requirements = requirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all/any requirements are met (depends on {@linkcode isAnd})
|
||||
* @param scene The {@linkcode BattleScene} to check against
|
||||
* @returns true if all/any requirements are met (depends on {@linkcode isAnd})
|
||||
*/
|
||||
override meetsRequirement(scene: BattleScene): boolean {
|
||||
for (const req of this.orRequirements) {
|
||||
if (req.meetsRequirement(scene)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return this.isAnd
|
||||
? this.requirements.every(req => req.meetsRequirement(scene))
|
||||
: this.requirements.some(req => req.meetsRequirement(scene));
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a dialogue token key/value pair for the given {@linkcode EncounterSceneRequirement | requirements}.
|
||||
* @param scene The {@linkcode BattleScene} to check against
|
||||
* @param pokemon The {@linkcode PlayerPokemon} to check against
|
||||
* @returns A dialogue token key/value pair
|
||||
* @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported)
|
||||
*/
|
||||
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
|
||||
for (const req of this.orRequirements) {
|
||||
if (this.isAnd) {
|
||||
throw new Error("Not implemented (Sorry)");
|
||||
} else {
|
||||
for (const req of this.requirements) {
|
||||
if (req.meetsRequirement(scene)) {
|
||||
return req.getDialogueToken(scene, pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
return this.orRequirements[0].getDialogueToken(scene, pokemon);
|
||||
return this.requirements[0].getDialogueToken(scene, pokemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,44 +117,74 @@ export abstract class EncounterPokemonRequirement implements EncounterRequiremen
|
||||
abstract getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string];
|
||||
}
|
||||
|
||||
/**
|
||||
* Combination of multiple {@linkcode EncounterPokemonRequirement | EncounterPokemonRequirements} (OR/AND possible. See {@linkcode isAnd})
|
||||
*/
|
||||
export class CombinationPokemonRequirement extends EncounterPokemonRequirement {
|
||||
orRequirements: EncounterPokemonRequirement[];
|
||||
/** If `true`, all requirements must be met (AND). If `false`, any requirement must be met (OR) */
|
||||
private isAnd: boolean;
|
||||
private requirements: EncounterPokemonRequirement[];
|
||||
|
||||
constructor(...orRequirements: EncounterPokemonRequirement[]) {
|
||||
public static Some(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement {
|
||||
return new CombinationPokemonRequirement(false, ...requirements);
|
||||
}
|
||||
|
||||
public static Every(...requirements: EncounterPokemonRequirement[]): CombinationPokemonRequirement {
|
||||
return new CombinationPokemonRequirement(true, ...requirements);
|
||||
}
|
||||
|
||||
private constructor(isAnd: boolean, ...requirements: EncounterPokemonRequirement[]) {
|
||||
super();
|
||||
this.isAnd = isAnd;
|
||||
this.invertQuery = false;
|
||||
this.minNumberOfPokemon = 1;
|
||||
this.orRequirements = orRequirements;
|
||||
this.requirements = requirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if all/any requirements are met (depends on {@linkcode isAnd})
|
||||
* @param scene The {@linkcode BattleScene} to check against
|
||||
* @returns true if all/any requirements are met (depends on {@linkcode isAnd})
|
||||
*/
|
||||
override meetsRequirement(scene: BattleScene): boolean {
|
||||
for (const req of this.orRequirements) {
|
||||
if (req.meetsRequirement(scene)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return this.isAnd
|
||||
? this.requirements.every(req => req.meetsRequirement(scene))
|
||||
: this.requirements.some(req => req.meetsRequirement(scene));
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the players party for all party members that are compatible with all/any requirements (depends on {@linkcode isAnd})
|
||||
* @param partyPokemon The party of {@linkcode PlayerPokemon}
|
||||
* @returns All party members that are compatible with all/any requirements (depends on {@linkcode isAnd})
|
||||
*/
|
||||
override queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] {
|
||||
for (const req of this.orRequirements) {
|
||||
const result = req.queryParty(partyPokemon);
|
||||
if (result?.length > 0) {
|
||||
return result;
|
||||
if (this.isAnd) {
|
||||
return this.requirements.reduce((relevantPokemon, req) => req.queryParty(relevantPokemon), partyPokemon);
|
||||
} else {
|
||||
const matchingRequirement = this.requirements.find(req => req.queryParty(partyPokemon).length > 0);
|
||||
return matchingRequirement ? matchingRequirement.queryParty(partyPokemon) : [];
|
||||
}
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a dialogue token key/value pair for the given {@linkcode EncounterPokemonRequirement | requirements}.
|
||||
* @param scene The {@linkcode BattleScene} to check against
|
||||
* @param pokemon The {@linkcode PlayerPokemon} to check against
|
||||
* @returns A dialogue token key/value pair
|
||||
* @throws An {@linkcode Error} if {@linkcode isAnd} is `true` (not supported)
|
||||
*/
|
||||
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
|
||||
for (const req of this.orRequirements) {
|
||||
if (this.isAnd) {
|
||||
throw new Error("Not implemented (Sorry)");
|
||||
} else {
|
||||
for (const req of this.requirements) {
|
||||
if (req.meetsRequirement(scene)) {
|
||||
return req.getDialogueToken(scene, pokemon);
|
||||
}
|
||||
}
|
||||
|
||||
return this.orRequirements[0].getDialogueToken(scene, pokemon);
|
||||
return this.requirements[0].getDialogueToken(scene, pokemon);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -53,6 +53,7 @@ export interface IMysteryEncounter {
|
||||
hasBattleAnimationsWithoutTargets: boolean;
|
||||
skipEnemyBattleTurns: boolean;
|
||||
skipToFightInput: boolean;
|
||||
preventGameStatsUpdates: boolean;
|
||||
|
||||
onInit?: (scene: BattleScene) => boolean;
|
||||
onVisualsStart?: (scene: BattleScene) => boolean;
|
||||
@ -150,6 +151,10 @@ export default class MysteryEncounter implements IMysteryEncounter {
|
||||
* If true, will skip COMMAND input and go straight to FIGHT (move select) input menu
|
||||
*/
|
||||
skipToFightInput: boolean;
|
||||
/**
|
||||
* If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon
|
||||
*/
|
||||
preventGameStatsUpdates: boolean;
|
||||
|
||||
// #region Event callback functions
|
||||
|
||||
@ -548,6 +553,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
|
||||
hasBattleAnimationsWithoutTargets: boolean = false;
|
||||
skipEnemyBattleTurns: boolean = false;
|
||||
skipToFightInput: boolean = false;
|
||||
preventGameStatsUpdates: boolean = false;
|
||||
maxAllowedEncounters: number = 3;
|
||||
expMultiplier: number = 1;
|
||||
|
||||
@ -735,6 +741,14 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
|
||||
return Object.assign(this, { skipToFightInput });
|
||||
}
|
||||
|
||||
/**
|
||||
* If true, will prevent updating {@linkcode GameStats} for encountering and/or defeating Pokemon
|
||||
* Default `false`
|
||||
*/
|
||||
withPreventGameStatsUpdates(preventGameStatsUpdates: boolean): this & Required<Pick<IMysteryEncounter, "preventGameStatsUpdates">> {
|
||||
return Object.assign(this, { preventGameStatsUpdates });
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the maximum number of times that an encounter can spawn in a given Classic run
|
||||
* @param maxAllowedEncounters
|
||||
|
@ -118,3 +118,20 @@ export const EXTORTION_ABILITIES = [
|
||||
Abilities.SUCTION_CUPS,
|
||||
Abilities.STICKY_HOLD
|
||||
];
|
||||
|
||||
/**
|
||||
* Abilities that signify resistance to fire
|
||||
*/
|
||||
export const FIRE_RESISTANT_ABILITIES = [
|
||||
Abilities.FLAME_BODY,
|
||||
Abilities.FLASH_FIRE,
|
||||
Abilities.WELL_BAKED_BODY,
|
||||
Abilities.HEATPROOF,
|
||||
Abilities.THERMAL_EXCHANGE,
|
||||
Abilities.THICK_FAT,
|
||||
Abilities.WATER_BUBBLE,
|
||||
Abilities.MAGMA_ARMOR,
|
||||
Abilities.WATER_VEIL,
|
||||
Abilities.STEAM_ENGINE,
|
||||
Abilities.PRIMORDIAL_SEA
|
||||
];
|
||||
|
@ -21,6 +21,8 @@ import { Gender } from "#app/data/gender";
|
||||
import { PermanentStat } from "#enums/stat";
|
||||
import { VictoryPhase } from "#app/phases/victory-phase";
|
||||
import { SummaryUiMode } from "#app/ui/summary-ui-handler";
|
||||
import { CustomPokemonData } from "#app/data/custom-pokemon-data";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
|
||||
/** Will give +1 level every 10 waves */
|
||||
export const STANDARD_ENCOUNTER_BOOSTED_LEVEL_MODIFIER = 1;
|
||||
@ -833,3 +835,21 @@ export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scen
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Permanently overrides the ability (not passive) of a pokemon.
|
||||
* If the pokemon is a fusion, instead overrides the fused pokemon's ability.
|
||||
*/
|
||||
export function applyAbilityOverrideToPokemon(pokemon: Pokemon, ability: Abilities) {
|
||||
if (pokemon.isFusion()) {
|
||||
if (!pokemon.fusionCustomPokemonData) {
|
||||
pokemon.fusionCustomPokemonData = new CustomPokemonData();
|
||||
}
|
||||
pokemon.fusionCustomPokemonData.ability = ability;
|
||||
} else {
|
||||
if (!pokemon.customPokemonData) {
|
||||
pokemon.customPokemonData = new CustomPokemonData();
|
||||
}
|
||||
pokemon.customPokemonData.ability = ability;
|
||||
}
|
||||
}
|
||||
|
@ -2500,6 +2500,22 @@ export const trainerConfigs: TrainerConfigs = {
|
||||
[TrainerType.BUG_TYPE_SUPERFAN]: new TrainerConfig(++t).setMoneyMultiplier(2.25).setEncounterBgm(TrainerType.ACE_TRAINER)
|
||||
.setPartyTemplates(new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE)),
|
||||
[TrainerType.EXPERT_POKEMON_BREEDER]: new TrainerConfig(++t).setMoneyMultiplier(3).setEncounterBgm(TrainerType.ACE_TRAINER).setLocalizedName("Expert Pokemon Breeder")
|
||||
.setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.STRONG))
|
||||
.setPartyTemplates(new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE)),
|
||||
[TrainerType.FUTURE_SELF_M]: new TrainerConfig(++t)
|
||||
.setMoneyMultiplier(0)
|
||||
.setEncounterBgm("mystery_encounter_weird_dream")
|
||||
.setBattleBgm("mystery_encounter_weird_dream")
|
||||
.setMixedBattleBgm("mystery_encounter_weird_dream")
|
||||
.setVictoryBgm("mystery_encounter_weird_dream")
|
||||
.setLocalizedName("Future Self M")
|
||||
.setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG)),
|
||||
[TrainerType.FUTURE_SELF_F]: new TrainerConfig(++t)
|
||||
.setMoneyMultiplier(0)
|
||||
.setEncounterBgm("mystery_encounter_weird_dream")
|
||||
.setBattleBgm("mystery_encounter_weird_dream")
|
||||
.setMixedBattleBgm("mystery_encounter_weird_dream")
|
||||
.setVictoryBgm("mystery_encounter_weird_dream")
|
||||
.setLocalizedName("Future Self F")
|
||||
.setPartyTemplates(new TrainerPartyTemplate(6, PartyMemberStrength.STRONG))
|
||||
};
|
||||
|
||||
|
@ -116,6 +116,8 @@ export enum TrainerType {
|
||||
VITO,
|
||||
BUG_TYPE_SUPERFAN,
|
||||
EXPERT_POKEMON_BREEDER,
|
||||
FUTURE_SELF_M,
|
||||
FUTURE_SELF_F,
|
||||
|
||||
BROCK = 200,
|
||||
MISTY,
|
||||
|
@ -93,7 +93,6 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
|
||||
public stats: integer[];
|
||||
public ivs: integer[];
|
||||
public nature: Nature;
|
||||
public natureOverride: Nature | -1;
|
||||
public moveset: (PokemonMove | null)[];
|
||||
public status: Status | null;
|
||||
public friendship: integer;
|
||||
@ -4478,7 +4477,6 @@ export class PlayerPokemon extends Pokemon {
|
||||
|
||||
if (newEvolution.condition?.predicate(this)) {
|
||||
const newPokemon = this.scene.addPlayerPokemon(this.species, this.level, this.abilityIndex, this.formIndex, undefined, this.shiny, this.variant, this.ivs, this.nature);
|
||||
newPokemon.natureOverride = this.natureOverride;
|
||||
newPokemon.passive = this.passive;
|
||||
newPokemon.moveset = this.moveset.slice();
|
||||
newPokemon.moveset = this.copyMoveset();
|
||||
|
@ -165,6 +165,8 @@ export class LoadingScene extends SceneBase {
|
||||
this.loadImage("discord", "ui");
|
||||
this.loadImage("google", "ui");
|
||||
this.loadImage("settings_icon", "ui");
|
||||
this.loadImage("link_icon", "ui");
|
||||
this.loadImage("unlink_icon", "ui");
|
||||
|
||||
this.loadImage("default_bg", "arenas");
|
||||
// Load arena images
|
||||
|
@ -10,7 +10,9 @@ import { getStatusEffectDescriptor, StatusEffect } from "#app/data/status-effect
|
||||
import { Type } from "#app/data/type";
|
||||
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
|
||||
import { getPokemonNameWithAffix } from "#app/messages";
|
||||
import { AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier } from "#app/modifier/modifier";
|
||||
import {
|
||||
AddPokeballModifier, AddVoucherModifier, AttackTypeBoosterModifier, BaseStatModifier, BerryModifier, BoostBugSpawnModifier, BypassSpeedChanceModifier, ContactHeldItemTransferChanceModifier, CritBoosterModifier, DamageMoneyRewardModifier, DoubleBattleChanceBoosterModifier, EnemyAttackStatusEffectChanceModifier, EnemyDamageBoosterModifier, EnemyDamageReducerModifier, EnemyEndureChanceModifier, EnemyFusionChanceModifier, EnemyStatusEffectHealChanceModifier, EnemyTurnHealModifier, EvolutionItemModifier, EvolutionStatBoosterModifier, EvoTrackerModifier, ExpBalanceModifier, ExpBoosterModifier, ExpShareModifier, ExtraModifierModifier, FlinchChanceModifier, FusePokemonModifier, GigantamaxAccessModifier, HealingBoosterModifier, HealShopCostModifier, HiddenAbilityRateBoosterModifier, HitHealModifier, IvScannerModifier, LevelIncrementBoosterModifier, LockModifierTiersModifier, MapModifier, MegaEvolutionAccessModifier, MoneyInterestModifier, MoneyMultiplierModifier, MoneyRewardModifier, MultipleParticipantExpBonusModifier, PokemonAllMovePpRestoreModifier, PokemonBaseStatFlatModifier, PokemonBaseStatTotalModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonFriendshipBoosterModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, PokemonInstantReviveModifier, PokemonLevelIncrementModifier, PokemonMoveAccuracyBoosterModifier, PokemonMultiHitModifier, PokemonNatureChangeModifier, PokemonNatureWeightModifier, PokemonPpRestoreModifier, PokemonPpUpModifier, PokemonStatusHealModifier, PreserveBerryModifier, RememberMoveModifier, ResetNegativeStatStageModifier, ShinyRateBoosterModifier, SpeciesCritBoosterModifier, SpeciesStatBoosterModifier, SurviveDamageModifier, SwitchEffectTransferModifier, TempCritBoosterModifier, TempStatStageBoosterModifier, TerastallizeAccessModifier, TerastallizeModifier, TmModifier, TurnHealModifier, TurnHeldItemTransferModifier, TurnStatusEffectModifier, type EnemyPersistentModifier, type Modifier, type PersistentModifier, TempExtraModifierModifier
|
||||
} from "#app/modifier/modifier";
|
||||
import { ModifierTier } from "#app/modifier/modifier-tier";
|
||||
import Overrides from "#app/overrides";
|
||||
import { Unlockables } from "#app/system/unlockables";
|
||||
@ -1561,6 +1563,7 @@ export const modifierTypes = {
|
||||
VOUCHER_PREMIUM: () => new AddVoucherModifierType(VoucherType.PREMIUM, 1),
|
||||
|
||||
GOLDEN_POKEBALL: () => new ModifierType("modifierType:ModifierType.GOLDEN_POKEBALL", "pb_gold", (type, _args) => new ExtraModifierModifier(type), undefined, "se/pb_bounce_1"),
|
||||
SILVER_POKEBALL: () => new ModifierType("modifierType:ModifierType.SILVER_POKEBALL", "pb_silver", (type, _args) => new TempExtraModifierModifier(type, 100), undefined, "se/pb_bounce_1"),
|
||||
|
||||
ENEMY_DAMAGE_BOOSTER: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_BOOSTER", "wl_item_drop", (type, _args) => new EnemyDamageBoosterModifier(type, 5)),
|
||||
ENEMY_DAMAGE_REDUCTION: () => new ModifierType("modifierType:ModifierType.ENEMY_DAMAGE_REDUCTION", "wl_guard_spec", (type, _args) => new EnemyDamageReducerModifier(type, 2.5)),
|
||||
@ -1577,13 +1580,13 @@ export const modifierTypes = {
|
||||
if (pregenArgs) {
|
||||
return new PokemonBaseStatTotalModifierType(pregenArgs[0] as number);
|
||||
}
|
||||
return new PokemonBaseStatTotalModifierType(randSeedInt(20));
|
||||
return new PokemonBaseStatTotalModifierType(randSeedInt(20, 1));
|
||||
}),
|
||||
MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
|
||||
if (pregenArgs) {
|
||||
return new PokemonBaseStatFlatModifierType(pregenArgs[0] as number, pregenArgs[1] as Stat[]);
|
||||
}
|
||||
return new PokemonBaseStatFlatModifierType(randSeedInt(20), [ Stat.HP, Stat.ATK, Stat.DEF ]);
|
||||
return new PokemonBaseStatFlatModifierType(randSeedInt(20, 1), [ Stat.HP, Stat.ATK, Stat.DEF ]);
|
||||
}),
|
||||
MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
|
||||
if (pregenArgs) {
|
||||
|
@ -404,6 +404,14 @@ export abstract class LapsingPersistentModifier extends PersistentModifier {
|
||||
this.battleCount = this.maxBattles;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates an existing modifier with a new `maxBattles` and `battleCount`.
|
||||
*/
|
||||
setNewBattleCount(count: number): void {
|
||||
this.maxBattles = count;
|
||||
this.battleCount = count;
|
||||
}
|
||||
|
||||
getMaxBattles(): number {
|
||||
return this.maxBattles;
|
||||
}
|
||||
@ -960,7 +968,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
|
||||
|
||||
this.stackCount = pokemon
|
||||
? pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
|
||||
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length
|
||||
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length
|
||||
: this.stackCount;
|
||||
|
||||
const text = scene.add.bitmapText(10, 15, "item-count", this.stackCount.toString(), 11);
|
||||
@ -975,7 +983,7 @@ export class EvoTrackerModifier extends PokemonHeldItemModifier {
|
||||
|
||||
getMaxHeldItemCount(pokemon: Pokemon): number {
|
||||
this.stackCount = pokemon.evoCounter + pokemon.getHeldItems().filter(m => m instanceof DamageMoneyRewardModifier).length
|
||||
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier).length;
|
||||
+ pokemon.scene.findModifiers(m => m instanceof MoneyMultiplierModifier || m instanceof ExtraModifierModifier || m instanceof TempExtraModifierModifier).length;
|
||||
return 999;
|
||||
}
|
||||
}
|
||||
@ -3288,6 +3296,60 @@ export class ExtraModifierModifier extends PersistentModifier {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifier used for timed boosts to the player's shop item rewards.
|
||||
* @extends LapsingPersistentModifier
|
||||
* @see {@linkcode apply}
|
||||
*/
|
||||
export class TempExtraModifierModifier extends LapsingPersistentModifier {
|
||||
constructor(type: ModifierType, maxBattles: number, battleCount?: number, stackCount?: number) {
|
||||
super(type, maxBattles, battleCount, stackCount);
|
||||
}
|
||||
|
||||
/**
|
||||
* Goes through existing modifiers for any that match Silver Pokeball,
|
||||
* which will then add the max count of the new item to the existing count of the current item.
|
||||
* If no existing Silver Pokeballs are found, will add a new one.
|
||||
* @param modifiers {@linkcode PersistentModifier} array of the player's modifiers
|
||||
* @param _virtual N/A
|
||||
* @param scene
|
||||
* @returns true if the modifier was successfully added or applied, false otherwise
|
||||
*/
|
||||
add(modifiers: PersistentModifier[], _virtual: boolean, scene: BattleScene): boolean {
|
||||
for (const modifier of modifiers) {
|
||||
if (this.match(modifier)) {
|
||||
const modifierInstance = modifier as TempExtraModifierModifier;
|
||||
const newBattleCount = this.getMaxBattles() + modifierInstance.getBattleCount();
|
||||
|
||||
modifierInstance.setNewBattleCount(newBattleCount);
|
||||
scene.playSound("se/restore");
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
modifiers.push(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
clone() {
|
||||
return new TempExtraModifierModifier(this.type, this.getMaxBattles(), this.getBattleCount(), this.stackCount);
|
||||
}
|
||||
|
||||
match(modifier: Modifier): boolean {
|
||||
return (modifier instanceof TempExtraModifierModifier);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the current rewards in the battle by the `stackCount`.
|
||||
* @returns `true` if the shop reward number modifier applies successfully
|
||||
* @param count {@linkcode NumberHolder} that holds the resulting shop item reward count
|
||||
*/
|
||||
apply(count: NumberHolder): boolean {
|
||||
count.value += this.getStackCount();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class EnemyPersistentModifier extends PersistentModifier {
|
||||
constructor(type: ModifierType, stackCount?: number) {
|
||||
super(type, stackCount);
|
||||
|
@ -1,7 +1,7 @@
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { ModifierTier } from "#app/modifier/modifier-tier";
|
||||
import { regenerateModifierPoolThresholds, ModifierTypeOption, ModifierType, getPlayerShopModifierTypeOptionsForWave, PokemonModifierType, FusePokemonModifierType, PokemonMoveModifierType, TmModifierType, RememberMoveModifierType, PokemonPpRestoreModifierType, PokemonPpUpModifierType, ModifierPoolType, getPlayerModifierTypeOptions } from "#app/modifier/modifier-type";
|
||||
import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
|
||||
import { ExtraModifierModifier, HealShopCostModifier, Modifier, PokemonHeldItemModifier, TempExtraModifierModifier } from "#app/modifier/modifier";
|
||||
import ModifierSelectUiHandler, { SHOP_OPTIONS_ROW_LIMIT } from "#app/ui/modifier-select-ui-handler";
|
||||
import PartyUiHandler, { PartyUiMode, PartyOption } from "#app/ui/party-ui-handler";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
@ -45,6 +45,7 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
const modifierCount = new Utils.IntegerHolder(3);
|
||||
if (this.isPlayer()) {
|
||||
this.scene.applyModifiers(ExtraModifierModifier, true, modifierCount);
|
||||
this.scene.applyModifiers(TempExtraModifierModifier, true, modifierCount);
|
||||
}
|
||||
|
||||
// If custom modifiers are specified, overrides default item count
|
||||
@ -274,7 +275,13 @@ export class SelectModifierPhase extends BattlePhase {
|
||||
// Otherwise, continue with custom multiplier
|
||||
multiplier = this.customModifierSettings.rerollMultiplier;
|
||||
}
|
||||
return Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * Math.pow(2, this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
const baseMultiplier = Math.min(Math.ceil(this.scene.currentBattle.waveIndex / 10) * baseValue * (2 ** this.rerollCount) * multiplier, Number.MAX_SAFE_INTEGER);
|
||||
|
||||
// Apply Black Sludge to reroll cost
|
||||
const modifiedRerollCost = new NumberHolder(baseMultiplier);
|
||||
this.scene.applyModifier(HealShopCostModifier, true, modifiedRerollCost);
|
||||
return modifiedRerollCost.value;
|
||||
}
|
||||
|
||||
getPoolType(): ModifierPoolType {
|
||||
|
@ -25,12 +25,17 @@ export class VictoryPhase extends PokemonPhase {
|
||||
start() {
|
||||
super.start();
|
||||
|
||||
const isMysteryEncounter = this.scene.currentBattle.isBattleMysteryEncounter();
|
||||
|
||||
// update Pokemon defeated count except for MEs that disable it
|
||||
if (!isMysteryEncounter || !this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) {
|
||||
this.scene.gameData.gameStats.pokemonDefeated++;
|
||||
}
|
||||
|
||||
const expValue = this.getPokemon().getExpValue();
|
||||
this.scene.applyPartyExp(expValue, true);
|
||||
|
||||
if (this.scene.currentBattle.isBattleMysteryEncounter()) {
|
||||
if (isMysteryEncounter) {
|
||||
handleMysteryEncounterVictory(this.scene, false, this.isExpOnly);
|
||||
return this.end();
|
||||
}
|
||||
|
@ -1569,6 +1569,10 @@ export class GameData {
|
||||
}
|
||||
|
||||
setPokemonSeen(pokemon: Pokemon, incrementCount: boolean = true, trainer: boolean = false): void {
|
||||
// Some Mystery Encounters block updates to these stats
|
||||
if (this.scene.currentBattle?.isBattleMysteryEncounter() && this.scene.currentBattle.mysteryEncounter?.preventGameStatsUpdates) {
|
||||
return;
|
||||
}
|
||||
const dexEntry = this.dexData[pokemon.species.speciesId];
|
||||
dexEntry.seenAttr |= pokemon.getDexAttr();
|
||||
if (incrementCount) {
|
||||
|
@ -92,7 +92,6 @@ export default class PokemonData {
|
||||
this.stats = source.stats;
|
||||
this.ivs = source.ivs;
|
||||
this.nature = source.nature !== undefined ? source.nature : 0 as Nature;
|
||||
this.natureOverride = source.natureOverride !== undefined ? source.natureOverride : -1;
|
||||
this.friendship = source.friendship !== undefined ? source.friendship : getPokemonSpecies(this.species).baseFriendship;
|
||||
this.metLevel = source.metLevel || 5;
|
||||
this.metBiome = source.metBiome !== undefined ? source.metBiome : -1;
|
||||
@ -117,6 +116,8 @@ export default class PokemonData {
|
||||
|
||||
this.customPokemonData = new CustomPokemonData(source.customPokemonData);
|
||||
|
||||
// Deprecated, but needed for session data migration
|
||||
this.natureOverride = source.natureOverride;
|
||||
this.mysteryEncounterPokemonData = new CustomPokemonData(source.mysteryEncounterPokemonData);
|
||||
this.fusionMysteryEncounterPokemonData = new CustomPokemonData(source.fusionMysteryEncounterPokemonData);
|
||||
|
||||
|
@ -206,7 +206,7 @@ describe("Delibird-y - Mystery Encounter", () => {
|
||||
expect(candyJarAfter?.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Should remove Reviver Seed and give the player a Healing Charm", async () => {
|
||||
it("Should remove Reviver Seed and give the player a Berry Pouch", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
// Set 1 Reviver Seed on party lead
|
||||
@ -220,11 +220,11 @@ describe("Delibird-y - Mystery Encounter", () => {
|
||||
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
|
||||
|
||||
const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier);
|
||||
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
|
||||
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
|
||||
|
||||
expect(reviverSeedAfter).toBeUndefined();
|
||||
expect(healingCharmAfter).toBeDefined();
|
||||
expect(healingCharmAfter?.stackCount).toBe(1);
|
||||
expect(berryPouchAfter).toBeDefined();
|
||||
expect(berryPouchAfter?.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Should give the player a Shell Bell if they have max stacks of Candy Jars", async () => {
|
||||
@ -256,13 +256,13 @@ describe("Delibird-y - Mystery Encounter", () => {
|
||||
expect(shellBellAfter?.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => {
|
||||
it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
// 5 Healing Charms
|
||||
// 3 Berry Pouches
|
||||
scene.modifiers = [];
|
||||
const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier;
|
||||
healingCharm.stackCount = 5;
|
||||
const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier;
|
||||
healingCharm.stackCount = 3;
|
||||
await scene.addModifier(healingCharm, true, false, false, true);
|
||||
|
||||
// Set 1 Reviver Seed on party lead
|
||||
@ -275,12 +275,12 @@ describe("Delibird-y - Mystery Encounter", () => {
|
||||
await runMysteryEncounterToEnd(game, 2, { pokemonNo: 1, optionNo: 1 });
|
||||
|
||||
const reviverSeedAfter = scene.findModifier(m => m instanceof PokemonInstantReviveModifier);
|
||||
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
|
||||
const healingCharmAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
|
||||
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
|
||||
|
||||
expect(reviverSeedAfter).toBeUndefined();
|
||||
expect(healingCharmAfter).toBeDefined();
|
||||
expect(healingCharmAfter?.stackCount).toBe(5);
|
||||
expect(healingCharmAfter?.stackCount).toBe(3);
|
||||
expect(shellBellAfter).toBeDefined();
|
||||
expect(shellBellAfter?.stackCount).toBe(1);
|
||||
});
|
||||
@ -347,7 +347,7 @@ describe("Delibird-y - Mystery Encounter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("Should decrease held item stacks and give the player a Berry Pouch", async () => {
|
||||
it("Should decrease held item stacks and give the player a Healing Charm", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
// Set 2 Soul Dew on party lead
|
||||
@ -361,14 +361,14 @@ describe("Delibird-y - Mystery Encounter", () => {
|
||||
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
|
||||
|
||||
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
|
||||
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
|
||||
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
|
||||
|
||||
expect(soulDewAfter?.stackCount).toBe(1);
|
||||
expect(berryPouchAfter).toBeDefined();
|
||||
expect(berryPouchAfter?.stackCount).toBe(1);
|
||||
expect(healingCharmAfter).toBeDefined();
|
||||
expect(healingCharmAfter?.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Should remove held item and give the player a Berry Pouch", async () => {
|
||||
it("Should remove held item and give the player a Healing Charm", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
// Set 1 Soul Dew on party lead
|
||||
@ -382,20 +382,20 @@ describe("Delibird-y - Mystery Encounter", () => {
|
||||
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
|
||||
|
||||
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
|
||||
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
|
||||
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
|
||||
|
||||
expect(soulDewAfter).toBeUndefined();
|
||||
expect(berryPouchAfter).toBeDefined();
|
||||
expect(berryPouchAfter?.stackCount).toBe(1);
|
||||
expect(healingCharmAfter).toBeDefined();
|
||||
expect(healingCharmAfter?.stackCount).toBe(1);
|
||||
});
|
||||
|
||||
it("Should give the player a Shell Bell if they have max stacks of Berry Pouches", async () => {
|
||||
it("Should give the player a Shell Bell if they have max stacks of Healing Charms", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.DELIBIRDY, defaultParty);
|
||||
|
||||
// 5 Healing Charms
|
||||
scene.modifiers = [];
|
||||
const healingCharm = generateModifierType(scene, modifierTypes.BERRY_POUCH)!.newModifier() as PreserveBerryModifier;
|
||||
healingCharm.stackCount = 3;
|
||||
const healingCharm = generateModifierType(scene, modifierTypes.HEALING_CHARM)!.newModifier() as HealingBoosterModifier;
|
||||
healingCharm.stackCount = 5;
|
||||
await scene.addModifier(healingCharm, true, false, false, true);
|
||||
|
||||
// Set 1 Soul Dew on party lead
|
||||
@ -408,12 +408,12 @@ describe("Delibird-y - Mystery Encounter", () => {
|
||||
await runMysteryEncounterToEnd(game, 3, { pokemonNo: 1, optionNo: 1 });
|
||||
|
||||
const soulDewAfter = scene.findModifier(m => m instanceof PokemonNatureWeightModifier);
|
||||
const berryPouchAfter = scene.findModifier(m => m instanceof PreserveBerryModifier);
|
||||
const healingCharmAfter = scene.findModifier(m => m instanceof HealingBoosterModifier);
|
||||
const shellBellAfter = scene.findModifier(m => m instanceof HitHealModifier);
|
||||
|
||||
expect(soulDewAfter).toBeUndefined();
|
||||
expect(berryPouchAfter).toBeDefined();
|
||||
expect(berryPouchAfter?.stackCount).toBe(3);
|
||||
expect(healingCharmAfter).toBeDefined();
|
||||
expect(healingCharmAfter?.stackCount).toBe(5);
|
||||
expect(shellBellAfter).toBeDefined();
|
||||
expect(shellBellAfter?.stackCount).toBe(1);
|
||||
});
|
||||
|
@ -12,7 +12,7 @@ import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encount
|
||||
import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils";
|
||||
import { Moves } from "#enums/moves";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { PokemonHeldItemModifier } from "#app/modifier/modifier";
|
||||
import { AttackTypeBoosterModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
|
||||
import { Type } from "#app/data/type";
|
||||
import { Status, StatusEffect } from "#app/data/status-effect";
|
||||
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
|
||||
@ -22,6 +22,8 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { MovePhase } from "#app/phases/move-phase";
|
||||
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||
import { BattlerTagType } from "#enums/battler-tag-type";
|
||||
import { Abilities } from "#enums/abilities";
|
||||
import i18next from "i18next";
|
||||
|
||||
const namespace = "mysteryEncounters/fieryFallout";
|
||||
@ -42,10 +44,11 @@ describe("Fiery Fallout - Mystery Encounter", () => {
|
||||
beforeEach(async () => {
|
||||
game = new GameManager(phaserGame);
|
||||
scene = game.scene;
|
||||
game.override.mysteryEncounterChance(100);
|
||||
game.override.startingWave(defaultWave);
|
||||
game.override.startingBiome(defaultBiome);
|
||||
game.override.disableTrainerWaves();
|
||||
game.override.mysteryEncounterChance(100)
|
||||
.startingWave(defaultWave)
|
||||
.startingBiome(defaultBiome)
|
||||
.disableTrainerWaves()
|
||||
.moveset([ Moves.PAYBACK, Moves.THUNDERBOLT ]); // Required for attack type booster item generation
|
||||
|
||||
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
|
||||
new Map<Biome, MysteryEncounterType[]>([
|
||||
@ -109,12 +112,16 @@ describe("Fiery Fallout - Mystery Encounter", () => {
|
||||
{
|
||||
species: getPokemonSpecies(Species.VOLCARONA),
|
||||
isBoss: false,
|
||||
gender: Gender.MALE
|
||||
gender: Gender.MALE,
|
||||
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
|
||||
mysteryEncounterBattleEffects: expect.any(Function)
|
||||
},
|
||||
{
|
||||
species: getPokemonSpecies(Species.VOLCARONA),
|
||||
isBoss: false,
|
||||
gender: Gender.FEMALE
|
||||
gender: Gender.FEMALE,
|
||||
tags: [ BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON ],
|
||||
mysteryEncounterBattleEffects: expect.any(Function)
|
||||
}
|
||||
],
|
||||
doubleBattle: true,
|
||||
@ -157,12 +164,11 @@ describe("Fiery Fallout - Mystery Encounter", () => {
|
||||
expect(enemyField[0].gender).not.toEqual(enemyField[1].gender); // Should be opposite gender
|
||||
|
||||
const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]);
|
||||
expect(movePhases.length).toBe(4);
|
||||
expect(movePhases.length).toBe(2);
|
||||
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.FIRE_SPIN).length).toBe(2); // Fire spin used twice before battle
|
||||
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.QUIVER_DANCE).length).toBe(2); // Quiver Dance used twice before battle
|
||||
});
|
||||
|
||||
it("should give charcoal to lead pokemon", async () => {
|
||||
it("should give attack type boosting item to lead pokemon", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 1, undefined, true);
|
||||
await skipBattleRunMysteryEncounterRewardsPhase(game);
|
||||
@ -172,8 +178,8 @@ describe("Fiery Fallout - Mystery Encounter", () => {
|
||||
const leadPokemonId = scene.getParty()?.[0].id;
|
||||
const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier
|
||||
&& (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[];
|
||||
const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal");
|
||||
expect(charcoal).toBeDefined;
|
||||
const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier);
|
||||
expect(item).toBeDefined;
|
||||
});
|
||||
});
|
||||
|
||||
@ -193,7 +199,7 @@ describe("Fiery Fallout - Mystery Encounter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should damage all non-fire party PKM by 20% and randomly burn 1", async () => {
|
||||
it("should damage all non-fire party PKM by 20%, and burn + give Heatproof to a random Pokemon", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
|
||||
|
||||
const party = scene.getParty();
|
||||
@ -210,7 +216,8 @@ describe("Fiery Fallout - Mystery Encounter", () => {
|
||||
burnablePokemon.forEach((pkm) => {
|
||||
expect(pkm.hp, `${pkm.name} should have received 20% damage: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp() - Math.floor(pkm.getMaxHp() * 0.2));
|
||||
});
|
||||
expect(burnablePokemon.some(pkm => pkm?.status?.effect === StatusEffect.BURN)).toBeTruthy();
|
||||
expect(burnablePokemon.some(pkm => pkm.status?.effect === StatusEffect.BURN)).toBeTruthy();
|
||||
expect(burnablePokemon.some(pkm => pkm.customPokemonData.ability === Abilities.HEATPROOF));
|
||||
notBurnablePokemon.forEach((pkm) => expect(pkm.hp, `${pkm.name} should be full hp: ${pkm.hp} / ${pkm.getMaxHp()} HP`).toBe(pkm.getMaxHp()));
|
||||
});
|
||||
|
||||
@ -241,17 +248,15 @@ describe("Fiery Fallout - Mystery Encounter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should give charcoal to lead pokemon", async () => {
|
||||
it("should give attack type boosting item to lead pokemon", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 3);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
|
||||
|
||||
const leadPokemonId = scene.getParty()?.[0].id;
|
||||
const leadPokemonItems = scene.findModifiers(m => m instanceof PokemonHeldItemModifier
|
||||
&& (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[];
|
||||
const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal");
|
||||
expect(charcoal).toBeDefined;
|
||||
const leadPokemonItems = scene.getParty()?.[0].getHeldItems() as PokemonHeldItemModifier[];
|
||||
const item = leadPokemonItems.find(i => i instanceof AttackTypeBoosterModifier);
|
||||
expect(item).toBeDefined;
|
||||
});
|
||||
|
||||
it("should leave encounter without battle", async () => {
|
||||
@ -264,7 +269,7 @@ describe("Fiery Fallout - Mystery Encounter", () => {
|
||||
});
|
||||
|
||||
it("should be disabled if not enough FIRE types are in party", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [ Species.MAGIKARP, Species.ARCANINE ]);
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, [ Species.MAGIKARP ]);
|
||||
await game.phaseInterceptor.to(MysteryEncounterPhase, false);
|
||||
|
||||
const encounterPhase = scene.getCurrentPhase();
|
||||
|
@ -86,7 +86,7 @@ describe("Trash to Treasure - Mystery Encounter", () => {
|
||||
|
||||
expect(TrashToTreasureEncounter.enemyPartyConfigs).toEqual([
|
||||
{
|
||||
levelAdditiveModifier: 1,
|
||||
levelAdditiveModifier: 0.5,
|
||||
disableSwitch: true,
|
||||
pokemonConfigs: [
|
||||
{
|
||||
|
@ -5,7 +5,7 @@ import { Species } from "#app/enums/species";
|
||||
import GameManager from "#app/test/utils/gameManager";
|
||||
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
|
||||
import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounter-test-utils";
|
||||
import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounter-test-utils";
|
||||
import BattleScene from "#app/battle-scene";
|
||||
import { Mode } from "#app/ui/ui";
|
||||
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
|
||||
@ -15,6 +15,8 @@ import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
|
||||
import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter";
|
||||
import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
|
||||
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||
import { CommandPhase } from "#app/phases/command-phase";
|
||||
import { ModifierTier } from "#app/modifier/modifier-tier";
|
||||
|
||||
const namespace = "mysteryEncounters/weirdDream";
|
||||
const defaultParty = [ Species.MAGBY, Species.HAUNTER, Species.ABRA ];
|
||||
@ -70,7 +72,7 @@ describe("Weird Dream - Mystery Encounter", () => {
|
||||
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.title).toBe(`${namespace}:title`);
|
||||
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.description).toBe(`${namespace}:description`);
|
||||
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue?.query).toBe(`${namespace}:query`);
|
||||
expect(WeirdDreamEncounter.options.length).toBe(2);
|
||||
expect(WeirdDreamEncounter.options.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should initialize fully", async () => {
|
||||
@ -132,7 +134,7 @@ describe("Weird Dream - Mystery Encounter", () => {
|
||||
expect(plus40To50.length).toBe(1);
|
||||
});
|
||||
|
||||
it("should have 1 Memory Mushroom, 5 Rogue Balls, and 2 Mints in rewards", async () => {
|
||||
it("should have 1 Memory Mushroom, 5 Rogue Balls, and 3 Mints in rewards", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 1);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
@ -141,11 +143,12 @@ describe("Weird Dream - Mystery Encounter", () => {
|
||||
|
||||
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.length).toEqual(5);
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM");
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL");
|
||||
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("MINT");
|
||||
expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT");
|
||||
expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT");
|
||||
});
|
||||
|
||||
it("should leave encounter without battle", async () => {
|
||||
@ -158,7 +161,7 @@ describe("Weird Dream - Mystery Encounter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("Option 2 - Leave", () => {
|
||||
describe("Option 2 - Battle Future Self", () => {
|
||||
it("should have the correct properties", () => {
|
||||
const option = WeirdDreamEncounter.options[1];
|
||||
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
|
||||
@ -174,17 +177,63 @@ describe("Weird Dream - Mystery Encounter", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("should reduce party levels by 12.5%", async () => {
|
||||
it("should start a battle against the player's transformation team", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 2, undefined, true);
|
||||
|
||||
const enemyField = scene.getEnemyField();
|
||||
expect(scene.getCurrentPhase()?.constructor.name).toBe(CommandPhase.name);
|
||||
expect(enemyField.length).toBe(1);
|
||||
expect(scene.getEnemyParty().length).toBe(scene.getParty().length);
|
||||
});
|
||||
|
||||
it("should have 2 Rogue/2 Ultra/2 Great items in rewards", async () => {
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 2, undefined, true);
|
||||
await skipBattleRunMysteryEncounterRewardsPhase(game);
|
||||
await game.phaseInterceptor.to(SelectModifierPhase, false);
|
||||
expect(scene.getCurrentPhase()?.constructor.name).toBe(SelectModifierPhase.name);
|
||||
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(6);
|
||||
expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE);
|
||||
expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toEqual(ModifierTier.ROGUE);
|
||||
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.ULTRA);
|
||||
expect(modifierSelectHandler.options[4].modifierTypeOption.type.tier - modifierSelectHandler.options[4].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT);
|
||||
expect(modifierSelectHandler.options[5].modifierTypeOption.type.tier - modifierSelectHandler.options[5].modifierTypeOption.upgradeCount).toEqual(ModifierTier.GREAT);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Option 3 - Leave", () => {
|
||||
it("should have the correct properties", () => {
|
||||
const option = WeirdDreamEncounter.options[2];
|
||||
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
|
||||
expect(option.dialogue).toBeDefined();
|
||||
expect(option.dialogue).toStrictEqual({
|
||||
buttonLabel: `${namespace}:option.3.label`,
|
||||
buttonTooltip: `${namespace}:option.3.tooltip`,
|
||||
selected: [
|
||||
{
|
||||
text: `${namespace}:option.3.selected`,
|
||||
},
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it("should reduce party levels by 10%", async () => {
|
||||
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
|
||||
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
|
||||
const levelsPrior = scene.getParty().map(p => p.level);
|
||||
await runMysteryEncounterToEnd(game, 2);
|
||||
await runMysteryEncounterToEnd(game, 3);
|
||||
|
||||
const levelsAfter = scene.getParty().map(p => p.level);
|
||||
|
||||
for (let i = 0; i < levelsPrior.length; i++) {
|
||||
expect(Math.max(Math.ceil(0.8875 * levelsPrior[i]), 1)).toBe(levelsAfter[i]);
|
||||
expect(Math.max(Math.ceil(0.9 * levelsPrior[i]), 1)).toBe(levelsAfter[i]);
|
||||
expect(scene.getParty()[i].levelExp).toBe(0);
|
||||
}
|
||||
|
||||
@ -195,7 +244,7 @@ describe("Weird Dream - Mystery Encounter", () => {
|
||||
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
|
||||
|
||||
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
|
||||
await runMysteryEncounterToEnd(game, 2);
|
||||
await runMysteryEncounterToEnd(game, 3);
|
||||
|
||||
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
|
||||
});
|
||||
|
@ -2,37 +2,83 @@ import BattleScene from "#app/battle-scene";
|
||||
import { ModalConfig } from "./modal-ui-handler";
|
||||
import { Mode } from "./ui";
|
||||
import * as Utils from "../utils";
|
||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||
import { Button } from "#app/enums/buttons";
|
||||
import { TextStyle } from "./text";
|
||||
|
||||
export default class AdminUiHandler extends FormModalUiHandler {
|
||||
|
||||
private adminMode: AdminMode;
|
||||
private adminResult: AdminSearchInfo;
|
||||
private config: ModalConfig;
|
||||
|
||||
private readonly buttonGap = 10;
|
||||
// http response from the server when a username isn't found in the server
|
||||
private readonly httpUserNotFoundErrorCode: number = 404;
|
||||
private readonly ERR_REQUIRED_FIELD = (field: string) => {
|
||||
if (field === "username") {
|
||||
return `${Utils.formatText(field)} is required`;
|
||||
} else {
|
||||
return `${Utils.formatText(field)} Id is required`;
|
||||
}
|
||||
};
|
||||
// returns a string saying whether a username has been successfully linked/unlinked to discord/google
|
||||
private readonly SUCCESS_SERVICE_MODE = (service: string, mode: string) => {
|
||||
return `Username and ${service} successfully ${mode.toLowerCase()}ed`;
|
||||
};
|
||||
private readonly ERR_USERNAME_NOT_FOUND: string = "Username not found!";
|
||||
private readonly ERR_GENERIC_ERROR: string = "There was an error";
|
||||
|
||||
constructor(scene: BattleScene, mode: Mode | null = null) {
|
||||
super(scene, mode);
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
super.setup();
|
||||
}
|
||||
|
||||
getModalTitle(config?: ModalConfig): string {
|
||||
override getModalTitle(): string {
|
||||
return "Admin panel";
|
||||
}
|
||||
|
||||
getFields(config?: ModalConfig): string[] {
|
||||
return [ "Username", "Discord ID" ];
|
||||
override getWidth(): number {
|
||||
return this.adminMode === AdminMode.ADMIN ? 180 : 160;
|
||||
}
|
||||
|
||||
getWidth(config?: ModalConfig): number {
|
||||
return 160;
|
||||
override getMargin(): [number, number, number, number] {
|
||||
return [ 0, 0, 0, 0 ];
|
||||
}
|
||||
|
||||
getMargin(config?: ModalConfig): [number, number, number, number] {
|
||||
return [ 0, 0, 48, 0 ];
|
||||
override getButtonLabels(): string[] {
|
||||
switch (this.adminMode) {
|
||||
case AdminMode.LINK:
|
||||
return [ "Link Account", "Cancel" ];
|
||||
case AdminMode.SEARCH:
|
||||
return [ "Find account", "Cancel" ];
|
||||
case AdminMode.ADMIN:
|
||||
return [ "Back to search", "Cancel" ];
|
||||
default:
|
||||
return [ "Activate ADMIN", "Cancel" ];
|
||||
}
|
||||
}
|
||||
|
||||
getButtonLabels(config?: ModalConfig): string[] {
|
||||
return [ "Link account", "Cancel" ];
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
const inputFieldConfigs: InputFieldConfig[] = [];
|
||||
switch (this.adminMode) {
|
||||
case AdminMode.LINK:
|
||||
inputFieldConfigs.push( { label: "Username" });
|
||||
inputFieldConfigs.push( { label: "Discord ID" });
|
||||
break;
|
||||
case AdminMode.SEARCH:
|
||||
inputFieldConfigs.push( { label: "Username" });
|
||||
break;
|
||||
case AdminMode.ADMIN:
|
||||
const adminResult = this.adminResult ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" };
|
||||
// Discord and Google ID fields that are not empty get locked, other fields are all locked
|
||||
inputFieldConfigs.push( { label: "Username", isReadOnly: true });
|
||||
inputFieldConfigs.push( { label: "Discord ID", isReadOnly: adminResult.discordId !== "" });
|
||||
inputFieldConfigs.push( { label: "Google ID", isReadOnly: adminResult.googleId !== "" });
|
||||
inputFieldConfigs.push( { label: "Last played", isReadOnly: true });
|
||||
inputFieldConfigs.push( { label: "Registered", isReadOnly: true });
|
||||
break;
|
||||
}
|
||||
return inputFieldConfigs;
|
||||
}
|
||||
|
||||
processInput(button: Button): boolean {
|
||||
@ -45,44 +91,281 @@ export default class AdminUiHandler extends FormModalUiHandler {
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
this.config = args[0] as ModalConfig; // config
|
||||
this.adminMode = args[1] as AdminMode; // admin mode
|
||||
this.adminResult = args[2] ?? { username: "", discordId: "", googleId: "", lastLoggedIn: "", registered: "" }; // admin result, if any
|
||||
const isMessageError = args[3]; // is the message shown a success or error
|
||||
|
||||
const fields = this.getInputFieldConfigs();
|
||||
const hasTitle = !!this.getModalTitle();
|
||||
|
||||
this.updateFields(fields, hasTitle);
|
||||
this.updateContainer(this.config);
|
||||
|
||||
const labels = this.getButtonLabels();
|
||||
for (let i = 0; i < labels.length; i++) {
|
||||
this.buttonLabels[i].setText(labels[i]); // sets the label text
|
||||
}
|
||||
|
||||
this.errorMessage.setPosition(10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin()); // sets the position of the message dynamically
|
||||
if (isMessageError) {
|
||||
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
|
||||
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
|
||||
} else {
|
||||
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_GREEN));
|
||||
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_GREEN, true));
|
||||
}
|
||||
|
||||
if (super.show(args)) {
|
||||
const config = args[0] as ModalConfig;
|
||||
this.populateFields(this.adminMode, this.adminResult);
|
||||
const originalSubmitAction = this.submitAction;
|
||||
this.submitAction = (_) => {
|
||||
this.submitAction = originalSubmitAction;
|
||||
const adminSearchResult: AdminSearchInfo = this.convertInputsToAdmin(); // this converts the input texts into a single object for use later
|
||||
const validFields = this.areFieldsValid(this.adminMode);
|
||||
if (validFields.error) {
|
||||
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error
|
||||
return this.showMessage(validFields.errorMessage ?? "", adminSearchResult, true);
|
||||
}
|
||||
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
|
||||
const onFail = error => {
|
||||
this.scene.ui.setMode(Mode.ADMIN, Object.assign(config, { errorMessage: error?.trim() }));
|
||||
this.scene.ui.playError();
|
||||
};
|
||||
if (!this.inputs[0].text) {
|
||||
return onFail("Username is required");
|
||||
}
|
||||
if (!this.inputs[1].text) {
|
||||
return onFail("Discord Id is required");
|
||||
}
|
||||
Utils.apiPost("admin/account/discord-link", `username=${encodeURIComponent(this.inputs[0].text)}&discordId=${encodeURIComponent(this.inputs[1].text)}`, "application/x-www-form-urlencoded", true)
|
||||
if (this.adminMode === AdminMode.LINK) {
|
||||
this.adminLinkUnlink(adminSearchResult, "discord", "Link") // calls server to link discord
|
||||
.then(response => {
|
||||
if (!response.ok) {
|
||||
console.error(response);
|
||||
if (response.error) {
|
||||
return this.showMessage(response.errorType, adminSearchResult, true); // error or some kind
|
||||
} else {
|
||||
return this.showMessage(this.SUCCESS_SERVICE_MODE("discord", "link"), adminSearchResult, false); // success
|
||||
}
|
||||
this.inputs[0].setText("");
|
||||
this.inputs[1].setText("");
|
||||
this.scene.ui.revertMode();
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error(err);
|
||||
this.scene.ui.revertMode();
|
||||
});
|
||||
} else if (this.adminMode === AdminMode.SEARCH) {
|
||||
this.adminSearch(adminSearchResult) // admin search for username
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
return this.showMessage(response.errorType, adminSearchResult, true); // failure
|
||||
}
|
||||
this.updateAdminPanelInfo(response.adminSearchResult ?? adminSearchResult); // success
|
||||
});
|
||||
} else if (this.adminMode === AdminMode.ADMIN) {
|
||||
this.updateAdminPanelInfo(adminSearchResult, AdminMode.SEARCH);
|
||||
}
|
||||
return false;
|
||||
};
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
showMessage(message: string, adminResult: AdminSearchInfo, isError: boolean) {
|
||||
this.scene.ui.setMode(Mode.ADMIN, Object.assign(this.config, { errorMessage: message?.trim() }), this.adminMode, adminResult, isError);
|
||||
if (isError) {
|
||||
this.scene.ui.playError();
|
||||
} else {
|
||||
this.scene.ui.playSelect();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This is used to update the fields' text when loading in a new admin ui handler. It uses the {@linkcode adminResult}
|
||||
* to update the input text based on the {@linkcode adminMode}. For a linking adminMode, it sets the username and discord.
|
||||
* For a search adminMode, it sets the username. For an admin adminMode, it sets all the info from adminResult in the
|
||||
* appropriate text boxes, and also sets the link/unlink icons for discord/google depending on the result
|
||||
*/
|
||||
private populateFields(adminMode: AdminMode, adminResult: AdminSearchInfo) {
|
||||
switch (adminMode) {
|
||||
case AdminMode.LINK:
|
||||
this.inputs[0].setText(adminResult.username);
|
||||
this.inputs[1].setText(adminResult.discordId);
|
||||
break;
|
||||
case AdminMode.SEARCH:
|
||||
this.inputs[0].setText(adminResult.username);
|
||||
break;
|
||||
case AdminMode.ADMIN:
|
||||
Object.keys(adminResult).forEach((aR, i) => {
|
||||
this.inputs[i].setText(adminResult[aR]);
|
||||
if (aR === "discordId" || aR === "googleId") { // this is here to add the icons for linking/unlinking of google/discord IDs
|
||||
const nineSlice = this.inputContainers[i].list.find(iC => iC.type === "NineSlice");
|
||||
const img = this.scene.add.image(this.inputContainers[i].x + nineSlice!.width + this.buttonGap, this.inputContainers[i].y + (Math.floor(nineSlice!.height / 2)), adminResult[aR] === "" ? "link_icon" : "unlink_icon");
|
||||
img.setName(`adminBtn_${aR}`);
|
||||
img.setOrigin(0.5, 0.5);
|
||||
img.setInteractive();
|
||||
img.on("pointerdown", () => {
|
||||
const service = aR.toLowerCase().replace("id", ""); // this takes our key (discordId or googleId) and removes the "Id" at the end to make it more url friendly
|
||||
const mode = adminResult[aR] === "" ? "Link" : "Unlink"; // this figures out if we're linking or unlinking a service
|
||||
const validFields = this.areFieldsValid(this.adminMode, service);
|
||||
if (validFields.error) {
|
||||
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []}); // this is here to force a loading screen to allow the admin tool to reopen again if there's an error
|
||||
return this.showMessage(validFields.errorMessage ?? "", adminResult, true);
|
||||
}
|
||||
this.adminLinkUnlink(this.convertInputsToAdmin(), service, mode).then(response => { // attempts to link/unlink depending on the service
|
||||
if (response.error) {
|
||||
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
|
||||
return this.showMessage(response.errorType, adminResult, true); // fail
|
||||
} else { // success, reload panel with new results
|
||||
this.scene.ui.setMode(Mode.LOADING, { buttonActions: []});
|
||||
this.adminSearch(adminResult)
|
||||
.then(response => {
|
||||
if (response.error) {
|
||||
return this.showMessage(response.errorType, adminResult, true);
|
||||
}
|
||||
return this.showMessage(this.SUCCESS_SERVICE_MODE(service, mode), response.adminSearchResult ?? adminResult, false);
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
this.addInteractionHoverEffect(img);
|
||||
this.modalContainer.add(img);
|
||||
}
|
||||
});
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
private areFieldsValid(adminMode: AdminMode, service?: string): { error: boolean; errorMessage?: string; } {
|
||||
switch (adminMode) {
|
||||
case AdminMode.LINK:
|
||||
if (!this.inputs[0].text) { // username missing from link panel
|
||||
return {
|
||||
error: true,
|
||||
errorMessage: this.ERR_REQUIRED_FIELD("username")
|
||||
};
|
||||
}
|
||||
if (!this.inputs[1].text) { // discordId missing from linking panel
|
||||
return {
|
||||
error: true,
|
||||
errorMessage: this.ERR_REQUIRED_FIELD("discord")
|
||||
};
|
||||
}
|
||||
break;
|
||||
case AdminMode.SEARCH:
|
||||
if (!this.inputs[0].text) { // username missing from search panel
|
||||
return {
|
||||
error: true,
|
||||
errorMessage: this.ERR_REQUIRED_FIELD("username")
|
||||
};
|
||||
}
|
||||
break;
|
||||
case AdminMode.ADMIN:
|
||||
if (!this.inputs[1].text && service === "discord") { // discordId missing from admin panel
|
||||
return {
|
||||
error: true,
|
||||
errorMessage: this.ERR_REQUIRED_FIELD(service)
|
||||
};
|
||||
}
|
||||
if (!this.inputs[2].text && service === "google") { // googleId missing from admin panel
|
||||
return {
|
||||
error: true,
|
||||
errorMessage: this.ERR_REQUIRED_FIELD(service)
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
return {
|
||||
error: false
|
||||
};
|
||||
}
|
||||
|
||||
private convertInputsToAdmin(): AdminSearchInfo {
|
||||
return {
|
||||
username: this.inputs[0]?.node ? this.inputs[0].text : "",
|
||||
discordId: this.inputs[1]?.node ? this.inputs[1]?.text : "",
|
||||
googleId: this.inputs[2]?.node ? this.inputs[2]?.text : "",
|
||||
lastLoggedIn: this.inputs[3]?.node ? this.inputs[3]?.text : "",
|
||||
registered: this.inputs[4]?.node ? this.inputs[4]?.text : ""
|
||||
};
|
||||
}
|
||||
|
||||
private async adminSearch(adminSearchResult: AdminSearchInfo) {
|
||||
try {
|
||||
const adminInfo = await Utils.apiFetch(`admin/account/adminSearch?username=${encodeURIComponent(adminSearchResult.username)}`, true);
|
||||
if (!adminInfo.ok) { // error - if adminInfo.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db
|
||||
return { adminSearchResult: adminSearchResult, error: true, errorType: adminInfo.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR };
|
||||
} else { // success
|
||||
const adminInfoJson: AdminSearchInfo = await adminInfo.json();
|
||||
return { adminSearchResult: adminInfoJson, error: false };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return { error: true, errorType: err };
|
||||
}
|
||||
}
|
||||
|
||||
private async adminLinkUnlink(adminSearchResult: AdminSearchInfo, service: string, mode: string) {
|
||||
try {
|
||||
const response = await Utils.apiPost(`admin/account/${service}${mode}`, `username=${encodeURIComponent(adminSearchResult.username)}&${service}Id=${encodeURIComponent(service === "discord" ? adminSearchResult.discordId : adminSearchResult.googleId)}`, "application/x-www-form-urlencoded", true);
|
||||
if (!response.ok) { // error - if response.status === this.httpUserNotFoundErrorCode that means the username can't be found in the db
|
||||
return { adminSearchResult: adminSearchResult, error: true, errorType: response.status === this.httpUserNotFoundErrorCode ? this.ERR_USERNAME_NOT_FOUND : this.ERR_GENERIC_ERROR };
|
||||
} else { // success!
|
||||
return { adminSearchResult: adminSearchResult, error: false };
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
return { error: true, errorType: err };
|
||||
}
|
||||
}
|
||||
|
||||
private updateAdminPanelInfo(adminSearchResult: AdminSearchInfo, mode?: AdminMode) {
|
||||
mode = mode ?? AdminMode.ADMIN;
|
||||
this.scene.ui.setMode(Mode.ADMIN, {
|
||||
buttonActions: [
|
||||
// we double revert here and below to go back 2 layers of menus
|
||||
() => {
|
||||
this.scene.ui.revertMode();
|
||||
this.scene.ui.revertMode();
|
||||
},
|
||||
() => {
|
||||
this.scene.ui.revertMode();
|
||||
this.scene.ui.revertMode();
|
||||
}
|
||||
]
|
||||
}, mode, adminSearchResult);
|
||||
}
|
||||
|
||||
clear(): void {
|
||||
super.clear();
|
||||
|
||||
// this is used to remove the existing fields on the admin panel so they can be updated
|
||||
|
||||
const itemsToRemove: string[] = [ "formLabel", "adminBtn" ]; // this is the start of the names for each element we want to remove
|
||||
const removeArray: any[] = [];
|
||||
const mC = this.modalContainer.list;
|
||||
for (let i = mC.length - 1; i >= 0; i--) {
|
||||
/* This code looks for a few things before destroying the specific field; first it looks to see if the name of the element is %like% the itemsToRemove labels
|
||||
* this means that anything with, for example, "formLabel", will be true.
|
||||
* It then also checks for any containers that are within this.modalContainer, and checks if any of its child elements are of type rexInputText
|
||||
* and if either of these conditions are met, the element is destroyed.
|
||||
*/
|
||||
if (itemsToRemove.some(iTR => mC[i].name.includes(iTR)) || (mC[i].type === "Container" && (mC[i] as Phaser.GameObjects.Container).list.find(m => m.type === "rexInputText"))) {
|
||||
removeArray.push(mC[i]);
|
||||
}
|
||||
}
|
||||
|
||||
while (removeArray.length > 0) {
|
||||
this.modalContainer.remove(removeArray.pop(), true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export enum AdminMode {
|
||||
LINK,
|
||||
SEARCH,
|
||||
ADMIN
|
||||
}
|
||||
|
||||
export function getAdminModeName(adminMode: AdminMode): string {
|
||||
switch (adminMode) {
|
||||
case AdminMode.LINK:
|
||||
return "Link";
|
||||
case AdminMode.SEARCH:
|
||||
return "Search";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
interface AdminSearchInfo {
|
||||
username: string;
|
||||
discordId: string;
|
||||
googleId: string;
|
||||
lastLoggedIn: string;
|
||||
registered: string;
|
||||
}
|
||||
|
@ -5,7 +5,6 @@ import { TextStyle, addTextInputObject, addTextObject } from "./text";
|
||||
import { WindowVariant, addWindow } from "./ui-theme";
|
||||
import InputText from "phaser3-rex-plugins/plugins/inputtext";
|
||||
import * as Utils from "../utils";
|
||||
import i18next from "i18next";
|
||||
import { Button } from "#enums/buttons";
|
||||
|
||||
export interface FormModalConfig extends ModalConfig {
|
||||
@ -19,6 +18,7 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
protected errorMessage: Phaser.GameObjects.Text;
|
||||
protected submitAction: Function | null;
|
||||
protected tween: Phaser.Tweens.Tween;
|
||||
protected formLabels: Phaser.GameObjects.Text[];
|
||||
|
||||
constructor(scene: BattleScene, mode: Mode | null = null) {
|
||||
super(scene, mode);
|
||||
@ -26,12 +26,18 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
this.editing = false;
|
||||
this.inputContainers = [];
|
||||
this.inputs = [];
|
||||
this.formLabels = [];
|
||||
}
|
||||
|
||||
abstract getFields(): string[];
|
||||
/**
|
||||
* Get configuration for all fields that should be part of the modal
|
||||
* Gets used by {@linkcode updateFields} to add the proper text inputs and labels to the view
|
||||
* @returns array of {@linkcode InputFieldConfig}
|
||||
*/
|
||||
abstract getInputFieldConfigs(): InputFieldConfig[];
|
||||
|
||||
getHeight(config?: ModalConfig): number {
|
||||
return 20 * this.getFields().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28;
|
||||
return 20 * this.getInputFieldConfigs().length + (this.getModalTitle() ? 26 : 0) + ((config as FormModalConfig)?.errorMessage ? 12 : 0) + this.getButtonTopMargin() + 28;
|
||||
}
|
||||
|
||||
getReadableErrorMessage(error: string): string {
|
||||
@ -45,37 +51,50 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
setup(): void {
|
||||
super.setup();
|
||||
|
||||
const fields = this.getFields();
|
||||
const config = this.getInputFieldConfigs();
|
||||
|
||||
const hasTitle = !!this.getModalTitle();
|
||||
|
||||
fields.forEach((field, f) => {
|
||||
const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, field, TextStyle.TOOLTIP_CONTENT);
|
||||
if (config.length >= 1) {
|
||||
this.updateFields(config, hasTitle);
|
||||
}
|
||||
|
||||
this.modalContainer.add(label);
|
||||
this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (config.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT);
|
||||
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
|
||||
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
|
||||
this.errorMessage.setVisible(false);
|
||||
this.modalContainer.add(this.errorMessage);
|
||||
}
|
||||
|
||||
protected updateFields(fieldsConfig: InputFieldConfig[], hasTitle: boolean) {
|
||||
this.inputContainers = [];
|
||||
this.inputs = [];
|
||||
this.formLabels = [];
|
||||
fieldsConfig.forEach((config, f) => {
|
||||
const label = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * f, config.label, TextStyle.TOOLTIP_CONTENT);
|
||||
label.name = "formLabel" + f;
|
||||
|
||||
this.formLabels.push(label);
|
||||
this.modalContainer.add(this.formLabels[this.formLabels.length - 1]);
|
||||
|
||||
const inputContainer = this.scene.add.container(70, (hasTitle ? 28 : 2) + 20 * f);
|
||||
inputContainer.setVisible(false);
|
||||
|
||||
const inputBg = addWindow(this.scene, 0, 0, 80, 16, false, false, 0, 0, WindowVariant.XTHIN);
|
||||
|
||||
const isPassword = field.includes(i18next.t("menu:password")) || field.includes(i18next.t("menu:confirmPassword"));
|
||||
const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20 });
|
||||
const isPassword = config?.isPassword;
|
||||
const isReadOnly = config?.isReadOnly;
|
||||
const input = addTextInputObject(this.scene, 4, -2, 440, 116, TextStyle.TOOLTIP_CONTENT, { type: isPassword ? "password" : "text", maxLength: isPassword ? 64 : 20, readOnly: isReadOnly });
|
||||
input.setOrigin(0, 0);
|
||||
|
||||
inputContainer.add(inputBg);
|
||||
inputContainer.add(input);
|
||||
this.modalContainer.add(inputContainer);
|
||||
|
||||
this.inputContainers.push(inputContainer);
|
||||
this.modalContainer.add(inputContainer);
|
||||
|
||||
this.inputs.push(input);
|
||||
});
|
||||
|
||||
this.errorMessage = addTextObject(this.scene, 10, (hasTitle ? 31 : 5) + 20 * (fields.length - 1) + 16 + this.getButtonTopMargin(), "", TextStyle.TOOLTIP_CONTENT);
|
||||
this.errorMessage.setColor(this.getTextColor(TextStyle.SUMMARY_PINK));
|
||||
this.errorMessage.setShadowColor(this.getTextColor(TextStyle.SUMMARY_PINK, true));
|
||||
this.errorMessage.setVisible(false);
|
||||
this.modalContainer.add(this.errorMessage);
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
@ -149,3 +168,9 @@ export abstract class FormModalUiHandler extends ModalUiHandler {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface InputFieldConfig {
|
||||
label: string,
|
||||
isPassword?: boolean,
|
||||
isReadOnly?: boolean
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||
import { ModalConfig } from "./modal-ui-handler";
|
||||
import * as Utils from "../utils";
|
||||
import { Mode } from "./ui";
|
||||
@ -75,10 +75,6 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
return i18next.t("menu:login");
|
||||
}
|
||||
|
||||
override getFields(_config?: ModalConfig): string[] {
|
||||
return [ i18next.t("menu:username"), i18next.t("menu:password") ];
|
||||
}
|
||||
|
||||
override getWidth(_config?: ModalConfig): number {
|
||||
return 160;
|
||||
}
|
||||
@ -106,14 +102,21 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
case this.ERR_PASSWORD_MATCH:
|
||||
return i18next.t("menu:unmatchingPassword");
|
||||
case this.ERR_NO_SAVES:
|
||||
return i18next.t("menu:noSaves");
|
||||
return "P01: " + i18next.t("menu:noSaves");
|
||||
case this.ERR_TOO_MANY_SAVES:
|
||||
return i18next.t("menu:tooManySaves");
|
||||
return "P02: " + i18next.t("menu:tooManySaves");
|
||||
}
|
||||
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
const inputFieldConfigs: InputFieldConfig[] = [];
|
||||
inputFieldConfigs.push({ label: i18next.t("menu:username") });
|
||||
inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true });
|
||||
return inputFieldConfigs;
|
||||
}
|
||||
|
||||
override show(args: any[]): boolean {
|
||||
if (super.show(args)) {
|
||||
|
||||
@ -164,7 +167,7 @@ export default class LoginFormUiHandler extends FormModalUiHandler {
|
||||
[ this.discordImage, this.googleImage, this.usernameInfoImage ].forEach((img) => img.off("pointerdown"));
|
||||
}
|
||||
|
||||
private processExternalProvider(config: ModalConfig) : void {
|
||||
private processExternalProvider(config: ModalConfig): void {
|
||||
this.externalPartyTitle.setText(i18next.t("menu:orUse") ?? "");
|
||||
this.externalPartyTitle.setX(20 + this.externalPartyTitle.text.length);
|
||||
this.externalPartyTitle.setVisible(true);
|
||||
|
@ -13,6 +13,7 @@ import { GameDataType } from "#enums/game-data-type";
|
||||
import BgmBar from "#app/ui/bgm-bar";
|
||||
import AwaitableUiHandler from "./awaitable-ui-handler";
|
||||
import { SelectModifierPhase } from "#app/phases/select-modifier-phase";
|
||||
import { AdminMode, getAdminModeName } from "./admin-ui-handler";
|
||||
|
||||
enum MenuOptions {
|
||||
GAME_SETTINGS,
|
||||
@ -386,17 +387,42 @@ export default class MenuUiHandler extends MessageUiHandler {
|
||||
if (!bypassLogin && loggedInUser?.hasAdminRole) {
|
||||
communityOptions.push({
|
||||
label: "Admin",
|
||||
handler: () => {
|
||||
|
||||
const skippedAdminModes: AdminMode[] = [ AdminMode.ADMIN ]; // this is here so that we can skip the menu populating enums that aren't meant for the menu, such as the AdminMode.ADMIN
|
||||
const options: OptionSelectItem[] = [];
|
||||
Object.values(AdminMode).filter((v) => !isNaN(Number(v)) && !skippedAdminModes.includes(v as AdminMode)).forEach((mode) => { // this gets all the enums in a way we can use
|
||||
options.push({
|
||||
label: getAdminModeName(mode as AdminMode),
|
||||
handler: () => {
|
||||
ui.playSelect();
|
||||
ui.setOverlayMode(Mode.ADMIN, {
|
||||
buttonActions: [
|
||||
// we double revert here and below to go back 2 layers of menus
|
||||
() => {
|
||||
ui.revertMode();
|
||||
ui.revertMode();
|
||||
},
|
||||
() => {
|
||||
ui.revertMode();
|
||||
ui.revertMode();
|
||||
}
|
||||
]
|
||||
}, mode); // mode is our AdminMode enum
|
||||
return true;
|
||||
}
|
||||
});
|
||||
});
|
||||
options.push({
|
||||
label: "Cancel",
|
||||
handler: () => {
|
||||
ui.revertMode();
|
||||
return true;
|
||||
}
|
||||
});
|
||||
this.scene.ui.setOverlayMode(Mode.OPTION_SELECT, {
|
||||
options: options,
|
||||
delay: 0
|
||||
});
|
||||
return true;
|
||||
},
|
||||
|
@ -15,12 +15,14 @@ export abstract class ModalUiHandler extends UiHandler {
|
||||
protected titleText: Phaser.GameObjects.Text;
|
||||
protected buttonContainers: Phaser.GameObjects.Container[];
|
||||
protected buttonBgs: Phaser.GameObjects.NineSlice[];
|
||||
protected buttonLabels: Phaser.GameObjects.Text[];
|
||||
|
||||
constructor(scene: BattleScene, mode: Mode | null = null) {
|
||||
super(scene, mode);
|
||||
|
||||
this.buttonContainers = [];
|
||||
this.buttonBgs = [];
|
||||
this.buttonLabels = [];
|
||||
}
|
||||
|
||||
abstract getModalTitle(config?: ModalConfig): string;
|
||||
@ -75,6 +77,7 @@ export abstract class ModalUiHandler extends UiHandler {
|
||||
|
||||
const buttonContainer = this.scene.add.container(0, buttonTopMargin);
|
||||
|
||||
this.buttonLabels.push(buttonLabel);
|
||||
this.buttonBgs.push(buttonBg);
|
||||
this.buttonContainers.push(buttonContainer);
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||
import { ModalConfig } from "./modal-ui-handler";
|
||||
import * as Utils from "../utils";
|
||||
import { Mode } from "./ui";
|
||||
@ -24,10 +24,6 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
return i18next.t("menu:register");
|
||||
}
|
||||
|
||||
getFields(config?: ModalConfig): string[] {
|
||||
return [ i18next.t("menu:username"), i18next.t("menu:password"), i18next.t("menu:confirmPassword") ];
|
||||
}
|
||||
|
||||
getWidth(config?: ModalConfig): number {
|
||||
return 160;
|
||||
}
|
||||
@ -61,6 +57,14 @@ export default class RegistrationFormUiHandler extends FormModalUiHandler {
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
const inputFieldConfigs: InputFieldConfig[] = [];
|
||||
inputFieldConfigs.push({ label: i18next.t("menu:username") });
|
||||
inputFieldConfigs.push({ label: i18next.t("menu:password"), isPassword: true });
|
||||
inputFieldConfigs.push({ label: i18next.t("menu:confirmPassword"), isPassword: true });
|
||||
return inputFieldConfigs;
|
||||
}
|
||||
|
||||
setup(): void {
|
||||
super.setup();
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||
import { ModalConfig } from "./modal-ui-handler";
|
||||
import i18next from "i18next";
|
||||
import { PlayerPokemon } from "#app/field/pokemon";
|
||||
@ -8,10 +8,6 @@ export default class RenameFormUiHandler extends FormModalUiHandler {
|
||||
return i18next.t("menu:renamePokemon");
|
||||
}
|
||||
|
||||
getFields(config?: ModalConfig): string[] {
|
||||
return [ i18next.t("menu:nickname") ];
|
||||
}
|
||||
|
||||
getWidth(config?: ModalConfig): number {
|
||||
return 160;
|
||||
}
|
||||
@ -33,6 +29,10 @@ export default class RenameFormUiHandler extends FormModalUiHandler {
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
return [{ label: i18next.t("menu:nickname") }];
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
if (super.show(args)) {
|
||||
const config = args[0] as ModalConfig;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { FormModalUiHandler } from "./form-modal-ui-handler";
|
||||
import { FormModalUiHandler, InputFieldConfig } from "./form-modal-ui-handler";
|
||||
import { ModalConfig } from "./modal-ui-handler";
|
||||
import i18next from "i18next";
|
||||
import { PlayerPokemon } from "#app/field/pokemon";
|
||||
@ -43,10 +43,6 @@ export default class TestDialogueUiHandler extends FormModalUiHandler {
|
||||
return "Test Dialogue";
|
||||
}
|
||||
|
||||
getFields(config?: ModalConfig): string[] {
|
||||
return [ "Dialogue" ];
|
||||
}
|
||||
|
||||
getWidth(config?: ModalConfig): number {
|
||||
return 300;
|
||||
}
|
||||
@ -68,8 +64,15 @@ export default class TestDialogueUiHandler extends FormModalUiHandler {
|
||||
return super.getReadableErrorMessage(error);
|
||||
}
|
||||
|
||||
override getInputFieldConfigs(): InputFieldConfig[] {
|
||||
return [{ label: "Dialogue" }];
|
||||
}
|
||||
|
||||
show(args: any[]): boolean {
|
||||
const ui = this.getUi();
|
||||
const hasTitle = !!this.getModalTitle();
|
||||
this.updateFields(this.getInputFieldConfigs(), hasTitle);
|
||||
this.updateContainer(args[0] as ModalConfig);
|
||||
const input = this.inputs[0];
|
||||
input.setMaxLength(255);
|
||||
|
||||
|