Merge pull request #145 from AsdarDevelops/weird-dream

Add Weird Dream encounter and slight reworks to Berries Abound/Fight or Flight
This commit is contained in:
ImperialSympathizer 2024-08-15 12:55:29 -04:00 committed by GitHub
commit dddbc093ab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
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");
}
});
}
}