[Bugs/Balance] Fix various ME bugs and small balance adjustments (#4369)

* various bug fixes for MEs

* various bug fixes for MEs

* fix final isTransferable rename that was missed

* change Trainer's test vouchers for second option

* change unit test skips

* cut down excess ME track length and loop properly

* ME bug fix cleanup

* updating AI for Slumbering Snorlax ME, and small ME balance changes

* fix ts error

* fix bug type superfan dialogue discrepancy

* ME bug fixes PR feedback

* ME PR nits and fixes

* update naming convention of sprites

* ME balance changes and bug fixes

* fix tests

* fix An Offer You Can't Refuse ME requirements

* clean up post-battle logic for Breeder ME

* party size requirement cleanup

* clean up challenge requirements for disabling certain MEs

---------

Co-authored-by: ImperialSympathizer <imperialsympathizer@gmail.com>
This commit is contained in:
ImperialSympathizer 2024-09-21 23:47:32 -04:00 committed by GitHub
parent 612dcc5f27
commit 1c87532e64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
77 changed files with 571 additions and 303 deletions

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "berry_bush.png", "image": "berries_abound_bush.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 49, "w": 49,

View File

Before

Width:  |  Height:  |  Size: 719 B

After

Width:  |  Height:  |  Size: 719 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "mad_scientist_m.png", "image": "dark_deal_scientist.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 46, "w": 46,

View File

Before

Width:  |  Height:  |  Size: 920 B

After

Width:  |  Height:  |  Size: 920 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "b2w2_lady.png", "image": "department_store_sale_lady.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 399, "w": 399,

View File

Before

Width:  |  Height:  |  Size: 23 KiB

After

Width:  |  Height:  |  Size: 23 KiB

View File

Before

Width:  |  Height:  |  Size: 378 B

After

Width:  |  Height:  |  Size: 378 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "teacher.png", "image": "field_trip_teacher.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 43, "w": 43,

View File

Before

Width:  |  Height:  |  Size: 727 B

After

Width:  |  Height:  |  Size: 727 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "carnival_game.png", "image": "fun_and_games_game.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 38, "w": 38,

View File

Before

Width:  |  Height:  |  Size: 517 B

After

Width:  |  Height:  |  Size: 517 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "carnival_man.png", "image": "fun_and_games_man.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 50, "w": 50,

View File

Before

Width:  |  Height:  |  Size: 833 B

After

Width:  |  Height:  |  Size: 833 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "carnival_wobbuffet.png", "image": "fun_and_games_wobbuffet.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 45, "w": 45,

View File

Before

Width:  |  Height:  |  Size: 772 B

After

Width:  |  Height:  |  Size: 772 B

View File

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "chest_blue.png", "image": "mysterious_chest_blue.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 54, "w": 54,

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "chest_red.png", "image": "mysterious_chest_red.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 54, "w": 54,

View File

Before

Width:  |  Height:  |  Size: 2.5 KiB

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "warehouse_crate.png", "image": "part_timer_crate.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 71, "w": 71,

View File

Before

Width:  |  Height:  |  Size: 868 B

After

Width:  |  Height:  |  Size: 868 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "bait.png", "image": "safari_zone_bait.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 14, "w": 14,

View File

Before

Width:  |  Height:  |  Size: 277 B

After

Width:  |  Height:  |  Size: 277 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "mud.png", "image": "safari_zone_mud.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 14, "w": 14,

View File

Before

Width:  |  Height:  |  Size: 375 B

After

Width:  |  Height:  |  Size: 375 B

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "b2w2_veteran_m.png", "image": "shady_vitamin_dealer.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 424, "w": 424,

View File

Before

Width:  |  Height:  |  Size: 19 KiB

After

Width:  |  Height:  |  Size: 19 KiB

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "teleporter.png", "image": "teleporting_hijinks_teleporter.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 74, "w": 74,

View File

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -1,7 +1,7 @@
{ {
"textures": [ "textures": [
{ {
"image": "training_gear.png", "image": "training_session_gear.png",
"format": "RGBA8888", "format": "RGBA8888",
"size": { "size": {
"w": 76, "w": 76,

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

@ -1,10 +1,10 @@
import Phaser from "phaser"; import Phaser from "phaser";
import UI from "./ui/ui"; import UI from "./ui/ui";
import Pokemon, { PlayerPokemon, EnemyPokemon } from "./field/pokemon"; import Pokemon, { EnemyPokemon, PlayerPokemon } from "./field/pokemon";
import PokemonSpecies, { PokemonSpeciesFilter, allSpecies, getPokemonSpecies } from "./data/pokemon-species"; import PokemonSpecies, { allSpecies, getPokemonSpecies, PokemonSpeciesFilter } from "./data/pokemon-species";
import { Constructor, isNullOrUndefined } from "#app/utils"; import { Constructor, isNullOrUndefined } from "#app/utils";
import * as Utils from "./utils"; import * as Utils from "./utils";
import { Modifier, ModifierBar, ConsumablePokemonModifier, ConsumableModifier, PokemonHpRestoreModifier, TurnHeldItemTransferModifier, HealingBoosterModifier, PersistentModifier, PokemonHeldItemModifier, ModifierPredicate, DoubleBattleChanceBoosterModifier, FusePokemonModifier, PokemonFormChangeItemModifier, TerastallizeModifier, overrideModifiers, overrideHeldItems, PokemonIncrementingStatModifier, ExpShareModifier, ExpBalanceModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "./modifier/modifier"; import { ConsumableModifier, ConsumablePokemonModifier, DoubleBattleChanceBoosterModifier, ExpBalanceModifier, ExpShareModifier, FusePokemonModifier, HealingBoosterModifier, Modifier, ModifierBar, ModifierPredicate, MultipleParticipantExpBonusModifier, overrideHeldItems, overrideModifiers, PersistentModifier, PokemonExpBoosterModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier, PokemonHpRestoreModifier, PokemonIncrementingStatModifier, TerastallizeModifier, TurnHeldItemTransferModifier } from "./modifier/modifier";
import { PokeballType } from "./data/pokeball"; import { PokeballType } from "./data/pokeball";
import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims"; import { initCommonAnims, initMoveAnim, loadCommonAnimAssets, loadMoveAnimAssets, populateAnims } from "./data/battle-anims";
import { Phase } from "./phase"; import { Phase } from "./phase";
@ -13,20 +13,9 @@ import { Arena, ArenaBase } from "./field/arena";
import { GameData } from "./system/game-data"; import { GameData } from "./system/game-data";
import { addTextObject, getTextColor, TextStyle } from "./ui/text"; import { addTextObject, getTextColor, TextStyle } from "./ui/text";
import { allMoves } from "./data/move"; import { allMoves } from "./data/move";
import { import { getDefaultModifierTypeForTier, getEnemyModifierTypesForWave, getLuckString, getLuckTextTint, getModifierPoolForType, getModifierType, getPartyLuckValue, ModifierPoolType, modifierTypes, PokemonHeldItemModifierType } from "./modifier/modifier-type";
ModifierPoolType,
getDefaultModifierTypeForTier,
getEnemyModifierTypesForWave,
getLuckString,
getLuckTextTint,
getModifierPoolForType,
getModifierType,
getPartyLuckValue,
modifierTypes, PokemonHeldItemModifierType
} from "./modifier/modifier-type";
import AbilityBar from "./ui/ability-bar"; import AbilityBar from "./ui/ability-bar";
import { BlockItemTheftAbAttr, DoubleBattleChanceAbAttr, ChangeMovePriorityAbAttr, PostBattleInitAbAttr, applyAbAttrs, applyPostBattleInitAbAttrs } from "./data/ability"; import { allAbilities, applyAbAttrs, applyPostBattleInitAbAttrs, BlockItemTheftAbAttr, ChangeMovePriorityAbAttr, DoubleBattleChanceAbAttr, PostBattleInitAbAttr } from "./data/ability";
import { allAbilities } from "./data/ability";
import Battle, { BattleType, FixedBattleConfig } from "./battle"; import Battle, { BattleType, FixedBattleConfig } from "./battle";
import { GameMode, GameModes, getGameMode } from "./game-mode"; import { GameMode, GameModes, getGameMode } from "./game-mode";
import FieldSpritePipeline from "./pipelines/field-sprite"; import FieldSpritePipeline from "./pipelines/field-sprite";
@ -46,7 +35,7 @@ import UIPlugin from "phaser3-rex-plugins/templates/ui/ui-plugin";
import { addUiThemeOverrides } from "./ui/ui-theme"; import { addUiThemeOverrides } from "./ui/ui-theme";
import PokemonData from "./system/pokemon-data"; import PokemonData from "./system/pokemon-data";
import { Nature } from "./data/nature"; import { Nature } from "./data/nature";
import { SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger, pokemonFormChanges, FormChangeItem, SpeciesFormChange } from "./data/pokemon-forms"; import { FormChangeItem, pokemonFormChanges, SpeciesFormChange, SpeciesFormChangeManualTrigger, SpeciesFormChangeTimeOfDayTrigger, SpeciesFormChangeTrigger } from "./data/pokemon-forms";
import { FormChangePhase } from "./phases/form-change-phase"; import { FormChangePhase } from "./phases/form-change-phase";
import { getTypeRgb } from "./data/type"; import { getTypeRgb } from "./data/type";
import PokemonSpriteSparkleHandler from "./field/pokemon-sprite-sparkle-handler"; import PokemonSpriteSparkleHandler from "./field/pokemon-sprite-sparkle-handler";
@ -1081,6 +1070,11 @@ export default class BattleScene extends SceneBase {
p.destroy(); p.destroy();
} }
// If this is a ME, clear any residual visual sprites before reloading
if (this.currentBattle?.mysteryEncounter?.introVisuals) {
this.field.remove(this.currentBattle.mysteryEncounter?.introVisuals, true);
}
//@ts-ignore - allowing `null` for currentBattle causes a lot of trouble //@ts-ignore - allowing `null` for currentBattle causes a lot of trouble
this.currentBattle = null; // TODO: resolve ts-ignore this.currentBattle = null; // TODO: resolve ts-ignore
@ -1111,6 +1105,8 @@ export default class BattleScene extends SceneBase {
this.trainer.setPosition(406, 186); this.trainer.setPosition(406, 186);
this.trainer.setVisible(true); this.trainer.setVisible(true);
this.mysteryEncounterSaveData = new MysteryEncounterSaveData();
this.updateGameInfo(); this.updateGameInfo();
if (reloadI18n) { if (reloadI18n) {
@ -3173,11 +3169,17 @@ export default class BattleScene extends SceneBase {
if (encounterCandidate.encounterTier !== tier) { // Encounter is in tier if (encounterCandidate.encounterTier !== tier) { // Encounter is in tier
return false; return false;
} }
const disabledModes = encounterCandidate.disabledGameModes; const disallowedGameModes = encounterCandidate.disallowedGameModes;
if (disabledModes && disabledModes.length > 0 if (disallowedGameModes && disallowedGameModes.length > 0
&& disabledModes.includes(this.gameMode.modeId)) { // Encounter is enabled for game mode && disallowedGameModes.includes(this.gameMode.modeId)) { // Encounter is enabled for game mode
return false; return false;
} }
if (this.gameMode.modeId === GameModes.CHALLENGE) { // Encounter is enabled for challenges
const disallowedChallenges = encounterCandidate.disallowedChallenges;
if (disallowedChallenges && disallowedChallenges.length > 0 && this.gameMode.challenges.some(challenge => disallowedChallenges.includes(challenge.id))) {
return false;
}
}
if (!encounterCandidate.meetsRequirements(this)) { // Meets encounter requirements if (!encounterCandidate.meetsRequirements(this)) { // Meets encounter requirements
return false; return false;
} }

View File

@ -743,16 +743,21 @@ export abstract class BattleAnim {
public target: Pokemon | null; public target: Pokemon | null;
public sprites: Phaser.GameObjects.Sprite[]; public sprites: Phaser.GameObjects.Sprite[];
public bgSprite: Phaser.GameObjects.TileSprite | Phaser.GameObjects.Rectangle; public bgSprite: Phaser.GameObjects.TileSprite | Phaser.GameObjects.Rectangle;
public playOnEmptyField: boolean; /**
* Will attempt to play as much of an animation as possible, even if not all targets are on the field.
* Will also play the animation, even if the user has selected "Move Animations" OFF in Settings.
* Exclusively used by MEs atm, for visual animations at the start of an encounter.
*/
public playRegardlessOfIssues: boolean;
private srcLine: number[]; private srcLine: number[];
private dstLine: number[]; private dstLine: number[];
constructor(user?: Pokemon, target?: Pokemon, playOnEmptyField: boolean = false) { constructor(user?: Pokemon, target?: Pokemon, playRegardlessOfIssues: boolean = false) {
this.user = user ?? null; this.user = user ?? null;
this.target = target ?? null; this.target = target ?? null;
this.sprites = []; this.sprites = [];
this.playOnEmptyField = playOnEmptyField; this.playRegardlessOfIssues = playRegardlessOfIssues;
} }
abstract getAnim(): AnimConfig | null; abstract getAnim(): AnimConfig | null;
@ -829,7 +834,7 @@ export abstract class BattleAnim {
const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct? const user = !isOppAnim ? this.user! : this.target!; // TODO: are those bangs correct?
const target = !isOppAnim ? this.target! : this.user!; const target = !isOppAnim ? this.target! : this.user!;
if (!target?.isOnField() && !this.playOnEmptyField) { if (!target?.isOnField() && !this.playRegardlessOfIssues) {
if (callback) { if (callback) {
callback(); callback();
} }
@ -896,7 +901,7 @@ export abstract class BattleAnim {
} }
}; };
if (!scene.moveAnimations) { if (!scene.moveAnimations && !this.playRegardlessOfIssues) {
return cleanUpAndComplete(); return cleanUpAndComplete();
} }
@ -932,7 +937,7 @@ export abstract class BattleAnim {
const isUser = frame.target === AnimFrameTarget.USER; const isUser = frame.target === AnimFrameTarget.USER;
if (isUser && target === user) { if (isUser && target === user) {
continue; continue;
} else if (this.playOnEmptyField && frame.target === AnimFrameTarget.TARGET && !target.isOnField()) { } else if (this.playRegardlessOfIssues && frame.target === AnimFrameTarget.TARGET && !target.isOnField()) {
continue; continue;
} }
const sprites = spriteCache[isUser ? AnimFrameTarget.USER : AnimFrameTarget.TARGET]; const sprites = spriteCache[isUser ? AnimFrameTarget.USER : AnimFrameTarget.TARGET];
@ -1145,7 +1150,7 @@ export abstract class BattleAnim {
} }
}; };
if (!scene.moveAnimations) { if (!scene.moveAnimations && !this.playRegardlessOfIssues) {
return cleanUpAndComplete(); return cleanUpAndComplete();
} }

View File

@ -338,7 +338,7 @@ export const AbsoluteAvariceEncounter: MysteryEncounter =
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Let it have the food // Let it have the food
// Greedent joins the team, level equal to 2 below highest party member // Greedent joins the team, level equal to 2 below highest party member
const level = getHighestLevelPlayerPokemon(scene).level - 2; const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2;
const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false); const greedent = new EnemyPokemon(scene, getPokemonSpecies(Species.GREEDENT), level, TrainerSlot.NONE, false);
greedent.moveset = [new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF)]; greedent.moveset = [new PokemonMove(Moves.THRASH), new PokemonMove(Moves.BODY_PRESS), new PokemonMove(Moves.STUFF_CHEEKS), new PokemonMove(Moves.SLACK_OFF)];
greedent.passive = true; greedent.passive = true;

View File

@ -26,7 +26,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE) MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE)
.withEncounterTier(MysteryEncounterTier.GREAT) .withEncounterTier(MysteryEncounterTier.GREAT)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withScenePartySizeRequirement(2, 6) // Must have at least 2 pokemon in party .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 pokemon in party
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: Species.LIEPARD.toString(), spriteKey: Species.LIEPARD.toString(),
@ -60,7 +60,7 @@ export const AnOfferYouCantRefuseEncounter: MysteryEncounter =
.withQuery(`${namespace}.query`) .withQuery(`${namespace}.query`)
.withOnInit((scene: BattleScene) => { .withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
const pokemon = getHighestStatTotalPlayerPokemon(scene, false); const pokemon = getHighestStatTotalPlayerPokemon(scene, true, true);
const price = scene.getWaveMoneyAmount(10); const price = scene.getWaveMoneyAmount(10);
encounter.setDialogueToken("strongestPokemon", pokemon.getNameToRender()); encounter.setDialogueToken("strongestPokemon", pokemon.getNameToRender());

View File

@ -82,7 +82,7 @@ export const BerriesAboundEncounter: MysteryEncounter =
const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon); const { spriteKey, fileRoot } = getSpriteKeysFromPokemon(bossPokemon);
encounter.spriteConfigs = [ encounter.spriteConfigs = [
{ {
spriteKey: "berry_bush", spriteKey: "berries_abound_bush",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
x: 25, x: 25,
y: -6, y: -6,
@ -102,7 +102,7 @@ export const BerriesAboundEncounter: MysteryEncounter =
]; ];
// Get fastest party pokemon for option 2 // Get fastest party pokemon for option 2
const fastestPokemon = getHighestStatPlayerPokemon(scene, PERMANENT_STATS[Stat.SPD], true); const fastestPokemon = getHighestStatPlayerPokemon(scene, PERMANENT_STATS[Stat.SPD], true, false);
encounter.misc.fastestPokemon = fastestPokemon; encounter.misc.fastestPokemon = fastestPokemon;
encounter.misc.enemySpeed = bossPokemon.getStat(Stat.SPD); encounter.misc.enemySpeed = bossPokemon.getStat(Stat.SPD);
encounter.setDialogueToken("fastestPokemon", fastestPokemon.getNameToRender()); encounter.setDialogueToken("fastestPokemon", fastestPokemon.getNameToRender());

View File

@ -376,9 +376,10 @@ export const BugTypeSuperfanEncounter: MysteryEncounter =
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter(item => { const validItems = pokemon.getHeldItems().filter(item => {
return item instanceof BypassSpeedChanceModifier || return (item instanceof BypassSpeedChanceModifier ||
item instanceof ContactHeldItemTransferChanceModifier || item instanceof ContactHeldItemTransferChanceModifier ||
(item instanceof AttackTypeBoosterModifier && (item.type as AttackTypeBoosterModifierType).moveType === Type.BUG); (item instanceof AttackTypeBoosterModifier && (item.type as AttackTypeBoosterModifierType).moveType === Type.BUG)) &&
item.isTransferable;
}); });
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((modifier: PokemonHeldItemModifier) => {

View File

@ -29,8 +29,9 @@ import { Moves } from "#enums/moves";
import { EncounterBattleAnim } from "#app/data/battle-anims"; import { EncounterBattleAnim } from "#app/data/battle-anims";
import { MoveCategory } from "#app/data/move"; import { MoveCategory } from "#app/data/move";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES, GameModes } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { Challenges } from "#enums/challenges";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:clowningAround"; const namespace = "mysteryEncounter:clowningAround";
@ -61,7 +62,7 @@ const RANDOM_ABILITY_POOL = [
export const ClowningAroundEncounter: MysteryEncounter = export const ClowningAroundEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.CLOWNING_AROUND) MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.CLOWNING_AROUND)
.withEncounterTier(MysteryEncounterTier.ULTRA) .withEncounterTier(MysteryEncounterTier.ULTRA)
.withDisabledGameModes(GameModes.CHALLENGE) .withDisallowedChallenges(Challenges.SINGLE_TYPE)
.withSceneWaveRangeRequirement(80, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withSceneWaveRangeRequirement(80, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1])
.withAnimations(EncounterAnim.SMOKESCREEN) .withAnimations(EncounterAnim.SMOKESCREEN)
.withAutoHideIntroVisuals(false) .withAutoHideIntroVisuals(false)
@ -349,10 +350,18 @@ export const ClowningAroundEncounter: MysteryEncounter =
} }
} }
newTypes.push(secondType); newTypes.push(secondType);
// Apply the type changes (to both base and fusion, if pokemon is fused)
if (!pokemon.mysteryEncounterPokemonData) { if (!pokemon.mysteryEncounterPokemonData) {
pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData();
} }
pokemon.mysteryEncounterPokemonData.types = newTypes; pokemon.mysteryEncounterPokemonData.types = newTypes;
if (pokemon.isFusion()) {
if (!pokemon.fusionMysteryEncounterPokemonData) {
pokemon.fusionMysteryEncounterPokemonData = new MysteryEncounterPokemonData();
}
pokemon.fusionMysteryEncounterPokemonData.types = newTypes;
}
} }
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
@ -415,10 +424,17 @@ function onYesAbilitySwap(scene: BattleScene, resolve) {
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Do ability swap // Do ability swap
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
if (!pokemon.mysteryEncounterPokemonData) { if (pokemon.isFusion()) {
pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(); if (!pokemon.fusionMysteryEncounterPokemonData) {
pokemon.fusionMysteryEncounterPokemonData = new MysteryEncounterPokemonData();
}
pokemon.fusionMysteryEncounterPokemonData.ability = encounter.misc.ability;
} else {
if (!pokemon.mysteryEncounterPokemonData) {
pokemon.mysteryEncounterPokemonData = new MysteryEncounterPokemonData();
}
pokemon.mysteryEncounterPokemonData.ability = encounter.misc.ability;
} }
pokemon.mysteryEncounterPokemonData.ability = encounter.misc.ability;
encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender());
scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true)); scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true));
}; };

View File

@ -27,6 +27,7 @@ import { StatStageChangePhase } from "#app/phases/stat-stage-change-phase";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import i18next from "i18next";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounter:dancingLessons"; const namespace = "mysteryEncounter:dancingLessons";
@ -268,9 +269,12 @@ export const DancingLessonsEncounter: MysteryEncounter =
}); });
}; };
// Only Pokemon that have a Dancing move can be selected // Only challenge legal/unfainted Pokemon that have a Dancing move can be selected
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected // If pokemon meets primary pokemon reqs, it can be selected
if (!pokemon.isAllowedInBattle()) {
return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null;
}
const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon); const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon);
if (!meetsReqs) { if (!meetsReqs) {
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;

View File

@ -94,7 +94,7 @@ export const DarkDealEncounter: MysteryEncounter =
.withEncounterTier(MysteryEncounterTier.ROGUE) .withEncounterTier(MysteryEncounterTier.ROGUE)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "mad_scientist_m", spriteKey: "dark_deal_scientist",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: true, hasShadow: true,
}, },
@ -115,7 +115,7 @@ export const DarkDealEncounter: MysteryEncounter =
}, },
]) ])
.withSceneWaveRangeRequirement(30, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1]) .withSceneWaveRangeRequirement(30, CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[1])
.withScenePartySizeRequirement(2, 6) // Must have at least 2 pokemon in party .withScenePartySizeRequirement(2, 6, true) // Must have at least 2 pokemon in party
.withCatchAllowed(true) .withCatchAllowed(true)
.withTitle(`${namespace}.title`) .withTitle(`${namespace}.title`)
.withDescription(`${namespace}.description`) .withDescription(`${namespace}.description`)
@ -139,7 +139,7 @@ export const DarkDealEncounter: MysteryEncounter =
.withPreOptionPhase(async (scene: BattleScene) => { .withPreOptionPhase(async (scene: BattleScene) => {
// Removes random pokemon (including fainted) from party and adds name to dialogue data tokens // 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 // Will never return last battle able mon and instead pick fainted/unable to battle
const removedPokemon = getRandomPlayerPokemon(scene, false, true); const removedPokemon = getRandomPlayerPokemon(scene, true, false, true);
// Get all the pokemon's held items // Get all the pokemon's held items
const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier)); const modifiers = removedPokemon.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
scene.removePokemonFromPlayerParty(removedPokemon); scene.removePokemonFromPlayerParty(removedPokemon);

View File

@ -10,7 +10,7 @@ import { CombinationPokemonRequirement, HeldItemRequirement, MoneyRequirement }
import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier"; import { BerryModifier, HealingBoosterModifier, HiddenAbilityRateBoosterModifier, LevelIncrementBoosterModifier, PokemonHeldItemModifier, PreserveBerryModifier } from "#app/modifier/modifier";
import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler";
import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import i18next from "#app/plugins/i18n"; import i18next from "#app/plugins/i18n";
@ -159,7 +159,7 @@ export const DelibirdyEncounter: MysteryEncounter =
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter((it) => { const validItems = pokemon.getHeldItems().filter((it) => {
return OPTION_2_ALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem); return OPTION_2_ALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable;
}); });
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((modifier: PokemonHeldItemModifier) => {
@ -179,9 +179,8 @@ export const DelibirdyEncounter: MysteryEncounter =
}); });
}; };
// Only Pokemon that can gain benefits are above 1/3rd HP with no status
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected // If pokemon has valid item, it can be selected
const meetsReqs = encounter.options[1].pokemonMeetsPrimaryRequirements(scene, pokemon); const meetsReqs = encounter.options[1].pokemonMeetsPrimaryRequirements(scene, pokemon);
if (!meetsReqs) { if (!meetsReqs) {
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;
@ -197,7 +196,7 @@ export const DelibirdyEncounter: MysteryEncounter =
const modifier = encounter.misc.chosenModifier; const modifier = 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 Healing Charm for Reviver Seed
if (modifier.type.name.includes("Berry")) { if (modifier instanceof BerryModifier) {
// Check if the player has max stacks of that Candy Jar already // Check if the player has max stacks of that Candy Jar already
const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier; const existing = scene.findModifier(m => m instanceof LevelIncrementBoosterModifier) as LevelIncrementBoosterModifier;
@ -254,7 +253,7 @@ export const DelibirdyEncounter: MysteryEncounter =
const onPokemonSelected = (pokemon: PlayerPokemon) => { const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Get Pokemon held items and filter for valid ones // Get Pokemon held items and filter for valid ones
const validItems = pokemon.getHeldItems().filter((it) => { const validItems = pokemon.getHeldItems().filter((it) => {
return !OPTION_3_DISALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem); return !OPTION_3_DISALLOWED_MODIFIERS.some(heldItem => it.constructor.name === heldItem) && it.isTransferable;
}); });
return validItems.map((modifier: PokemonHeldItemModifier) => { return validItems.map((modifier: PokemonHeldItemModifier) => {
@ -274,9 +273,8 @@ export const DelibirdyEncounter: MysteryEncounter =
}); });
}; };
// Only Pokemon that can gain benefits are above 1/3rd HP with no status
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected // If pokemon has valid item, it can be selected
const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon); const meetsReqs = encounter.options[2].pokemonMeetsPrimaryRequirements(scene, pokemon);
if (!meetsReqs) { if (!meetsReqs) {
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;

View File

@ -27,7 +27,7 @@ export const DepartmentStoreSaleEncounter: MysteryEncounter =
.withSceneWaveRangeRequirement(CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[0], 100) .withSceneWaveRangeRequirement(CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES[0], 100)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "b2w2_lady", spriteKey: "department_store_sale_lady",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: true, hasShadow: true,
x: -20, x: -20,

View File

@ -32,7 +32,7 @@ export const FieldTripEncounter: MysteryEncounter =
hasShadow: true, hasShadow: true,
}, },
{ {
spriteKey: "teacher", spriteKey: "field_trip_teacher",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: true, hasShadow: true,
}, },

View File

@ -189,7 +189,7 @@ export const FieryFalloutEncounter: MysteryEncounter =
} }
// Burn random member // Burn random member
const burnable = nonFireTypes.filter(p => isNullOrUndefined(p.status) || isNullOrUndefined(p.status!.effect) || p.status?.effect === StatusEffect.BURN); const burnable = nonFireTypes.filter(p => isNullOrUndefined(p.status) || isNullOrUndefined(p.status!.effect) || p.status?.effect === StatusEffect.NONE);
if (burnable?.length > 0) { if (burnable?.length > 0) {
const roll = randSeedInt(burnable.length); const roll = randSeedInt(burnable.length);
const chosenPokemon = burnable[roll]; const chosenPokemon = burnable[roll];

View File

@ -68,6 +68,7 @@ export const FightOrFlightEncounter: MysteryEncounter =
mysteryEncounterBattleEffects: (pokemon: Pokemon) => { mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(pokemon.scene, `${namespace}.option.1.stat_boost`); queueEncounterMessage(pokemon.scene, `${namespace}.option.1.stat_boost`);
// Randomly boost 1 stat 2 stages // Randomly boost 1 stat 2 stages
// Cannot boost Spd, Acc, or Evasion
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [randSeedInt(4, 1)], 2)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [randSeedInt(4, 1)], 2));
} }
}], }],

View File

@ -7,7 +7,7 @@ import { TrainerSlot } from "#app/data/trainer-config";
import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon"; import Pokemon, { FieldPosition, PlayerPokemon } from "#app/field/pokemon";
import { getPokemonSpecies } from "#app/data/pokemon-species"; import { getPokemonSpecies } from "#app/data/pokemon-species";
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
@ -22,6 +22,7 @@ import { PostSummonPhase } from "#app/phases/post-summon-phase";
import { modifierTypes } from "#app/modifier/modifier-type"; import { modifierTypes } from "#app/modifier/modifier-type";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:funAndGames"; const namespace = "mysteryEncounter:funAndGames";
@ -45,14 +46,14 @@ export const FunAndGamesEncounter: MysteryEncounter =
.withSkipToFightInput(true) .withSkipToFightInput(true)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "carnival_game", spriteKey: "fun_and_games_game",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: false, hasShadow: false,
x: 0, x: 0,
y: 6, y: 6,
}, },
{ {
spriteKey: "carnival_wobbuffet", spriteKey: "fun_and_games_wobbuffet",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: true, hasShadow: true,
x: -28, x: -28,
@ -60,7 +61,7 @@ export const FunAndGamesEncounter: MysteryEncounter =
yShadow: 6 yShadow: 6
}, },
{ {
spriteKey: "carnival_man", spriteKey: "fun_and_games_man",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: true, hasShadow: true,
x: 40, x: 40,
@ -110,12 +111,7 @@ export const FunAndGamesEncounter: MysteryEncounter =
// Only Pokemon that are not KOed/legal can be selected // Only Pokemon that are not KOed/legal can be selected
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
const meetsReqs = pokemon.isAllowedInBattle(); return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`);
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;
}
return null;
}; };
return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter);

View File

@ -317,7 +317,6 @@ export const GlobalTradeSystemEncounter: MysteryEncounter =
}); });
}; };
// Only Pokemon that can gain benefits are above 1/3rd HP with no status
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon has items to trade // If pokemon has items to trade
const meetsReqs = pokemon.getHeldItems().filter((it) => { const meetsReqs = pokemon.getHeldItems().filter((it) => {

View File

@ -33,7 +33,7 @@ export const LostAtSeaEncounter: MysteryEncounter = MysteryEncounterBuilder.with
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "buoy", spriteKey: "lost_at_sea_buoy",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: false, hasShadow: false,
x: 20, x: 20,

View File

@ -37,7 +37,7 @@ export const MysteriousChestEncounter: MysteryEncounter =
.withCatchAllowed(true) .withCatchAllowed(true)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "chest_blue", spriteKey: "mysterious_chest_blue",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: true, hasShadow: true,
y: 8, y: 8,
@ -46,7 +46,7 @@ export const MysteriousChestEncounter: MysteryEncounter =
disableAnimation: true, // Re-enabled after option select disableAnimation: true, // Re-enabled after option select
}, },
{ {
spriteKey: "chest_red", spriteKey: "mysterious_chest_red",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: false, hasShadow: false,
y: 8, y: 8,
@ -163,11 +163,12 @@ export const MysteriousChestEncounter: MysteryEncounter =
leaveEncounterWithoutBattle(scene); leaveEncounterWithoutBattle(scene);
} else { } else {
// Your highest level unfainted Pokemon gets OHKO. Start battle against a Gimmighoul (35%) // Your highest level unfainted Pokemon gets OHKO. Start battle against a Gimmighoul (35%)
const highestLevelPokemon = getHighestLevelPlayerPokemon( const highestLevelPokemon = getHighestLevelPlayerPokemon(scene, true, false);
scene,
true
);
koPlayerPokemon(scene, highestLevelPokemon); koPlayerPokemon(scene, highestLevelPokemon);
encounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender());
await showEncounterText(scene, `${namespace}.option.1.bad`);
// Handle game over edge case // Handle game over edge case
const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle()); const allowedPokemon = scene.getParty().filter(p => p.isAllowedInBattle());
if (allowedPokemon.length === 0) { if (allowedPokemon.length === 0) {
@ -176,8 +177,6 @@ export const MysteriousChestEncounter: MysteryEncounter =
scene.unshiftPhase(new GameOverPhase(scene)); scene.unshiftPhase(new GameOverPhase(scene));
} else { } else {
// Show which Pokemon was KOed, then start battle against Gimmighoul // Show which Pokemon was KOed, then start battle against Gimmighoul
encounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender());
await showEncounterText(scene, `${namespace}.option.1.bad`);
transitionMysteryEncounterIntroVisuals(scene, true, true, 500); transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
setEncounterRewards(scene, { fillRemaining: true }); setEncounterRewards(scene, { fillRemaining: true });
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]); await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);

View File

@ -8,10 +8,11 @@ import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups"; import { CHARMING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import { getEncounterText, showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import i18next from "i18next"; import i18next from "i18next";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:partTimer"; const namespace = "mysteryEncounter:partTimer";
@ -27,7 +28,7 @@ export const PartTimerEncounter: MysteryEncounter =
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "warehouse_crate", spriteKey: "part_timer_crate",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: false, hasShadow: false,
y: 6, y: 6,
@ -117,11 +118,7 @@ export const PartTimerEncounter: MysteryEncounter =
// Only Pokemon non-KOd pokemon can be selected // Only Pokemon non-KOd pokemon can be selected
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
if (!pokemon.isAllowedInBattle()) { return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`);
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;
}
return null;
}; };
return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter);
@ -198,11 +195,7 @@ export const PartTimerEncounter: MysteryEncounter =
// Only Pokemon non-KOd pokemon can be selected // Only Pokemon non-KOd pokemon can be selected
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
if (!pokemon.isAllowedInBattle()) { return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`);
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;
}
return null;
}; };
return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter);

View File

@ -79,8 +79,8 @@ export const SafariZoneEncounter: MysteryEncounter =
scene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav"); scene.loadSe("PRSFX- Bug Bite", "battle_anims", "PRSFX- Bug Bite.wav");
scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims", "PRSFX- Sludge Bomb2.wav"); scene.loadSe("PRSFX- Sludge Bomb2", "battle_anims", "PRSFX- Sludge Bomb2.wav");
scene.loadSe("PRSFX- Taunt2", "battle_anims", "PRSFX- Taunt2.wav"); scene.loadSe("PRSFX- Taunt2", "battle_anims", "PRSFX- Taunt2.wav");
scene.loadAtlas("bait", "mystery-encounters"); scene.loadAtlas("safari_zone_bait", "mystery-encounters");
scene.loadAtlas("mud", "mystery-encounters"); scene.loadAtlas("safari_zone_mud", "mystery-encounters");
// Clear enemy party // Clear enemy party
scene.currentBattle.enemyParty = []; scene.currentBattle.enemyParty = [];
await transitionMysteryEncounterIntroVisuals(scene); await transitionMysteryEncounterIntroVisuals(scene);
@ -254,7 +254,7 @@ async function summonSafariPokemon(scene: BattleScene) {
let enemySpecies; let enemySpecies;
let pokemon; let pokemon;
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); enemySpecies = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5], undefined, undefined, false, false, false));
const level = scene.currentBattle.getLevelForWave(); const level = scene.currentBattle.getLevelForWave();
enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(level, true, false, scene.gameMode)); enemySpecies = getPokemonSpecies(enemySpecies.getWildSpeciesForLevel(level, true, false, scene.gameMode));
pokemon = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, false); pokemon = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.NONE, false);
@ -282,7 +282,7 @@ async function summonSafariPokemon(scene: BattleScene) {
pokemon.calculateStats(); pokemon.calculateStats();
scene.currentBattle.enemyParty.unshift(pokemon); scene.currentBattle.enemyParty.unshift(pokemon);
}, scene.currentBattle.waveIndex * 1000 + encounter.misc.safariPokemonRemaining); }, scene.currentBattle.waveIndex * 1000 * encounter.misc.safariPokemonRemaining);
scene.gameData.setPokemonSeen(pokemon, true); scene.gameData.setPokemonSeen(pokemon, true);
await pokemon.loadAssets(); await pokemon.loadAssets();
@ -322,7 +322,7 @@ async function throwBait(scene: BattleScene, pokemon: EnemyPokemon): Promise<boo
const originalY: number = pokemon.y; const originalY: number = pokemon.y;
const fpOffset = pokemon.getFieldPositionOffset(); const fpOffset = pokemon.getFieldPositionOffset();
const bait: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "bait", "0001.png"); const bait: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 25, "safari_zone_bait", "0001.png");
bait.setOrigin(0.5, 0.625); bait.setOrigin(0.5, 0.625);
scene.field.add(bait); scene.field.add(bait);
@ -388,7 +388,7 @@ async function throwMud(scene: BattleScene, pokemon: EnemyPokemon): Promise<bool
const originalY: number = pokemon.y; const originalY: number = pokemon.y;
const fpOffset = pokemon.getFieldPositionOffset(); const fpOffset = pokemon.getFieldPositionOffset();
const mud: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 35, "mud", "0001.png"); const mud: Phaser.GameObjects.Sprite = scene.addFieldSprite(16 + 75, 80 + 35, "safari_zone_mud", "0001.png");
mud.setOrigin(0.5, 0.625); mud.setOrigin(0.5, 0.625);
scene.field.add(mud); scene.field.add(mud);

View File

@ -9,12 +9,13 @@ import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-enc
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements"; import { MoneyRequirement } from "#app/data/mystery-encounters/mystery-encounter-requirements";
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; import { applyDamageToPokemon, applyModifierTypeToPlayerPokemon, isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { getNatureName } from "#app/data/nature"; import { getNatureName } from "#app/data/nature";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import i18next from "i18next";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounter:shadyVitaminDealer"; const namespace = "mysteryEncounter:shadyVitaminDealer";
@ -32,7 +33,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
.withEncounterTier(MysteryEncounterTier.COMMON) .withEncounterTier(MysteryEncounterTier.COMMON)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withSceneRequirement(new MoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER)) // Must have the money for at least the cheap deal .withSceneRequirement(new MoneyRequirement(0, VITAMIN_DEALER_CHEAP_PRICE_MULTIPLIER)) // Must have the money for at least the cheap deal
.withPrimaryPokemonHealthRatioRequirement([0.5, 1]) // At least 1 Pokemon must have above half HP .withPrimaryPokemonHealthRatioRequirement([0.51, 1]) // At least 1 Pokemon must have above half HP
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: Species.KROOKODILE.toString(), spriteKey: Species.KROOKODILE.toString(),
@ -44,7 +45,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
yShadow: -5 yShadow: -5
}, },
{ {
spriteKey: "b2w2_veteran_m", spriteKey: "shady_vitamin_dealer",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: true, hasShadow: true,
x: -12, x: -12,
@ -98,8 +99,10 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
// Only Pokemon that can gain benefits are above half HP with no status // Only Pokemon that can gain benefits are above half HP with no status
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected // If pokemon meets primary pokemon reqs, it can be selected
const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon); if (!pokemon.isAllowed()) {
if (!meetsReqs) { return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null;
}
if (!encounter.pokemonMeetsPrimaryRequirements(scene, pokemon)) {
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null; return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;
} }
@ -175,13 +178,7 @@ export const ShadyVitaminDealerEncounter: MysteryEncounter =
// Only Pokemon that can gain benefits are unfainted // Only Pokemon that can gain benefits are unfainted
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
// If pokemon is unfainted it can be selected return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`);
const meetsReqs = !pokemon.isFainted(true);
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;
}
return null;
}; };
return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter);

View File

@ -44,7 +44,7 @@ export const TeleportingHijinksEncounter: MysteryEncounter =
.withCatchAllowed(true) .withCatchAllowed(true)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "teleporter", spriteKey: "teleporting_hijinks_teleporter",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: true, hasShadow: true,
x: 4, x: 4,
@ -171,6 +171,12 @@ async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) {
const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true);
const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true); const bossPokemon = new EnemyPokemon(scene, bossSpecies, level, TrainerSlot.NONE, true);
encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon)); encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon));
// Defense/Spd buffs below wave 50, Atk/Def/Spd buffs otherwise
const statChangesForBattle: (Stat.ATK | Stat.DEF | Stat.SPATK | Stat.SPDEF | Stat.SPD | Stat.ACC | Stat.EVA)[] = scene.currentBattle.waveIndex < 50 ?
[Stat.DEF, Stat.SPDEF, Stat.SPD] :
[Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD];
const config: EnemyPartyConfig = { const config: EnemyPartyConfig = {
pokemonConfigs: [{ pokemonConfigs: [{
level: level, level: level,
@ -180,7 +186,7 @@ async function doBiomeTransitionDialogueAndBattleInit(scene: BattleScene) {
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON], tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => { mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(pokemon.scene, `${namespace}.boss_enraged`); queueEncounterMessage(pokemon.scene, `${namespace}.boss_enraged`);
pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [Stat.ATK, Stat.DEF, Stat.SPATK, Stat.SPDEF, Stat.SPD], 1)); pokemon.scene.unshiftPhase(new StatStageChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, statChangesForBattle, 1));
} }
}], }],
}; };

View File

@ -1,6 +1,5 @@
import { EnemyPartyConfig, generateModifierType, initBattleWithEnemyConfig, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { EnemyPartyConfig, generateModifierType, handleMysteryEncounterBattleFailed, initBattleWithEnemyConfig, setEncounterRewards, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { trainerConfigs } from "#app/data/trainer-config"; import { trainerConfigs } from "#app/data/trainer-config";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import { randSeedShuffle } from "#app/utils"; import { randSeedShuffle } from "#app/utils";
@ -14,8 +13,6 @@ import { Species } from "#enums/species";
import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species";
import { Nature } from "#enums/nature"; import { Nature } from "#enums/nature";
import { Moves } from "#enums/moves"; import { Moves } from "#enums/moves";
import { Type } from "#app/data/type";
import { Stat } from "#enums/stat";
import { PlayerPokemon } from "#app/field/pokemon"; import { PlayerPokemon } from "#app/field/pokemon";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { IEggOptions } from "#app/data/egg"; import { IEggOptions } from "#app/data/egg";
@ -24,15 +21,18 @@ import { EggTier } from "#enums/egg-type";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { achvs } from "#app/system/achv"; import { achvs } from "#app/system/achv";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Type } from "#app/data/type";
import { getPokeballTintColor } from "#app/data/pokeball";
/** the i18n namespace for the encounter */ /** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:expertPokemonBreeder"; const namespace = "mysteryEncounter:expertPokemonBreeder";
const trainerNameKey = "trainerNames:expert_pokemon_breeder"; const trainerNameKey = "trainerNames:expert_pokemon_breeder";
const FIRST_STAGE_EVOLUTION_WAVE = 30; const FIRST_STAGE_EVOLUTION_WAVE = 45;
const SECOND_STAGE_EVOLUTION_WAVE = 45; const SECOND_STAGE_EVOLUTION_WAVE = 60;
const FINAL_STAGE_EVOLUTION_WAVE = 60; const FINAL_STAGE_EVOLUTION_WAVE = 75;
const FRIENDSHIP_ADDED = 20; const FRIENDSHIP_ADDED = 20;
@ -216,6 +216,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
const { pokemon1, pokemon1CommonEggs, pokemon1RareEggs } = encounter.misc; const { pokemon1, pokemon1CommonEggs, pokemon1RareEggs } = encounter.misc;
encounter.misc.chosenPokemon = pokemon1;
encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon1.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs); const eggOptions = getEggOptions(scene, pokemon1CommonEggs, pokemon1RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions); setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
@ -241,14 +242,11 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
}); });
} }
encounter.onGameOver = onGameOver;
initBattleWithEnemyConfig(scene, config); initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => { .withPostOptionPhase(async (scene: BattleScene) => {
// Give achievement if in Space biome await doPostEncounterCleanup(scene);
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
scene.currentBattle.mysteryEncounter!.misc.pokemon1.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
}) })
.build() .build()
) )
@ -270,6 +268,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
const { pokemon2, pokemon2CommonEggs, pokemon2RareEggs } = encounter.misc; const { pokemon2, pokemon2CommonEggs, pokemon2RareEggs } = encounter.misc;
encounter.misc.chosenPokemon = pokemon2;
encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon2.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs); const eggOptions = getEggOptions(scene, pokemon2CommonEggs, pokemon2RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions); setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
@ -295,14 +294,11 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
}); });
} }
encounter.onGameOver = onGameOver;
initBattleWithEnemyConfig(scene, config); initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => { .withPostOptionPhase(async (scene: BattleScene) => {
// Give achievement if in Space biome await doPostEncounterCleanup(scene);
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
scene.currentBattle.mysteryEncounter!.misc.pokemon2.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
}) })
.build() .build()
) )
@ -324,6 +320,7 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0];
const { pokemon3, pokemon3CommonEggs, pokemon3RareEggs } = encounter.misc; const { pokemon3, pokemon3CommonEggs, pokemon3RareEggs } = encounter.misc;
encounter.misc.chosenPokemon = pokemon3;
encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender()); encounter.setDialogueToken("chosenPokemon", pokemon3.getNameToRender());
const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs); const eggOptions = getEggOptions(scene, pokemon3CommonEggs, pokemon3RareEggs);
setEncounterRewards(scene, { fillRemaining: true }, eggOptions); setEncounterRewards(scene, { fillRemaining: true }, eggOptions);
@ -349,19 +346,17 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
}); });
} }
encounter.onGameOver = onGameOver;
initBattleWithEnemyConfig(scene, config); initBattleWithEnemyConfig(scene, config);
}) })
.withPostOptionPhase(async (scene: BattleScene) => { .withPostOptionPhase(async (scene: BattleScene) => {
// Give achievement if in Space biome await doPostEncounterCleanup(scene);
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
scene.currentBattle.mysteryEncounter!.misc.pokemon3.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
}) })
.build() .build()
) )
.withOutroDialogue([ .withOutroDialogue([
{ {
speaker: trainerNameKey,
text: `${namespace}.outro`, text: `${namespace}.outro`,
}, },
]) ])
@ -390,15 +385,7 @@ function getPartyConfig(scene: BattleScene): EnemyPartyConfig {
modifierConfigs: [ modifierConfigs: [
{ {
modifier: generateModifierType(scene, modifierTypes.TERA_SHARD, [Type.STEEL]) as PokemonHeldItemModifierType, modifier: generateModifierType(scene, modifierTypes.TERA_SHARD, [Type.STEEL]) as PokemonHeldItemModifierType,
}, }
{
modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.ATK]) as PokemonHeldItemModifierType,
stackCount: 1 + Math.floor(waveIndex / 20), // +1 Protein every 20 waves
},
{
modifier: generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER, [Stat.SPD]) as PokemonHeldItemModifierType,
stackCount: 1 + Math.floor(waveIndex / 40), // +1 Carbos every 40 waves
},
] ]
} }
] ]
@ -547,3 +534,83 @@ async function restorePartyAndHeldItems(scene: BattleScene) {
}); });
await scene.updateModifiers(true); await scene.updateModifiers(true);
} }
function onGameOver(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!;
encounter.dialogue.outro = [
{
speaker: trainerNameKey,
text: `${namespace}.outro_failed`,
},
];
// Restore original party, player loses all friendship with chosen mon (it remains fainted)
restorePartyAndHeldItems(scene);
const chosenPokemon = encounter.misc.chosenPokemon;
chosenPokemon.friendship = 0;
// Clear all rewards that would have been earned
encounter.doEncounterRewards = undefined;
// Set flag that encounter was failed
encounter.misc.encounterFailed = true;
// Revert BGM
scene.playBgm(scene.arena.bgm);
// Return enemy Pokemon
const pokemon = scene.getEnemyPokemon();
if (pokemon) {
scene.playSound("se/pb_rel");
pokemon.hideInfo();
pokemon.tint(getPokeballTintColor(pokemon.pokeball), 1, 250, "Sine.easeIn");
scene.tweens.add({
targets: pokemon,
duration: 250,
ease: "Sine.easeIn",
scale: 0.5,
onComplete: () => {
scene.field.remove(pokemon, true);
}
});
}
// Show the enemy trainer
scene.time.delayedCall(250, () => {
const sprites = scene.currentBattle.trainer?.getSprites();
const tintSprites = scene.currentBattle.trainer?.getTintSprites();
if (sprites && tintSprites) {
for (let i = 0; i < sprites.length; i++) {
sprites[i].setVisible(true);
tintSprites[i].setVisible(true);
sprites[i].clearTint();
tintSprites[i].clearTint();
}
}
scene.tweens.add({
targets: scene.currentBattle.trainer,
x: "-=16",
y: "+=16",
alpha: 1,
ease: "Sine.easeInOut",
duration: 750
});
});
handleMysteryEncounterBattleFailed(scene, true);
return false;
}
async function doPostEncounterCleanup(scene: BattleScene) {
const encounter = scene.currentBattle.mysteryEncounter!;
if (!encounter.misc.encounterFailed) {
// Give achievement if in Space biome
checkAchievement(scene);
// Give 20 friendship to the chosen pokemon
encounter.misc.chosenPokemon.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
}
}

View File

@ -58,12 +58,12 @@ export const ThePokemonSalesmanEncounter: MysteryEncounter =
.withOnInit((scene: BattleScene) => { .withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter!; const encounter = scene.currentBattle.mysteryEncounter!;
let species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); let species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5], undefined, undefined, false, false, false));
const tries = 0; const tries = 0;
// Reroll any species that don't have HAs // Reroll any species that don't have HAs
while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) && tries < 5) { while ((isNullOrUndefined(species.abilityHidden) || species.abilityHidden === Abilities.NONE) && tries < 5) {
species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5], undefined, undefined, false, false, false));
} }
let pokemon: PlayerPokemon; let pokemon: PlayerPokemon;

View File

@ -13,13 +13,14 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene"; import BattleScene from "#app/battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter"; import MysteryEncounter, { MysteryEncounterBuilder } from "#app/data/mystery-encounters/mystery-encounter";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import HeldModifierConfig from "#app/interfaces/held-modifier-config"; import HeldModifierConfig from "#app/interfaces/held-modifier-config";
import i18next from "i18next"; import i18next from "i18next";
import { getStatKey } from "#enums/stat"; import { getStatKey } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { isPokemonValidForEncounterOptionSelection } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** The i18n namespace for the encounter */ /** The i18n namespace for the encounter */
const namespace = "mysteryEncounter:trainingSession"; const namespace = "mysteryEncounter:trainingSession";
@ -38,7 +39,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
.withHideWildIntroMessage(true) .withHideWildIntroMessage(true)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "training_gear", spriteKey: "training_session_gear",
fileRoot: "mystery-encounters", fileRoot: "mystery-encounters",
hasShadow: true, hasShadow: true,
y: 6, y: 6,
@ -77,12 +78,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
// Only Pokemon that are not KOed/legal can be trained // Only Pokemon that are not KOed/legal can be trained
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
const meetsReqs = pokemon.isAllowedInBattle(); return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`);
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;
}
return null;
}; };
return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter);
@ -211,12 +207,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
// Only Pokemon that are not KOed/legal can be trained // Only Pokemon that are not KOed/legal can be trained
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
const meetsReqs = pokemon.isAllowedInBattle(); return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`);
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;
}
return null;
}; };
return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter);
@ -307,12 +298,7 @@ export const TrainingSessionEncounter: MysteryEncounter =
// Only Pokemon that are not KOed/legal can be trained // Only Pokemon that are not KOed/legal can be trained
const selectableFilter = (pokemon: Pokemon) => { const selectableFilter = (pokemon: Pokemon) => {
const meetsReqs = pokemon.isAllowedInBattle(); return isPokemonValidForEncounterOptionSelection(pokemon, scene, `${namespace}.invalid_selection`);
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}.invalid_selection`) ?? null;
}
return null;
}; };
return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter); return selectPokemonForOption(scene, onPokemonSelected, undefined, selectableFilter);

View File

@ -50,7 +50,7 @@ export const UncommonBreedEncounter: MysteryEncounter =
// Calculate boss mon // Calculate boss mon
// Level equal to 2 below highest party member // Level equal to 2 below highest party member
const level = getHighestLevelPlayerPokemon(scene).level - 2; const level = getHighestLevelPlayerPokemon(scene, false, true).level - 2;
const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true); const species = scene.arena.randomSpecies(scene.currentBattle.waveIndex, level, 0, getPartyLuckValue(scene.getParty()), true);
const pokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, true); const pokemon = new EnemyPokemon(scene, species, level, TrainerSlot.NONE, true);
const speciesRootForm = pokemon.species.getRootSpeciesId(); const speciesRootForm = pokemon.species.getRootSpeciesId();

View File

@ -20,7 +20,8 @@ import i18next from "#app/plugins/i18n";
import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence"; import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
import { getLevelTotalExp } from "#app/data/exp"; import { getLevelTotalExp } from "#app/data/exp";
import { Stat } from "#enums/stat"; import { Stat } from "#enums/stat";
import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES, GameModes } from "#app/game-mode"; import { CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES } from "#app/game-mode";
import { Challenges } from "#enums/challenges";
/** i18n namespace for encounter */ /** i18n namespace for encounter */
const namespace = "mysteryEncounter:weirdDream"; const namespace = "mysteryEncounter:weirdDream";
@ -82,6 +83,9 @@ const SUPER_LEGENDARY_BST_THRESHOLD = 600;
const NON_LEGENDARY_BST_THRESHOLD = 570; const NON_LEGENDARY_BST_THRESHOLD = 570;
const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450; const GAIN_OLD_GATEAU_ITEM_BST_THRESHOLD = 450;
/** 0-100 */
const PERCENT_LEVEL_LOSS_ON_REFUSE = 12.5;
/** /**
* Value ranges of the resulting species BST transformations after adding values to original species * Value ranges of the resulting species BST transformations after adding values to original species
* 2 Pokemon in the party use this range * 2 Pokemon in the party use this range
@ -101,7 +105,7 @@ const STANDARD_BST_TRANSFORM_BASE_VALUES: [number, number] = [40, 50];
export const WeirdDreamEncounter: MysteryEncounter = export const WeirdDreamEncounter: MysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM) MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM)
.withEncounterTier(MysteryEncounterTier.ROGUE) .withEncounterTier(MysteryEncounterTier.ROGUE)
.withDisabledGameModes(GameModes.CHALLENGE) .withDisallowedChallenges(Challenges.SINGLE_TYPE)
.withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES) .withSceneWaveRangeRequirement(...CLASSIC_MODE_MYSTERY_ENCOUNTER_WAVES)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
@ -207,7 +211,7 @@ export const WeirdDreamEncounter: MysteryEncounter =
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Reduce party levels by 20% // Reduce party levels by 20%
for (const pokemon of scene.getParty()) { for (const pokemon of scene.getParty()) {
pokemon.level = Math.max(Math.ceil(0.8 * pokemon.level), 1); pokemon.level = Math.max(Math.ceil((100 - PERCENT_LEVEL_LOSS_ON_REFUSE) / 100 * pokemon.level), 1);
pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate); pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate);
pokemon.levelExp = 0; pokemon.levelExp = 0;
@ -339,6 +343,9 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
} }
} }
// If the previous pokemon had pokerus, transfer to new pokemon
newPokemon.pokerus = previousPokemon.pokerus;
// If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock) // If the previous pokemon had higher IVs, override to those (after updating dex IVs > prevents perfect 31s on a new unlock)
newPokemon.ivs = newPokemon.ivs.map((iv, index) => { newPokemon.ivs = newPokemon.ivs.map((iv, index) => {
return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv; return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv;
@ -349,22 +356,46 @@ async function doNewTeamPostProcess(scene: BattleScene, transformations: Pokemon
scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1); scene.gameData.addStarterCandy(getPokemonSpecies(speciesRootForm), 1);
} }
// Set the moveset of the new pokemon to be the same as previous, but with 1 egg move of the new species // Set the moveset of the new pokemon to be the same as previous, but with 1 egg move and 1 (attempted) STAB move of the new species
newPokemon.generateAndPopulateMoveset();
// Try to find a favored STAB move
let favoredMove;
for (const move of newPokemon.moveset) {
// Needs to match first type, second type will be replaced
if (move?.getMove().type === newPokemon.getTypes()[0]) {
favoredMove = move;
break;
}
}
// If was unable to find a move, uses first move in moveset (typically a high power STAB move)
favoredMove = favoredMove ?? newPokemon.moveset[0];
newPokemon.moveset = previousPokemon.moveset; newPokemon.moveset = previousPokemon.moveset;
let eggMoveIndex: null | number = null;
if (speciesEggMoves.hasOwnProperty(speciesRootForm)) { if (speciesEggMoves.hasOwnProperty(speciesRootForm)) {
const eggMoves = speciesEggMoves[speciesRootForm]; const eggMoves = speciesEggMoves[speciesRootForm];
const eggMoveIndex = randSeedInt(4); const randomEggMoveIndex = randSeedInt(4);
const randomEggMove = eggMoves[eggMoveIndex]; const randomEggMove = eggMoves[randomEggMoveIndex];
if (newPokemon.moveset.length < 4) { if (newPokemon.moveset.length < 4) {
newPokemon.moveset.push(new PokemonMove(randomEggMove)); newPokemon.moveset.push(new PokemonMove(randomEggMove));
} else { } else {
newPokemon.moveset[randSeedInt(4)] = new PokemonMove(randomEggMove); eggMoveIndex = randSeedInt(4);
newPokemon.moveset[eggMoveIndex] = new PokemonMove(randomEggMove);
} }
// For pokemon that the player owns (including ones just caught), unlock the egg move // For pokemon that the player owns (including ones just caught), unlock the egg move
if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) { if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) {
await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), eggMoveIndex, true); await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), randomEggMoveIndex, true);
} }
} }
if (favoredMove) {
let favoredMoveIndex = randSeedInt(4);
while (favoredMoveIndex === eggMoveIndex) {
favoredMoveIndex = randSeedInt(4);
}
newPokemon.moveset[favoredMoveIndex] = favoredMove;
}
// Randomize the second type of the pokemon // Randomize the second type of the pokemon
// If the pokemon does not normally have a second type, it will gain 1 // If the pokemon does not normally have a second type, it will gain 1

View File

@ -259,23 +259,23 @@ export class WeatherRequirement extends EncounterSceneRequirement {
export class PartySizeRequirement extends EncounterSceneRequirement { export class PartySizeRequirement extends EncounterSceneRequirement {
partySizeRange: [number, number]; partySizeRange: [number, number];
excludeFainted: boolean; excludeDisallowedPokemon: boolean;
/** /**
* Used for specifying a party size requirement * Used for specifying a party size requirement
* If min and max are equivalent, will check for exact size * If min and max are equivalent, will check for exact size
* @param partySizeRange * @param partySizeRange
* @param excludeFainted * @param excludeDisallowedPokemon
*/ */
constructor(partySizeRange: [number, number], excludeFainted: boolean) { constructor(partySizeRange: [number, number], excludeDisallowedPokemon: boolean) {
super(); super();
this.partySizeRange = partySizeRange; this.partySizeRange = partySizeRange;
this.excludeFainted = excludeFainted; this.excludeDisallowedPokemon = excludeDisallowedPokemon;
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) { if (!isNullOrUndefined(this.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) {
const partySize = this.excludeFainted ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length; const partySize = this.excludeDisallowedPokemon ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length;
if (partySize >= 0 && (this.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) { if (partySize >= 0 && (this.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) {
return false; return false;
} }
@ -767,12 +767,14 @@ export class HeldItemRequirement extends EncounterPokemonRequirement {
requiredHeldItemModifiers: string[]; requiredHeldItemModifiers: string[];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; invertQuery: boolean;
requireTransferable: boolean;
constructor(heldItem: string | string[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { constructor(heldItem: string | string[], minNumberOfPokemon: number = 1, invertQuery: boolean = false, requireTransferable: boolean = true) {
super(); super();
this.minNumberOfPokemon = minNumberOfPokemon; this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery; this.invertQuery = invertQuery;
this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem]; this.requiredHeldItemModifiers = Array.isArray(heldItem) ? heldItem : [heldItem];
this.requireTransferable = requireTransferable;
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
@ -787,21 +789,23 @@ export class HeldItemRequirement extends EncounterPokemonRequirement {
if (!this.invertQuery) { if (!this.invertQuery) {
return partyPokemon.filter((pokemon) => this.requiredHeldItemModifiers.some((heldItem) => { return partyPokemon.filter((pokemon) => this.requiredHeldItemModifiers.some((heldItem) => {
return pokemon.getHeldItems().some((it) => { return pokemon.getHeldItems().some((it) => {
return it.constructor.name === heldItem; return it.constructor.name === heldItem && (!this.requireTransferable || it.isTransferable);
}); });
})); }));
} else { } else {
// for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers
// E.g. functions as a blacklist // E.g. functions as a blacklist
return partyPokemon.filter((pokemon) => pokemon.getHeldItems().filter((it) => { return partyPokemon.filter((pokemon) => pokemon.getHeldItems().filter((it) => {
return !this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem); return !this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem)
&& (!this.requireTransferable || it.isTransferable);
}).length > 0); }).length > 0);
} }
} }
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
const requiredItems = pokemon?.getHeldItems().filter((it) => { const requiredItems = pokemon?.getHeldItems().filter((it) => {
return this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem); return this.requiredHeldItemModifiers.some(heldItem => it.constructor.name === heldItem)
&& (!this.requireTransferable || it.isTransferable);
}); });
if (requiredItems && requiredItems.length > 0) { if (requiredItems && requiredItems.length > 0) {
return ["heldItem", requiredItems[0].type.name]; return ["heldItem", requiredItems[0].type.name];
@ -814,12 +818,14 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe
requiredHeldItemTypes: Type[]; requiredHeldItemTypes: Type[];
minNumberOfPokemon: number; minNumberOfPokemon: number;
invertQuery: boolean; invertQuery: boolean;
requireTransferable: boolean;
constructor(heldItemTypes: Type | Type[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { constructor(heldItemTypes: Type | Type[], minNumberOfPokemon: number = 1, invertQuery: boolean = false, requireTransferable: boolean = true) {
super(); super();
this.minNumberOfPokemon = minNumberOfPokemon; this.minNumberOfPokemon = minNumberOfPokemon;
this.invertQuery = invertQuery; this.invertQuery = invertQuery;
this.requiredHeldItemTypes = Array.isArray(heldItemTypes) ? heldItemTypes : [heldItemTypes]; this.requiredHeldItemTypes = Array.isArray(heldItemTypes) ? heldItemTypes : [heldItemTypes];
this.requireTransferable = requireTransferable;
} }
override meetsRequirement(scene: BattleScene): boolean { override meetsRequirement(scene: BattleScene): boolean {
@ -834,21 +840,29 @@ export class AttackTypeBoosterHeldItemTypeRequirement extends EncounterPokemonRe
if (!this.invertQuery) { if (!this.invertQuery) {
return partyPokemon.filter((pokemon) => this.requiredHeldItemTypes.some((heldItemType) => { return partyPokemon.filter((pokemon) => this.requiredHeldItemTypes.some((heldItemType) => {
return pokemon.getHeldItems().some((it) => { return pokemon.getHeldItems().some((it) => {
return it instanceof AttackTypeBoosterModifier && (it.type as AttackTypeBoosterModifierType).moveType === heldItemType; return it instanceof AttackTypeBoosterModifier
&& (it.type as AttackTypeBoosterModifierType).moveType === heldItemType
&& (!this.requireTransferable || it.isTransferable);
}); });
})); }));
} else { } else {
// for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers // for an inverted query, we only want to get the pokemon that have any held items that are NOT in requiredHeldItemModifiers
// E.g. functions as a blacklist // E.g. functions as a blacklist
return partyPokemon.filter((pokemon) => pokemon.getHeldItems().filter((it) => { return partyPokemon.filter((pokemon) => pokemon.getHeldItems().filter((it) => {
return !this.requiredHeldItemTypes.some(heldItemType => it instanceof AttackTypeBoosterModifier && (it.type as AttackTypeBoosterModifierType).moveType === heldItemType); return !this.requiredHeldItemTypes.some(heldItemType =>
it instanceof AttackTypeBoosterModifier
&& (it.type as AttackTypeBoosterModifierType).moveType === heldItemType
&& (!this.requireTransferable || it.isTransferable));
}).length > 0); }).length > 0);
} }
} }
override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { override getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] {
const requiredItems = pokemon?.getHeldItems().filter((it) => { const requiredItems = pokemon?.getHeldItems().filter((it) => {
return this.requiredHeldItemTypes.some(heldItemType => it instanceof AttackTypeBoosterModifier && (it.type as AttackTypeBoosterModifierType).moveType === heldItemType); return this.requiredHeldItemTypes.some(heldItemType =>
it instanceof AttackTypeBoosterModifier
&& (it.type as AttackTypeBoosterModifierType).moveType === heldItemType)
&& (!this.requireTransferable || it.isTransferable);
}); });
if (requiredItems && requiredItems.length > 0) { if (requiredItems && requiredItems.length > 0) {
return ["heldItem", requiredItems[0].type.name]; return ["heldItem", requiredItems[0].type.name];

View File

@ -15,6 +15,7 @@ import { MysteryEncounterMode } from "#enums/mystery-encounter-mode";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { GameModes } from "#app/game-mode"; import { GameModes } from "#app/game-mode";
import { EncounterAnim } from "#enums/encounter-anims"; import { EncounterAnim } from "#enums/encounter-anims";
import { Challenges } from "#enums/challenges";
export interface EncounterStartOfBattleEffect { export interface EncounterStartOfBattleEffect {
sourcePokemon?: Pokemon; sourcePokemon?: Pokemon;
@ -40,7 +41,8 @@ export interface IMysteryEncounter {
spriteConfigs: MysteryEncounterSpriteConfig[]; spriteConfigs: MysteryEncounterSpriteConfig[];
encounterTier: MysteryEncounterTier; encounterTier: MysteryEncounterTier;
encounterAnimations?: EncounterAnim[]; encounterAnimations?: EncounterAnim[];
disabledGameModes?: GameModes[]; disallowedGameModes?: GameModes[];
disallowedChallenges?: Challenges[];
hideBattleIntroMessage: boolean; hideBattleIntroMessage: boolean;
autoHideIntroVisuals: boolean; autoHideIntroVisuals: boolean;
enterIntroVisualsFromRight: boolean; enterIntroVisualsFromRight: boolean;
@ -93,7 +95,11 @@ export default class MysteryEncounter implements IMysteryEncounter {
/** /**
* If specified, defines any game modes where the {@linkcode MysteryEncounter} should *NOT* spawn * If specified, defines any game modes where the {@linkcode MysteryEncounter} should *NOT* spawn
*/ */
disabledGameModes?: GameModes[]; disallowedGameModes?: GameModes[];
/**
* If specified, defines any challenges (from Challenge game mode) where the {@linkcode MysteryEncounter} should *NOT* spawn
*/
disallowedChallenges?: Challenges[];
/** /**
* If true, hides "A Wild X Appeared" etc. messages * If true, hides "A Wild X Appeared" etc. messages
* Default true * Default true
@ -161,6 +167,11 @@ export default class MysteryEncounter implements IMysteryEncounter {
doEncounterRewards?: (scene: BattleScene) => boolean; doEncounterRewards?: (scene: BattleScene) => boolean;
/** Will execute callback during VictoryPhase of a continuousEncounter */ /** Will execute callback during VictoryPhase of a continuousEncounter */
doContinueEncounter?: (scene: BattleScene) => Promise<void>; doContinueEncounter?: (scene: BattleScene) => Promise<void>;
/**
* Can perform special logic when a ME battle is lost, before GameOver/battle retry prompt.
* Should return `true` if it is treated as "real" Game Over, `false` if not.
*/
onGameOver?: (scene: BattleScene) => boolean;
/** /**
* Requirements * Requirements
@ -656,11 +667,21 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
/** /**
* Defines any game modes where the Mystery Encounter should *NOT* spawn * Defines any game modes where the Mystery Encounter should *NOT* spawn
* @returns * @returns
* @param disabledGameModes * @param disallowedGameModes
*/ */
withDisabledGameModes(...disabledGameModes: GameModes[]): this & Required<Pick<IMysteryEncounter, "disabledGameModes">> { withDisallowedGameModes(...disallowedGameModes: GameModes[]): this & Required<Pick<IMysteryEncounter, "disallowedGameModes">> {
const gameModes = Array.isArray(disabledGameModes) ? disabledGameModes : [disabledGameModes]; const gameModes = Array.isArray(disallowedGameModes) ? disallowedGameModes : [disallowedGameModes];
return Object.assign(this, { disabledGameModes: gameModes }); return Object.assign(this, { disallowedGameModes: gameModes });
}
/**
* Defines any challenges (from Challenge game mode) where the Mystery Encounter should *NOT* spawn
* @returns
* @param disallowedChallenges
*/
withDisallowedChallenges(...disallowedChallenges: Challenges[]): this & Required<Pick<IMysteryEncounter, "disallowedChallenges">> {
const challenges = Array.isArray(disallowedChallenges) ? disallowedChallenges : [disallowedChallenges];
return Object.assign(this, { disallowedChallenges: challenges });
} }
/** /**
@ -742,11 +763,11 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* *
* @param min min wave (or exact size if only min is given) * @param min min wave (or exact size if only min is given)
* @param max optional max size. If not given, defaults to min => exact wave * @param max optional max size. If not given, defaults to min => exact wave
* @param excludeFainted if true, only counts unfainted mons * @param excludeDisallowedPokemon if true, only counts allowed (legal in Challenge/unfainted) mons
* @returns * @returns
*/ */
withScenePartySizeRequirement(min: number, max?: number, excludeFainted: boolean = false): this & Required<Pick<IMysteryEncounter, "requirements">> { withScenePartySizeRequirement(min: number, max?: number, excludeDisallowedPokemon: boolean = false): this & Required<Pick<IMysteryEncounter, "requirements">> {
return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min], excludeFainted)); return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min], excludeDisallowedPokemon));
} }
/** /**

View File

@ -43,7 +43,7 @@ import { Variant } from "#app/data/variant";
* @param scene * @param scene
*/ */
export function doTrainerExclamation(scene: BattleScene) { export function doTrainerExclamation(scene: BattleScene) {
const exclamationSprite = scene.add.sprite(0, 0, "exclaim"); const exclamationSprite = scene.add.sprite(0, 0, "encounter_exclaim");
exclamationSprite.setName("exclamation"); exclamationSprite.setName("exclamation");
scene.field.add(exclamationSprite); scene.field.add(exclamationSprite);
scene.field.moveTo(exclamationSprite, scene.field.getAll().length - 1); scene.field.moveTo(exclamationSprite, scene.field.getAll().length - 1);
@ -744,6 +744,37 @@ export function handleMysteryEncounterVictory(scene: BattleScene, addHealPhase:
} }
} }
/**
* Similar to {@linkcode handleMysteryEncounterVictory}, but for cases where the player lost a battle or failed a challenge
* @param scene
* @param addHealPhase
*/
export function handleMysteryEncounterBattleFailed(scene: BattleScene, addHealPhase: boolean = false, doNotContinue: boolean = false) {
const allowedPkm = scene.getParty().filter((pkm) => pkm.isAllowedInBattle());
if (allowedPkm.length === 0) {
scene.clearPhaseQueue();
scene.unshiftPhase(new GameOverPhase(scene));
return;
}
// If in repeated encounter variant, do nothing
// Variant must eventually be swapped in order to handle "true" end of the encounter
const encounter = scene.currentBattle.mysteryEncounter!;
if (encounter.continuousEncounter || doNotContinue) {
return;
} else if (encounter.encounterMode !== MysteryEncounterMode.NO_BATTLE) {
scene.pushPhase(new BattleEndPhase(scene, false));
}
scene.pushPhase(new MysteryEncounterRewardsPhase(scene, addHealPhase));
if (!encounter.doContinueEncounter) {
// Only lapse eggs once for multi-battle encounters
scene.pushPhase(new EggLapsePhase(scene));
}
}
/** /**
* *
* @param scene * @param scene

View File

@ -13,7 +13,7 @@ import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { Type } from "#app/data/type"; import { Type } from "#app/data/type";
import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getPokemonNameWithAffix } from "#app/messages"; import { getPokemonNameWithAffix } from "#app/messages";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Gender } from "#app/data/gender"; import { Gender } from "#app/data/gender";
@ -50,28 +50,39 @@ export function getSpriteKeysFromPokemon(pokemon: Pokemon): { spriteKey: string,
} }
/** /**
*
* Will never remove the player's last non-fainted Pokemon (if they only have 1) * Will never remove the player's last non-fainted Pokemon (if they only have 1)
* Otherwise, picks a Pokemon completely at random and removes from the party * Otherwise, picks a Pokemon completely at random and removes from the party
* @param scene * @param scene
* @param isAllowedInBattle Default false. If true, only picks from unfainted mons. If there is only 1 unfainted mon left and doNotReturnLastAbleMon is also true, will return fainted mon * @param isAllowed Default false. If true, only picks from legal mons. If no legal mons are found (or there is 1, with `doNotReturnLastAllowedMon = true), will return a mon that is not allowed.
* @param doNotReturnLastAbleMon Default false. If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted) * @param isFainted Default false. If true, includes fainted mons.
* @param doNotReturnLastAllowedMon Default false. If true, will never return the last unfainted pokemon in the party. Useful when this function is being used to determine what Pokemon to remove from the party (Don't want to remove last unfainted)
* @returns * @returns
*/ */
export function getRandomPlayerPokemon(scene: BattleScene, isAllowedInBattle: boolean = false, doNotReturnLastAbleMon: boolean = false): PlayerPokemon { export function getRandomPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false, doNotReturnLastAllowedMon: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getParty();
let chosenIndex: number; let chosenIndex: number;
let chosenPokemon: PlayerPokemon; let chosenPokemon: PlayerPokemon | null = null;
const unfaintedMons = party.filter(p => p.isAllowedInBattle()); const fullyLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && (isFainted || !p.isFainted()));
const faintedMons = party.filter(p => !p.isAllowedInBattle()); const allowedOnlyMons = party.filter(p => p.isAllowed());
if (doNotReturnLastAbleMon && unfaintedMons.length === 1) { if (doNotReturnLastAllowedMon && fullyLegalMons.length === 1) {
chosenIndex = randSeedInt(faintedMons.length); // If there is only 1 legal/unfainted mon left, select from fainted legal mons
chosenPokemon = faintedMons[chosenIndex]; const faintedLegalMons = party.filter(p => (!isAllowed || p.isAllowed()) && p.isFainted());
} else if (isAllowedInBattle) { if (faintedLegalMons.length > 0) {
chosenIndex = randSeedInt(unfaintedMons.length); chosenIndex = randSeedInt(faintedLegalMons.length);
chosenPokemon = unfaintedMons[chosenIndex]; chosenPokemon = faintedLegalMons[chosenIndex];
} else { }
}
if (!chosenPokemon && fullyLegalMons.length > 0) {
chosenIndex = randSeedInt(fullyLegalMons.length);
chosenPokemon = fullyLegalMons[chosenIndex];
}
if (!chosenPokemon && isAllowed && allowedOnlyMons.length > 0) {
chosenIndex = randSeedInt(allowedOnlyMons.length);
chosenPokemon = allowedOnlyMons[chosenIndex];
}
if (!chosenPokemon) {
// If no other options worked, returns fully random
chosenIndex = randSeedInt(party.length); chosenIndex = randSeedInt(party.length);
chosenPokemon = party[chosenIndex]; chosenPokemon = party[chosenIndex];
} }
@ -82,15 +93,19 @@ export function getRandomPlayerPokemon(scene: BattleScene, isAllowedInBattle: bo
/** /**
* Ties are broken by whatever mon is closer to the front of the party * Ties are broken by whatever mon is closer to the front of the party
* @param scene * @param scene
* @param unfainted Default false. If true, only picks from unfainted mons. * @param isAllowed Default false. If true, only picks from legal mons.
* @param isFainted Default false. If true, includes fainted mons.
* @returns * @returns
*/ */
export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { export function getHighestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getParty();
let pokemon: PlayerPokemon | null = null; let pokemon: PlayerPokemon | null = null;
for (const p of party) { for (const p of party) {
if (unfainted && p.isFainted()) { if (isAllowed && !p.isAllowed()) {
continue;
}
if (!isFainted && p.isFainted()) {
continue; continue;
} }
@ -104,15 +119,19 @@ export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: bool
* Ties are broken by whatever mon is closer to the front of the party * Ties are broken by whatever mon is closer to the front of the party
* @param scene * @param scene
* @param stat Stat to search for * @param stat Stat to search for
* @param unfainted Default false. If true, only picks from unfainted mons. * @param isAllowed Default false. If true, only picks from legal mons.
* @param isFainted Default false. If true, includes fainted mons.
* @returns * @returns
*/ */
export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, unfainted: boolean = false): PlayerPokemon { export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentStat, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getParty();
let pokemon: PlayerPokemon | null = null; let pokemon: PlayerPokemon | null = null;
for (const p of party) { for (const p of party) {
if (unfainted && p.isFainted()) { if (isAllowed && !p.isAllowed()) {
continue;
}
if (!isFainted && p.isFainted()) {
continue; continue;
} }
@ -125,15 +144,19 @@ export function getHighestStatPlayerPokemon(scene: BattleScene, stat: PermanentS
/** /**
* Ties are broken by whatever mon is closer to the front of the party * Ties are broken by whatever mon is closer to the front of the party
* @param scene * @param scene
* @param unfainted - default false. If true, only picks from unfainted mons. * @param isAllowed Default false. If true, only picks from legal mons.
* @param isFainted Default false. If true, includes fainted mons.
* @returns * @returns
*/ */
export function getLowestLevelPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { export function getLowestLevelPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getParty();
let pokemon: PlayerPokemon | null = null; let pokemon: PlayerPokemon | null = null;
for (const p of party) { for (const p of party) {
if (unfainted && p.isFainted()) { if (isAllowed && !p.isAllowed()) {
continue;
}
if (!isFainted && p.isFainted()) {
continue; continue;
} }
@ -146,15 +169,19 @@ export function getLowestLevelPlayerPokemon(scene: BattleScene, unfainted: boole
/** /**
* Ties are broken by whatever mon is closer to the front of the party * Ties are broken by whatever mon is closer to the front of the party
* @param scene * @param scene
* @param unfainted - default false. If true, only picks from unfainted mons. * @param isAllowed Default false. If true, only picks from legal mons.
* @param isFainted Default false. If true, includes fainted mons.
* @returns * @returns
*/ */
export function getHighestStatTotalPlayerPokemon(scene: BattleScene, unfainted: boolean = false): PlayerPokemon { export function getHighestStatTotalPlayerPokemon(scene: BattleScene, isAllowed: boolean = false, isFainted: boolean = false): PlayerPokemon {
const party = scene.getParty(); const party = scene.getParty();
let pokemon: PlayerPokemon | null = null; let pokemon: PlayerPokemon | null = null;
for (const p of party) { for (const p of party) {
if (unfainted && p.isFainted()) { if (isAllowed && !p.isAllowed()) {
continue;
}
if (!isFainted && p.isFainted()) {
continue; continue;
} }
@ -170,15 +197,24 @@ export function getHighestStatTotalPlayerPokemon(scene: BattleScene, unfainted:
* @param starterTiers * @param starterTiers
* @param excludedSpecies * @param excludedSpecies
* @param types * @param types
* @param allowSubLegendary
* @param allowLegendary
* @param allowMythical
* @returns * @returns
*/ */
export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[]): Species { export function getRandomSpeciesByStarterTier(starterTiers: number | [number, number], excludedSpecies?: Species[], types?: Type[], allowSubLegendary: boolean = true, allowLegendary: boolean = true, allowMythical: boolean = true): Species {
let min = Array.isArray(starterTiers) ? starterTiers[0] : starterTiers; let min = Array.isArray(starterTiers) ? starterTiers[0] : starterTiers;
let max = Array.isArray(starterTiers) ? starterTiers[1] : starterTiers; let max = Array.isArray(starterTiers) ? starterTiers[1] : starterTiers;
let filteredSpecies: [PokemonSpecies, number][] = Object.keys(speciesStarters) let filteredSpecies: [PokemonSpecies, number][] = Object.keys(speciesStarters)
.map(s => [parseInt(s) as Species, speciesStarters[s] as number]) .map(s => [parseInt(s) as Species, speciesStarters[s] as number])
.filter(s => getPokemonSpecies(s[0]) && (!excludedSpecies || !excludedSpecies.includes(s[0]))) .filter(s => {
const pokemonSpecies = getPokemonSpecies(s[0]);
return pokemonSpecies && (!excludedSpecies || !excludedSpecies.includes(s[0])
&& (allowSubLegendary || !pokemonSpecies.subLegendary)
&& (allowLegendary || !pokemonSpecies.legendary)
&& (allowMythical || !pokemonSpecies.mythical));
})
.map(s => [getPokemonSpecies(s[0]), s[1]]); .map(s => [getPokemonSpecies(s[0]), s[1]]);
if (types && types.length > 0) { if (types && types.length > 0) {
@ -773,3 +809,23 @@ export async function addPokemonDataToDexAndValidateAchievements(scene: BattleSc
scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
return scene.gameData.setPokemonCaught(pokemon, true, false, false); return scene.gameData.setPokemonCaught(pokemon, true, false, false);
} }
/**
* Checks if a Pokemon is allowed under a challenge, and allowed in battle.
* If both are true, returns `null`.
* If one of them is not true, returns message content that the Pokemon is invalid.
* Typically used for cheecking whether a Pokemon can be selected for a {@linkcode MysteryEncounterOption}
* @param pokemon
* @param scene
* @param invalidSelectionKey
*/
export function isPokemonValidForEncounterOptionSelection(pokemon: Pokemon, scene: BattleScene, invalidSelectionKey: string): string | null {
if (!pokemon.isAllowed()) {
return i18next.t("partyUiHandler:cantBeUsed", { pokemonName: pokemon.getNameToRender() }) ?? null;
}
if (!pokemon.isAllowedInBattle()) {
return getEncounterText(scene, invalidSelectionKey) ?? null;
}
return null;
}

View File

@ -109,6 +109,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public fusionVariant: Variant; public fusionVariant: Variant;
public fusionGender: Gender; public fusionGender: Gender;
public fusionLuck: integer; public fusionLuck: integer;
public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData | null;
private summonDataPrimer: PokemonSummonData | null; private summonDataPrimer: PokemonSummonData | null;
@ -206,6 +207,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = dataSource.fusionVariant || 0; this.fusionVariant = dataSource.fusionVariant || 0;
this.fusionGender = dataSource.fusionGender; this.fusionGender = dataSource.fusionGender;
this.fusionLuck = dataSource.fusionLuck; this.fusionLuck = dataSource.fusionLuck;
this.fusionMysteryEncounterPokemonData = dataSource.fusionMysteryEncounterPokemonData;
this.usedTMs = dataSource.usedTMs ?? []; this.usedTMs = dataSource.usedTMs ?? [];
this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(dataSource.mysteryEncounterPokemonData); this.mysteryEncounterPokemonData = new MysteryEncounterPokemonData(dataSource.mysteryEncounterPokemonData);
} else { } else {
@ -343,7 +345,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
isAllowed(): boolean { isAllowed(): boolean {
const challengeAllowed = new Utils.BooleanHolder(true); const challengeAllowed = new Utils.BooleanHolder(true);
applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed); applyChallenges(this.scene.gameMode, ChallengeType.POKEMON_IN_BATTLE, this, challengeAllowed);
return !this.isFainted() && challengeAllowed.value; return challengeAllowed.value;
} }
isActive(onField?: boolean): boolean { isActive(onField?: boolean): boolean {
@ -1164,11 +1166,31 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
} }
if (!types.length || !includeTeraType) { if (!types.length || !includeTeraType) {
if (this.mysteryEncounterPokemonData.types && this.mysteryEncounterPokemonData.types.length > 0) { if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) {
// "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters
this.mysteryEncounterPokemonData.types.forEach(t => types.push(t));
} else if (!ignoreOverride && this.summonData?.types && this.summonData.types.length > 0) {
this.summonData.types.forEach(t => types.push(t)); this.summonData.types.forEach(t => types.push(t));
} else if (this.mysteryEncounterPokemonData.types && this.mysteryEncounterPokemonData.types.length > 0) {
// "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters
types.push(this.mysteryEncounterPokemonData.types[0]);
// Fusing a Pokemon onto something with "permanently changed" types will still apply the fusion's types as normal
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride);
if (fusionSpeciesForm) {
// Check if the fusion Pokemon also had "permanently changed" types
const fusionMETypes = this.fusionMysteryEncounterPokemonData?.types;
if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) {
types.push(fusionMETypes[1]);
} else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) {
types.push(fusionMETypes[0]);
} else if (fusionSpeciesForm.type2 !== null && fusionSpeciesForm.type2 !== types[0]) {
types.push(fusionSpeciesForm.type2);
} else if (fusionSpeciesForm.type1 !== types[0]) {
types.push(fusionSpeciesForm.type1);
}
}
if (types.length === 1 && this.mysteryEncounterPokemonData.types.length >= 2) {
types.push(this.mysteryEncounterPokemonData.types[1]);
}
} else { } else {
const speciesForm = this.getSpeciesForm(ignoreOverride); const speciesForm = this.getSpeciesForm(ignoreOverride);
@ -1176,7 +1198,14 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride); const fusionSpeciesForm = this.getFusionSpeciesForm(ignoreOverride);
if (fusionSpeciesForm) { if (fusionSpeciesForm) {
if (fusionSpeciesForm.type2 !== null && fusionSpeciesForm.type2 !== speciesForm.type1) { // Check if the fusion Pokemon also had "permanently changed" types
// Otherwise, use standard fusion type logic
const fusionMETypes = this.fusionMysteryEncounterPokemonData?.types;
if (fusionMETypes && fusionMETypes.length >= 2 && fusionMETypes[1] !== types[0]) {
types.push(fusionMETypes[1]);
} else if (fusionMETypes && fusionMETypes.length === 1 && fusionMETypes[0] !== types[0]) {
types.push(fusionMETypes[0]);
} else if (fusionSpeciesForm.type2 !== null && fusionSpeciesForm.type2 !== speciesForm.type1) {
types.push(fusionSpeciesForm.type2); types.push(fusionSpeciesForm.type2);
} else if (fusionSpeciesForm.type1 !== speciesForm.type1) { } else if (fusionSpeciesForm.type1 !== speciesForm.type1) {
types.push(fusionSpeciesForm.type1); types.push(fusionSpeciesForm.type1);
@ -1228,12 +1257,16 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) { if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) {
return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; return allAbilities[Overrides.OPP_ABILITY_OVERRIDE];
} }
if (this.isFusion()) {
if (!isNullOrUndefined(this.fusionMysteryEncounterPokemonData?.ability) && this.fusionMysteryEncounterPokemonData!.ability !== -1) {
return allAbilities[this.fusionMysteryEncounterPokemonData!.ability];
} else {
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
}
}
if (!isNullOrUndefined(this.mysteryEncounterPokemonData.ability) && this.mysteryEncounterPokemonData.ability !== -1) { if (!isNullOrUndefined(this.mysteryEncounterPokemonData.ability) && this.mysteryEncounterPokemonData.ability !== -1) {
return allAbilities[this.mysteryEncounterPokemonData.ability]; return allAbilities[this.mysteryEncounterPokemonData.ability];
} }
if (this.isFusion()) {
return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)];
}
let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex); let abilityId = this.getSpeciesForm(ignoreOverride).getAbility(this.abilityIndex);
if (abilityId === Abilities.NONE) { if (abilityId === Abilities.NONE) {
abilityId = this.species.ability1; abilityId = this.species.ability1;
@ -1927,6 +1960,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
this.fusionVariant = 0; this.fusionVariant = 0;
this.fusionGender = 0; this.fusionGender = 0;
this.fusionLuck = 0; this.fusionLuck = 0;
this.fusionMysteryEncounterPokemonData = null;
this.generateName(); this.generateName();
this.calculateStats(); this.calculateStats();
@ -4231,6 +4265,7 @@ export class PlayerPokemon extends Pokemon {
this.fusionVariant = pokemon.variant; this.fusionVariant = pokemon.variant;
this.fusionGender = pokemon.gender; this.fusionGender = pokemon.gender;
this.fusionLuck = pokemon.luck; this.fusionLuck = pokemon.luck;
this.fusionMysteryEncounterPokemonData = pokemon.mysteryEncounterPokemonData;
if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) { if ((pokemon.pauseEvolutions) || (this.pauseEvolutions)) {
this.pauseEvolutions = true; this.pauseEvolutions = true;
} }

View File

@ -23,6 +23,7 @@
"selected": "Let's do this!" "selected": "Let's do this!"
}, },
"outro": "Look how happy your {{chosenPokemon}} is now!$Here, you can have these as well.", "outro": "Look how happy your {{chosenPokemon}} is now!$Here, you can have these as well.",
"outro_failed": "How disappointing...$It looks like you still have a long way\nto go to earn your Pokémon's trust!",
"gained_eggs": "@s{item_fanfare}You received {{numEggs}}!", "gained_eggs": "@s{item_fanfare}You received {{numEggs}}!",
"eggs_tooltip": "\n(+) Earn {{eggs}}", "eggs_tooltip": "\n(+) Earn {{eggs}}",
"numEggs_one": "{{count}} {{rarity}} Egg", "numEggs_one": "{{count}} {{rarity}} Egg",

View File

@ -888,7 +888,7 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
} }
override matchType(modifier: Modifier): boolean { override matchType(modifier: Modifier): boolean {
return modifier instanceof PokemonBaseStatTotalModifier; return modifier instanceof PokemonBaseStatTotalModifier && this.statModifier === modifier.statModifier;
} }
override clone(): PersistentModifier { override clone(): PersistentModifier {
@ -939,7 +939,7 @@ export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier {
} }
override matchType(modifier: Modifier): boolean { override matchType(modifier: Modifier): boolean {
return modifier instanceof PokemonBaseStatFlatModifier; return modifier instanceof PokemonBaseStatFlatModifier && modifier.statModifier === this.statModifier && this.stats.every(s => modifier.stats.some(stat => s === stat));
} }
override clone(): PersistentModifier { override clone(): PersistentModifier {

View File

@ -2,19 +2,31 @@ import { applyPostBattleAbAttrs, PostBattleAbAttr } from "#app/data/ability";
import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier"; import { LapsingPersistentModifier, LapsingPokemonHeldItemModifier } from "#app/modifier/modifier";
import { BattlePhase } from "./battle-phase"; import { BattlePhase } from "./battle-phase";
import { GameOverPhase } from "./game-over-phase"; import { GameOverPhase } from "./game-over-phase";
import BattleScene from "#app/battle-scene";
export class BattleEndPhase extends BattlePhase { export class BattleEndPhase extends BattlePhase {
/** If true, will increment battles won */
isVictory: boolean;
constructor(scene: BattleScene, isVictory: boolean = true) {
super(scene);
this.isVictory = isVictory;
}
start() { start() {
super.start(); super.start();
this.scene.currentBattle.addBattleScore(this.scene); if (this.isVictory) {
this.scene.currentBattle.addBattleScore(this.scene);
this.scene.gameData.gameStats.battles++; this.scene.gameData.gameStats.battles++;
if (this.scene.currentBattle.trainer) { if (this.scene.currentBattle.trainer) {
this.scene.gameData.gameStats.trainersDefeated++; this.scene.gameData.gameStats.trainersDefeated++;
} }
if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) { if (this.scene.gameMode.isEndless && this.scene.currentBattle.waveIndex + 1 > this.scene.gameData.gameStats.highestEndlessWave) {
this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1; this.scene.gameData.gameStats.highestEndlessWave = this.scene.currentBattle.waveIndex + 1;
}
} }
// Endless graceful end // Endless graceful end

View File

@ -164,7 +164,7 @@ export class EncounterPhase extends BattlePhase {
// Load Mystery Encounter Exclamation bubble and sfx // Load Mystery Encounter Exclamation bubble and sfx
loadEnemyAssets.push(new Promise<void>(resolve => { loadEnemyAssets.push(new Promise<void>(resolve => {
this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav"); this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav");
this.scene.loadImage("exclaim", "mystery-encounters"); this.scene.loadImage("encounter_exclaim", "mystery-encounters");
this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve()); this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve());
if (!this.scene.load.isLoading()) { if (!this.scene.load.isLoading()) {
this.scene.load.start(); this.scene.load.start();

View File

@ -48,6 +48,14 @@ export class GameOverPhase extends BattlePhase {
this.victory = true; this.victory = true;
} }
// Handle Mystery Encounter special Game Over cases
// Situations such as when player lost a battle, but it isn't treated as full Game Over
if (!this.victory && this.scene.currentBattle.mysteryEncounter?.onGameOver && !this.scene.currentBattle.mysteryEncounter.onGameOver(this.scene)) {
// Do not end the game
return this.end();
}
// Otherwise, continue standard Game Over logic
if (this.victory && this.scene.gameMode.isEndless) { if (this.victory && this.scene.gameMode.isEndless) {
const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET; const genderIndex = this.scene.gameData.gender ?? PlayerGender.UNSET;
const genderStr = PlayerGender[genderIndex].toLowerCase(); const genderStr = PlayerGender[genderIndex].toLowerCase();
@ -60,11 +68,6 @@ export class GameOverPhase extends BattlePhase {
this.scene.ui.fadeOut(1250).then(() => { this.scene.ui.fadeOut(1250).then(() => {
this.scene.reset(); this.scene.reset();
this.scene.clearPhaseQueue(); this.scene.clearPhaseQueue();
// If this is a ME, clear any residual visual sprites before reloading
const encounter = this.scene.currentBattle.mysteryEncounter;
if (encounter?.introVisuals) {
this.scene.field.remove(encounter.introVisuals, true);
}
this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => { this.scene.gameData.loadSession(this.scene, this.scene.sessionSlotId).then(() => {
this.scene.pushPhase(new EncounterPhase(this.scene, true)); this.scene.pushPhase(new EncounterPhase(this.scene, true));

View File

@ -54,6 +54,7 @@ export default class PokemonData {
public fusionVariant: Variant; public fusionVariant: Variant;
public fusionGender: Gender; public fusionGender: Gender;
public fusionLuck: integer; public fusionLuck: integer;
public fusionMysteryEncounterPokemonData: MysteryEncounterPokemonData;
public boss: boolean; public boss: boolean;
public bossSegments?: integer; public bossSegments?: integer;

View File

@ -175,7 +175,16 @@ describe("Teleporting Hijinks - Mystery Encounter", () => {
expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType); expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType);
}); });
it("should start a battle against an enraged boss", { retry: 5 }, async () => { it("should start a battle against an enraged boss below wave 50", { retry: 5 }, async () => {
await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty);
await runMysteryEncounterToEnd(game, 1, undefined, true);
const enemyField = scene.getEnemyField();
expect(enemyField[0].summonData.statStages).toEqual([0, 1, 0, 1, 1, 0, 0]);
expect(enemyField[0].isBoss()).toBe(true);
});
it("should start a battle against an extra enraged boss above wave 50", { retry: 5 }, async () => {
game.override.startingWave(56);
await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty);
await runMysteryEncounterToEnd(game, 1, undefined, true); await runMysteryEncounterToEnd(game, 1, undefined, true);
const enemyField = scene.getEnemyField(); const enemyField = scene.getEnemyField();
@ -238,10 +247,19 @@ describe("Teleporting Hijinks - Mystery Encounter", () => {
expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType); expect(TRANSPORT_BIOMES).toContain(scene.arena.biomeType);
}); });
it("should start a battle against an enraged boss", async () => { it("should start a battle against an enraged boss below wave 50", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.PIKACHU]); await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, [Species.PIKACHU]);
await runMysteryEncounterToEnd(game, 2, undefined, true); await runMysteryEncounterToEnd(game, 2, undefined, true);
const enemyField = scene.getEnemyField(); const enemyField = scene.getEnemyField();
expect(enemyField[0].summonData.statStages).toEqual([0, 1, 0, 1, 1, 0, 0]);
expect(enemyField[0].isBoss()).toBe(true);
});
it("should start a battle against an extra enraged boss above wave 50", { retry: 5 }, async () => {
game.override.startingWave(56);
await game.runToMysteryEncounter(MysteryEncounterType.TELEPORTING_HIJINKS, defaultParty);
await runMysteryEncounterToEnd(game, 1, undefined, true);
const enemyField = scene.getEnemyField();
expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]); expect(enemyField[0].summonData.statStages).toEqual([1, 1, 1, 1, 1, 0, 0]);
expect(enemyField[0].isBoss()).toBe(true); expect(enemyField[0].isBoss()).toBe(true);
}); });

View File

@ -174,7 +174,7 @@ describe("Weird Dream - Mystery Encounter", () => {
}); });
}); });
it("should reduce party levels by 20%", async () => { it("should reduce party levels by 12.5%", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty); await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
@ -184,7 +184,7 @@ describe("Weird Dream - Mystery Encounter", () => {
const levelsAfter = scene.getParty().map(p => p.level); const levelsAfter = scene.getParty().map(p => p.level);
for (let i = 0; i < levelsPrior.length; i++) { for (let i = 0; i < levelsPrior.length; i++) {
expect(Math.max(Math.ceil(0.8 * levelsPrior[i]), 1)).toBe(levelsAfter[i]); expect(Math.max(Math.ceil(0.8875 * levelsPrior[i]), 1)).toBe(levelsAfter[i]);
expect(scene.getParty()[i].levelExp).toBe(0); expect(scene.getParty()[i].levelExp).toBe(0);
} }

View File

@ -67,7 +67,7 @@ describe("Mystery Encounter Utils", () => {
expect(result.species.speciesId).toBe(Species.ARCEUS); expect(result.species.speciesId).toBe(Species.ARCEUS);
}); });
it("gets an unfainted pokemon from player party if isAllowedInBattle is true", () => { it("gets an unfainted legal pokemon from player party if isAllowed is true and isFainted is false", () => {
// Only faint 1st pokemon // Only faint 1st pokemon
const party = scene.getParty(); const party = scene.getParty();
party[0].hp = 0; party[0].hp = 0;
@ -115,12 +115,12 @@ describe("Mystery Encounter Utils", () => {
// Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal) // Seeds are calculated to return index 0 first, 1 second (if both pokemon are legal)
game.override.seed("random"); game.override.seed("random");
let result = getRandomPlayerPokemon(scene, true, true); let result = getRandomPlayerPokemon(scene, true, false, true);
expect(result.species.speciesId).toBe(Species.ARCEUS); expect(result.species.speciesId).toBe(Species.ARCEUS);
game.override.seed("random2"); game.override.seed("random2");
result = getRandomPlayerPokemon(scene, true, true); result = getRandomPlayerPokemon(scene, true, false, true);
expect(result.species.speciesId).toBe(Species.ARCEUS); expect(result.species.speciesId).toBe(Species.ARCEUS);
}); });
}); });
@ -301,41 +301,5 @@ describe("Mystery Encounter Utils", () => {
expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", "valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0); expect(spy).toHaveBeenCalledWith("valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", "valuevalue {{testvalue}} {{test1}} {{test}} {{test\\}} {{test\\}} {test}}", null, expect.any(Function), 0);
}); });
}); });
describe("initBattleWithEnemyConfig", () => {
it("", () => {
});
});
describe("setCustomEncounterRewards", () => {
it("", () => {
});
});
describe("selectPokemonForOption", () => {
it("", () => {
});
});
describe("setEncounterExp", () => {
it("", () => {
});
});
describe("leaveEncounterWithoutBattle", () => {
it("", () => {
});
});
describe("handleMysteryEncounterVictory", () => {
it("", () => {
});
});
}); });

View File

@ -169,12 +169,14 @@ export class UiInputs {
} }
switch (this.scene.ui?.getMode()) { switch (this.scene.ui?.getMode()) {
case Mode.MESSAGE: case Mode.MESSAGE:
if (!(this.scene.ui.getHandler() as MessageUiHandler).pendingPrompt) { const messageHandler = this.scene.ui.getHandler<MessageUiHandler>();
if (!messageHandler.pendingPrompt || messageHandler.isTextAnimationInProgress()) {
return; return;
} }
case Mode.TITLE: case Mode.TITLE:
case Mode.COMMAND: case Mode.COMMAND:
case Mode.MODIFIER_SELECT: case Mode.MODIFIER_SELECT:
case Mode.MYSTERY_ENCOUNTER:
this.scene.ui.setOverlayMode(Mode.MENU); this.scene.ui.setOverlayMode(Mode.MENU);
break; break;
case Mode.STARTER_SELECT: case Mode.STARTER_SELECT:

View File

@ -223,6 +223,14 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler {
}; };
} }
isTextAnimationInProgress() {
if (this.textTimer) {
return this.textTimer.repeatCount < this.textTimer.repeat;
}
return false;
}
clearText() { clearText() {
this.message.setText(""); this.message.setText("");
this.pendingPrompt = false; this.pendingPrompt = false;

View File

@ -701,6 +701,7 @@ export default class SummaryUiHandler extends UiHandler {
const profileContainer = this.scene.add.container(0, -pageBg.height); const profileContainer = this.scene.add.container(0, -pageBg.height);
pageContainer.add(profileContainer); pageContainer.add(profileContainer);
// TODO: should add field for original trainer name to Pokemon object, to support gift/traded Pokemon from MEs
const trainerText = addBBCodeTextObject(this.scene, 7, 12, `${i18next.t("pokemonSummary:ot")}/${getBBCodeFrag(loggedInUser?.username || i18next.t("pokemonSummary:unknown"), this.scene.gameData.gender === PlayerGender.FEMALE ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE)}`, TextStyle.SUMMARY_ALT); const trainerText = addBBCodeTextObject(this.scene, 7, 12, `${i18next.t("pokemonSummary:ot")}/${getBBCodeFrag(loggedInUser?.username || i18next.t("pokemonSummary:unknown"), this.scene.gameData.gender === PlayerGender.FEMALE ? TextStyle.SUMMARY_PINK : TextStyle.SUMMARY_BLUE)}`, TextStyle.SUMMARY_ALT);
trainerText.setOrigin(0, 0); trainerText.setOrigin(0, 0);
profileContainer.add(trainerText); profileContainer.add(trainerText);