ME balance changes and bug fixes

This commit is contained in:
ImperialSympathizer 2024-09-21 15:51:14 -04:00
parent 64aee2ffd7
commit 3e751a477c
32 changed files with 365 additions and 174 deletions

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

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

@ -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

@ -349,10 +349,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 +423,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.isFusion()) {
if (!pokemon.fusionMysteryEncounterPokemonData) {
pokemon.fusionMysteryEncounterPokemonData = new MysteryEncounterPokemonData();
}
pokemon.fusionMysteryEncounterPokemonData.ability = encounter.misc.ability;
} else {
if (!pokemon.mysteryEncounterPokemonData) { if (!pokemon.mysteryEncounterPokemonData) {
pokemon.mysteryEncounterPokemonData = new 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";
@ -271,6 +272,9 @@ export const DancingLessonsEncounter: MysteryEncounter =
// Only Pokemon that have a Dancing move can be selected // Only 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.isAllowed()) {
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

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

@ -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";
@ -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

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

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

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(),
@ -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

@ -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, 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";
@ -30,9 +27,9 @@ 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;
@ -193,6 +190,12 @@ export const TheExpertPokemonBreederEncounter: MysteryEncounter =
pokemon3RareEggs pokemon3RareEggs
}; };
encounter.dialogue.outro = [
{
text: `${namespace}.outro`,
},
];
return true; return true;
}) })
.withTitle(`${namespace}.title`) .withTitle(`${namespace}.title`)
@ -241,14 +244,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()
) )
@ -295,14 +295,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()
) )
@ -349,14 +346,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.pokemon3.addFriendship(FRIENDSHIP_ADDED);
await restorePartyAndHeldItems(scene);
}) })
.build() .build()
) )
@ -387,19 +381,6 @@ function getPartyConfig(scene: BattleScene): EnemyPartyConfig {
nature: Nature.ADAMANT, nature: Nature.ADAMANT,
moveSet: [Moves.METEOR_MASH, Moves.FIRE_PUNCH, Moves.ICE_PUNCH, Moves.THUNDER_PUNCH], moveSet: [Moves.METEOR_MASH, Moves.FIRE_PUNCH, Moves.ICE_PUNCH, Moves.THUNDER_PUNCH],
ivs: [31, 31, 31, 31, 31, 31], ivs: [31, 31, 31, 31, 31, 31],
modifierConfigs: [
{
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 +528,39 @@ 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 = [
{
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;
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";
@ -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

@ -82,6 +82,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
@ -207,7 +210,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 +342,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 +355,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

@ -161,6 +161,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
@ -742,11 +747,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

@ -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));
}
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";
@ -170,15 +170,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 +782,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 {
@ -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();
@ -4207,6 +4241,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_dialogue": "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

@ -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

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