Add Weird Dream encounter and slight reworks to Berries Abound/Fight or Flight

This commit is contained in:
ImperialSympathizer 2024-08-15 12:47:50 -04:00
parent 741137d74b
commit 6fe16ff0ba
30 changed files with 5252 additions and 4005 deletions

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 240 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.7 KiB

After

Width:  |  Height:  |  Size: 1.7 KiB

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "girawitch.png",
"format": "RGBA8888",
"size": {
"w": 46,
"h": 76
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 46,
"h": 76
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 46,
"h": 76
},
"frame": {
"x": 0,
"y": 0,
"w": 46,
"h": 76
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:e68bbc186f511d505c53b2beec3c3741:7108795fc29d953a1d3729ad93d70936:1661aeeeb2f0e4561c644aff254770b3$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1021 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

View File

@ -6,8 +6,7 @@ import {
leaveEncounterWithoutBattle, setEncounterExp,
setEncounterRewards
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import Pokemon, { EnemyPokemon } from "#app/field/pokemon";
import Pokemon, { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import {
BerryModifierType,
getPartyLuckValue,
@ -21,17 +20,17 @@ import { BattlerTagType } from "#enums/battler-tag-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MoveRequirement } from "../mystery-encounter-requirements";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getPokemonNameWithAffix } from "#app/messages";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { TrainerSlot } from "#app/data/trainer-config";
import { applyModifierTypeToPlayerPokemon, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { applyModifierTypeToPlayerPokemon, getHighestStatPlayerPokemon, getSpriteKeysFromPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import PokemonData from "#app/system/pokemon-data";
import { BerryModifier } from "#app/modifier/modifier";
import i18next from "#app/plugins/i18n";
import { BerryType } from "#enums/berry-type";
import { Stat } from "#enums/stat";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:berriesAbound";
@ -148,6 +147,7 @@ export const BerriesAboundEncounter: IMysteryEncounter =
// Calculate boss mon
const bossSpecies = scene.arena.randomSpecies(scene.currentBattle.waveIndex, scene.currentBattle.waveIndex, 0, getPartyLuckValue(scene.getParty()), true);
const bossPokemon = new EnemyPokemon(scene, bossSpecies, scene.currentBattle.waveIndex, TrainerSlot.NONE, true, null);
encounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(bossPokemon));
const config: EnemyPartyConfig = {
levelAdditiveMultiplier: 1,
pokemonConfigs: [{
@ -159,11 +159,11 @@ export const BerriesAboundEncounter: IMysteryEncounter =
encounter.enemyPartyConfigs = [config];
// Calculate the number of extra berries that player receives
// 10-60 GREAT, 60-110 ULTRA, 110-160 ROGUE, 160-180 MASTER
// 10-40: 2, 40-120: 4, 120-160: 5, 160-180: 7
const numBerries =
scene.currentBattle.waveIndex > 160 ? 7
: scene.currentBattle.waveIndex > 110 ? 5
: scene.currentBattle.waveIndex > 60 ? 4 : 2;
: scene.currentBattle.waveIndex > 120 ? 5
: scene.currentBattle.waveIndex > 40 ? 4 : 2;
regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0);
encounter.misc = { numBerries };
@ -178,15 +178,11 @@ export const BerriesAboundEncounter: IMysteryEncounter =
isPokemon: true
});
// If player has a stealing move, they succeed automatically
encounter.options[1].meetsRequirements(scene);
const primaryPokemon = encounter.options[1].primaryPokemon;
if (primaryPokemon) {
// Use primaryPokemon to execute the thievery
encounter.options[1].dialogue.buttonTooltip = `${namespace}.option.2.tooltip_special`;
} else {
encounter.options[1].dialogue.buttonTooltip = `${namespace}.option.2.tooltip`;
}
// Get fastest party pokemon for option 2
const fastestPokemon = getHighestStatPlayerPokemon(scene, Stat.SPD, true);
encounter.misc.fastestPokemon = fastestPokemon;
encounter.misc.enemySpeed = bossPokemon.getStat(Stat.SPD);
encounter.setDialogueToken("fastestPokemon", fastestPokemon.getNameToRender());
return true;
})
@ -232,63 +228,68 @@ export const BerriesAboundEncounter: IMysteryEncounter =
)
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
.withDialogue({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
buttonTooltip: `${namespace}.option.2.tooltip`
})
.withOptionPhase(async (scene: BattleScene) => {
// Pick steal
// Pick race for berries
const encounter = scene.currentBattle.mysteryEncounter;
const fastestPokemon = encounter.misc.fastestPokemon;
const enemySpeed = encounter.misc.enemySpeed;
const speedDiff = fastestPokemon.getStat(Stat.SPD) / enemySpeed;
const numBerries = encounter.misc.numBerries;
const doBerryRewards = async () => {
const berryText = numBerries + " " + i18next.t(`${namespace}.berries`);
scene.playSound("item_fanfare");
queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText }));
// Generate a random berry and give it to the first Pokemon with room for it
for (let i = 0; i < numBerries; i++) {
await tryGiveBerry(scene);
}
};
const shopOptions: ModifierTypeOption[] = [];
for (let i = 0; i < 5; i++) {
// Generate shop berries
shopOptions.push(generateModifierTypeOption(scene, modifierTypes.BERRY));
}
setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, null, doBerryRewards);
if (speedDiff < 1) {
// Caught and attacked by boss, gets +1 to all stats at start of fight
const doBerryRewards = async () => {
const berryText = numBerries + " " + i18next.t(`${namespace}.berries`);
// If player has a stealing move, they succeed automatically
const primaryPokemon = encounter.options[1].primaryPokemon;
if (primaryPokemon) {
// Use primaryPokemon to execute the thievery
await showEncounterText(scene, `${namespace}.option.2.special_result`);
setEncounterExp(scene, primaryPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs[0].species.baseExp, true);
leaveEncounterWithoutBattle(scene);
return;
}
scene.playSound("item_fanfare");
queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText }));
// Generate a random berry and give it to the first Pokemon with room for it
for (let i = 0; i < numBerries; i++) {
await tryGiveBerry(scene);
}
};
const roll = randSeedInt(16);
if (roll > 6) {
// Noticed and attacked by boss, gets +1 to all stats at start of fight (62.5%)
const config = scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0];
config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => {
pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(pokemon));
queueEncounterMessage(pokemon.scene, `${namespace}option.2.boss_enraged`);
queueEncounterMessage(pokemon.scene, `${namespace}.option.2.boss_enraged`);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1));
};
await showEncounterText(scene, `${namespace}.option.2.bad_result`);
setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, null, doBerryRewards);
await showEncounterText(scene, `${namespace}.option.2.selected_bad`);
await initBattleWithEnemyConfig(scene, config);
return;
} else {
// Steal item (37.5%)
// Display result message then proceed to rewards
await showEncounterText(scene, `${namespace}.option.2.good_result`);
// Gains 1 berry for every 10% faster the player's pokemon is than the enemy, up to a max of numBerries, minimum of 1
const numBerriesGrabbed = Math.max(Math.min(Math.round((speedDiff - 1)/0.1), numBerries), 1);
encounter.setDialogueToken("numBerries", String(numBerriesGrabbed));
const doFasterBerryRewards = async () => {
const berryText = numBerriesGrabbed + " " + i18next.t(`${namespace}.berries`);
scene.playSound("item_fanfare");
queueEncounterMessage(scene, i18next.t("battle:rewardGain", { modifierName: berryText }));
// Generate a random berry and give it to the first Pokemon with room for it (trying to give to fastest first)
for (let i = 0; i < numBerriesGrabbed; i++) {
await tryGiveBerry(scene, fastestPokemon);
}
};
setEncounterExp(scene, fastestPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs[0].species.baseExp);
setEncounterRewards(scene, { guaranteedModifierTypeOptions: shopOptions, fillRemaining: false }, null, doFasterBerryRewards);
await showEncounterText(scene, `${namespace}.option.2.selected`);
leaveEncounterWithoutBattle(scene);
}
})
@ -312,12 +313,23 @@ export const BerriesAboundEncounter: IMysteryEncounter =
)
.build();
async function tryGiveBerry(scene: BattleScene) {
async function tryGiveBerry(scene: BattleScene, prioritizedPokemon?: PlayerPokemon) {
const berryType = randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType;
const berry = generateModifierTypeOption(scene, modifierTypes.BERRY, [berryType]).type as BerryModifierType;
const party = scene.getParty();
// Will try to apply to prioritized pokemon first, then do normal application method if it fails
if (prioritizedPokemon) {
const heldBerriesOfType = scene.findModifier(m => m instanceof BerryModifier
&& m.pokemonId === prioritizedPokemon.id && (m as BerryModifier).berryType === berryType, true) as BerryModifier;
if (!heldBerriesOfType || heldBerriesOfType.getStackCount() < heldBerriesOfType.getMaxStackCount(scene)) {
await applyModifierTypeToPlayerPokemon(scene, prioritizedPokemon, berry);
return;
}
}
// Iterate over the party until berry was successfully given
for (const pokemon of party) {
const heldBerriesOfType = scene.findModifier(m => m instanceof BerryModifier

View File

@ -1,4 +1,3 @@
import { BattleStat } from "#app/data/battle-stat";
import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import {
EnemyPartyConfig,
@ -7,7 +6,7 @@ import {
setEncounterRewards
} from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { STEALING_MOVES } from "#app/data/mystery-encounters/requirements/requirement-groups";
import Pokemon, { EnemyPokemon } from "#app/field/pokemon";
import { EnemyPokemon } from "#app/field/pokemon";
import { ModifierTier } from "#app/modifier/modifier-tier";
import {
getPartyLuckValue,
@ -16,15 +15,10 @@ import {
ModifierTypeOption,
regenerateModifierPoolThresholds,
} from "#app/modifier/modifier-type";
import { StatChangePhase } from "#app/phases";
import { randSeedInt } from "#app/utils";
import { BattlerTagType } from "#enums/battler-tag-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "#app/battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MoveRequirement } from "../mystery-encounter-requirements";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getPokemonNameWithAffix } from "#app/messages";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { TrainerSlot } from "#app/data/trainer-config";
@ -68,17 +62,21 @@ export const FightOrFlightEncounter: IMysteryEncounter =
encounter.enemyPartyConfigs = [config];
// Calculate item
// 10-60 GREAT, 60-110 ULTRA, 110-160 ROGUE, 160-180 MASTER
// 10-40 GREAT, 60-120 ULTRA, 120-160 ROGUE, 160-180 MASTER
const tier =
scene.currentBattle.waveIndex > 160
? ModifierTier.MASTER
: scene.currentBattle.waveIndex > 110
: scene.currentBattle.waveIndex > 120
? ModifierTier.ROGUE
: scene.currentBattle.waveIndex > 60
: scene.currentBattle.waveIndex > 40
? ModifierTier.ULTRA
: ModifierTier.GREAT;
regenerateModifierPoolThresholds(scene.getParty(), ModifierPoolType.PLAYER, 0);
const item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [tier] })[0];
let item: ModifierTypeOption;
// TMs excluded from possible rewards as they're too swingy in value for a singular item reward
while (!item || item.type.id.includes("TM_")) {
item = getPlayerModifierTypeOptions(1, scene.getParty(), [], { guaranteedModifierTiers: [tier], allowLuckUpgrades: false })[0];
}
encounter.setDialogueToken("itemName", item.type.name);
encounter.misc = item;
@ -105,16 +103,6 @@ export const FightOrFlightEncounter: IMysteryEncounter =
},
];
// If player has a stealing move, they succeed automatically
encounter.options[1].meetsRequirements(scene);
const primaryPokemon = encounter.options[1].primaryPokemon;
if (primaryPokemon) {
// Use primaryPokemon to execute the thievery
encounter.options[1].dialogue.buttonTooltip = `${namespace}.option.2.tooltip_special`;
} else {
encounter.options[1].dialogue.buttonTooltip = `${namespace}.option.2.tooltip`;
}
return true;
})
.withTitle(`${namespace}.title`)
@ -140,11 +128,17 @@ export const FightOrFlightEncounter: IMysteryEncounter =
)
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL)
.withOptionMode(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL)
.withPrimaryPokemonRequirement(new MoveRequirement(STEALING_MOVES)) // Will set option2PrimaryName and option2PrimaryMove dialogue tokens automatically
.withDialogue({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`,
selected: [
{
text: `${namespace}.option.2.selected`
}
]
})
.withOptionPhase(async (scene: BattleScene) => {
// Pick steal
@ -152,34 +146,10 @@ export const FightOrFlightEncounter: IMysteryEncounter =
const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption;
setEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false });
// If player has a stealing move, they succeed automatically
// Use primaryPokemon to execute the thievery
const primaryPokemon = encounter.options[1].primaryPokemon;
if (primaryPokemon) {
// Use primaryPokemon to execute the thievery
await showEncounterText(scene, `${namespace}.option.2.special_result`);
setEncounterExp(scene, primaryPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs[0].species.baseExp, true);
leaveEncounterWithoutBattle(scene);
return;
}
const roll = randSeedInt(16);
if (roll > 6) {
// Noticed and attacked by boss, gets +1 to all stats at start of fight (62.5%)
const config = scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0];
config.pokemonConfigs[0].tags = [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON];
config.pokemonConfigs[0].mysteryEncounterBattleEffects = (pokemon: Pokemon) => {
pokemon.scene.currentBattle.mysteryEncounter.setDialogueToken("enemyPokemon", getPokemonNameWithAffix(pokemon));
queueEncounterMessage(pokemon.scene, `${namespace}option.2.boss_enraged`);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.ATK, BattleStat.DEF, BattleStat.SPATK, BattleStat.SPDEF, BattleStat.SPD], 1));
};
await showEncounterText(scene, `${namespace}.option.2.bad_result`);
await initBattleWithEnemyConfig(scene, config);
} else {
// Steal item (37.5%)
// Display result message then proceed to rewards
await showEncounterText(scene, `${namespace}.option.2.good_result`);
leaveEncounterWithoutBattle(scene);
}
setEncounterExp(scene, primaryPokemon.id, encounter.enemyPartyConfigs[0].pokemonConfigs[0].species.baseExp);
leaveEncounterWithoutBattle(scene);
})
.build()
)

View File

@ -0,0 +1,541 @@
import { Type } from "#app/data/type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import BattleScene from "#app/battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter";
import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
import { leaveEncounterWithoutBattle, setEncounterRewards, } from "../utils/encounter-phase-utils";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { IntegerHolder, randSeedInt, randSeedShuffle } from "#app/utils";
import PokemonSpecies, { allSpecies, getPokemonSpecies } from "#app/data/pokemon-species";
import { HiddenAbilityRateBoosterModifier, PokemonBaseStatTotalModifier, PokemonFormChangeItemModifier, PokemonHeldItemModifier } from "#app/modifier/modifier";
import { achvs } from "#app/system/achv";
import { speciesEggMoves } from "#app/data/egg-moves";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { modifierTypes } from "#app/modifier/modifier-type";
import { Stat } from "#app/data/pokemon-stat";
import i18next from "#app/plugins/i18n";
import { doPokemonTransformationSequence, TransformationScreenPosition } from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
import { getLevelTotalExp } from "#app/data/exp";
/** i18n namespace for encounter */
const namespace = "mysteryEncounter:weirdDream";
/** Exclude Ultra Beasts (inludes Cosmog/Solgaleo/Lunala/Necrozma), Paradox (includes Miraidon/Koraidon), Eternatus, Urshifu, the Poison Chain trio, Ogerpon */
const excludedPokemon = [
Species.ETERNATUS,
/** UBs */
Species.NIHILEGO,
Species.BUZZWOLE,
Species.PHEROMOSA,
Species.XURKITREE,
Species.CELESTEELA,
Species.KARTANA,
Species.GUZZLORD,
Species.POIPOLE,
Species.NAGANADEL,
Species.STAKATAKA,
Species.BLACEPHALON,
/** Paradox */
Species.GREAT_TUSK,
Species.SCREAM_TAIL,
Species.BRUTE_BONNET,
Species.FLUTTER_MANE,
Species.SLITHER_WING,
Species.SANDY_SHOCKS,
Species.ROARING_MOON,
Species.WALKING_WAKE,
Species.GOUGING_FIRE,
Species.RAGING_BOLT,
Species.IRON_TREADS,
Species.IRON_BUNDLE,
Species.IRON_HANDS,
Species.IRON_JUGULIS,
Species.IRON_MOTH,
Species.IRON_THORNS,
Species.IRON_VALIANT,
Species.IRON_LEAVES,
Species.IRON_BOULDER,
Species.IRON_CROWN,
/** These are banned so they don't appear in the < 570 BST pool */
Species.URSHIFU,
Species.CUBCHOO,
Species.OKIDOGI,
Species.MUNKIDORI,
Species.FEZANDIPITI,
Species.OGERPON,
Species.CALYREX,
Species.TYPE_NULL,
Species.TERAPAGOS,
Species.COSMOG,
Species.COSMOEM,
];
/**
* Weird Dream encounter.
* @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/137 | GitHub Issue #137}
* @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/
export const WeirdDreamEncounter: IMysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.WEIRD_DREAM)
.withEncounterTier(MysteryEncounterTier.ROGUE)
.withIntroSpriteConfigs([
{
spriteKey: "girawitch",
fileRoot: "mystery-encounters",
hasShadow: false,
y: 4
},
])
.withIntroDialogue([
{
text: `${namespace}.intro`,
},
{
speaker: `${namespace}.speaker`,
text: `${namespace}.intro_dialogue`,
},
])
.withSceneWaveRangeRequirement(10, 180)
.withTitle(`${namespace}.title`)
.withDescription(`${namespace}.description`)
.withQuery(`${namespace}.query`)
.withOnInit((scene: BattleScene) => {
scene.loadBgm("mystery_encounter_weird_dream", "mystery_encounter_weird_dream.mp3");
return true;
})
.withOnVisualsStart((scene: BattleScene) => {
// Change the bgm
scene.fadeOutBgm(3000, false);
scene.time.delayedCall(3000, () => {
scene.playBgm("mystery_encounter_weird_dream");
});
return true;
})
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(MysteryEncounterOptionMode.DEFAULT)
.withHasDexProgress(true)
.withDialogue({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
{
text: `${namespace}.option.1.selected`,
}
],
})
.withPreOptionPhase(async (scene: BattleScene) => {
// Play the animation as the player goes through the dialogue
scene.time.delayedCall(1000, () => {
doShowDreamBackground(scene);
});
// Calculate all the newly transformed Pokemon and begin asset load
const teamTransformations = getTeamTransformations(scene);
const loadAssets = teamTransformations.map(t => (t.newPokemon as PlayerPokemon).loadAssets());
scene.currentBattle.mysteryEncounter.misc = {
teamTransformations,
loadAssets
};
})
.withOptionPhase(async (scene: BattleScene) => {
// Starts cutscene dialogue, but does not await so that cutscene plays as player goes through dialogue
const cutsceneDialoguePromise = showEncounterText(scene, `${namespace}.option.1.cutscene`);
// Change the entire player's party
// Wait for all new Pokemon assets to be loaded before showing transformation animations
await Promise.all(scene.currentBattle.mysteryEncounter.misc.loadAssets);
const transformations = scene.currentBattle.mysteryEncounter.misc.teamTransformations;
// If there are 1-3 transformations, do them centered back to back
// Otherwise, the first 3 transformations are executed side-by-side, then any remaining 1-3 transformations occur in those same respective positions
if (transformations.length <= 3) {
for (const transformation of transformations) {
const pokemon1 = transformation.previousPokemon;
const pokemon2 = transformation.newPokemon;
await doPokemonTransformationSequence(scene, pokemon1, pokemon2, TransformationScreenPosition.CENTER);
}
} else {
await doSideBySideTransformations(scene, transformations);
}
// Make sure player has finished cutscene dialogue
await cutsceneDialoguePromise;
doHideDreamBackground(scene);
await showEncounterText(scene, `${namespace}.option.1.dream_complete`);
await doNewTeamPostProcess(scene, transformations);
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.MEMORY_MUSHROOM, modifierTypes.ROGUE_BALL, modifierTypes.MINT, modifierTypes.MINT]});
leaveEncounterWithoutBattle(scene, true);
})
.build()
)
.withSimpleOption(
{
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
selected: [
{
text: `${namespace}.option.2.selected`,
},
],
},
async (scene: BattleScene) => {
// Reduce party levels by 20%
for (const pokemon of scene.getParty()) {
pokemon.level = Math.max(Math.ceil(0.8 * pokemon.level), 1);
pokemon.exp = getLevelTotalExp(pokemon.level, pokemon.species.growthRate);
pokemon.levelExp = 0;
pokemon.calculateStats();
pokemon.updateInfo();
}
leaveEncounterWithoutBattle(scene, true);
return true;
}
)
.withOutroDialogue([
{
text: `${namespace}.outro`
}
])
.build();
interface PokemonTransformation {
previousPokemon: PlayerPokemon;
newSpecies: PokemonSpecies;
newPokemon: PlayerPokemon;
heldItems: PokemonHeldItemModifier[];
}
function getTeamTransformations(scene: BattleScene): PokemonTransformation[] {
const party = scene.getParty();
// Removes all pokemon from the party
const alreadyUsedSpecies: PokemonSpecies[] = [];
const pokemonTransformations: PokemonTransformation[] = party.map(p => {
return {
previousPokemon: p
} as PokemonTransformation;
});
// Only 1 Pokemon can be transformed into BST higher than 600
let hasPokemonBstHigherThan600 = false;
// Only 1 other Pokemon can be transformed into BST between 570-600
let hasPokemonBstBetween570And600 = false;
// First, roll 2 of the party members to new Pokemon at a +90 to +110 BST difference
// Then, roll the remainder of the party members at a +40 to +50 BST difference
const numPokemon = party.length;
for (let i = 0; i < numPokemon; i++) {
const removed = party[randSeedInt(party.length)];
const index = pokemonTransformations.findIndex(p => p.previousPokemon.id === removed.id);
pokemonTransformations[index].heldItems = removed.getHeldItems().filter(m => !(m instanceof PokemonFormChangeItemModifier));
scene.removePokemonFromPlayerParty(removed, false);
const bst = getOriginalBst(scene, removed);
let newBstRange;
if (i < 2) {
newBstRange = [90, 110];
} else {
newBstRange = [40, 50];
}
const newSpecies = getTransformedSpecies(bst, newBstRange, hasPokemonBstHigherThan600, hasPokemonBstBetween570And600, alreadyUsedSpecies);
const newSpeciesBst = newSpecies.getBaseStatTotal();
if (newSpeciesBst > 600) {
hasPokemonBstHigherThan600 = true;
}
if (newSpeciesBst <= 600 && newSpeciesBst >= 570) {
hasPokemonBstBetween570And600 = true;
}
pokemonTransformations[index].newSpecies = newSpecies;
alreadyUsedSpecies.push(newSpecies);
}
for (const transformation of pokemonTransformations) {
const newAbilityIndex = randSeedInt(transformation.newSpecies.getAbilityCount());
const newPlayerPokemon = scene.addPlayerPokemon(transformation.newSpecies, transformation.previousPokemon.level, newAbilityIndex, undefined);
transformation.newPokemon = newPlayerPokemon;
scene.getParty().push(newPlayerPokemon);
}
return pokemonTransformations;
}
async function doNewTeamPostProcess(scene: BattleScene, transformations: PokemonTransformation[]) {
let atLeastOneNewStarter = false;
for (const transformation of transformations) {
const previousPokemon = transformation.previousPokemon;
const newPokemon = transformation.newPokemon;
const speciesRootForm = newPokemon.species.getRootSpeciesId();
// Roll HA a second time
if (newPokemon.species.abilityHidden) {
const hiddenIndex = newPokemon.species.ability2 ? 2 : 1;
if (newPokemon.abilityIndex < hiddenIndex) {
const hiddenAbilityChance = new IntegerHolder(256);
scene.applyModifiers(HiddenAbilityRateBoosterModifier, true, hiddenAbilityChance);
const hasHiddenAbility = !randSeedInt(hiddenAbilityChance.value);
if (hasHiddenAbility) {
newPokemon.abilityIndex = hiddenIndex;
}
}
}
// Roll IVs a second time
newPokemon.ivs = newPokemon.ivs.map(iv => {
const newValue = randSeedInt(31);
return newValue > iv ? newValue : iv;
});
// For pokemon at/below 570 BST or any shiny pokemon, unlock it permanently as if you had caught it
if (newPokemon.getSpeciesForm().getBaseStatTotal() <= 570 || newPokemon.isShiny()) {
if (newPokemon.getSpeciesForm().abilityHidden && newPokemon.abilityIndex === newPokemon.getSpeciesForm().getAbilityCount() - 1) {
scene.validateAchv(achvs.HIDDEN_ABILITY);
}
if (newPokemon.species.subLegendary) {
scene.validateAchv(achvs.CATCH_SUB_LEGENDARY);
}
if (newPokemon.species.legendary) {
scene.validateAchv(achvs.CATCH_LEGENDARY);
}
if (newPokemon.species.mythical) {
scene.validateAchv(achvs.CATCH_MYTHICAL);
}
scene.gameData.updateSpeciesDexIvs(newPokemon.species.getRootSpeciesId(true), newPokemon.ivs);
const newStarterUnlocked = await scene.gameData.setPokemonCaught(newPokemon, true, false, false);
if (newStarterUnlocked) {
atLeastOneNewStarter = true;
queueEncounterMessage(scene, i18next.t("battle:addedAsAStarter", { pokemonName: getPokemonSpecies(speciesRootForm).getName() }));
}
}
// 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) => {
return previousPokemon.ivs[index] > iv ? previousPokemon.ivs[index] : iv;
});
// For pokemon that the player owns (including ones just caught), gain a candy
if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) {
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
newPokemon.moveset = previousPokemon.moveset;
if (speciesEggMoves.hasOwnProperty(speciesRootForm)) {
const eggMoves = speciesEggMoves[speciesRootForm];
const eggMoveIndex = randSeedInt(4);
const randomEggMove = eggMoves[eggMoveIndex];
if (newPokemon.moveset.length < 4) {
newPokemon.moveset.push(new PokemonMove(randomEggMove));
} else {
newPokemon.moveset[randSeedInt(4)] = new PokemonMove(randomEggMove);
}
// For pokemon that the player owns (including ones just caught), unlock the egg move
if (!!scene.gameData.dexData[speciesRootForm].caughtAttr) {
await scene.gameData.setEggMoveUnlocked(getPokemonSpecies(speciesRootForm), eggMoveIndex, true);
}
}
// Randomize the second type of the pokemon
// If the pokemon does not normally have a second type, it will gain 1
const newTypes = [newPokemon.getTypes()[0]];
let newType = randSeedInt(18) as Type;
while (newType === newTypes[0]) {
newType = randSeedInt(18) as Type;
}
newTypes.push(newType);
if (!newPokemon.mysteryEncounterData) {
newPokemon.mysteryEncounterData = new MysteryEncounterPokemonData(null, null, null, newTypes);
} else {
newPokemon.mysteryEncounterData.types = newTypes;
}
for (const item of transformation.heldItems) {
item.pokemonId = newPokemon.id;
scene.addModifier(item, false, false, false, true);
}
// Any pokemon that is at or below 450 BST gets +20 permanent BST to 3 stats: HP, lowest of Atk/SpAtk, and lowest of Def/SpDef
if (newPokemon.getSpeciesForm().getBaseStatTotal() <= 600) {
const stats: Stat[] = [Stat.HP];
const baseStats = newPokemon.getSpeciesForm().baseStats.slice(0);
// Attack or SpAtk
stats.push(baseStats[Stat.ATK] < baseStats[Stat.SPATK] ? Stat.ATK : Stat.SPATK);
// Def or SpDef
stats.push(baseStats[Stat.DEF] < baseStats[Stat.SPDEF] ? Stat.DEF : Stat.SPDEF);
// const mod = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU().newModifier(newPokemon, 20, stats);
const modType = modifierTypes.MYSTERY_ENCOUNTER_OLD_GATEAU().generateType(null, [20, stats]);
const modifier = modType.newModifier(newPokemon);
scene.addModifier(modifier);
}
// Enable passive if previous had it
newPokemon.passive = previousPokemon.passive;
newPokemon.calculateStats();
newPokemon.initBattleInfo();
}
// One random pokemon will get its passive unlocked
const passiveDisabledPokemon = scene.getParty().filter(p => !p.passive);
if (passiveDisabledPokemon?.length > 0) {
passiveDisabledPokemon[randSeedInt(passiveDisabledPokemon.length)].passive = true;
}
// If at least one new starter was unlocked, play 1 fanfare
if (atLeastOneNewStarter) {
scene.playSound("level_up_fanfare");
}
}
function getOriginalBst(scene: BattleScene, pokemon: Pokemon) {
const baseStats = pokemon.getSpeciesForm().baseStats.slice(0);
scene.applyModifiers(PokemonBaseStatTotalModifier, true, pokemon, baseStats);
if (pokemon.fusionSpecies) {
const fusionBaseStats = pokemon.getFusionSpeciesForm().baseStats;
for (let s = 0; s < pokemon.stats.length; s++) {
baseStats[s] = Math.ceil((baseStats[s] + fusionBaseStats[s]) / 2);
}
} else if (scene.gameMode.isSplicedOnly) {
for (let s = 0; s < pokemon.stats.length; s++) {
baseStats[s] = Math.ceil(baseStats[s] / 2);
}
}
return baseStats.reduce((a, b) => a + b, 0);
}
function getTransformedSpecies(originalBst: number, bstSearchRange: [number, number], hasPokemonBstHigherThan600: boolean, hasPokemonBstBetween570And600: boolean, alreadyUsedSpecies: PokemonSpecies[]): PokemonSpecies {
let newSpecies: PokemonSpecies;
while (!newSpecies) {
const bstCap = originalBst + bstSearchRange[1];
const bstMin = Math.max(originalBst + bstSearchRange[0], 0);
// Get any/all species that fall within the Bst range requirements
let validSpecies = allSpecies
.filter(s => {
const speciesBst = s.getBaseStatTotal();
const bstInRange = speciesBst >= bstMin && speciesBst <= bstCap;
// Checks that a Pokemon has not already been added in the +600 or 570-600 slots;
const validBst = (!hasPokemonBstBetween570And600 || (speciesBst < 570 || speciesBst > 600)) &&
(!hasPokemonBstHigherThan600 || speciesBst <= 600);
return bstInRange && validBst && !excludedPokemon.includes(s.speciesId);
});
// There must be at least 20 species available before it will choose one
if (validSpecies?.length > 20) {
validSpecies = randSeedShuffle(validSpecies);
newSpecies = validSpecies.pop();
while (alreadyUsedSpecies.includes(newSpecies)) {
newSpecies = validSpecies.pop();
}
} else {
// Expands search rand until a Pokemon is found
bstSearchRange[0] -= 10;
bstSearchRange[1] += 10;
}
}
return newSpecies;
}
function doShowDreamBackground(scene: BattleScene) {
const transformationContainer = scene.add.container(0, -scene.game.canvas.height / 6);
transformationContainer.name = "Dream Background";
// In case it takes a bit for video to load
const transformationStaticBg = scene.add.rectangle(0, 0, scene.game.canvas.width / 6, scene.game.canvas.height / 6, 0);
transformationStaticBg.setName("Black Background");
transformationStaticBg.setOrigin(0, 0);
transformationContainer.add(transformationStaticBg);
transformationStaticBg.setVisible(true);
const transformationVideoBg: Phaser.GameObjects.Video = scene.add.video(0, 0, "evo_bg").stop();
transformationVideoBg.setLoop(true);
transformationVideoBg.setOrigin(0, 0);
transformationVideoBg.setScale(0.4359673025);
transformationContainer.add(transformationVideoBg);
scene.fieldUI.add(transformationContainer);
scene.fieldUI.bringToTop(transformationContainer);
transformationVideoBg.play();
transformationContainer.setVisible(true);
transformationContainer.alpha = 0;
scene.tweens.add({
targets: transformationContainer,
alpha: 1,
duration: 3000,
ease: "Sine.easeInOut"
});
}
function doHideDreamBackground(scene: BattleScene) {
const transformationContainer = scene.fieldUI.getByName("Dream Background");
scene.tweens.add({
targets: transformationContainer,
alpha: 0,
duration: 3000,
ease: "Sine.easeInOut",
onComplete: () => {
scene.fieldUI.remove(transformationContainer, true);
}
});
}
function doSideBySideTransformations(scene: BattleScene, transformations: PokemonTransformation[]) {
return new Promise<void>(resolve => {
const allTransformationPromises = [];
for (let i = 0; i < 3; i++) {
const delay = i * 4000;
scene.time.delayedCall(delay, () => {
const transformation = transformations[i];
const pokemon1 = transformation.previousPokemon;
const pokemon2 = transformation.newPokemon;
const screenPosition = i as TransformationScreenPosition;
const transformationPromise = doPokemonTransformationSequence(scene, pokemon1, pokemon2, screenPosition)
.then(() => {
if (transformations.length > i + 3) {
const nextTransformationAtPosition = transformations[i + 3];
const nextPokemon1 = nextTransformationAtPosition.previousPokemon;
const nextPokemon2 = nextTransformationAtPosition.newPokemon;
allTransformationPromises.push(doPokemonTransformationSequence(scene, nextPokemon1, nextPokemon2, screenPosition));
}
});
allTransformationPromises.push(transformationPromise);
});
}
// Wait for all transformations to be loaded into promise array
const id = setInterval(checkAllPromisesExist, 500);
async function checkAllPromisesExist() {
if (allTransformationPromises.length === transformations.length) {
clearInterval(id);
await Promise.all(allTransformationPromises);
resolve();
}
}
});
}

View File

@ -24,6 +24,7 @@ import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/
import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter";
import { PartTimerEncounter } from "#app/data/mystery-encounters/encounters/part-timer-encounter";
import { DancingLessonsEncounter } from "#app/data/mystery-encounters/encounters/dancing-lessons-encounter";
import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter";
// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256
export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
@ -165,7 +166,8 @@ const anyBiomeEncounters: MysteryEncounterType[] = [
MysteryEncounterType.A_TRAINERS_TEST,
MysteryEncounterType.TRASH_TO_TREASURE,
MysteryEncounterType.BERRIES_ABOUND,
MysteryEncounterType.CLOWNING_AROUND
MysteryEncounterType.CLOWNING_AROUND,
MysteryEncounterType.WEIRD_DREAM
];
/**
@ -267,6 +269,7 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.CLOWNING_AROUND] = ClowningAroundEncounter;
allMysteryEncounters[MysteryEncounterType.PART_TIMER] = PartTimerEncounter;
allMysteryEncounters[MysteryEncounterType.DANCING_LESSONS] = DancingLessonsEncounter;
allMysteryEncounters[MysteryEncounterType.WEIRD_DREAM] = WeirdDreamEncounter;
// Add extreme encounters to biome map
extremeBiomeEncounters.forEach(encounter => {

View File

@ -19,6 +19,7 @@ import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-enco
import { getPokemonNameWithAffix } from "#app/messages";
import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
import { Gender } from "#app/data/gender";
import { Stat } from "#enums/stat";
export function getSpriteKeysFromSpecies(species: Species, female?: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): { spriteKey: string, fileRoot: string } {
const spriteKey = getPokemonSpecies(species).getSpriteKey(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0);
@ -84,6 +85,28 @@ export function getHighestLevelPlayerPokemon(scene: BattleScene, unfainted: bool
return pokemon;
}
/**
* Ties are broken by whatever mon is closer to the front of the party
* @param scene
* @param stat - stat to search for
* @param unfainted - default false. If true, only picks from unfainted mons.
* @returns
*/
export function getHighestStatPlayerPokemon(scene: BattleScene, stat: Stat, unfainted: boolean = false): PlayerPokemon {
const party = scene.getParty();
let pokemon: PlayerPokemon;
party.every(p => {
if (unfainted && p.isFainted()) {
return true;
}
pokemon = pokemon ? pokemon.getStat(stat) < p?.getStat(stat) ? p : pokemon : p;
return true;
});
return pokemon;
}
/**
* Ties are broken by whatever mon is closer to the front of the party
* @param scene

View File

@ -0,0 +1,330 @@
import BattleScene from "#app/battle-scene";
import { PlayerPokemon } from "#app/field/pokemon";
import { getFrameMs } from "#app/utils";
import { cos, sin } from "#app/field/anims";
import { getTypeRgb } from "#app/data/type";
export enum TransformationScreenPosition {
CENTER,
LEFT,
RIGHT
}
/**
* Initiates an "evolution-like" animation to transform a pokemon (presumably from the player's party) into a new one, not necessarily an evolution species.
* @param scene
* @param pokemon
* @param transformedPokemon
* @param screenPosition
*/
export function doPokemonTransformationSequence(scene: BattleScene, pokemon: PlayerPokemon, transformedPokemon: PlayerPokemon, screenPosition: TransformationScreenPosition) {
return new Promise<void>(resolve => {
const transformationContainer = scene.fieldUI.getByName("Dream Background") as Phaser.GameObjects.Container;
const transformationBaseBg = scene.add.image(0, 0, "default_bg");
transformationBaseBg.setOrigin(0, 0);
transformationBaseBg.setVisible(false);
transformationContainer.add(transformationBaseBg);
let pokemonSprite: Phaser.GameObjects.Sprite;
let pokemonTintSprite: Phaser.GameObjects.Sprite;
let pokemonEvoSprite: Phaser.GameObjects.Sprite;
let pokemonEvoTintSprite: Phaser.GameObjects.Sprite;
const xOffset = screenPosition === TransformationScreenPosition.CENTER ? 0 :
screenPosition === TransformationScreenPosition.RIGHT ? 100 : -100;
// Centered transformations occur at a lower y Position
const yOffset = screenPosition !== TransformationScreenPosition.CENTER ? -15 : 0;
const getPokemonSprite = () => {
const ret = scene.addPokemonSprite(pokemon, transformationBaseBg.displayWidth / 2 + xOffset, transformationBaseBg.displayHeight / 2 + yOffset, "pkmn__sub");
ret.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], ignoreTimeTint: true });
return ret;
};
transformationContainer.add((pokemonSprite = getPokemonSprite()));
transformationContainer.add((pokemonTintSprite = getPokemonSprite()));
transformationContainer.add((pokemonEvoSprite = getPokemonSprite()));
transformationContainer.add((pokemonEvoTintSprite = getPokemonSprite()));
pokemonSprite.setAlpha(0);
pokemonTintSprite.setAlpha(0);
pokemonTintSprite.setTintFill(0xFFFFFF);
pokemonEvoSprite.setVisible(false);
pokemonEvoTintSprite.setVisible(false);
pokemonEvoTintSprite.setTintFill(0xFFFFFF);
[ pokemonSprite, pokemonTintSprite, pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => {
sprite.play(pokemon.getSpriteKey(true));
sprite.setPipeline(scene.spritePipeline, { tone: [ 0.0, 0.0, 0.0, 0.0 ], hasShadow: false, teraColor: getTypeRgb(pokemon.getTeraType()) });
sprite.setPipelineData("ignoreTimeTint", true);
sprite.setPipelineData("spriteKey", pokemon.getSpriteKey());
sprite.setPipelineData("shiny", pokemon.shiny);
sprite.setPipelineData("variant", pokemon.variant);
[ "spriteColors", "fusionSpriteColors" ].map(k => {
if (pokemon.summonData?.speciesForm) {
k += "Base";
}
sprite.pipelineData[k] = pokemon.getSprite().pipelineData[k];
});
});
[ pokemonEvoSprite, pokemonEvoTintSprite ].map(sprite => {
sprite.play(transformedPokemon.getSpriteKey(true));
sprite.setPipelineData("ignoreTimeTint", true);
sprite.setPipelineData("spriteKey", transformedPokemon.getSpriteKey());
sprite.setPipelineData("shiny", transformedPokemon.shiny);
sprite.setPipelineData("variant", transformedPokemon.variant);
[ "spriteColors", "fusionSpriteColors" ].map(k => {
if (transformedPokemon.summonData?.speciesForm) {
k += "Base";
}
sprite.pipelineData[k] = transformedPokemon.getSprite().pipelineData[k];
});
});
scene.tweens.add({
targets: pokemonSprite,
alpha: 1,
ease: "Cubic.easeInOut",
duration: 2000,
onComplete: () => {
doSpiralUpward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset);
scene.tweens.addCounter({
from: 0,
to: 1,
duration: 1000,
onUpdate: t => {
pokemonTintSprite.setAlpha(t.getValue());
},
onComplete: () => {
pokemonSprite.setVisible(false);
scene.time.delayedCall(700, () => {
doArcDownward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset);
scene.time.delayedCall(1000, () => {
pokemonEvoTintSprite.setScale(0.25);
pokemonEvoTintSprite.setVisible(true);
doCycle(scene, 2, 6, pokemonTintSprite, pokemonEvoTintSprite).then(success => {
pokemonEvoSprite.setVisible(true);
doCircleInward(scene, transformationBaseBg, transformationContainer, xOffset, yOffset);
scene.time.delayedCall(900, () => {
scene.tweens.add({
targets: pokemonEvoTintSprite,
alpha: 0,
duration: 1500,
delay: 150,
easing: "Sine.easeIn",
onComplete: () => {
scene.time.delayedCall(2500, () => {
resolve();
scene.tweens.add({
targets: pokemonEvoSprite,
alpha: 0,
duration: 2000,
delay: 150,
easing: "Sine.easeIn",
// onComplete: () => {
// transformedPokemon.destroy();
// }
});
});
}
});
});
});
});
});
}
});
}
});
});
}
function doSpiralUpward(scene: BattleScene, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) {
let f = 0;
scene.tweens.addCounter({
repeat: 64,
duration: getFrameMs(1),
onRepeat: () => {
if (f < 64) {
if (!(f & 7)) {
for (let i = 0; i < 4; i++) {
doSpiralUpwardParticle(scene, (f & 120) * 2 + i * 64, transformationBaseBg, transformationContainer, xOffset, yOffset);
}
}
f++;
}
}
});
}
function doArcDownward(scene: BattleScene, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) {
let f = 0;
scene.tweens.addCounter({
repeat: 96,
duration: getFrameMs(1),
onRepeat: () => {
if (f < 96) {
if (f < 6) {
for (let i = 0; i < 9; i++) {
doArcDownParticle(scene, i * 16, transformationBaseBg, transformationContainer, xOffset, yOffset);
}
}
f++;
}
}
});
}
function doCycle(scene: BattleScene, l: number, lastCycle: integer, pokemonTintSprite, pokemonEvoTintSprite): Promise<boolean> {
return new Promise(resolve => {
const isLastCycle = l === lastCycle;
scene.tweens.add({
targets: pokemonTintSprite,
scale: 0.25,
ease: "Cubic.easeInOut",
duration: 500 / l,
yoyo: !isLastCycle
});
scene.tweens.add({
targets: pokemonEvoTintSprite,
scale: 1,
ease: "Cubic.easeInOut",
duration: 500 / l,
yoyo: !isLastCycle,
onComplete: () => {
if (l < lastCycle) {
doCycle(scene, l + 0.5, lastCycle, pokemonTintSprite, pokemonEvoTintSprite).then(success => resolve(success));
} else {
pokemonTintSprite.setVisible(false);
resolve(true);
}
}
});
});
}
function doCircleInward(scene: BattleScene, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) {
let f = 0;
scene.tweens.addCounter({
repeat: 48,
duration: getFrameMs(1),
onRepeat: () => {
if (!f) {
for (let i = 0; i < 16; i++) {
doCircleInwardParticle(scene, i * 16, 4, transformationBaseBg, transformationContainer, xOffset, yOffset);
}
} else if (f === 32) {
for (let i = 0; i < 16; i++) {
doCircleInwardParticle(scene, i * 16, 8, transformationBaseBg, transformationContainer, xOffset, yOffset);
}
}
f++;
}
});
}
function doSpiralUpwardParticle(scene: BattleScene, trigIndex: integer, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) {
const initialX = transformationBaseBg.displayWidth / 2 + xOffset;
const particle = scene.add.image(initialX, 0, "evo_sparkle");
transformationContainer.add(particle);
let f = 0;
let amp = 48;
const particleTimer = scene.tweens.addCounter({
repeat: -1,
duration: getFrameMs(1),
onRepeat: () => {
updateParticle();
}
});
const updateParticle = () => {
if (!f || particle.y > 8) {
particle.setPosition(initialX, 88 - (f * f) / 80 + yOffset);
particle.y += sin(trigIndex, amp) / 4;
particle.x += cos(trigIndex, amp);
particle.setScale(1 - (f / 80));
trigIndex += 4;
if (f & 1) {
amp--;
}
f++;
} else {
particle.destroy();
particleTimer.remove();
}
};
updateParticle();
}
function doArcDownParticle(scene: BattleScene, trigIndex: integer, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) {
const initialX = transformationBaseBg.displayWidth / 2 + xOffset;
const particle = scene.add.image(initialX, 0, "evo_sparkle");
particle.setScale(0.5);
transformationContainer.add(particle);
let f = 0;
let amp = 8;
const particleTimer = scene.tweens.addCounter({
repeat: -1,
duration: getFrameMs(1),
onRepeat: () => {
updateParticle();
}
});
const updateParticle = () => {
if (!f || particle.y < 88) {
particle.setPosition(initialX, 8 + (f * f) / 5 + yOffset);
particle.y += sin(trigIndex, amp) / 4;
particle.x += cos(trigIndex, amp);
amp = 8 + sin(f * 4, 40);
f++;
} else {
particle.destroy();
particleTimer.remove();
}
};
updateParticle();
}
function doCircleInwardParticle(scene: BattleScene, trigIndex: integer, speed: integer, transformationBaseBg, transformationContainer, xOffset: number, yOffset: number) {
const initialX = transformationBaseBg.displayWidth / 2 + xOffset;
const initialY = transformationBaseBg.displayHeight / 2 + yOffset;
const particle = scene.add.image(initialX, initialY, "evo_sparkle");
transformationContainer.add(particle);
let amp = 120;
const particleTimer = scene.tweens.addCounter({
repeat: -1,
duration: getFrameMs(1),
onRepeat: () => {
updateParticle();
}
});
const updateParticle = () => {
if (amp > 8) {
particle.setPosition(initialX, initialY);
particle.y += sin(trigIndex, amp);
particle.x += cos(trigIndex, amp);
amp -= speed;
trigIndex += 4;
} else {
particle.destroy();
particleTimer.remove();
}
};
updateParticle();
}

View File

@ -21,5 +21,6 @@ export enum MysteryEncounterType {
BERRIES_ABOUND,
CLOWNING_AROUND,
PART_TIMER,
DANCING_LESSONS
DANCING_LESSONS,
WEIRD_DREAM
}

View File

@ -76,6 +76,10 @@ export const modifierType: ModifierTypeTranslationEntries = {
"cursed": "cursed"
},
},
"PokemonBaseStatFlatModifierType": {
name: "Old Gateau",
description: "Increases the holder's {{stats}} base stats by {{statValue}}. Found after a strange dream.",
},
"AllPokemonFullHpRestoreModifierType": {
description: "Restores 100% HP for all Pokémon.",
},
@ -258,6 +262,7 @@ export const modifierType: ModifierTypeTranslationEntries = {
"MYSTERY_ENCOUNTER_SHUCKLE_JUICE": { name: "Shuckle Juice" },
"MYSTERY_ENCOUNTER_BLACK_SLUDGE": { name: "Black Sludge", description: "The stench is so powerful that healing items are no longer available to purchase in shops." },
"MYSTERY_ENCOUNTER_OLD_GATEAU": { name: "Old Gateau", description: "Increases the holder's {{stats}} stats by {{statValue}}." },
},
SpeciesBoosterItem: {
"LIGHT_BALL": { name: "Light Ball", description: "It's a mysterious orb that boosts Pikachu's Attack and Sp. Atk stats." },

View File

@ -21,6 +21,7 @@ import { berriesAboundDialogue } from "#app/locales/en/mystery-encounters/berrie
import { clowningAroundDialogue } from "#app/locales/en/mystery-encounters/clowning-around-dialogue";
import { partTimerDialogue } from "#app/locales/en/mystery-encounters/part-timer-dialogue";
import { dancingLessonsDialogue } from "#app/locales/en/mystery-encounters/dancing-lessons-dialogue";
import { weirdDreamDialogue } from "#app/locales/en/mystery-encounters/weird-dream-dialogue";
/**
* Injection patterns that can be used:
@ -69,5 +70,6 @@ export const mysteryEncounter = {
berriesAbound: berriesAboundDialogue,
clowningAround: clowningAroundDialogue,
partTimer: partTimerDialogue,
dancingLessons: dancingLessonsDialogue
dancingLessons: dancingLessonsDialogue,
weirdDream: weirdDreamDialogue
} as const;

View File

@ -1,26 +1,23 @@
export const berriesAboundDialogue = {
intro: "There's a huge berry bush\nnear that Pokémon!",
title: "Berries Abound",
description: "It looks like there's a strong Pokémon guarding a berry bush. Battling is the straightforward approach, but this Pokémon looks strong. You could also try to sneak around, though the Pokémon might catch you.",
description: "It looks like there's a strong Pokémon guarding a berry bush. Battling is the straightforward approach, but this Pokémon looks strong. Maybe a fast Pokémon would be able to grab some without getting caught?",
query: "What will you do?",
berries: "Berries!",
option: {
1: {
label: "Battle the Pokémon",
tooltip: "(-) Hard Battle\n(+) New Item",
tooltip: "(-) Hard Battle\n(+) Gain Berries",
selected: "You approach the\nPokémon without fear.",
},
2: {
label: "Steal the Berries",
tooltip: "@[SUMMARY_GREEN]{(35%) Steal Item}\n@[SUMMARY_BLUE]{(65%) Harder Battle}",
tooltip_special: "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}",
good_result: `.@d{32}.@d{32}.@d{32}
$You manage to sneak your way\npast and grab the berries!`,
special_result: `.@d{32}.@d{32}.@d{32}
$Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}!
$You nabbed the berries!`,
bad_result: `.@d{32}.@d{32}.@d{32}
$The Pokémon catches you\nas you try to sneak around!`,
label: "Race to the Bush",
tooltip: "(-) {{fastestPokemon}} Uses its Speed\n(+) Gain Berries",
selected: `Your {{fastestPokemon}} races for the berry bush!
$It manages to nab {{numBerries}} before the {{enemyPokemon}} can react!
$You quickly retreat with your newfound prize.`,
selected_bad: `Your {{fastestPokemon}} races for the berry bush!
$Oh no! The {{enemyPokemon}} was faster and blocked off the approach!`,
boss_enraged: "The opposing {{enemyPokemon}} has become enraged!"
},
3: {

View File

@ -11,16 +11,11 @@ export const fightOrFlightDialogue = {
},
2: {
label: "Steal the Item",
tooltip: "@[SUMMARY_GREEN]{(35%) Steal Item}\n@[SUMMARY_BLUE]{(65%) Harder Battle}",
tooltip_special: "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}",
good_result: `.@d{32}.@d{32}.@d{32}
$You manage to sneak your way\npast and grab the item!`,
special_result: `.@d{32}.@d{32}.@d{32}
disabled_tooltip: "Your Pokémon need to know certain moves to choose this",
tooltip: "(+) {{option2PrimaryName}} uses {{option2PrimaryMove}}",
selected: `.@d{32}.@d{32}.@d{32}
$Your {{option2PrimaryName}} helps you out and uses {{option2PrimaryMove}}!
$You nabbed the item!`,
bad_result: `.@d{32}.@d{32}.@d{32}
$The Pokémon catches you\nas you try to sneak around!`,
boss_enraged: "The opposing {{enemyPokemon}} has become enraged!"
},
3: {
label: "Leave",

View File

@ -0,0 +1,29 @@
export const weirdDreamDialogue = {
intro: "A shadowy woman blocks your path.\nSomething about her is unsettling...",
speaker: "Woman",
intro_dialogue: `I have seen your futures, your pasts...
$Child, do you see them too?`,
title: "???",
description: "The woman's words echo in your head. It wasn't just a singular voice, but a vast multitude, from all timelines and realities. You begin to feel dizzy, the question lingering on your mind...\n\n@[TOOLTIP_TITLE]{\"I have seen your futures, your pasts... Child, do you see them too?\"}",
query: "What will you do?",
option: {
1: {
label: "\"I See Them\"",
tooltip: "@[SUMMARY_GREEN]{(?) Affects your Pokémon}",
selected: `Her hand reaches out to touch you,\nand everything goes black.
$Then...@d{64} You see everything.\nEvery timeline, all your different selves,\n past and future.
$Everything that has made you,\neverything you will become...@d{64}`,
cutscene: "You see your Pokémon,@d{32} converging from\nevery reality to become something new...@d{64}",
dream_complete: `When you awaken, the woman - was it a woman or a ghost? - is gone...
$.@d{32}.@d{32}.@d{32}
$Your Pokémon team has changed...\nOr is it the same team you've always had?`
},
2: {
label: "Quickly Leave",
tooltip: "(-) Affects your Pokémon",
selected: `You tear your mind from a numbing grip, and hastily depart.
$When you finally stop to collect yourself, you check the Pokémon in your team.
$For some reason, all of their levels have decreased!`,
}
},
};

View File

@ -1,27 +1,27 @@
import * as Modifiers from "./modifier";
import { AttackMove, allMoves, selfStatLowerMoves } from "../data/move";
import { MAX_PER_TYPE_POKEBALLS, PokeballType, getPokeballCatchMultiplier, getPokeballName } from "../data/pokeball";
import { MoneyMultiplierModifier } from "./modifier";
import { allMoves, AttackMove, selfStatLowerMoves } from "../data/move";
import { getPokeballCatchMultiplier, getPokeballName, MAX_PER_TYPE_POKEBALLS, PokeballType } from "../data/pokeball";
import Pokemon, { EnemyPokemon, PlayerPokemon, PokemonMove } from "../field/pokemon";
import { EvolutionItem, pokemonEvolutions } from "../data/pokemon-evolutions";
import { Stat, getStatName } from "../data/pokemon-stat";
import { getStatName, Stat } from "../data/pokemon-stat";
import { tmPoolTiers, tmSpecies } from "../data/tms";
import { Type } from "../data/type";
import PartyUiHandler, { PokemonMoveSelectFilter, PokemonSelectFilter } from "../ui/party-ui-handler";
import * as Utils from "../utils";
import { TempBattleStat, getTempBattleStatBoosterItemName, getTempBattleStatName } from "../data/temp-battle-stat";
import { getTempBattleStatBoosterItemName, getTempBattleStatName, TempBattleStat } from "../data/temp-battle-stat";
import { getBerryEffectDescription, getBerryName } from "../data/berry";
import { Unlockables } from "../system/unlockables";
import { StatusEffect, getStatusEffectDescriptor } from "../data/status-effect";
import { getStatusEffectDescriptor, StatusEffect } from "../data/status-effect";
import { SpeciesFormKey } from "../data/pokemon-species";
import BattleScene from "../battle-scene";
import { VoucherType, getVoucherTypeIcon, getVoucherTypeName } from "../system/voucher";
import { FormChangeItem, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger, pokemonFormChanges } from "../data/pokemon-forms";
import { getVoucherTypeIcon, getVoucherTypeName, VoucherType } from "../system/voucher";
import { FormChangeItem, pokemonFormChanges, SpeciesFormChangeCondition, SpeciesFormChangeItemTrigger } from "../data/pokemon-forms";
import { ModifierTier } from "./modifier-tier";
import { Nature, getNatureName, getNatureStatMultiplier } from "#app/data/nature";
import { getNatureName, getNatureStatMultiplier, Nature } from "#app/data/nature";
import i18next from "i18next";
import { getModifierTierTextTint } from "#app/ui/text";
import Overrides from "#app/overrides";
import { MoneyMultiplierModifier } from "./modifier";
import { Abilities } from "#enums/abilities";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
@ -680,6 +680,28 @@ export class PokemonBaseStatTotalModifierType extends PokemonHeldItemModifierTyp
}
}
export class PokemonBaseStatFlatModifierType extends PokemonHeldItemModifierType implements GeneratedPersistentModifierType {
private readonly statModifier: integer;
private readonly stats: Stat[];
constructor(statModifier: integer, stats: Stat[]) {
super("modifierType:ModifierType.MYSTERY_ENCOUNTER_OLD_GATEAU", "old_gateau", (_type, args) => new Modifiers.PokemonBaseStatFlatModifier(this, (args[0] as Pokemon).id, this.statModifier, this.stats));
this.statModifier = statModifier;
this.stats = stats;
}
getDescription(scene: BattleScene): string {
return i18next.t("modifierType:ModifierType.PokemonBaseStatFlatModifierType.description", {
stats: this.stats.map(stat => getStatName(stat)).join("/"),
statValue: this.statModifier,
});
}
getPregenArgs(): any[] {
return [ this.statModifier, this.stats ];
}
}
class AllPokemonFullHpRestoreModifierType extends ModifierType {
private descriptionKey: string;
@ -1477,7 +1499,12 @@ export const modifierTypes = {
}
return new PokemonBaseStatTotalModifierType(Utils.randSeedInt(20));
}),
MYSTERY_ENCOUNTER_OLD_GATEAU: () => new ModifierTypeGenerator((party: Pokemon[], pregenArgs?: any[]) => {
if (pregenArgs) {
return new PokemonBaseStatFlatModifierType(pregenArgs[0] as integer, pregenArgs[1] as Stat[]);
}
return new PokemonBaseStatFlatModifierType(Utils.randSeedInt(20), [Stat.HP, Stat.ATK, Stat.DEF]);
}),
MYSTERY_ENCOUNTER_BLACK_SLUDGE: () => new ModifierType("modifierType:ModifierType.MYSTERY_ENCOUNTER_BLACK_SLUDGE", "black_sludge", (type, _args) => new Modifiers.RemoveHealShopModifier(type)),
};
@ -1948,6 +1975,7 @@ export interface CustomModifierSettings {
guaranteedModifierTypeFuncs?: ModifierTypeFunc[];
fillRemaining?: boolean;
rerollMultiplier?: number;
allowLuckUpgrades?: boolean;
}
export function getModifierTypeFuncById(id: string): ModifierTypeFunc {
@ -1968,6 +1996,7 @@ export function getModifierTypeFuncById(id: string): ModifierTypeFunc {
* - The first item in the shop will be `GREAT` tier, and the remaining 3 items will be generated normally.
* - If `fillRemaining = false` in the same scenario, only 1 `GREAT` tier item will appear in the shop (regardless of `count` value).
* - `rerollMultiplier?: number` - If specified, can adjust the amount of money required for a shop reroll. If set to 0, the shop will not allow rerolls at all.
* - `allowLuckUpgrades?: boolean` - Default true, if false will prevent set item tiers from upgrading via luck
*/
export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemon[], modifierTiers?: ModifierTier[], customModifierSettings?: CustomModifierSettings): ModifierTypeOption[] {
const options: ModifierTypeOption[] = [];
@ -2001,8 +2030,9 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
// Guaranteed tiers third
if (customModifierSettings?.guaranteedModifierTiers?.length > 0) {
customModifierSettings?.guaranteedModifierTiers.forEach((tier) => {
options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier));
const allowLuckUpgrades = customModifierSettings.allowLuckUpgrades ?? true;
customModifierSettings.guaranteedModifierTiers.forEach((tier) => {
options.push(getModifierTypeOptionWithRetry(options, retryCount, party, tier, allowLuckUpgrades));
});
}
@ -2028,11 +2058,12 @@ export function getPlayerModifierTypeOptions(count: integer, party: PlayerPokemo
return options;
}
function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier): ModifierTypeOption {
let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier);
function getModifierTypeOptionWithRetry(existingOptions: ModifierTypeOption[], retryCount: integer, party: PlayerPokemon[], tier?: ModifierTier, allowLuckUpgrades?: boolean): ModifierTypeOption {
allowLuckUpgrades = allowLuckUpgrades ?? true;
let candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, tier, undefined, 0, allowLuckUpgrades);
let r = 0;
while (existingOptions.length && ++r < retryCount && existingOptions.filter(o => o.type.name === candidate.type.name || o.type.group === candidate.type.group).length) {
candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate.type.tier, candidate.upgradeCount);
candidate = getNewModifierTypeOption(party, ModifierPoolType.PLAYER, candidate.type.tier, candidate.upgradeCount, 0, allowLuckUpgrades);
}
return candidate;
}
@ -2112,7 +2143,7 @@ export function getDailyRunStarterModifiers(party: PlayerPokemon[]): Modifiers.P
return ret;
}
function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0): ModifierTypeOption {
function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType, tier?: ModifierTier, upgradeCount?: integer, retryCount: integer = 0, allowLuckUpgrades: boolean = true): ModifierTypeOption {
const player = !poolType;
const pool = getModifierPoolForType(poolType);
let thresholds: object;
@ -2138,7 +2169,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType,
if (!upgradeCount) {
upgradeCount = 0;
}
if (player && tierValue) {
if (player && tierValue && allowLuckUpgrades) {
const partyLuckValue = getPartyLuckValue(party);
const upgradeOdds = Math.floor(128 / ((partyLuckValue + 4) / 4));
let upgraded = false;
@ -2163,7 +2194,7 @@ function getNewModifierTypeOption(party: Pokemon[], poolType: ModifierPoolType,
}
} else if (upgradeCount === undefined && player) {
upgradeCount = 0;
if (tier < ModifierTier.MASTER) {
if (tier < ModifierTier.MASTER && allowLuckUpgrades) {
const partyShinyCount = party.filter(p => p.isShiny() && !p.isFainted()).length;
const upgradeOdds = Math.floor(32 / ((partyShinyCount + 2) / 2));
while (modifierPool.hasOwnProperty(tier + upgradeCount + 1) && modifierPool[tier + upgradeCount + 1].length) {

View File

@ -702,6 +702,7 @@ export class PokemonBaseStatModifier extends PokemonHeldItemModifier {
export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
private statModifier: integer;
readonly isTransferrable: boolean = false;
constructor(type: ModifierTypes.PokemonBaseStatTotalModifierType, pokemonId: integer, statModifier: integer, stackCount?: integer) {
super(type, pokemonId, stackCount);
@ -709,10 +710,7 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
}
matchType(modifier: Modifier): boolean {
if (modifier instanceof PokemonBaseStatTotalModifier) {
return (modifier as PokemonBaseStatTotalModifier).statModifier === this.statModifier;
}
return false;
return modifier instanceof PokemonBaseStatTotalModifier;
}
clone(): PersistentModifier {
@ -728,6 +726,7 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
}
apply(args: any[]): boolean {
// Modifies the passed in baseStats[] array
args[1].forEach((v, i) => {
const newVal = Math.floor(v + this.statModifier);
args[1][i] = Math.min(Math.max(newVal, 1), 999999);
@ -736,10 +735,6 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
return true;
}
getTransferrable(_withinParty: boolean): boolean {
return false;
}
getScoreMultiplier(): number {
return 1.2;
}
@ -749,6 +744,55 @@ export class PokemonBaseStatTotalModifier extends PokemonHeldItemModifier {
}
}
export class PokemonBaseStatFlatModifier extends PokemonHeldItemModifier {
private statModifier: integer;
private stats: Stat[];
readonly isTransferrable: boolean = false;
constructor (type: ModifierType, pokemonId: integer, statModifier: integer, stats: Stat[], stackCount?: integer) {
super(type, pokemonId, stackCount);
this.statModifier = statModifier;
this.stats = stats;
}
matchType(modifier: Modifier): boolean {
return modifier instanceof PokemonBaseStatFlatModifier;
}
clone(): PersistentModifier {
return new PokemonBaseStatFlatModifier(this.type, this.pokemonId, this.statModifier, this.stats, this.stackCount);
}
getArgs(): any[] {
return super.getArgs().concat(this.statModifier, this.stats);
}
shouldApply(args: any[]): boolean {
return super.shouldApply(args) && args.length === 2 && args[1] instanceof Array;
}
apply(args: any[]): boolean {
// Modifies the passed in baseStats[] array by a flat value, only if the stat is specified in this.stats
args[1].forEach((v, i) => {
if (this.stats.includes(i)) {
const newVal = Math.floor(v + this.statModifier);
args[1][i] = Math.min(Math.max(newVal, 1), 999999);
}
});
return true;
}
getScoreMultiplier(): number {
return 1.1;
}
getMaxHeldItemCount(pokemon: Pokemon): integer {
return 1;
}
}
/**
* Modifier used for held items that apply {@linkcode Stat} boost(s)
* using a multiplier.

View File

@ -1444,12 +1444,20 @@ export class GameData {
}
}
setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false): Promise<void> {
return this.setPokemonSpeciesCaught(pokemon, pokemon.species, incrementCount, fromEgg);
/**
*
* @param pokemon
* @param incrementCount
* @param fromEgg
* @param showNewStarterMessage
* @returns - true if Pokemon catch unlocked a new starter, false if Pokemon catch did not unlock a starter
*/
setPokemonCaught(pokemon: Pokemon, incrementCount: boolean = true, fromEgg: boolean = false, showNewStarterMessage: boolean = true): Promise<boolean> {
return this.setPokemonSpeciesCaught(pokemon, pokemon.species, incrementCount, fromEgg, showNewStarterMessage);
}
setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false): Promise<void> {
return new Promise<void>(resolve => {
setPokemonSpeciesCaught(pokemon: Pokemon, species: PokemonSpecies, incrementCount: boolean = true, fromEgg: boolean = false, showNewStarterMessage: boolean = true): Promise<boolean> {
return new Promise<boolean>(resolve => {
const dexEntry = this.dexData[species.speciesId];
const caughtAttr = dexEntry.caughtAttr;
const formIndex = pokemon.formIndex;
@ -1504,20 +1512,20 @@ export class GameData {
}
}
const checkPrevolution = () => {
const checkPrevolution = (newStarter: boolean) => {
if (hasPrevolution) {
const prevolutionSpecies = pokemonPrevolutions[species.speciesId];
return this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg).then(() => resolve());
return this.setPokemonSpeciesCaught(pokemon, getPokemonSpecies(prevolutionSpecies), incrementCount, fromEgg, showNewStarterMessage).then(result => resolve(result));
} else {
resolve();
resolve(newStarter);
}
};
if (newCatch && speciesStarters.hasOwnProperty(species.speciesId)) {
if (newCatch && speciesStarters.hasOwnProperty(species.speciesId) && showNewStarterMessage) {
this.scene.playSound("level_up_fanfare");
this.scene.ui.showText(i18next.t("battle:addedAsAStarter", { pokemonName: species.name }), null, () => checkPrevolution(), null, true);
this.scene.ui.showText(i18next.t("battle:addedAsAStarter", { pokemonName: species.name }), null, () => checkPrevolution(true), null, true);
} else {
checkPrevolution();
checkPrevolution(false);
}
});
}
@ -1559,7 +1567,7 @@ export class GameData {
this.starterData[species.speciesId].candyCount += count;
}
setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer): Promise<boolean> {
setEggMoveUnlocked(species: PokemonSpecies, eggMoveIndex: integer, prependSpeciesToMessage: boolean = false): Promise<boolean> {
return new Promise<boolean>(resolve => {
const speciesId = species.speciesId;
if (!speciesEggMoves.hasOwnProperty(speciesId) || !speciesEggMoves[speciesId][eggMoveIndex]) {
@ -1583,7 +1591,10 @@ export class GameData {
this.scene.playSound("level_up_fanfare");
const moveName = allMoves[speciesEggMoves[speciesId][eggMoveIndex]].name;
this.scene.ui.showText(eggMoveIndex === 3 ? i18next.t("egg:rareEggMoveUnlock", { moveName: moveName }) : i18next.t("egg:eggMoveUnlock", { moveName: moveName }), null, () => resolve(true), null, true);
let message = prependSpeciesToMessage ? species.getName() + " " : "";
message += eggMoveIndex === 3 ? i18next.t("egg:rareEggMoveUnlock", { moveName: moveName }) : i18next.t("egg:eggMoveUnlock", { moveName: moveName });
this.scene.ui.showText(message, null, () => resolve(true), null, true);
});
}

View File

@ -6,9 +6,7 @@ import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils";
import { CommandPhase, SelectModifierPhase } from "#app/phases";
import { Moves } from "#enums/moves";
import BattleScene from "#app/battle-scene";
import { PokemonMove } from "#app/field/pokemon";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { BerryModifier } from "#app/modifier/modifier";
@ -16,13 +14,11 @@ import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter";
import * as Utils from "utils";
import { isNullOrUndefined } from "utils";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import * as EncounterDialogueUtils from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
const namespace = "mysteryEncounter:berriesAbound";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultParty = [Species.PYUKUMUKU];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
@ -105,10 +101,10 @@ describe("Berries Abound - Mystery Encounter", () => {
describe("Option 1 - Fight", () => {
it("should have the correct properties", () => {
const option1 = BerriesAboundEncounter.options[0];
expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option1.dialogue).toBeDefined();
expect(option1.dialogue).toStrictEqual({
const option = BerriesAboundEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
@ -166,32 +162,25 @@ describe("Berries Abound - Mystery Encounter", () => {
});
});
describe("Option 2 - Attempt to Steal", () => {
describe("Option 2 - Race to the Bush", () => {
it("should have the correct properties", () => {
const option1 = BerriesAboundEncounter.options[1];
expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL);
expect(option1.dialogue).toBeDefined();
expect(option1.dialogue).toStrictEqual({
const option = BerriesAboundEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
});
});
it("should start battle on failing to steal", async () => {
it("should start battle if fastest pokemon is slower than boss", async () => {
const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText");
await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty);
const config = game.scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0];
const speciesToSpawn = config.pokemonConfigs[0].species.speciesId;
const realFn = Utils.randSeedInt;
vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min) => {
if (range === 16 && isNullOrUndefined(min)) {
// Mock the steal roll
return 12;
} else {
return realFn(range, min);
}
});
// Setting enemy's level arbitrarily high to outspeed
config.pokemonConfigs[0].dataSource.level = 1000;
await runMysteryEncounterToEnd(game, 2, null, true);
@ -202,60 +191,33 @@ describe("Berries Abound - Mystery Encounter", () => {
// Should be enraged
expect(enemyField[0].summonData.battleStats).toEqual([1, 1, 1, 1, 1, 0, 0]);
expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.selected_bad`);
});
it("Should skip battle when succeed on steal", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty);
const realFn = Utils.randSeedInt;
vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min) => {
if (range === 16 && isNullOrUndefined(min)) {
// Mock the steal roll
return 6;
} else {
return realFn(range, min);
}
});
await runMysteryEncounterToEnd(game, 2);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(5);
for (const option of modifierSelectHandler.options) {
expect(option.modifierTypeOption.type.id).toContain("BERRY");
}
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
it("Should skip fight when special requirements are met", async () => {
it("Should skip battle when fastest pokemon is faster than boss", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText");
await game.runToMysteryEncounter(MysteryEncounterType.BERRIES_ABOUND, defaultParty);
// Mock moveset
scene.getParty()[0].moveset = [new PokemonMove(Moves.KNOCK_OFF)];
// Setting party pokemon's level arbitrarily high to outspeed
const fastestPokemon = scene.getParty()[0];
fastestPokemon.level = 1000;
fastestPokemon.calculateStats();
await runMysteryEncounterToEnd(game, 2);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(5);
for (const option of modifierSelectHandler.options) {
expect(option.modifierTypeOption.type.id).toContain("BERRY");
}
expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.special_result`);
expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.selected`);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});

View File

@ -4,7 +4,7 @@ import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils";
import { runMysteryEncounterToEnd, runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils";
import { CommandPhase, SelectModifierPhase } from "#app/phases";
import { Moves } from "#enums/moves";
import BattleScene from "#app/battle-scene";
@ -14,11 +14,9 @@ import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import * as Utils from "utils";
import { isNullOrUndefined } from "utils";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import * as EncounterDialogueUtils from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { FightOrFlightEncounter } from "#app/data/mystery-encounters/encounters/fight-or-flight-encounter";
import { MysteryEncounterPhase } from "#app/phases/mystery-encounter-phases";
const namespace = "mysteryEncounter:fightOrFlight";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
@ -104,10 +102,10 @@ describe("Fight or Flight - Mystery Encounter", () => {
describe("Option 1 - Fight", () => {
it("should have the correct properties", () => {
const option1 = FightOrFlightEncounter.options[0];
expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option1.dialogue).toBeDefined();
expect(option1.dialogue).toStrictEqual({
const option = FightOrFlightEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
@ -152,77 +150,46 @@ describe("Fight or Flight - Mystery Encounter", () => {
describe("Option 2 - Attempt to Steal", () => {
it("should have the correct properties", () => {
const option1 = FightOrFlightEncounter.options[1];
expect(option1.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT_OR_SPECIAL);
expect(option1.dialogue).toBeDefined();
expect(option1.dialogue).toStrictEqual({
const option = FightOrFlightEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DISABLED_OR_SPECIAL);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
disabledButtonTooltip: `${namespace}.option.2.disabled_tooltip`,
selected: [
{
text: `${namespace}.option.2.selected`,
}
],
});
});
it("should start battle on failing to steal", async () => {
it("should NOT be selectable if the player doesn't have a Stealing move", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty);
scene.getParty().forEach(p => p.moveset = []);
await game.phaseInterceptor.to(MysteryEncounterPhase, false);
const config = game.scene.currentBattle.mysteryEncounter.enemyPartyConfigs[0];
const speciesToSpawn = config.pokemonConfigs[0].species.speciesId;
const encounterPhase = scene.getCurrentPhase();
expect(encounterPhase.constructor.name).toBe(MysteryEncounterPhase.name);
const mysteryEncounterPhase = encounterPhase as MysteryEncounterPhase;
vi.spyOn(mysteryEncounterPhase, "continueEncounter");
vi.spyOn(mysteryEncounterPhase, "handleOptionSelect");
vi.spyOn(scene.ui, "playError");
const realFn = Utils.randSeedInt;
vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min) => {
if (range === 16 && isNullOrUndefined(min)) {
// Mock the steal roll
return 12;
} else {
return realFn(range, min);
}
});
await runSelectMysteryEncounterOption(game, 2);
await runMysteryEncounterToEnd(game, 2, null, true);
const enemyField = scene.getEnemyField();
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
expect(enemyField.length).toBe(1);
expect(enemyField[0].species.speciesId).toBe(speciesToSpawn);
// Should be enraged
expect(enemyField[0].summonData.battleStats).toEqual([1, 1, 1, 1, 1, 0, 0]);
expect(scene.getCurrentPhase().constructor.name).toBe(MysteryEncounterPhase.name);
expect(scene.ui.playError).not.toHaveBeenCalled(); // No error sfx, option is disabled
expect(mysteryEncounterPhase.handleOptionSelect).not.toHaveBeenCalled();
expect(mysteryEncounterPhase.continueEncounter).not.toHaveBeenCalled();
});
it("Should skip battle when succeed on steal", async () => {
it("Should skip fight when player meets requirements", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty);
const item = game.scene.currentBattle.mysteryEncounter.misc;
const realFn = Utils.randSeedInt;
vi.spyOn(Utils, "randSeedInt").mockImplementation((range, min) => {
if (range === 16 && isNullOrUndefined(min)) {
// Mock the steal roll
return 6;
} else {
return realFn(range, min);
}
});
await runMysteryEncounterToEnd(game, 2);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(1);
expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
it("Should skip fight when special requirements are met", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
const encounterTextSpy = vi.spyOn(EncounterDialogueUtils, "showEncounterText");
await game.runToMysteryEncounter(MysteryEncounterType.FIGHT_OR_FLIGHT, defaultParty);
// Mock moveset
scene.getParty()[0].moveset = [new PokemonMove(Moves.KNOCK_OFF)];
const item = game.scene.currentBattle.mysteryEncounter.misc;
@ -237,7 +204,6 @@ describe("Fight or Flight - Mystery Encounter", () => {
expect(modifierSelectHandler.options.length).toEqual(1);
expect(item.type.name).toBe(modifierSelectHandler.options[0].modifierTypeOption.type.name);
expect(encounterTextSpy).toHaveBeenCalledWith(expect.any(BattleScene), `${namespace}.option.2.special_result`);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});

View File

@ -0,0 +1,219 @@
import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters";
import { Biome } from "#app/enums/biome";
import { MysteryEncounterType } from "#app/enums/mystery-encounter-type";
import { Species } from "#app/enums/species";
import GameManager from "#app/test/utils/gameManager";
import { afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { runMysteryEncounterToEnd } from "#test/mystery-encounter/encounterTestUtils";
import { SelectModifierPhase } from "#app/phases";
import BattleScene from "#app/battle-scene";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode";
import { MysteryEncounterTier } from "#enums/mystery-encounter-tier";
import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils";
import { WeirdDreamEncounter } from "#app/data/mystery-encounters/encounters/weird-dream-encounter";
import * as EncounterTransformationSequence from "#app/data/mystery-encounters/utils/encounter-transformation-sequence";
const namespace = "mysteryEncounter:weirdDream";
const defaultParty = [Species.MAGBY, Species.HAUNTER, Species.ABRA];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
describe("Weird Dream - Mystery Encounter", () => {
let phaserGame: Phaser.Game;
let game: GameManager;
let scene: BattleScene;
beforeAll(() => {
phaserGame = new Phaser.Game({ type: Phaser.HEADLESS });
});
beforeEach(async () => {
game = new GameManager(phaserGame);
scene = game.scene;
game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
vi.spyOn(EncounterTransformationSequence, "doPokemonTransformationSequence").mockImplementation(() => new Promise<void>(resolve => resolve()));
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([
[Biome.CAVE, [MysteryEncounterType.WEIRD_DREAM]],
])
);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
expect(WeirdDreamEncounter.encounterType).toBe(MysteryEncounterType.WEIRD_DREAM);
expect(WeirdDreamEncounter.encounterTier).toBe(MysteryEncounterTier.ROGUE);
expect(WeirdDreamEncounter.dialogue).toBeDefined();
expect(WeirdDreamEncounter.dialogue.intro).toStrictEqual([
{
text: `${namespace}.intro`
},
{
speaker: `${namespace}.speaker`,
text: `${namespace}.intro_dialogue`,
},
]);
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`);
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`);
expect(WeirdDreamEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`);
expect(WeirdDreamEncounter.options.length).toBe(2);
});
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.WEIRD_DREAM);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
it("should initialize fully", async () => {
initSceneWithoutEncounterPhase(scene, defaultParty);
scene.currentBattle.mysteryEncounter = WeirdDreamEncounter;
const loadBgmSpy = vi.spyOn(scene, "loadBgm");
const { onInit } = WeirdDreamEncounter;
expect(WeirdDreamEncounter.onInit).toBeDefined();
WeirdDreamEncounter.populateDialogueTokensFromRequirements(scene);
const onInitResult = onInit(scene);
expect(loadBgmSpy).toHaveBeenCalled();
expect(onInitResult).toBe(true);
});
describe("Option 1 - Accept Transformation", () => {
it("should have the correct properties", () => {
const option = WeirdDreamEncounter.options[0];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.1.label`,
buttonTooltip: `${namespace}.option.1.tooltip`,
selected: [
{
text: `${namespace}.option.1.selected`,
},
],
});
});
it("should transform the new party into new species, 2 at +90/+110, the rest at +40/50 BST", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
const pokemonPrior = scene.getParty().map(pokemon => pokemon);
const bstsPrior = pokemonPrior.map(species => species.getSpeciesForm().getBaseStatTotal());
await runMysteryEncounterToEnd(game, 1);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
const pokemonAfter = scene.getParty();
const bstsAfter = pokemonAfter.map(pokemon => pokemon.getSpeciesForm().getBaseStatTotal());
const bstDiff = bstsAfter.map((bst, index) => bst - bstsPrior[index]);
for (let i = 0; i < pokemonAfter.length; i++) {
const newPokemon = pokemonAfter[i];
expect(newPokemon.getSpeciesForm().speciesId).not.toBe(pokemonPrior[i].getSpeciesForm().speciesId);
expect(newPokemon.mysteryEncounterData.types.length).toBe(2);
}
const plus90To110 = bstDiff.filter(bst => bst > 80);
const plus40To50 = bstDiff.filter(bst => bst < 80);
expect(plus90To110.length).toBe(2);
expect(plus40To50.length).toBe(1);
});
it("should have 1 Memory Mushroom, 5 Rogue Balls, and 2 Mints in rewards", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
await runMysteryEncounterToEnd(game, 1);
await game.phaseInterceptor.to(SelectModifierPhase, false);
expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name);
await game.phaseInterceptor.run(SelectModifierPhase);
expect(scene.ui.getMode()).to.equal(Mode.MODIFIER_SELECT);
const modifierSelectHandler = scene.ui.handlers.find(h => h instanceof ModifierSelectUiHandler) as ModifierSelectUiHandler;
expect(modifierSelectHandler.options.length).toEqual(4);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("MEMORY_MUSHROOM");
expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toEqual("ROGUE_BALL");
expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toEqual("MINT");
expect(modifierSelectHandler.options[3].modifierTypeOption.type.id).toEqual("MINT");
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
await runMysteryEncounterToEnd(game, 1);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 2 - Leave", () => {
it("should have the correct properties", () => {
const option = WeirdDreamEncounter.options[1];
expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}.option.2.label`,
buttonTooltip: `${namespace}.option.2.tooltip`,
selected: [
{
text: `${namespace}.option.2.selected`,
},
],
});
});
it("should reduce party levels by 20%", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
const levelsPrior = scene.getParty().map(p => p.level);
await runMysteryEncounterToEnd(game, 2);
const levelsAfter = scene.getParty().map(p => p.level);
for (let i = 0; i < levelsPrior.length; i++) {
expect(Math.max(Math.ceil(0.8 * levelsPrior[i]), 1)).toBe(levelsAfter[i]);
expect(scene.getParty()[i].levelExp).toBe(0);
}
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.WEIRD_DREAM, defaultParty);
await runMysteryEncounterToEnd(game, 2);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});

View File

@ -5,6 +5,7 @@ import MockNineslice from "#app/test/utils/mocks/mocksContainer/mockNineslice";
import MockImage from "#app/test/utils/mocks/mocksContainer/mockImage";
import MockText from "#app/test/utils/mocks/mocksContainer/mockText";
import MockPolygon from "#app/test/utils/mocks/mocksContainer/mockPolygon";
import MockVideo from "#test/utils/mocks/mocksContainer/mockVideo";
export default class MockTextureManager {
@ -30,6 +31,7 @@ export default class MockTextureManager {
text: this.text.bind(this),
bitmapText: this.text.bind(this),
displayList: this.displayList,
video: this.video.bind(this)
};
}
@ -82,4 +84,10 @@ export default class MockTextureManager {
this.list.push(polygon);
return polygon;
}
video(x: number, y: number, key?: string) {
const video = new MockVideo(this, x, y, key);
this.list.push(video);
return video;
}
}

View File

@ -212,4 +212,8 @@ export default class MockContainer {
return this.list;
}
getByName(key: string) {
return this.list.find(v => v.name === key) ?? new MockContainer(this.textureManager, 0, 0);
}
}

View File

@ -0,0 +1,26 @@
import MockContainer from "#test/utils/mocks/mocksContainer/mockContainer";
export default class MockVideo extends MockContainer {
private video: HTMLVideoElement | null;
private videoTexture: Phaser.Textures.Texture | null;
private videoTextureSource: Phaser.Textures.TextureSource | null;
constructor(textureManager, x: number, y: number, key?: string) {
super(textureManager, x, y);
this.video = null;
this.videoTexture = null;
this.videoTextureSource = null;
}
stop(): this {
return this;
}
play(): this {
return this;
}
setLoop(value?: boolean): this {
return this;
}
}

View File

@ -85,12 +85,6 @@ export default class MysteryEncounterUiHandler extends UiHandler {
dexProgressIndicator.setScale(0.80);
this.dexProgressContainer.add(dexProgressIndicator);
this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains);
this.dexProgressContainer.on("pointerover", () => {
(this.scene as BattleScene).ui.showTooltip(null, i18next.t("mysteryEncounter:affects_pokedex"), true);
});
this.dexProgressContainer.on("pointerout", () => {
(this.scene as BattleScene).ui.hideTooltip();
});
}
show(args: any[]): boolean {
@ -555,7 +549,15 @@ export default class MysteryEncounterUiHandler extends UiHandler {
targets: this.dexProgressContainer,
y: -63,
ease: "Sine.easeInOut",
duration: 750
duration: 750,
onComplete: () => {
this.dexProgressContainer.on("pointerover", () => {
(this.scene as BattleScene).ui.showTooltip(null, i18next.t("mysteryEncounter:affects_pokedex"), true);
});
this.dexProgressContainer.on("pointerout", () => {
(this.scene as BattleScene).ui.hideTooltip();
});
}
});
} else if (!show && this.showDexProgress) {
this.showDexProgress = false;
@ -565,6 +567,10 @@ export default class MysteryEncounterUiHandler extends UiHandler {
y: -43,
ease: "Sine.easeInOut",
duration: 750,
onComplete: () => {
this.dexProgressContainer.off("pointerover");
this.dexProgressContainer.off("pointerout");
}
});
}
}