add pokemon salesman encounter and affects pokedex UI display

This commit is contained in:
ImperialSympathizer 2024-07-22 15:15:42 -04:00
parent c775495e1f
commit 792af86f39
24 changed files with 433 additions and 304 deletions

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "encounter_radar.png",
"format": "RGBA8888",
"size": {
"w": 17,
"h": 16
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": false,
"sourceSize": {
"w": 15,
"h": 14
},
"spriteSourceSize": {
"x": 0,
"y": 0,
"w": 15,
"h": 14
},
"frame": {
"x": 1,
"y": 1,
"w": 15,
"h": 14
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:eb3445f19546ab36edb2909c89b8aa86:c8de156a28ef70ee4ddf70cffe1ba3ba:e7008b81ccf0cb0325145a809afa6165$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 255 B

View File

@ -67,7 +67,7 @@ import { UiTheme } from "#enums/ui-theme";
import { TimedEventManager } from "#app/timed-event-manager.js"; import { TimedEventManager } from "#app/timed-event-manager.js";
import i18next from "i18next"; import i18next from "i18next";
import IMysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter"; import IMysteryEncounter, { MysteryEncounterTier, MysteryEncounterVariant } from "./data/mystery-encounters/mystery-encounter";
import { allMysteryEncounters, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, mysteryEncountersByBiome, WIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters"; import { allMysteryEncounters, AVERAGE_ENCOUNTERS_PER_RUN_TARGET, BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT, mysteryEncountersByBiome, WEIGHT_INCREMENT_ON_SPAWN_MISS } from "./data/mystery-encounters/mystery-encounters";
import { MysteryEncounterData } from "#app/data/mystery-encounters/mystery-encounter-data"; import { MysteryEncounterData } from "#app/data/mystery-encounters/mystery-encounter-data";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
@ -1074,7 +1074,7 @@ export default class BattleScene extends SceneBase {
// let testStartingWeight = 0; // let testStartingWeight = 0;
// while (testStartingWeight < 3) { // while (testStartingWeight < 3) {
// calculateMEAggregateStats(this, testStartingWeight); // calculateMEAggregateStats(this, testStartingWeight);
// testStartingWeight += 1; // testStartingWeight += 2;
// } // }
// Check for mystery encounter // Check for mystery encounter
// Can only occur in place of a standard wild battle, waves 10-180 // Can only occur in place of a standard wild battle, waves 10-180
@ -1098,7 +1098,7 @@ export default class BattleScene extends SceneBase {
// Reset base spawn weight // Reset base spawn weight
this.mysteryEncounterData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT; this.mysteryEncounterData.encounterSpawnChance = BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT;
} else { } else {
this.mysteryEncounterData.encounterSpawnChance = sessionEncounterRate + WIGHT_INCREMENT_ON_SPAWN_MISS; this.mysteryEncounterData.encounterSpawnChance = sessionEncounterRate + WEIGHT_INCREMENT_ON_SPAWN_MISS;
} }
} }
} }

View File

@ -87,11 +87,17 @@ export const FieldTripEncounter: IMysteryEncounter =
if (!correctMove) { if (!correctMove) {
encounter.options[0].dialogue.selected = [ encounter.options[0].dialogue.selected = [
{ {
text: `${namespace}:incorrect`, text: `${namespace}:option:incorrect`,
speaker: `${namespace}:speaker`, speaker: `${namespace}:option:speaker`,
}, },
{ {
text: `${namespace}:lesson_learned`, text: `${namespace}:option:lesson_learned`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}:outro_bad`,
speaker: `${namespace}:speaker`,
}, },
]; ];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
@ -103,6 +109,12 @@ export const FieldTripEncounter: IMysteryEncounter =
text: `${namespace}:option:selected`, text: `${namespace}:option:selected`,
}, },
]; ];
encounter.dialogue.outro = [
{
text: `${namespace}:outro_good`,
speaker: `${namespace}:speaker`,
},
];
setEncounterExp(scene, [pokemon.id], 100); setEncounterExp(scene, [pokemon.id], 100);
} }
encounter.misc = { encounter.misc = {
@ -161,11 +173,23 @@ export const FieldTripEncounter: IMysteryEncounter =
if (!correctMove) { if (!correctMove) {
encounter.options[1].dialogue.selected = [ encounter.options[1].dialogue.selected = [
{ {
text: `${namespace}:incorrect`, text: `${namespace}:option:incorrect`,
speaker: `${namespace}:speaker`, speaker: `${namespace}:option:speaker`,
}, },
{ {
text: `${namespace}:lesson_learned`, text: `${namespace}:option:lesson_learned`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}:outro_bad`,
speaker: `${namespace}:speaker`,
},
];
encounter.dialogue.outro = [
{
text: `${namespace}:outro_bad`,
speaker: `${namespace}:speaker`,
}, },
]; ];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
@ -177,6 +201,12 @@ export const FieldTripEncounter: IMysteryEncounter =
text: `${namespace}:option:selected`, text: `${namespace}:option:selected`,
}, },
]; ];
encounter.dialogue.outro = [
{
text: `${namespace}:outro_good`,
speaker: `${namespace}:speaker`,
},
];
setEncounterExp(scene, [pokemon.id], 100); setEncounterExp(scene, [pokemon.id], 100);
} }
encounter.misc = { encounter.misc = {
@ -235,18 +265,20 @@ export const FieldTripEncounter: IMysteryEncounter =
if (!correctMove) { if (!correctMove) {
encounter.options[2].dialogue.selected = [ encounter.options[2].dialogue.selected = [
{ {
text: `${namespace}:incorrect`, text: `${namespace}:option:incorrect`,
speaker: `${namespace}:speaker`, speaker: `${namespace}:option:speaker`,
}, },
{ {
text: `${namespace}:lesson_learned`, text: `${namespace}:option:lesson_learned`,
}, },
]; ];
setEncounterExp( encounter.dialogue.outro = [
scene, {
scene.getParty().map((p) => p.id), text: `${namespace}:outro_bad`,
50 speaker: `${namespace}:speaker`,
); },
];
setEncounterExp(scene, scene.getParty().map((p) => p.id), 50);
} else { } else {
encounter.setDialogueToken("pokeName", pokemon.name); encounter.setDialogueToken("pokeName", pokemon.name);
encounter.setDialogueToken("move", move.getName()); encounter.setDialogueToken("move", move.getName());
@ -255,6 +287,12 @@ export const FieldTripEncounter: IMysteryEncounter =
text: `${namespace}:option:selected`, text: `${namespace}:option:selected`,
}, },
]; ];
encounter.dialogue.outro = [
{
text: `${namespace}:outro_good`,
speaker: `${namespace}:speaker`,
},
];
setEncounterExp(scene, [pokemon.id], 100); setEncounterExp(scene, [pokemon.id], 100);
} }
encounter.misc = { encounter.misc = {

View File

@ -150,7 +150,7 @@ export const FightOrFlightEncounter: IMysteryEncounter =
const primaryPokemon = encounter.options[1].primaryPokemon; const primaryPokemon = encounter.options[1].primaryPokemon;
if (primaryPokemon) { if (primaryPokemon) {
// Use primaryPokemon to execute the thievery // Use primaryPokemon to execute the thievery
await showEncounterText(scene, `${namespace}:option:2:steal_result`); await showEncounterText(scene, `${namespace}:option:2:special_result`);
leaveEncounterWithoutBattle(scene); leaveEncounterWithoutBattle(scene);
return; return;
} }

View File

@ -169,7 +169,7 @@ export const MysteriousChallengersEncounter: IMysteryEncounter =
// Spawn hard fight with ULTRA/GREAT reward (can improve with luck) // Spawn hard fight with ULTRA/GREAT reward (can improve with luck)
const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1];
setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ULTRA, ModifierTier.ULTRA, ModifierTier.GREAT, ModifierTier.GREAT], fillRemaining: true });
// Seed offsets to remove possibility of different trainers having exact same teams // Seed offsets to remove possibility of different trainers having exact same teams
let ret; let ret;
@ -197,7 +197,7 @@ export const MysteriousChallengersEncounter: IMysteryEncounter =
// To avoid player level snowballing from picking this option // To avoid player level snowballing from picking this option
encounter.expMultiplier = 0.9; encounter.expMultiplier = 0.9;
setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true }); setEncounterRewards(scene, { guaranteedModifierTiers: [ModifierTier.ROGUE, ModifierTier.ROGUE, ModifierTier.ULTRA, ModifierTier.GREAT], fillRemaining: true });
// Seed offsets to remove possibility of different trainers having exact same teams // Seed offsets to remove possibility of different trainers having exact same teams
let ret; let ret;

View File

@ -34,8 +34,8 @@ export const MysteriousChestEncounter: IMysteryEncounter =
]) ])
.withIntroDialogue([ .withIntroDialogue([
{ {
text: "${namespace}:intro", text: `${namespace}:intro`,
}, }
]) ])
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)

View File

@ -1,21 +1,23 @@
import { generateModifierTypeOption, leaveEncounterWithoutBattle, selectPokemonForOption, setEncounterExp, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { leaveEncounterWithoutBattle, transitionMysteryEncounterIntroVisuals, updatePlayerMoney, } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { StatusEffect } from "#app/data/status-effect";
import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import { modifierTypes } from "#app/modifier/modifier-type";
import { isNullOrUndefined, randSeedInt } from "#app/utils"; import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "../../../battle-scene"; import BattleScene from "../../../battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter";
import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
import { MoneyRequirement } from "../mystery-encounter-requirements"; import { MoneyRequirement } from "../mystery-encounter-requirements";
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { catchPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { applyDamageToPokemon, getRandomSpeciesByStarterTier } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species"; import { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species";
import { Species } from "#enums/species"; import { Species } from "#enums/species";
import { PokeballType } from "#app/data/pokeball";
import { EnemyPokemon, PlayerPokemon } from "#app/field/pokemon";
import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { showEncounterDialogue } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import PokemonData from "#app/system/pokemon-data";
/** the i18n namespace for this encounter */ /** the i18n namespace for this encounter */
const namespace = "mysteryEncounter:pokemonSalesman"; const namespace = "mysteryEncounter:pokemonSalesman";
const MAX_POKEMON_PRICE_MULTIPLIER = 6;
/** /**
* Pokemon Salesman encounter. * Pokemon Salesman encounter.
* @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/36 | GitHub Issue #36} * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/36 | GitHub Issue #36}
@ -25,7 +27,8 @@ export const PokemonSalesmanEncounter: IMysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.POKEMON_SALESMAN) MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.POKEMON_SALESMAN)
.withEncounterTier(MysteryEncounterTier.ULTRA) .withEncounterTier(MysteryEncounterTier.ULTRA)
.withSceneWaveRangeRequirement(10, 180) .withSceneWaveRangeRequirement(10, 180)
.withSceneRequirement(new MoneyRequirement(null, 8)) // Some costs may not be as significant, this is the max you'd pay .withSceneRequirement(new MoneyRequirement(null, MAX_POKEMON_PRICE_MULTIPLIER)) // Some costs may not be as significant, this is the max you'd pay
.withAutoHideIntroVisuals(false)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
spriteKey: "pokemon_salesman", spriteKey: "pokemon_salesman",
@ -56,8 +59,7 @@ export const PokemonSalesmanEncounter: IMysteryEncounter =
species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5])); species = getPokemonSpecies(getRandomSpeciesByStarterTier([0, 5]));
} }
let pokemon: PlayerPokemon;
let pokemon;
if (isNullOrUndefined(species.abilityHidden) || randSeedInt(100) === 0) { if (isNullOrUndefined(species.abilityHidden) || randSeedInt(100) === 0) {
// If no HA mon found or you roll 1%, give shiny Magikarp // If no HA mon found or you roll 1%, give shiny Magikarp
species = getPokemonSpecies(Species.MAGIKARP); species = getPokemonSpecies(Species.MAGIKARP);
@ -68,219 +70,82 @@ export const PokemonSalesmanEncounter: IMysteryEncounter =
pokemon = scene.addPlayerPokemon(species, 5, hiddenIndex, species.formIndex); pokemon = scene.addPlayerPokemon(species, 5, hiddenIndex, species.formIndex);
} }
const spriteKey = pokemon.getSpriteId();
const spriteRoot = pokemon.getSpriteAtlasPath();
encounter.spriteConfigs.push({
spriteKey: spriteKey,
fileRoot: spriteRoot,
hasShadow: true,
repeat: true,
isPokemon: true
});
const starterTier = speciesStarters[species.speciesId]; const starterTier = speciesStarters[species.speciesId];
// Prices by starter tier: 8/6.4/4.8/4/4 // Prices decrease by starter tier less than 5, but only reduces cost by half at max
let priceMultiplier = 8 * (Math.max(starterTier, 2.5) / 5); let priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER * (Math.max(starterTier, 2.5) / 5);
if (pokemon.shiny) { if (pokemon.shiny) {
// Always max price for shiny, and add special message to intro // Always max price for shiny (flip HA back to normal), and add special messaging
priceMultiplier = 8; priceMultiplier = MAX_POKEMON_PRICE_MULTIPLIER;
encounter.setDialogueToken("specialShinyText", `$t(${namespace}:shiny)`); pokemon.abilityIndex = 0;
} else { encounter.dialogue.encounterOptionsDialogue.description = `${namespace}:description_shiny`;
encounter.setDialogueToken("specialShinyText", ""); encounter.options[0].dialogue.buttonTooltip = `${namespace}:option:1:tooltip_shiny`;
} }
const price = scene.getWaveMoneyAmount(priceMultiplier);
encounter.setDialogueToken("purchasePokemon", pokemon.name); encounter.setDialogueToken("purchasePokemon", pokemon.name);
encounter.setDialogueToken("price", pokemon.name); encounter.setDialogueToken("price", price.toString());
encounter.misc = { encounter.misc = {
money: scene.getWaveMoneyAmount(priceMultiplier), money: price,
pokemon: pokemon, pokemon: pokemon
// shiny: pokemon.shiny
}; };
pokemon.calculateStats(); pokemon.calculateStats();
return true; return true;
}) })
.withSimpleOption({
buttonLabel: `${namespace}:option:1:label`,
buttonTooltip: `${namespace}:option:1:tooltip`,
selected: [
{
text: `${namespace}:option:selected`,
},
],
},
async (scene: BattleScene) => {
// Choose Cheap Option
const encounter = scene.currentBattle.mysteryEncounter;
const cost = encounter.misc.money;
// const purchasedPokemon = encounter.misc.pokemon;
// Update money
updatePlayerMoney(scene, -cost);
leaveEncounterWithoutBattle(scene);
})
.withOption( .withOption(
new MysteryEncounterOptionBuilder() new MysteryEncounterOptionBuilder()
.withOptionMode(EncounterOptionMode.DEFAULT) .withOptionMode(EncounterOptionMode.DEFAULT_OR_SPECIAL)
.withSceneMoneyRequirement(0, 2) // Wave scaling money multiplier of 2 .withHasDexProgress(true)
.withSceneMoneyRequirement(null, MAX_POKEMON_PRICE_MULTIPLIER) // Wave scaling money multiplier of 2
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option:1:label`, buttonLabel: `${namespace}:option:1:label`,
buttonTooltip: `${namespace}:option:1:tooltip`, buttonTooltip: `${namespace}:option:1:tooltip`,
selected: [ selected: [
{ {
text: `${namespace}:option:selected`, text: `${namespace}:option:1:selected_message`,
}, }
], ],
}) })
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Update money
updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney);
// Calculate modifiers and dialogue tokens
const modifiers = [
generateModifierTypeOption(scene, modifierTypes.BASE_STAT_BOOSTER).type,
generateModifierTypeOption(scene, modifierTypes.BASE_STAT_BOOSTER).type,
];
encounter.setDialogueToken("boost1", modifiers[0].name);
encounter.setDialogueToken("boost2", modifiers[1].name);
encounter.misc = {
chosenPokemon: pokemon,
modifiers: modifiers,
};
};
// Only Pokemon that can gain benefits are above 1/3rd HP with no status
const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected
const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon);
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}:invalid_selection`);
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter);
})
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
// Choose Cheap Option
const encounter = scene.currentBattle.mysteryEncounter; const encounter = scene.currentBattle.mysteryEncounter;
const chosenPokemon = encounter.misc.chosenPokemon; const cost = encounter.misc.money;
const modifiers = encounter.misc.modifiers; const purchasedPokemon = encounter.misc.pokemon as PlayerPokemon;
for (const modType of modifiers) { // Update money
const modifier = modType.newModifier(chosenPokemon); updatePlayerMoney(scene, -cost, true, false);
await scene.addModifier(modifier, true, false, false, true);
}
scene.updateModifiers(true);
leaveEncounterWithoutBattle(scene); // Show dialogue
}) await showEncounterDialogue(scene, `${namespace}:option:1:selected_dialogue`, `${namespace}:speaker`);
.withPostOptionPhase(async (scene: BattleScene) => { await transitionMysteryEncounterIntroVisuals(scene);
// Damage and status applied after dealer leaves (to make thematic sense)
const encounter = scene.currentBattle.mysteryEncounter;
const chosenPokemon = encounter.misc.chosenPokemon;
// Pokemon takes 1/3 max HP damage // "Catch" purchased pokemon
applyDamageToPokemon(scene, chosenPokemon, Math.floor(chosenPokemon.getMaxHp() / 3)); const data = new PokemonData(purchasedPokemon);
data.player = false;
await catchPokemon(scene, data.toPokemon(scene) as EnemyPokemon, null, PokeballType.POKEBALL, true);
// Roll for poison (80%) leaveEncounterWithoutBattle(scene, true);
if (randSeedInt(10) < 8) {
if (chosenPokemon.trySetStatus(StatusEffect.TOXIC)) {
// Toxic applied
queueEncounterMessage(scene, `${namespace}:bad_poison`);
} else {
// Pokemon immune or something else prevents status
queueEncounterMessage(scene, `${namespace}:damage_only`);
}
} else {
queueEncounterMessage(scene, `${namespace}:damage_only`);
}
setEncounterExp(scene, [chosenPokemon.id], 100);
chosenPokemon.updateInfo();
})
.build()
)
.withOption(
new MysteryEncounterOptionBuilder()
.withOptionMode(EncounterOptionMode.DISABLED_OR_DEFAULT)
.withSceneMoneyRequirement(0, 5) // Wave scaling money multiplier of 5
.withDialogue({
buttonLabel: `${namespace}:option:2:label`,
buttonTooltip: `${namespace}:option:2:tooltip`,
selected: [
{
text: `${namespace}:option:selected`,
},
],
})
.withPreOptionPhase(async (scene: BattleScene): Promise<boolean> => {
const encounter = scene.currentBattle.mysteryEncounter;
const onPokemonSelected = (pokemon: PlayerPokemon) => {
// Update money
updatePlayerMoney(scene, -(encounter.options[1].requirements[0] as MoneyRequirement).requiredMoney);
// Calculate modifiers and dialogue tokens
const modifiers = [
generateModifierTypeOption(scene, modifierTypes.BASE_STAT_BOOSTER).type,
generateModifierTypeOption(scene, modifierTypes.BASE_STAT_BOOSTER).type,
];
encounter.setDialogueToken("boost1", modifiers[0].name);
encounter.setDialogueToken("boost2", modifiers[1].name);
encounter.misc = {
chosenPokemon: pokemon,
modifiers: modifiers,
};
};
// Only Pokemon that can gain benefits are above 1/3rd HP with no status
const selectableFilter = (pokemon: Pokemon) => {
// If pokemon meets primary pokemon reqs, it can be selected
const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon);
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}:invalid_selection`);
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter);
})
.withOptionPhase(async (scene: BattleScene) => {
// Choose Expensive Option
const encounter = scene.currentBattle.mysteryEncounter;
const chosenPokemon = encounter.misc.chosenPokemon;
const modifiers = encounter.misc.modifiers;
for (const modType of modifiers) {
const modifier = modType.newModifier(chosenPokemon);
await scene.addModifier(modifier, true, false, false, true);
}
scene.updateModifiers(true);
leaveEncounterWithoutBattle(scene);
})
.withPostOptionPhase(async (scene: BattleScene) => {
// Status applied after dealer leaves (to make thematic sense)
const encounter = scene.currentBattle.mysteryEncounter;
const chosenPokemon = encounter.misc.chosenPokemon;
// Roll for poison (20%)
if (randSeedInt(10) < 2) {
if (chosenPokemon.trySetStatus(StatusEffect.POISON)) {
// Poison applied
queueEncounterMessage(scene, `${namespace}:poison`);
} else {
// Pokemon immune or something else prevents status
queueEncounterMessage(scene, `${namespace}:no_bad_effects`);
}
} else {
queueEncounterMessage(scene, `${namespace}:no_bad_effects`);
}
setEncounterExp(scene, [chosenPokemon.id], 100);
chosenPokemon.updateInfo();
}) })
.build() .build()
) )
.withSimpleOption( .withSimpleOption(
{ {
buttonLabel: `${namespace}:option:3:label`, buttonLabel: `${namespace}:option:2:label`,
buttonTooltip: `${namespace}:option:3:tooltip`, buttonTooltip: `${namespace}:option:2:tooltip`,
selected: [
{
text: `${namespace}:option:2:selected`,
},
],
}, },
async (scene: BattleScene) => { async (scene: BattleScene) => {
// Leave encounter with no rewards or exp // Leave encounter with no rewards or exp

View File

@ -3,7 +3,7 @@ import { EnemyPartyConfig, initBattleWithEnemyConfig, selectPokemonForOption, se
import { getNatureName, Nature } from "#app/data/nature"; import { getNatureName, Nature } from "#app/data/nature";
import { speciesStarters } from "#app/data/pokemon-species"; import { speciesStarters } from "#app/data/pokemon-species";
import { Stat } from "#app/data/pokemon-stat"; import { Stat } from "#app/data/pokemon-stat";
import { PlayerPokemon } from "#app/field/pokemon"; import Pokemon, { PlayerPokemon } from "#app/field/pokemon";
import { pokemonInfo } from "#app/locales/en/pokemon-info"; import { pokemonInfo } from "#app/locales/en/pokemon-info";
import { PokemonHeldItemModifier } from "#app/modifier/modifier"; import { PokemonHeldItemModifier } from "#app/modifier/modifier";
import { PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { PokemonHeldItemModifierType } from "#app/modifier/modifier-type";
@ -16,7 +16,7 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "../../../battle-scene"; import BattleScene from "../../../battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter"; import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter";
import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
/** The i18n namespace for the encounter */ /** The i18n namespace for the encounter */
const namespace = "mysteryEncounter:trainingSession"; const namespace = "mysteryEncounter:trainingSession";
@ -27,11 +27,10 @@ const namespace = "mysteryEncounter:trainingSession";
* @see For biome requirements check {@linkcode mysteryEncountersByBiome} * @see For biome requirements check {@linkcode mysteryEncountersByBiome}
*/ */
export const TrainingSessionEncounter: IMysteryEncounter = export const TrainingSessionEncounter: IMysteryEncounter =
MysteryEncounterBuilder.withEncounterType( MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.TRAINING_SESSION)
MysteryEncounterType.TRAINING_SESSION
)
.withEncounterTier(MysteryEncounterTier.ULTRA) .withEncounterTier(MysteryEncounterTier.ULTRA)
.withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180
.withScenePartySizeRequirement(2, 6, true) // Must have at least 2 unfainted pokemon in party
.withHideWildIntroMessage(true) .withHideWildIntroMessage(true)
.withIntroSpriteConfigs([ .withIntroSpriteConfigs([
{ {
@ -46,7 +45,7 @@ export const TrainingSessionEncounter: IMysteryEncounter =
.withIntroDialogue([ .withIntroDialogue([
{ {
text: `${namespace}:intro`, text: `${namespace}:intro`,
}, }
]) ])
.withTitle(`${namespace}:title`) .withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`) .withDescription(`${namespace}:description`)
@ -54,6 +53,7 @@ export const TrainingSessionEncounter: IMysteryEncounter =
.withOption( .withOption(
new MysteryEncounterOptionBuilder() new MysteryEncounterOptionBuilder()
.withOptionMode(EncounterOptionMode.DEFAULT) .withOptionMode(EncounterOptionMode.DEFAULT)
.withHasDexProgress(true)
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option:1:label`, buttonLabel: `${namespace}:option:1:label`,
buttonTooltip: `${namespace}:option:1:tooltip`, buttonTooltip: `${namespace}:option:1:tooltip`,
@ -71,7 +71,17 @@ export const TrainingSessionEncounter: IMysteryEncounter =
}; };
}; };
return selectPokemonForOption(scene, onPokemonSelected); // Only Pokemon that are not KOed/legal can be trained
const selectableFilter = (pokemon: Pokemon) => {
const meetsReqs = pokemon.isAllowedInBattle();
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}:invalid_selection`);
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter);
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter; const encounter = scene.currentBattle.mysteryEncounter;
@ -187,6 +197,7 @@ export const TrainingSessionEncounter: IMysteryEncounter =
.withOption( .withOption(
new MysteryEncounterOptionBuilder() new MysteryEncounterOptionBuilder()
.withOptionMode(EncounterOptionMode.DEFAULT) .withOptionMode(EncounterOptionMode.DEFAULT)
.withHasDexProgress(true)
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option:2:label`, buttonLabel: `${namespace}:option:2:label`,
buttonTooltip: `${namespace}:option:2:tooltip`, buttonTooltip: `${namespace}:option:2:tooltip`,
@ -220,7 +231,17 @@ export const TrainingSessionEncounter: IMysteryEncounter =
}); });
}; };
return selectPokemonForOption(scene, onPokemonSelected); // Only Pokemon that are not KOed/legal can be trained
const selectableFilter = (pokemon: Pokemon) => {
const meetsReqs = pokemon.isAllowedInBattle();
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}:invalid_selection`);
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter);
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter; const encounter = scene.currentBattle.mysteryEncounter;
@ -269,6 +290,7 @@ export const TrainingSessionEncounter: IMysteryEncounter =
.withOption( .withOption(
new MysteryEncounterOptionBuilder() new MysteryEncounterOptionBuilder()
.withOptionMode(EncounterOptionMode.DEFAULT) .withOptionMode(EncounterOptionMode.DEFAULT)
.withHasDexProgress(true)
.withDialogue({ .withDialogue({
buttonLabel: `${namespace}:option:3:label`, buttonLabel: `${namespace}:option:3:label`,
buttonTooltip: `${namespace}:option:3:tooltip`, buttonTooltip: `${namespace}:option:3:tooltip`,
@ -311,7 +333,17 @@ export const TrainingSessionEncounter: IMysteryEncounter =
}); });
}; };
return selectPokemonForOption(scene, onPokemonSelected); // Only Pokemon that are not KOed/legal can be trained
const selectableFilter = (pokemon: Pokemon) => {
const meetsReqs = pokemon.isAllowedInBattle();
if (!meetsReqs) {
return getEncounterText(scene, `${namespace}:invalid_selection`);
}
return null;
};
return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter);
}) })
.withOptionPhase(async (scene: BattleScene) => { .withOptionPhase(async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter; const encounter = scene.currentBattle.mysteryEncounter;
@ -393,23 +425,12 @@ export const TrainingSessionEncounter: IMysteryEncounter =
) )
.build(); .build();
function getEnemyConfig( function getEnemyConfig(scene: BattleScene, playerPokemon: PlayerPokemon,segments: number,modifiers: ModifiersHolder): EnemyPartyConfig {
scene: BattleScene,
playerPokemon: PlayerPokemon,
segments: number,
modifiers: ModifiersHolder
): EnemyPartyConfig {
playerPokemon.resetSummonData(); playerPokemon.resetSummonData();
// Passes modifiers by reference // Passes modifiers by reference
modifiers.value = scene.findModifiers( modifiers.value = scene.findModifiers((m) => m instanceof PokemonHeldItemModifier && (m as PokemonHeldItemModifier).pokemonId === playerPokemon.id) as PokemonHeldItemModifier[];
(m) => const modifierTypes = modifiers.value.map((mod) => mod.type) as PokemonHeldItemModifierType[];
m instanceof PokemonHeldItemModifier &&
(m as PokemonHeldItemModifier).pokemonId === playerPokemon.id
) as PokemonHeldItemModifier[];
const modifierTypes = modifiers.value.map(
(mod) => mod.type
) as PokemonHeldItemModifierType[];
const data = new PokemonData(playerPokemon); const data = new PokemonData(playerPokemon);
return { return {

View File

@ -6,6 +6,7 @@ import * as Utils from "../../utils";
import { Type } from "../type"; import { Type } from "../type";
import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements"; import { EncounterPokemonRequirement, EncounterSceneRequirement, MoneyRequirement, TypeRequirement } from "./mystery-encounter-requirements";
import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement"; import { CanLearnMoveRequirement, CanLearnMoveRequirementOptions } from "./requirements/can-learn-move-requirement";
import { isNullOrUndefined } from "../../utils";
export enum EncounterOptionMode { export enum EncounterOptionMode {
/** Default style */ /** Default style */
@ -23,6 +24,9 @@ export type OptionPhaseCallback = (scene: BattleScene) => Promise<void | boolean
export default interface MysteryEncounterOption { export default interface MysteryEncounterOption {
optionMode: EncounterOptionMode; optionMode: EncounterOptionMode;
hasDexProgress?: boolean;
requirements?: EncounterSceneRequirement[]; requirements?: EncounterSceneRequirement[];
primaryPokemonRequirements?: EncounterPokemonRequirement[]; primaryPokemonRequirements?: EncounterPokemonRequirement[];
secondaryPokemonRequirements?: EncounterPokemonRequirement[]; secondaryPokemonRequirements?: EncounterPokemonRequirement[];
@ -47,6 +51,7 @@ export default interface MysteryEncounterOption {
export default class MysteryEncounterOption implements MysteryEncounterOption { export default class MysteryEncounterOption implements MysteryEncounterOption {
constructor(option: MysteryEncounterOption) { constructor(option: MysteryEncounterOption) {
Object.assign(this, option); Object.assign(this, option);
this.hasDexProgress = !isNullOrUndefined(this.hasDexProgress) ? this.hasDexProgress : false;
this.requirements = this.requirements ? this.requirements : []; this.requirements = this.requirements ? this.requirements : [];
this.primaryPokemonRequirements = this.primaryPokemonRequirements ? this.primaryPokemonRequirements : []; this.primaryPokemonRequirements = this.primaryPokemonRequirements ? this.primaryPokemonRequirements : [];
this.secondaryPokemonRequirements = this.secondaryPokemonRequirements ? this.secondaryPokemonRequirements : []; this.secondaryPokemonRequirements = this.secondaryPokemonRequirements ? this.secondaryPokemonRequirements : [];
@ -155,6 +160,10 @@ export class MysteryEncounterOptionBuilder implements Partial<MysteryEncounterOp
return Object.assign(this, { optionMode }); return Object.assign(this, { optionMode });
} }
withHasDexProgress(hasDexProgress: boolean): this & Required<Pick<MysteryEncounterOption, "hasDexProgress">> {
return Object.assign(this, { hasDexProgress: hasDexProgress });
}
withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<MysteryEncounterOption, "requirements">> { withSceneRequirement(requirement: EncounterSceneRequirement): this & Required<Pick<MysteryEncounterOption, "requirements">> {
this.requirements.push(requirement); this.requirements.push(requirement);
return Object.assign(this, { requirements: this.requirements }); return Object.assign(this, { requirements: this.requirements });

View File

@ -144,20 +144,23 @@ export class WeatherRequirement extends EncounterSceneRequirement {
export class PartySizeRequirement extends EncounterSceneRequirement { export class PartySizeRequirement extends EncounterSceneRequirement {
partySizeRange: [number, number]; partySizeRange: [number, number];
excludeFainted: boolean;
/** /**
* Used for specifying a party size requirement * Used for specifying a party size requirement
* If min and max are equivalent, will check for exact size * If min and max are equivalent, will check for exact size
* @param partySizeRange - [min, max] * @param partySizeRange - [min, max]
* @param excludeFainted
*/ */
constructor(partySizeRange: [number, number]) { constructor(partySizeRange: [number, number], excludeFainted: boolean) {
super(); super();
this.partySizeRange = partySizeRange; this.partySizeRange = partySizeRange;
this.excludeFainted = excludeFainted;
} }
meetsRequirement(scene: BattleScene): boolean { meetsRequirement(scene: BattleScene): boolean {
if (!isNullOrUndefined(this?.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) { if (!isNullOrUndefined(this?.partySizeRange) && this.partySizeRange?.[0] <= this.partySizeRange?.[1]) {
const partySize = scene.getParty().length; const partySize = this.excludeFainted ? scene.getParty().filter(p => p.isAllowedInBattle()).length : scene.getParty().length;
if (partySize >= 0 && (this?.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this?.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) { if (partySize >= 0 && (this?.partySizeRange?.[0] >= 0 && this.partySizeRange?.[0] > partySize) || (this?.partySizeRange?.[1] >= 0 && this.partySizeRange?.[1] < partySize)) {
return false; return false;
} }

View File

@ -403,7 +403,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
options?: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]] = [null, null]; options?: [MysteryEncounterOption, MysteryEncounterOption, ...MysteryEncounterOption[]] = [null, null];
spriteConfigs?: MysteryEncounterSpriteConfig[]; spriteConfigs?: MysteryEncounterSpriteConfig[];
dialogue?: MysteryEncounterDialogue; dialogue?: MysteryEncounterDialogue = {};
encounterTier?: MysteryEncounterTier; encounterTier?: MysteryEncounterTier;
encounterAnimations?: EncounterAnim[]; encounterAnimations?: EncounterAnim[];
requirements?: EncounterSceneRequirement[] = []; requirements?: EncounterSceneRequirement[] = [];
@ -460,6 +460,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* There should be at least 2 options defined and no more than 4. * There should be at least 2 options defined and no more than 4.
* If complex use {@linkcode MysteryEncounterBuilder.withOption} * If complex use {@linkcode MysteryEncounterBuilder.withOption}
* *
* @param hasDexProgress -
* @param dialogue - {@linkcode OptionTextDisplay} * @param dialogue - {@linkcode OptionTextDisplay}
* @param callback - {@linkcode OptionPhaseCallback} * @param callback - {@linkcode OptionPhaseCallback}
* @returns * @returns
@ -468,6 +469,24 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
return this.withOption(new MysteryEncounterOptionBuilder().withOptionMode(EncounterOptionMode.DEFAULT).withDialogue(dialogue).withOptionPhase(callback).build()); return this.withOption(new MysteryEncounterOptionBuilder().withOptionMode(EncounterOptionMode.DEFAULT).withDialogue(dialogue).withOptionPhase(callback).build());
} }
/**
* Defines an option + phasefor the encounter.
* Use for easy/streamlined options.
* There should be at least 2 options defined and no more than 4.
* If complex use {@linkcode MysteryEncounterBuilder.withOption}
*
* @param dialogue - {@linkcode OptionTextDisplay}
* @param callback - {@linkcode OptionPhaseCallback}
* @returns
*/
withSimpleDexProgressOption(dialogue: OptionTextDisplay, callback: OptionPhaseCallback): this & Pick<IMysteryEncounter, "options"> {
return this.withOption(new MysteryEncounterOptionBuilder()
.withOptionMode(EncounterOptionMode.DEFAULT)
.withHasDexProgress(true)
.withDialogue(dialogue)
.withOptionPhase(callback).build());
}
/** /**
* Defines the sprites that will be shown on the enemy field when the encounter spawns * Defines the sprites that will be shown on the enemy field when the encounter spawns
* Can be one or more sprites, recommended not to exceed 4 * Can be one or more sprites, recommended not to exceed 4
@ -478,7 +497,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
return Object.assign(this, { spriteConfigs: spriteConfigs }); return Object.assign(this, { spriteConfigs: spriteConfigs });
} }
withIntroDialogue(dialogue: MysteryEncounterDialogue["intro"] = []) { withIntroDialogue(dialogue: MysteryEncounterDialogue["intro"] = []): this {
this.dialogue = {...this.dialogue, intro: dialogue }; this.dialogue = {...this.dialogue, intro: dialogue };
return this; return this;
} }
@ -550,7 +569,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* @param max optional max wave. If not given, defaults to min => exact wave * @param max optional max wave. If not given, defaults to min => exact wave
* @returns * @returns
*/ */
withSceneWaveRangeRequirement(min: number, max?: number) { withSceneWaveRangeRequirement(min: number, max?: number): this & Required<Pick<IMysteryEncounter, "requirements">> {
return this.withSceneRequirement(new WaveRangeRequirement([min, max ?? min])); return this.withSceneRequirement(new WaveRangeRequirement([min, max ?? min]));
} }
@ -559,10 +578,11 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* *
* @param min min wave (or exact size if only min is given) * @param min min wave (or exact size if only min is given)
* @param max optional max size. If not given, defaults to min => exact wave * @param max optional max size. If not given, defaults to min => exact wave
* @param excludeFainted - if true, only counts unfainted mons
* @returns * @returns
*/ */
withScenePartySizeRequirement(min: number, max?: number) { withScenePartySizeRequirement(min: number, max?: number, excludeFainted?: boolean): this & Required<Pick<IMysteryEncounter, "requirements">> {
return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min])); return this.withSceneRequirement(new PartySizeRequirement([min, max ?? min], excludeFainted));
} }
/** /**
@ -700,7 +720,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* @param title - title of the encounter * @param title - title of the encounter
* @returns * @returns
*/ */
withTitle(title: string) { withTitle(title: string): this {
const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {};
this.dialogue = { this.dialogue = {
@ -720,7 +740,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* @param description - description of the encounter * @param description - description of the encounter
* @returns * @returns
*/ */
withDescription(description: string) { withDescription(description: string): this {
const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {};
this.dialogue = { this.dialogue = {
@ -740,7 +760,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* @param query - query to use for the encounter * @param query - query to use for the encounter
* @returns * @returns
*/ */
withQuery(query: string) { withQuery(query: string): this {
const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {}; const encounterOptionsDialogue = this.dialogue.encounterOptionsDialogue ?? {};
this.dialogue = { this.dialogue = {
@ -760,7 +780,7 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
* @param dialogue - outro dialogue/s * @param dialogue - outro dialogue/s
* @returns * @returns
*/ */
withOutroDialogue(dialogue: MysteryEncounterDialogue["outro"] = []) { withOutroDialogue(dialogue: MysteryEncounterDialogue["outro"] = []): this {
this.dialogue = {...this.dialogue, outro: dialogue }; this.dialogue = {...this.dialogue, outro: dialogue };
return this; return this;
} }
@ -768,10 +788,9 @@ export class MysteryEncounterBuilder implements Partial<IMysteryEncounter> {
/** /**
* Builds the mystery encounter * Builds the mystery encounter
* *
* @param this - MysteryEncounter
* @returns * @returns
*/ */
build(this: IMysteryEncounter) { build(this: IMysteryEncounter): IMysteryEncounter {
return new IMysteryEncounter(this); return new IMysteryEncounter(this);
} }
} }

View File

@ -18,7 +18,7 @@ import { PokemonSalesmanEncounter } from "#app/data/mystery-encounters/encounter
// Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * <number of missed spawns>) / 256 // 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; export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1;
export const WIGHT_INCREMENT_ON_SPAWN_MISS = 5; export const WEIGHT_INCREMENT_ON_SPAWN_MISS = 5;
export const AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 15; export const AVERAGE_ENCOUNTERS_PER_RUN_TARGET = 15;
export const EXTREME_ENCOUNTER_BIOMES = [ export const EXTREME_ENCOUNTER_BIOMES = [

View File

@ -63,10 +63,12 @@ export function showEncounterText(scene: BattleScene, contentKey: string, callba
* @param scene * @param scene
* @param textContentKey * @param textContentKey
* @param speakerContentKey * @param speakerContentKey
* @param callback * @param callbackDelay
*/ */
export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, callback?: Function) { export function showEncounterDialogue(scene: BattleScene, textContentKey: string, speakerContentKey: string, callbackDelay: number = 0): Promise<void> {
const text: string = getEncounterText(scene, textContentKey); return new Promise<void>(resolve => {
const speaker: string = getEncounterText(scene, speakerContentKey); const text: string = getEncounterText(scene, textContentKey);
scene.ui.showDialogue(text, speaker, null, callback, 0, 0); const speaker: string = getEncounterText(scene, speakerContentKey);
scene.ui.showDialogue(text, speaker, null, () => resolve(), callbackDelay);
});
} }

View File

@ -1,7 +1,7 @@
import { BattlerIndex, BattleType } from "#app/battle"; import { BattlerIndex, BattleType } from "#app/battle";
import { biomeLinks } from "#app/data/biomes"; import { biomeLinks } from "#app/data/biomes";
import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option"; import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
import { WIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters"; import { WEIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon"; import Pokemon, { FieldPosition, PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier"; import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier";
@ -317,17 +317,19 @@ export function initCustomMovesForEncounter(scene: BattleScene, moves: Moves | M
* @param changeValue * @param changeValue
* @param playSound * @param playSound
*/ */
export function updatePlayerMoney(scene: BattleScene, changeValue: number, playSound: boolean = true) { export function updatePlayerMoney(scene: BattleScene, changeValue: number, playSound: boolean = true, showMessage: boolean = true) {
scene.money += changeValue; scene.money = Math.min(Math.max(scene.money + changeValue, 0), Number.MAX_SAFE_INTEGER);
scene.updateMoneyText(); scene.updateMoneyText();
scene.animateMoneyChanged(false); scene.animateMoneyChanged(false);
if (playSound) { if (playSound) {
scene.playSound("buy"); scene.playSound("buy");
} }
if (changeValue < 0) { if (showMessage) {
scene.queueMessage(i18next.t("mysteryEncounter:paid_money", { amount: -changeValue }), null, true); if (changeValue < 0) {
} else { scene.queueMessage(i18next.t("mysteryEncounter:paid_money", { amount: -changeValue }), null, true);
scene.queueMessage(i18next.t("mysteryEncounter:receive_money", { amount: changeValue }), null, true); } else {
scene.queueMessage(i18next.t("mysteryEncounter:receive_money", { amount: changeValue }), null, true);
}
} }
} }
@ -399,6 +401,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p
}).concat({ }).concat({
label: i18next.t("menu:cancel"), label: i18next.t("menu:cancel"),
handler: () => { handler: () => {
scene.ui.clearText();
scene.ui.setMode(Mode.MYSTERY_ENCOUNTER); scene.ui.setMode(Mode.MYSTERY_ENCOUNTER);
resolve(false); resolve(false);
return true; return true;
@ -730,12 +733,18 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n
const numRuns = 1000; const numRuns = 1000;
let run = 0; let run = 0;
const targetEncountersPerRun = 15; // AVERAGE_ENCOUNTERS_PER_RUN_TARGET const targetEncountersPerRun = 15; // AVERAGE_ENCOUNTERS_PER_RUN_TARGET
const biomes = Object.keys(Biome).filter(key => isNaN(Number(key)));
const alwaysPickTheseBiomes = [Biome.ISLAND, Biome.ABYSS, Biome.WASTELAND, Biome.FAIRY_CAVE, Biome.TEMPLE, Biome.LABORATORY, Biome.SPACE, Biome.WASTELAND];
const calculateNumEncounters = (): number[] => { const calculateNumEncounters = (): any[] => {
let encounterRate = baseSpawnWeight; // BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT let encounterRate = baseSpawnWeight; // BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT
const numEncounters = [0, 0, 0, 0]; const numEncounters = [0, 0, 0, 0];
const encountersByBiome = new Map<string, number>(biomes.map(b => [b, 0]));
const validMEfloorsByBiome = new Map<string, number>(biomes.map(b => [b, 0]));
let currentBiome = Biome.TOWN; let currentBiome = Biome.TOWN;
let currentArena = scene.newArena(currentBiome); let currentArena = scene.newArena(currentBiome);
scene.setSeed(Utils.randomString(24));
scene.resetSeed();
for (let i = 10; i < 180; i++) { for (let i = 10; i < 180; i++) {
// Boss // Boss
if (i % 10 === 0) { if (i % 10 === 0) {
@ -748,10 +757,17 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n
let biomes: Biome[]; let biomes: Biome[];
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
biomes = (biomeLinks[currentBiome] as (Biome | [Biome, integer])[]) biomes = (biomeLinks[currentBiome] as (Biome | [Biome, integer])[])
.filter(b => !Array.isArray(b) || !Utils.randSeedInt(b[1])) .filter(b => {
return !Array.isArray(b) || !Utils.randSeedInt(b[1]);
})
.map(b => !Array.isArray(b) ? b : b[0]); .map(b => !Array.isArray(b) ? b : b[0]);
}, i); }, i * 100);
currentBiome = biomes[Utils.randSeedInt(biomes.length)]; const specialBiomes = biomes.filter(b => alwaysPickTheseBiomes.includes(b));
if (specialBiomes.length > 0) {
currentBiome = specialBiomes[Utils.randSeedInt(specialBiomes.length)];
} else {
currentBiome = biomes[Utils.randSeedInt(biomes.length)];
}
} else if (biomeLinks.hasOwnProperty(currentBiome)) { } else if (biomeLinks.hasOwnProperty(currentBiome)) {
currentBiome = (biomeLinks[currentBiome] as Biome); currentBiome = (biomeLinks[currentBiome] as Biome);
} else { } else {
@ -778,6 +794,7 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n
// Otherwise, roll encounter // Otherwise, roll encounter
const roll = Utils.randSeedInt(256); const roll = Utils.randSeedInt(256);
validMEfloorsByBiome.set(Biome[currentBiome], validMEfloorsByBiome.get(Biome[currentBiome]) + 1);
// If total number of encounters is lower than expected for the run, slightly favor a new encounter // If total number of encounters is lower than expected for the run, slightly favor a new encounter
// Do the reverse as well // Do the reverse as well
@ -803,31 +820,63 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: n
const rareThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; // 64 - 32 - 16 - 10 = 6 const rareThreshold = totalWeight - tierWeights[0] - tierWeights[1] - tierWeights[2]; // 64 - 32 - 16 - 10 = 6
tierValue > commonThreshold ? ++numEncounters[0] : tierValue > uncommonThreshold ? ++numEncounters[1] : tierValue > rareThreshold ? ++numEncounters[2] : ++numEncounters[3]; tierValue > commonThreshold ? ++numEncounters[0] : tierValue > uncommonThreshold ? ++numEncounters[1] : tierValue > rareThreshold ? ++numEncounters[2] : ++numEncounters[3];
encountersByBiome.set(Biome[currentBiome], encountersByBiome.get(Biome[currentBiome]) + 1);
} else { } else {
encounterRate += WIGHT_INCREMENT_ON_SPAWN_MISS; encounterRate += WEIGHT_INCREMENT_ON_SPAWN_MISS;
} }
} }
return numEncounters; return [numEncounters, encountersByBiome, validMEfloorsByBiome];
}; };
const runs = []; const encounterRuns: number[][] = [];
const encountersByBiomeRuns: Map<string, number>[] = [];
const validFloorsByBiome: Map<string, number>[] = [];
while (run < numRuns) { while (run < numRuns) {
scene.executeWithSeedOffset(() => { scene.executeWithSeedOffset(() => {
const numEncounters = calculateNumEncounters(); const [numEncounters, encountersByBiome, validMEfloorsByBiome] = calculateNumEncounters();
runs.push(numEncounters); encounterRuns.push(numEncounters);
encountersByBiomeRuns.push(encountersByBiome);
validFloorsByBiome.push(validMEfloorsByBiome);
}, 1000 * run); }, 1000 * run);
run++; run++;
} }
const n = runs.length; const n = encounterRuns.length;
const totalEncountersInRun = runs.map(run => run.reduce((a, b) => a + b)); const totalEncountersInRun = encounterRuns.map(run => run.reduce((a, b) => a + b));
const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n; const totalMean = totalEncountersInRun.reduce((a, b) => a + b) / n;
const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n); const totalStd = Math.sqrt(totalEncountersInRun.map(x => Math.pow(x - totalMean, 2)).reduce((a, b) => a + b) / n);
const commonMean = runs.reduce((a, b) => a + b[0], 0) / n; const commonMean = encounterRuns.reduce((a, b) => a + b[0], 0) / n;
const uncommonMean = runs.reduce((a, b) => a + b[1], 0) / n; const uncommonMean = encounterRuns.reduce((a, b) => a + b[1], 0) / n;
const rareMean = runs.reduce((a, b) => a + b[2], 0) / n; const rareMean = encounterRuns.reduce((a, b) => a + b[2], 0) / n;
const superRareMean = runs.reduce((a, b) => a + b[3], 0) / n; const superRareMean = encounterRuns.reduce((a, b) => a + b[3], 0) / n;
console.log(`Starting weight: ${baseSpawnWeight}\nAverage MEs per run: ${totalMean}\nStandard Deviation: ${totalStd}\nAvg Commons: ${commonMean}\nAvg Uncommons: ${uncommonMean}\nAvg Rares: ${rareMean}\nAvg Super Rares: ${superRareMean}`); const encountersPerRunPerBiome = encountersByBiomeRuns.reduce((a, b) => {
for (const biome of a.keys()) {
a.set(biome, a.get(biome) + b.get(biome));
}
return a;
});
const meanEncountersPerRunPerBiome: Map<string, number> = new Map<string, number>();
encountersPerRunPerBiome.forEach((value, key) => {
meanEncountersPerRunPerBiome.set(key, value / n);
});
const validMEFloorsPerRunPerBiome = validFloorsByBiome.reduce((a, b) => {
for (const biome of a.keys()) {
a.set(biome, a.get(biome) + b.get(biome));
}
return a;
});
const meanMEFloorsPerRunPerBiome: Map<string, number> = new Map<string, number>();
validMEFloorsPerRunPerBiome.forEach((value, key) => {
meanMEFloorsPerRunPerBiome.set(key, value / n);
});
let stats = `Starting weight: ${baseSpawnWeight}\nAverage MEs per run: ${totalMean}\nStandard Deviation: ${totalStd}\nAvg Commons: ${commonMean}\nAvg Greats: ${uncommonMean}\nAvg Ultras: ${rareMean}\nAvg Rogues: ${superRareMean}\n`;
const meanEncountersPerRunPerBiomeSorted = [...meanEncountersPerRunPerBiome.entries()].sort((e1, e2) => e2[1] - e1[1]);
meanEncountersPerRunPerBiomeSorted.forEach(value => stats = stats + `${value[0]}: avg valid floors ${meanMEFloorsPerRunPerBiome.get(value[0])}, avg MEs ${value[1]},\n`);
console.log(stats);
} }

View File

@ -386,7 +386,7 @@ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number,
}); });
} }
export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType): Promise<void> { export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, pokeball: Phaser.GameObjects.Sprite, pokeballType: PokeballType, isObtain: boolean = false): Promise<void> {
scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY)); scene.unshiftPhase(new VictoryPhase(scene, BattlerIndex.ENEMY));
const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm(); const speciesForm = !pokemon.fusionSpecies ? pokemon.getSpeciesForm() : pokemon.getFusionSpeciesForm();
@ -412,14 +412,16 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs); scene.gameData.updateSpeciesDexIvs(pokemon.species.getRootSpeciesId(true), pokemon.ivs);
return new Promise(resolve => { return new Promise(resolve => {
scene.ui.showText(i18next.t("battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => { scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.name }), null, () => {
const end = () => { const end = () => {
scene.pokemonInfoContainer.hide(); scene.pokemonInfoContainer.hide();
removePb(scene, pokeball); removePb(scene, pokeball);
resolve(); resolve();
}; };
const removePokemon = () => { const removePokemon = () => {
scene.field.remove(pokemon, true); if (pokemon) {
scene.field.remove(pokemon, true);
}
}; };
const addToParty = () => { const addToParty = () => {
const newPokemon = pokemon.addToParty(pokeballType); const newPokemon = pokemon.addToParty(pokeballType);
@ -470,14 +472,18 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po
} }
function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) { function removePb(scene: BattleScene, pokeball: Phaser.GameObjects.Sprite) {
scene.tweens.add({ if (pokeball) {
targets: pokeball, scene.tweens.add({
duration: 250, targets: pokeball,
delay: 250, duration: 250,
ease: "Sine.easeIn", delay: 250,
alpha: 0, ease: "Sine.easeIn",
onComplete: () => pokeball.destroy() alpha: 0,
}); onComplete: () => {
pokeball.destroy();
}
});
}
} }
export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise<void> { export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise<void> {

View File

@ -17,6 +17,13 @@ type KnownFileRoot =
| "mystery-encounters" | "mystery-encounters"
| "pokeball" | "pokeball"
| "pokemon" | "pokemon"
| "pokemon/back"
| "pokemon/exp"
| "pokemon/female"
| "pokemon/icons"
| "pokemon/input"
| "pokemon/shiny"
| "pokemon/variant"
| "statuses" | "statuses"
| "trainer" | "trainer"
| "ui"; | "ui";
@ -25,7 +32,7 @@ export class MysteryEncounterSpriteConfig {
/** The sprite key (which is the image file name). e.g. "ace_trainer_f" */ /** The sprite key (which is the image file name). e.g. "ace_trainer_f" */
spriteKey: string; spriteKey: string;
/** Refer to [/public/images](../../public/images) directorty for all folder names */ /** Refer to [/public/images](../../public/images) directorty for all folder names */
fileRoot: KnownFileRoot & string; fileRoot: KnownFileRoot & string | string;
/** Enable shadow. Defaults to `false` */ /** Enable shadow. Defaults to `false` */
hasShadow?: boolean = false; hasShadow?: boolean = false;
/** Disable animation. Defaults to `false` */ /** Disable animation. Defaults to `false` */
@ -44,6 +51,8 @@ export class MysteryEncounterSpriteConfig {
yShadow?: number; yShadow?: number;
/** Sprite scale. `0` - `n` */ /** Sprite scale. `0` - `n` */
scale?: number; scale?: number;
/** If you are using a Pokemon sprite, set to `true`. This will ensure variant, form, gender, shiny sprites are loaded properly */
isPokemon?: boolean;
/** If you are using an item sprite, set to `true` */ /** If you are using an item sprite, set to `true` */
isItem?: boolean; isItem?: boolean;
/** The sprites alpha. `0` - `1` The lower the number, the more transparent */ /** The sprites alpha. `0` - `1` The lower the number, the more transparent */
@ -155,10 +164,12 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
} }
this.spriteConfigs.forEach((config) => { this.spriteConfigs.forEach((config) => {
if (!config.isItem) { if (config.isPokemon) {
this.scene.loadAtlas(config.spriteKey, config.fileRoot); this.scene.loadPokemonAtlas(config.spriteKey, config.fileRoot);
} else { } else if (config.isItem) {
this.scene.loadAtlas("items", ""); this.scene.loadAtlas("items", "");
} else {
this.scene.loadAtlas(config.spriteKey, config.fileRoot);
} }
}); });

View File

@ -273,6 +273,8 @@ export class LoadingScene extends SceneBase {
this.loadAtlas("xbox", "inputs"); this.loadAtlas("xbox", "inputs");
this.loadAtlas("keyboard", "inputs"); this.loadAtlas("keyboard", "inputs");
this.loadAtlas("encounter_radar", "mystery-encounters");
this.loadSe("select"); this.loadSe("select");
this.loadSe("menu_open"); this.loadSe("menu_open");
this.loadSe("hit"); this.loadSe("hit");

View File

@ -16,6 +16,7 @@ export const battle: SimpleTranslationEntries = {
"moneyWon": "You got\n₽{{moneyAmount}} for winning!", "moneyWon": "You got\n₽{{moneyAmount}} for winning!",
"moneyPickedUp": "You picked up ₽{{moneyAmount}}!", "moneyPickedUp": "You picked up ₽{{moneyAmount}}!",
"pokemonCaught": "{{pokemonName}} was caught!", "pokemonCaught": "{{pokemonName}} was caught!",
"pokemonObtained": "You got {{pokemonName}}!",
"pokemonBrokeFree": "Oh no!\nThe Pokémon broke free!", "pokemonBrokeFree": "Oh no!\nThe Pokémon broke free!",
"pokemonFled": "The wild {{pokemonName}} fled!", "pokemonFled": "The wild {{pokemonName}} fled!",
"playerFled": "You fled from the {{pokemonName}}!", "playerFled": "You fled from the {{pokemonName}}!",

View File

@ -33,6 +33,7 @@ export const mysteryEncounter = {
// General use content // General use content
"paid_money": "You paid ₽{{amount, number}}.", "paid_money": "You paid ₽{{amount, number}}.",
"receive_money": "You received ₽{{amount, number}}!", "receive_money": "You received ₽{{amount, number}}!",
"affects_pokedex": "Affects Pokédex Data",
mysteriousChallengers: mysteriousChallengersDialogue, mysteriousChallengers: mysteriousChallengersDialogue,
mysteriousChest: mysteriousChestDialogue, mysteriousChest: mysteriousChestDialogue,

View File

@ -1,19 +1,20 @@
export const pokemonSalesmanDialogue = { export const pokemonSalesmanDialogue = {
intro: "A chipper elderly man approaches you.", intro: "A chipper elderly man approaches you.",
speaker: "Gentleman", speaker: "Gentleman",
intro_dialogue: "Hello there! Have I got a deal just for YOU!{{specialShinyText}}", intro_dialogue: "Hello there! Have I got a deal just for YOU!",
title: "The Pokémon Salesman", title: "The Pokémon Salesman",
description: "\"This {{purchasePokemon}} is extremely unique, and carries an ability not normally found on its species! I'll let you have this swell {{purchasePokemon}} for just {{money, money}}!\"\n\n\"What do you say?\"", description: "\"This {{purchasePokemon}} is extremely unique and carries an ability not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"",
description_shiny: "\"This {{purchasePokemon}} is extremely unique and has a pigment not normally found in its species! I'll let you have this swell {{purchasePokemon}} for just {{price, money}}!\"\n\n\"What do you say?\"",
query: "What will you do?", query: "What will you do?",
shiny: "$I have SUPER amazing Pokémon that\nanyone would be dying to get!",
option: { option: {
1: { 1: {
label: "Accept", label: "Accept",
tooltip: "(-) Pay {{money, money}}\n(+) Gain a {{purchasePokemon}} with its Hidden Ability (also saved in Pokédex data)", tooltip: "(-) Pay {{price, money}}\n(+) Gain a {{purchasePokemon}} with its Hidden Ability",
selected_dialogue: `Excellent choice! tooltip_shiny: "(-) Pay {{price, money}}\n(+) Gain a shiny {{purchasePokemon}}",
$I can see you've a keen eye for business.`,
selected_message: "You paid an outrageous sum and bought the {{purchasePokemon}}.", selected_message: "You paid an outrageous sum and bought the {{purchasePokemon}}.",
selected_dialogue_2: "Oh, yeah...@d{64} Returns not accepted, got that?" selected_dialogue: `Excellent choice!
$I can see you've a keen eye for business.
$Oh, yeah...@d{64} Returns not accepted, got that?`,
}, },
2: { 2: {
label: "Refuse", label: "Refuse",

View File

@ -3,6 +3,7 @@ export const trainingSessionDialogue = {
title: "Training Session", title: "Training Session",
description: "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.", description: "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.",
query: "How should you train?", query: "How should you train?",
invalid_selection: "Pokémon must be healthy enough.",
option: { option: {
1: { 1: {
label: "Light Training", label: "Light Training",

View File

@ -117,9 +117,9 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0;
*/ */
// 1 to 256, set to null to ignore // 1 to 256, set to null to ignore
export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256;
export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null;
export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.POKEMON_SALESMAN;
/** /**
* MODIFIER / ITEM OVERRIDES * MODIFIER / ITEM OVERRIDES

View File

@ -13,6 +13,7 @@ import { getPokeballAtlasKey } from "../data/pokeball";
import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter"; import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter";
import i18next from "i18next";
export default class MysteryEncounterUiHandler extends UiHandler { export default class MysteryEncounterUiHandler extends UiHandler {
private cursorContainer: Phaser.GameObjects.Container; private cursorContainer: Phaser.GameObjects.Container;
@ -29,6 +30,10 @@ export default class MysteryEncounterUiHandler extends UiHandler {
private descriptionScrollTween: Phaser.Tweens.Tween; private descriptionScrollTween: Phaser.Tweens.Tween;
private rarityBall: Phaser.GameObjects.Sprite; private rarityBall: Phaser.GameObjects.Sprite;
private dexProgressWindow: Phaser.GameObjects.NineSlice;
private dexProgressContainer: Phaser.GameObjects.Container;
private showDexProgress: boolean = false;
private overrideSettings: OptionSelectSettings; private overrideSettings: OptionSelectSettings;
private encounterOptions: MysteryEncounterOption[] = []; private encounterOptions: MysteryEncounterOption[] = [];
private optionsMeetsReqs: boolean[]; private optionsMeetsReqs: boolean[];
@ -50,6 +55,9 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.optionsContainer = this.scene.add.container(12, -38.7); this.optionsContainer = this.scene.add.container(12, -38.7);
this.optionsContainer.setVisible(false); this.optionsContainer.setVisible(false);
ui.add(this.optionsContainer); ui.add(this.optionsContainer);
this.dexProgressContainer = this.scene.add.container(214, -43);
this.dexProgressContainer.setVisible(false);
ui.add(this.dexProgressContainer);
this.descriptionContainer = this.scene.add.container(0, -152); this.descriptionContainer = this.scene.add.container(0, -152);
this.descriptionContainer.setVisible(false); this.descriptionContainer.setVisible(false);
ui.add(this.descriptionContainer); ui.add(this.descriptionContainer);
@ -65,9 +73,23 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.tooltipWindow = addWindow(this.scene, 0, 0, 110, 48, false, false, 0, 0, WindowVariant.THIN); this.tooltipWindow = addWindow(this.scene, 0, 0, 110, 48, false, false, 0, 0, WindowVariant.THIN);
this.tooltipContainer.add(this.tooltipWindow); this.tooltipContainer.add(this.tooltipWindow);
this.dexProgressWindow = addWindow(this.scene, 0, 0, 24, 28, false, false, 0, 0, WindowVariant.THIN);
this.dexProgressContainer.add(this.dexProgressWindow);
this.rarityBall = this.scene.add.sprite(141, 9, "pb"); this.rarityBall = this.scene.add.sprite(141, 9, "pb");
this.rarityBall.setScale(0.75); this.rarityBall.setScale(0.75);
this.descriptionContainer.add(this.rarityBall); this.descriptionContainer.add(this.rarityBall);
const dexProgressIndicator = this.scene.add.sprite(12, 9, "encounter_radar");
dexProgressIndicator.setScale(0.85);
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 { show(args: any[]): boolean {
@ -81,6 +103,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.cursorContainer.setVisible(true); this.cursorContainer.setVisible(true);
this.descriptionContainer.setVisible(showDescriptionContainer); this.descriptionContainer.setVisible(showDescriptionContainer);
this.optionsContainer.setVisible(true); this.optionsContainer.setVisible(true);
this.dexProgressContainer.setVisible(true);
this.displayEncounterOptions(slideInDescription); this.displayEncounterOptions(slideInDescription);
const cursor = this.getCursor(); const cursor = this.getCursor();
if (cursor === (this?.optionsContainer?.length || 0) - 1) { if (cursor === (this?.optionsContainer?.length || 0) - 1) {
@ -317,7 +340,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
const queryText: string = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.query, TextStyle.TOOLTIP_CONTENT); const queryText: string = getEncounterText(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.query, TextStyle.TOOLTIP_CONTENT);
// Clear options container (except cursor) // Clear options container (except cursor)
this.optionsContainer.removeAll(); this.optionsContainer.removeAll(true);
// Options Window // Options Window
for (let i = 0; i < this.encounterOptions.length; i++) { for (let i = 0; i < this.encounterOptions.length; i++) {
@ -437,6 +460,8 @@ export default class MysteryEncounterUiHandler extends UiHandler {
if (isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) { if (isNullOrUndefined(cursor) || cursor > this.optionsContainer.length - 2) {
// Ignore hovers on view party button // Ignore hovers on view party button
// Hide dex progress if visible
this.showHideDexProgress(false);
return; return;
} }
@ -487,6 +512,13 @@ export default class MysteryEncounterUiHandler extends UiHandler {
}); });
} }
} }
// Dex progress indicator
if (cursorOption.hasDexProgress && !this.showDexProgress) {
this.showHideDexProgress(true);
} else if (!cursorOption.hasDexProgress) {
this.showHideDexProgress(false);
}
} }
clear(): void { clear(): void {
@ -494,6 +526,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.overrideSettings = null; this.overrideSettings = null;
this.optionsContainer.setVisible(false); this.optionsContainer.setVisible(false);
this.optionsContainer.removeAll(true); this.optionsContainer.removeAll(true);
this.dexProgressContainer.setVisible(false);
this.descriptionContainer.setVisible(false); this.descriptionContainer.setVisible(false);
this.tooltipContainer.setVisible(false); this.tooltipContainer.setVisible(false);
// Keeps container background and pokeball // Keeps container background and pokeball
@ -508,4 +541,30 @@ export default class MysteryEncounterUiHandler extends UiHandler {
} }
this.cursorObj = null; this.cursorObj = null;
} }
/**
*
* @param show - if true does show, if false does hide
*/
showHideDexProgress(show: boolean) {
if (show && !this.showDexProgress) {
this.showDexProgress = true;
this.scene.tweens.killTweensOf(this.dexProgressContainer);
this.scene.tweens.add({
targets: this.dexProgressContainer,
y: -63,
ease: "Sine.easeInOut",
duration: 750
});
} else if (!show && this.showDexProgress) {
this.showDexProgress = false;
this.scene.tweens.killTweensOf(this.dexProgressContainer);
this.scene.tweens.add({
targets: this.dexProgressContainer,
y: -43,
ease: "Sine.easeInOut",
duration: 750,
});
}
}
} }