Merge pull request #106 from AsdarDevelops/one-for-all

The Strong Stuff
This commit is contained in:
ImperialSympathizer 2024-07-20 14:43:01 -04:00 committed by GitHub
commit 4afcdad3db
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
33 changed files with 935 additions and 150 deletions

View File

@ -50,4 +50,4 @@ jobs:
run: npm ci # Use 'npm ci' to install dependencies
- name: tests # Step to run tests
run: npm run test${{ runner.debug == '0' &&':silent' || '' }} # silent on default. if debug run loud
run: npm run test${{ runner.debug == '0' &&':silent' || '' }} # silent on default. if debug run loud.

View File

@ -0,0 +1,41 @@
{
"textures": [
{
"image": "berry_juice.png",
"format": "RGBA8888",
"size": {
"w": 24,
"h": 23
},
"scale": 1,
"frames": [
{
"filename": "0001.png",
"rotated": false,
"trimmed": true,
"sourceSize": {
"w": 24,
"h": 24
},
"spriteSourceSize": {
"x": 1,
"y": 2,
"w": 22,
"h": 21
},
"frame": {
"x": 1,
"y": 1,
"w": 22,
"h": 21
}
}
]
}
],
"meta": {
"app": "https://www.codeandweb.com/texturepacker",
"version": "3.0",
"smartupdate": "$TexturePacker:SmartUpdate:04685a0eb6ef9095824b65408ec1b38f:9891674d538df100fcddde29330c21ae:927f117bdb1c2a27226a5540ce00ee8b$"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 318 B

View File

@ -4,7 +4,7 @@ import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { Species } from "#enums/species";
import BattleScene from "../../../battle-scene";
import { AddPokeballModifierType } from "../../../modifier/modifier-type";
import { AddPokeballModifierType } from "#app/modifier/modifier-type";
import { PokeballType } from "../../pokeball";
import { getPokemonSpecies } from "../../pokemon-species";
import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter";

View File

@ -1,5 +1,5 @@
import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option";
import { applyDamageToPokemon, EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterExp, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { modifierTypes, } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "../../../battle-scene";
@ -17,6 +17,7 @@ import { WeatherType } from "#app/data/weather";
import { isNullOrUndefined, randSeedInt } from "#app/utils";
import { StatusEffect } from "#app/data/status-effect";
import { queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:fieryFallout";

View File

@ -5,7 +5,8 @@ import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "../../../battle-scene";
import MysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier } from "../mystery-encounter";
import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
import { applyDamageToPokemon, leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils";
import { leaveEncounterWithoutBattle, setEncounterExp } from "../utils/encounter-phase-utils";
import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
const OPTION_1_REQUIRED_MOVE = Moves.SURF;
const OPTION_2_REQUIRED_MOVE = Moves.FLY;

View File

@ -28,7 +28,7 @@ export const MysteriousChestEncounter: IMysteryEncounter =
hasShadow: true,
x: 4,
y: 10,
yShadowOffset: 3,
yShadow: 3,
disableAnimation: true, // Re-enabled after option select
},
])
@ -109,7 +109,7 @@ export const MysteriousChestEncounter: IMysteryEncounter =
scene,
true
);
koPlayerPokemon(highestLevelPokemon);
koPlayerPokemon(scene, highestLevelPokemon);
scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.name);
// Show which Pokemon was KOed, then leave encounter with no rewards

View File

@ -38,7 +38,7 @@ export const SafariZoneEncounter: IMysteryEncounter =
hasShadow: true,
x: 4,
y: 10,
yShadowOffset: 3
yShadow: 3
},
])
.withIntroDialogue([

View File

@ -10,6 +10,7 @@ import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } fro
import { EncounterOptionMode, MysteryEncounterOptionBuilder } from "../mystery-encounter-option";
import { MoneyRequirement } from "../mystery-encounter-requirements";
import { getEncounterText, queueEncounterMessage } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { applyDamageToPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
/** the i18n namespace for this encounter */
const namespace = "mysteryEncounter:shadyVitaminDealer";
@ -35,7 +36,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter =
repeat: true,
x: 12,
y: -5,
yShadowOffset: -5
yShadow: -5
},
{
spriteKey: "b2w2_veteran_m",
@ -43,7 +44,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter =
hasShadow: true,
x: -12,
y: 3,
yShadowOffset: 3
yShadow: 3
},
])
.withIntroDialogue([
@ -122,8 +123,7 @@ export const ShadyVitaminDealerEncounter: IMysteryEncounter =
const chosenPokemon = encounter.misc.chosenPokemon;
// Pokemon takes 1/3 max HP damage
const damage = Math.round(chosenPokemon.getMaxHp() / 3);
chosenPokemon.hp = Math.max(chosenPokemon.hp - damage, 0);
applyDamageToPokemon(scene, chosenPokemon, Math.floor(chosenPokemon.getMaxHp() / 3));
// Roll for poison (80%)
if (randSeedInt(10) < 8) {

View File

@ -0,0 +1,183 @@
import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, initCustomMovesForEncounter, leaveEncounterWithoutBattle, setEncounterRewards, transitionMysteryEncounterIntroVisuals } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { modifierTypes, PokemonHeldItemModifierType, } from "#app/modifier/modifier-type";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import BattleScene from "../../../battle-scene";
import IMysteryEncounter, { MysteryEncounterBuilder, MysteryEncounterTier, } from "../mystery-encounter";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import { Species } from "#enums/species";
import { Nature } from "#app/data/nature";
import Pokemon, { PlayerPokemon, PokemonMove } from "#app/field/pokemon";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { modifyPlayerPokemonBST } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
import { Moves } from "#enums/moves";
import { BattlerIndex } from "#app/battle";
import { StatChangePhase } from "#app/phases";
import { BattleStat } from "#app/data/battle-stat";
import { BattlerTagType } from "#enums/battler-tag-type";
import { BerryType } from "#enums/berry-type";
/** the i18n namespace for the encounter */
const namespace = "mysteryEncounter:theStrongStuff";
export const TheStrongStuffEncounter: IMysteryEncounter =
MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.THE_STRONG_STUFF)
.withEncounterTier(MysteryEncounterTier.COMMON)
.withSceneWaveRangeRequirement(10, 180) // waves 10 to 180
.withHideWildIntroMessage(true)
.withAutoHideIntroVisuals(false)
.withIntroSpriteConfigs([
{
spriteKey: "berry_juice",
fileRoot: "mystery-encounters",
hasShadow: true,
scale: 1.5,
x: -15,
y: 3,
yShadow: 0
},
{
spriteKey: Species.SHUCKLE.toString(),
fileRoot: "pokemon",
hasShadow: true,
repeat: true,
scale: 1.5,
x: 20,
y: 10,
yShadow: 7
},
]) // Set in onInit()
.withIntroDialogue([
{
text: `${namespace}:intro`,
},
])
.withOnInit((scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter;
// Calculate boss mon
const config: EnemyPartyConfig = {
levelAdditiveMultiplier: 1,
disableSwitch: true,
pokemonConfigs: [
{
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
spriteScale: 1.5,
nature: Nature.BOLD,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierTypes: [
generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type as PokemonHeldItemModifierType,
generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.APICOT]).type as PokemonHeldItemModifierType,
generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.GANLON]).type as PokemonHeldItemModifierType,
generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.LUM]).type as PokemonHeldItemModifierType,
generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.LUM]).type as PokemonHeldItemModifierType
],
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: (pokemon: Pokemon) => {
queueEncounterMessage(pokemon.scene, `${namespace}:option:2:stat_boost`);
pokemon.scene.unshiftPhase(new StatChangePhase(pokemon.scene, pokemon.getBattlerIndex(), true, [BattleStat.DEF, BattleStat.SPDEF], 2));
}
}
],
};
encounter.enemyPartyConfigs = [config];
initCustomMovesForEncounter(scene, [Moves.GASTRO_ACID, Moves.STEALTH_ROCK]);
return true;
})
.withTitle(`${namespace}:title`)
.withDescription(`${namespace}:description`)
.withQuery(`${namespace}:query`)
.withSimpleOption(
{
buttonLabel: `${namespace}:option:1:label`,
buttonTooltip: `${namespace}:option:1:tooltip`,
selected: [
{
text: `${namespace}:option:1:selected`
}
]
},
async (scene: BattleScene) => {
const encounter = scene.currentBattle.mysteryEncounter;
// Do blackout and hide intro visuals during blackout
scene.time.delayedCall(750, () => {
transitionMysteryEncounterIntroVisuals(scene, true, true, 50);
});
// -20 to all base stats of highest BST, +10 to all base stats of rest of party
// Get highest BST mon
const party = scene.getParty();
let highestBst: PlayerPokemon = null;
let statTotal = 0;
for (const pokemon of party) {
if (!highestBst) {
highestBst = pokemon;
statTotal = pokemon.getSpeciesForm().getBaseStatTotal();
continue;
}
const total = pokemon.getSpeciesForm().getBaseStatTotal();
if (total > statTotal) {
highestBst = pokemon;
statTotal = total;
}
}
if (!highestBst) {
highestBst = party[0];
}
modifyPlayerPokemonBST(highestBst, -20);
for (const pokemon of party) {
if (highestBst.id === pokemon.id) {
continue;
}
modifyPlayerPokemonBST(pokemon, 10);
}
encounter.setDialogueToken("highBstPokemon", highestBst.name);
await showEncounterText(scene, `${namespace}:option:1:selected_2`, null, true);
setEncounterRewards(scene, { fillRemaining: true });
leaveEncounterWithoutBattle(scene, true);
return true;
}
)
.withSimpleOption(
{
buttonLabel: `${namespace}:option:2:label`,
buttonTooltip: `${namespace}:option:2:tooltip`,
selected: [
{
text: `${namespace}:option:2:selected`,
},
],
},
async (scene: BattleScene) => {
// Pick battle
const encounter = scene.currentBattle.mysteryEncounter;
setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.SOUL_DEW], fillRemaining: true });
encounter.startOfBattleEffects.push(
{
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER],
move: new PokemonMove(Moves.GASTRO_ACID),
ignorePp: true
},
{
sourceBattlerIndex: BattlerIndex.ENEMY,
targets: [BattlerIndex.PLAYER],
move: new PokemonMove(Moves.STEALTH_ROCK),
ignorePp: true
});
transitionMysteryEncounterIntroVisuals(scene, true, true, 500);
await initBattleWithEnemyConfig(scene, encounter.enemyPartyConfigs[0]);
}
)
.build();

View File

@ -40,7 +40,7 @@ export const TrainingSessionEncounter: IMysteryEncounter =
hasShadow: true,
y: 6,
x: 5,
yShadowOffset: -2
yShadow: -2
},
])
.withIntroDialogue([

View File

@ -13,6 +13,7 @@ import { TrainingSessionEncounter } from "./encounters/training-session-encounte
import IMysteryEncounter from "./mystery-encounter";
import { SafariZoneEncounter } from "#app/data/mystery-encounters/encounters/safari-zone-encounter";
import { FieryFalloutEncounter } from "#app/data/mystery-encounters/encounters/fiery-fallout-encounter";
import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-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;
@ -181,7 +182,9 @@ export const mysteryEncountersByBiome = new Map<Biome, MysteryEncounterType[]>([
[Biome.SEABED, []],
[Biome.MOUNTAIN, []],
[Biome.BADLANDS, []],
[Biome.CAVE, []],
[Biome.CAVE, [
MysteryEncounterType.THE_STRONG_STUFF
]],
[Biome.DESERT, []],
[Biome.ICE_CAVE, []],
[Biome.MEADOW, []],
@ -221,6 +224,7 @@ export function initMysteryEncounters() {
allMysteryEncounters[MysteryEncounterType.SAFARI_ZONE] = SafariZoneEncounter;
allMysteryEncounters[MysteryEncounterType.LOST_AT_SEA] = LostAtSeaEncounter;
allMysteryEncounters[MysteryEncounterType.FIERY_FALLOUT] = FieryFalloutEncounter;
allMysteryEncounters[MysteryEncounterType.THE_STRONG_STUFF] = TheStrongStuffEncounter;
// Add extreme encounters to biome map
extremeBiomeEncounters.forEach(encounter => {

View File

@ -2,9 +2,8 @@ import { BattlerIndex, BattleType } from "#app/battle";
import { biomeLinks } from "#app/data/biomes";
import MysteryEncounterOption from "#app/data/mystery-encounters/mystery-encounter-option";
import { WIGHT_INCREMENT_ON_SPAWN_MISS } from "#app/data/mystery-encounters/mystery-encounters";
import { queueEncounterMessage, 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 { getPokemonNameWithAffix } from "#app/messages";
import { ExpBalanceModifier, ExpShareModifier, MultipleParticipantExpBonusModifier, PokemonExpBoosterModifier } from "#app/modifier/modifier";
import { CustomModifierSettings, getModifierPoolForType, ModifierPoolType, ModifierType, ModifierTypeFunc, ModifierTypeGenerator, ModifierTypeOption, modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type";
import * as Overrides from "#app/overrides";
@ -27,6 +26,7 @@ import { Status, StatusEffect } from "../../status-effect";
import { TrainerConfig, trainerConfigs, TrainerSlot } from "../../trainer-config";
import { MysteryEncounterVariant } from "../mystery-encounter";
import { Gender } from "#app/data/gender";
import { Nature } from "#app/data/nature";
import { Moves } from "#enums/moves";
import { initMoveAnim, loadMoveAnimAssets } from "#app/data/battle-anims";
@ -54,7 +54,7 @@ export function doTrainerExclamation(scene: BattleScene) {
}
});
scene.playSound("GEN8- Exclaim.wav", { volume: 0.8 });
scene.playSound("GEN8- Exclaim.wav", { volume: 0.7 });
}
export interface EnemyPokemonConfig {
@ -62,11 +62,14 @@ export interface EnemyPokemonConfig {
isBoss: boolean;
bossSegments?: number;
bossSegmentModifier?: number; // Additive to the determined segment number
spriteScale?: number;
formIndex?: number;
level?: number;
gender?: Gender;
passive?: boolean;
moveSet?: Moves[];
nature?: Nature;
ivs?: [integer, integer, integer, integer, integer, integer];
/** Can set just the status, or pass a timer on the status turns */
status?: StatusEffect | [StatusEffect, number];
mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void;
@ -196,6 +199,13 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
enemyPokemon.formIndex = config.formIndex;
}
// Set scale
if (!isNullOrUndefined(config.spriteScale)) {
enemyPokemon.mysteryEncounterData = {
spriteScale: config.spriteScale
};
}
// Set Boss
if (config.isBoss) {
let segments = !isNullOrUndefined(config.bossSegments) ? config.bossSegments : scene.getEncounterBossSegments(scene.currentBattle.waveIndex, level, enemySpecies, true);
@ -206,12 +216,22 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig:
}
// Set Passive
if (partyConfig.pokemonConfigs[e].passive) {
if (config.passive) {
enemyPokemon.passive = true;
}
// Set Nature
if (config.nature) {
enemyPokemon.nature = config.nature;
}
// Set IVs
if (config.ivs) {
enemyPokemon.ivs = config.ivs;
}
// Set Status
const statusEffects = partyConfig.pokemonConfigs[e].status;
const statusEffects = config.status;
if (statusEffects) {
// Default to cureturn 3 for sleep
const status = Array.isArray(statusEffects) ? statusEffects[0] : statusEffects;
@ -811,65 +831,3 @@ export function calculateMEAggregateStats(scene: BattleScene, baseSpawnWeight: 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}`);
}
/**
* Takes care of handling player pokemon KO (with all its side effects)
*
* @param scene the battle scene
* @param pokemon the player pokemon to KO
*/
export function koPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon) {
pokemon.hp = 0;
pokemon.trySetStatus(StatusEffect.FAINT);
pokemon.updateInfo();
queueEncounterMessage(scene, i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
/**
* Handles applying hp changes to a player pokemon.
* Takes care of not going below `0`, above max-hp, adding `FNT` status correctly and updating the pokemon info.
* TODO: handle special cases like wonder-guard/ninjask
* @param scene the battle scene
* @param pokemon the player pokemon to apply the hp change to
* @param value the hp change amount. Positive for heal. Negative for damage
*
*/
function applyHpChangeToPokemon(scene: BattleScene, pokemon: PlayerPokemon, value: number) {
const hpChange = Math.round(pokemon.hp + value);
const nextHp = Math.max(Math.min(hpChange, pokemon.getMaxHp()), 0);
if (nextHp === 0) {
koPlayerPokemon(scene, pokemon);
} else {
pokemon.hp = nextHp;
}
}
/**
* Handles applying damage to a player pokemon
* @param scene the battle scene
* @param pokemon the player pokemon to apply damage to
* @param damage the amount of damage to apply
* @see {@linkcode applyHpChangeToPokemon}
*/
export function applyDamageToPokemon(scene: BattleScene, pokemon: PlayerPokemon, damage: number) {
if (damage <= 0) {
console.warn("Healing pokemon with `applyDamageToPokemon` is not recommended! Please use `applyHealToPokemon` instead.");
}
applyHpChangeToPokemon(scene, pokemon, -damage);
}
/**
* Handles applying heal to a player pokemon
* @param scene the battle scene
* @param pokemon the player pokemon to apply heal to
* @param heal the amount of heal to apply
* @see {@linkcode applyHpChangeToPokemon}
*/
export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, heal: number) {
if (heal <= 0) {
console.warn("Damaging pokemong with `applyHealToPokemon` is not recommended! Please use `applyDamageToPokemon` instead.");
}
applyHpChangeToPokemon(scene, pokemon, heal);
}

View File

@ -15,7 +15,12 @@ import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler";
import { Species } from "#enums/species";
import { Type } from "#app/data/type";
import PokemonSpecies, { getPokemonSpecies, speciesStarters } from "#app/data/pokemon-species";
import { showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { queueEncounterMessage, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { getPokemonNameWithAffix } from "#app/messages";
export interface MysteryEncounterPokemonData {
spriteScale?: number
}
/**
*
@ -131,10 +136,81 @@ export function getRandomSpeciesByStarterTier(starterTiers: number | [number, nu
return Species.BULBASAUR;
}
export function koPlayerPokemon(pokemon: PlayerPokemon) {
/**
* Takes care of handling player pokemon KO (with all its side effects)
*
* @param scene the battle scene
* @param pokemon the player pokemon to KO
*/
export function koPlayerPokemon(scene: BattleScene, pokemon: PlayerPokemon) {
pokemon.hp = 0;
pokemon.trySetStatus(StatusEffect.FAINT);
pokemon.updateInfo();
queueEncounterMessage(scene, i18next.t("battle:fainted", { pokemonNameWithAffix: getPokemonNameWithAffix(pokemon) }));
}
/**
* Handles applying hp changes to a player pokemon.
* Takes care of not going below `0`, above max-hp, adding `FNT` status correctly and updating the pokemon info.
* TODO: handle special cases like wonder-guard/ninjask
* @param scene the battle scene
* @param pokemon the player pokemon to apply the hp change to
* @param value the hp change amount. Positive for heal. Negative for damage
*
*/
function applyHpChangeToPokemon(scene: BattleScene, pokemon: PlayerPokemon, value: number) {
const hpChange = Math.round(pokemon.hp + value);
const nextHp = Math.max(Math.min(hpChange, pokemon.getMaxHp()), 0);
if (nextHp === 0) {
koPlayerPokemon(scene, pokemon);
} else {
pokemon.hp = nextHp;
}
}
/**
* Handles applying damage to a player pokemon
* @param scene the battle scene
* @param pokemon the player pokemon to apply damage to
* @param damage the amount of damage to apply
* @see {@linkcode applyHpChangeToPokemon}
*/
export function applyDamageToPokemon(scene: BattleScene, pokemon: PlayerPokemon, damage: number) {
if (damage <= 0) {
console.warn("Healing pokemon with `applyDamageToPokemon` is not recommended! Please use `applyHealToPokemon` instead.");
}
applyHpChangeToPokemon(scene, pokemon, -damage);
}
/**
* Handles applying heal to a player pokemon
* @param scene the battle scene
* @param pokemon the player pokemon to apply heal to
* @param heal the amount of heal to apply
* @see {@linkcode applyHpChangeToPokemon}
*/
export function applyHealToPokemon(scene: BattleScene, pokemon: PlayerPokemon, heal: number) {
if (heal <= 0) {
console.warn("Damaging pokemong with `applyHealToPokemon` is not recommended! Please use `applyDamageToPokemon` instead.");
}
applyHpChangeToPokemon(scene, pokemon, heal);
}
/**
* Will modify all of a Pokemon's base stats by a flat value
* Base stats can never go below 1
* @param pokemon
* @param value
*/
export function modifyPlayerPokemonBST(pokemon: PlayerPokemon, value: number) {
pokemon.getSpeciesForm().baseStats = [...pokemon.getSpeciesForm().baseStats].map(v => {
const newVal = Math.floor(v + value);
return Math.max(newVal, 1);
});
pokemon.calculateStats();
pokemon.updateInfo();
}
/**

View File

@ -221,6 +221,14 @@ export abstract class PokemonSpeciesForm {
return false;
}
/**
* Gets the BST for the species
* @returns The species' BST.
*/
getBaseStatTotal(): integer {
return this.baseStats.reduce((i, n) => n + i);
}
/**
* Gets the species' base stat amount for the given stat.
* @param stat The desired stat.

View File

@ -10,5 +10,6 @@ export enum MysteryEncounterType {
FIELD_TRIP,
SAFARI_ZONE,
LOST_AT_SEA, //might be generalized later on
FIERY_FALLOUT
FIERY_FALLOUT,
THE_STRONG_STUFF
}

View File

@ -41,7 +41,7 @@ export class MysteryEncounterSpriteConfig {
/** Y offset */
y?: number;
/** Y shadow offset */
yShadowOffset?: number;
yShadow?: number;
/** Sprite scale. `0` - `n` */
scale?: number;
/** If you are using an item sprite, set to `true` */
@ -72,10 +72,10 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
return;
}
const getSprite = (spriteKey: string, hasShadow?: boolean, yShadowOffset?: number) => {
const getSprite = (spriteKey: string, hasShadow?: boolean, yShadow?: number) => {
const ret = this.scene.addFieldSprite(0, 0, spriteKey);
ret.setOrigin(0.5, 1);
ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadowOffset ?? 0 });
ret.setPipeline(this.scene.spritePipeline, { tone: [0.0, 0.0, 0.0, 0.0], hasShadow: !!hasShadow, yShadowOffset: yShadow ?? 0 });
return ret;
};
@ -94,13 +94,13 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con
const spacingValue = Math.round((maxX - minX) / Math.max(this.spriteConfigs.filter(s => !s.x && !s.y).length, 1));
this.spriteConfigs?.forEach((config) => {
const { spriteKey, isItem, hasShadow, scale, x, y, yShadowOffset, alpha } = config;
const { spriteKey, isItem, hasShadow, scale, x, y, yShadow, alpha } = config;
let sprite: GameObjects.Sprite;
let tintSprite: GameObjects.Sprite;
if (!isItem) {
sprite = getSprite(spriteKey, hasShadow, yShadowOffset);
sprite = getSprite(spriteKey, hasShadow, yShadow);
tintSprite = getSprite(spriteKey);
} else {
sprite = getItemSprite(spriteKey);

View File

@ -50,6 +50,7 @@ import { BerryType } from "#enums/berry-type";
import { Biome } from "#enums/biome";
import { Moves } from "#enums/moves";
import { Species } from "#enums/species";
import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils";
export enum FieldPosition {
CENTER,
@ -100,6 +101,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
public battleData: PokemonBattleData;
public battleSummonData: PokemonBattleSummonData;
public turnData: PokemonTurnData;
public mysteryEncounterData: MysteryEncounterPokemonData;
/** Used by Mystery Encounters to execute pokemon-specific logic (such as stat boosts) at start of battle */
public mysteryEncounterBattleEffects: (pokemon: Pokemon) => void = null;
@ -524,6 +526,8 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container {
const formKey = this.getFormKey();
if (formKey.indexOf(SpeciesFormKey.GIGANTAMAX) > -1 || formKey.indexOf(SpeciesFormKey.ETERNAMAX) > -1) {
return 1.5;
} else if (this?.mysteryEncounterData?.spriteScale) {
return this.mysteryEncounterData.spriteScale;
}
return 1;
}

View File

@ -10,6 +10,7 @@ import { safariZoneDialogue } from "#app/locales/en/mystery-encounters/safari-zo
import { shadyVitaminDealerDialogue } from "#app/locales/en/mystery-encounters/shady-vitamin-dealer-dialogue";
import { slumberingSnorlaxDialogue } from "#app/locales/en/mystery-encounters/slumbering-snorlax-dialogue";
import { trainingSessionDialogue } from "#app/locales/en/mystery-encounters/training-session-dialogue";
import { theStrongStuffDialogue } from "#app/locales/en/mystery-encounters/the-strong-stuff-dialogue";
/**
* Patterns that can be used:
@ -44,4 +45,5 @@ export const mysteryEncounter = {
safariZone: safariZoneDialogue,
lostAtSea: lostAtSeaDialogue,
fieryFallout: fieryFalloutDialogue,
theStrongStuff: theStrongStuffDialogue,
} as const;

View File

@ -0,0 +1,25 @@
export const theStrongStuffDialogue = {
intro: "It's a massive Shuckle and what appears\nto be an equally large stash of... juice?",
title: "The Strong Stuff",
description: "The Shuckle that blocks your path looks incredibly strong. Meanwhile, the juice next to it is emanating power of some kind.\n\nThe Shuckle extends its feelers in your direction. It seems like it wants to touch you, but is that really a good idea?",
query: "What will you do?",
option: {
1: {
label: "Let it touch you",
tooltip: "(?) Something awful or amazing might happen",
selected: "You black out.",
selected_2: `@f{150}When you awaken, the Shuckle is gone\nand juice stash completely drained.
$Your {{highBstPokemon}} feels a\nterrible lethargy come over it!
$It's base stats were reduced by 20 in each stat!
$Your remaining Pokémon feel an incredible vigor, though!
$Their base stats are increased by 10 in each stat!`
},
2: {
label: "Battle the Shuckle",
tooltip: "(-) Hard Battle\n(+) Special Rewards",
selected: "Enraged, the Shuckle drinks some of its juice and attacks!",
stat_boost: "The Shuckle's juice boosts its stats!",
},
},
outro: "What a bizarre turn of events."
};

View File

@ -1107,18 +1107,19 @@ export class EncounterPhase extends BattlePhase {
if (showEncounterMessage) {
const introDialogue = this.scene.currentBattle.mysteryEncounter.dialogue.intro;
const FIRST_DIALOGUE_PROMPT_DELAY = 750;
let i = 0;
const showNextDialogue = () => {
const nextAction = i === introDialogue.length - 1 ? doShowEncounterOptions : showNextDialogue;
const dialogue = introDialogue[i];
const title = getEncounterText(this.scene, dialogue.speaker);
const text = getEncounterText(this.scene, dialogue.text);
if (title) {
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 750 : 0);
} else {
this.scene.ui.showText(text, null, nextAction, i === 0 ? 750 : 0, true);
}
i++;
if (title) {
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0);
} else {
this.scene.ui.showText(text, null, nextAction, i === 1 ? FIRST_DIALOGUE_PROMPT_DELAY : 0, true);
}
};
if (introDialogue.length > 0) {
@ -1686,6 +1687,7 @@ export class SummonPhase extends PartyMemberPokemonPhase {
pokemon.cry(pokemon.getHpRatio() > 0.25 ? undefined : { rate: 0.85 });
pokemon.getSprite().clearTint();
pokemon.resetSummonData();
this.scene.updateFieldScale();
this.scene.time.delayedCall(1000, () => this.end());
}
});

View File

@ -26,10 +26,9 @@ import { BattlerTagLapseType } from "#app/data/battler-tags";
* - Queuing of the MysteryEncounterOptionSelectedPhase
*/
export class MysteryEncounterPhase extends Phase {
private readonly FIRST_DIALOGUE_PROMPT_DELAY = 300;
optionSelectSettings: OptionSelectSettings;
private FIRST_DIALOGUE_PROMPT_DELAY = 300;
/**
*
* @param scene
@ -108,12 +107,12 @@ export class MysteryEncounterPhase extends Phase {
title = getEncounterText(this.scene, dialogue.speaker);
}
if (title) {
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0);
} else {
this.scene.ui.showText(text, null, nextAction, i === 0 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true);
}
i++;
if (title) {
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0);
} else {
this.scene.ui.showText(text, null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true);
}
};
showNextDialogue();
@ -420,6 +419,7 @@ export class MysteryEncounterRewardsPhase extends Phase {
* - Queuing of the next wave
*/
export class PostMysteryEncounterPhase extends Phase {
private readonly FIRST_DIALOGUE_PROMPT_DELAY = 750;
onPostOptionSelect: OptionPhaseCallback;
constructor(scene: BattleScene) {
@ -462,13 +462,13 @@ export class PostMysteryEncounterPhase extends Phase {
title = getEncounterText(this.scene, dialogue.speaker);
}
i++;
this.scene.ui.setMode(Mode.MESSAGE);
if (title) {
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 0 ? 750 : 0);
this.scene.ui.showDialogue(text, title, null, nextAction, 0, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0);
} else {
this.scene.ui.showText(text, null, nextAction, i === 0 ? 750 : 0, true);
this.scene.ui.showText(text, null, nextAction, i === 1 ? this.FIRST_DIALOGUE_PROMPT_DELAY : 0, true);
}
i++;
};
showNextDialogue();

View File

@ -0,0 +1,239 @@
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 { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option";
import { runSelectMysteryEncounterOption } from "#test/mystery-encounter/encounterTestUtils";
import { SelectModifierPhase } from "#app/phases";
import BattleScene from "#app/battle-scene";
import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
import { DepartmentStoreSaleEncounter } from "#app/data/mystery-encounters/encounters/department-store-sale-encounter";
import { CIVILIZATION_ENCOUNTER_BIOMES } from "#app/data/mystery-encounters/mystery-encounters";
const namespace = "mysteryEncounter:departmentStoreSale";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.PLAINS;
const defaultWave = 37;
describe("Department Store Sale - 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.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWaves(true);
const biomeMap = new Map<Biome, MysteryEncounterType[]>([
[Biome.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]],
]);
CIVILIZATION_ENCOUNTER_BIOMES.forEach(biome => {
biomeMap.set(biome, [MysteryEncounterType.DEPARTMENT_STORE_SALE]);
});
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(biomeMap);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty);
expect(DepartmentStoreSaleEncounter.encounterType).toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE);
expect(DepartmentStoreSaleEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON);
expect(DepartmentStoreSaleEncounter.dialogue).toBeDefined();
expect(DepartmentStoreSaleEncounter.dialogue.intro).toStrictEqual([
{ text: `${namespace}:intro` },
{
speaker: `${namespace}:speaker`,
text: `${namespace}:intro_dialogue`,
}
]);
expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`);
expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`);
expect(DepartmentStoreSaleEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}:query`);
expect(DepartmentStoreSaleEncounter.options.length).toBe(4);
});
it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => {
game.override.startingBiome(Biome.VOLCANO);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE);
});
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.DEPARTMENT_STORE_SALE);
});
it("should not run above wave 179", async () => {
game.override.startingWave(181);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter).toBeUndefined();
});
describe("Option 1 - TM Shop", () => {
it("should have the correct properties", () => {
const option = DepartmentStoreSaleEncounter.options[0];
expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option:1:label`,
buttonTooltip: `${namespace}:option:1:tooltip`,
});
});
it("should have shop with only TMs", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty);
await runSelectMysteryEncounterOption(game, 1);
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);
for (const option of modifierSelectHandler.options) {
expect(option.modifierTypeOption.type.id).toContain("TM_");
}
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty);
await runSelectMysteryEncounterOption(game, 1);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 2 - Vitamin Shop", () => {
it("should have the correct properties", () => {
const option = DepartmentStoreSaleEncounter.options[1];
expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option:2:label`,
buttonTooltip: `${namespace}:option:2:tooltip`,
});
});
it("should have shop with only Vitamins", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty);
await runSelectMysteryEncounterOption(game, 2);
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(3);
for (const option of modifierSelectHandler.options) {
expect(option.modifierTypeOption.type.id.includes("PP_UP") ||
option.modifierTypeOption.type.id.includes("BASE_STAT_BOOSTER")).toBeTruthy();
}
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty);
await runSelectMysteryEncounterOption(game, 2);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 3 - X Item Shop", () => {
it("should have the correct properties", () => {
const option = DepartmentStoreSaleEncounter.options[2];
expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option:3:label`,
buttonTooltip: `${namespace}:option:3:tooltip`,
});
});
it("should have shop with only X Items", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty);
await runSelectMysteryEncounterOption(game, 3);
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.includes("DIRE_HIT") ||
option.modifierTypeOption.type.id.includes("TEMP_STAT_BOOSTER")).toBeTruthy();
}
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty);
await runSelectMysteryEncounterOption(game, 3);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 4 - Pokeball Shop", () => {
it("should have the correct properties", () => {
const option = DepartmentStoreSaleEncounter.options[3];
expect(option.optionMode).toBe(EncounterOptionMode.DEFAULT);
expect(option.dialogue).toBeDefined();
expect(option.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option:4:label`,
buttonTooltip: `${namespace}:option:4:tooltip`,
});
});
it("should have shop with only Pokeballs", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty);
await runSelectMysteryEncounterOption(game, 4);
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);
for (const option of modifierSelectHandler.options) {
expect(option.modifierTypeOption.type.id).toContain("BALL");
}
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.DEPARTMENT_STORE_SALE, defaultParty);
await runSelectMysteryEncounterOption(game, 4);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
});

View File

@ -24,7 +24,7 @@ const namespace = "mysteryEncounter:fieryFallout";
/** Arcanine and Ninetails for 2 Fire types. Lapras, Gengar, Abra for burnable mon. */
const defaultParty = [Species.ARCANINE, Species.NINETALES, Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.VOLCANO;
const defaultWave = 45;
const defaultWave = 56;
describe("Fiery Fallout - Mystery Encounter", () => {
let phaserGame: Phaser.Game;
@ -42,7 +42,6 @@ describe("Fiery Fallout - Mystery Encounter", () => {
game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWave(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([
@ -54,10 +53,11 @@ describe("Fiery Fallout - Mystery Encounter", () => {
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT);
await game.runToMysteryEncounter(MysteryEncounterType.FIERY_FALLOUT, defaultParty);
expect(FieryFalloutEncounter.encounterType).toBe(MysteryEncounterType.FIERY_FALLOUT);
@ -74,7 +74,7 @@ describe("Fiery Fallout - Mystery Encounter", () => {
game.override.startingBiome(Biome.MOUNTAIN);
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT);
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT);
});
it("should not run below wave 41", async () => {
@ -82,7 +82,7 @@ describe("Fiery Fallout - Mystery Encounter", () => {
await game.runToMysteryEncounter();
expect(scene.currentBattle.mysteryEncounter.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT);
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.FIERY_FALLOUT);
});
it("should not run above wave 179", async () => {
@ -96,7 +96,7 @@ describe("Fiery Fallout - Mystery Encounter", () => {
it("should initialize fully ", async () => {
vi.spyOn(scene, "currentBattle", "get").mockReturnValue({ mysteryEncounter: FieryFalloutEncounter } as Battle);
const weatherSpy = vi.spyOn(scene.arena, "trySetWeather").mockReturnValue(true);
const moveInitSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets");
const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim");
const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets");
const { onInit } = FieryFalloutEncounter;
@ -130,10 +130,6 @@ describe("Fiery Fallout - Mystery Encounter", () => {
});
describe("Option 1 - Fight 2 Volcarona", () => {
beforeEach(async () => {
game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT);
});
it("should have the correct properties", () => {
const option1 = FieryFalloutEncounter.options[0];
expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT);
@ -180,14 +176,10 @@ describe("Fiery Fallout - Mystery Encounter", () => {
&& (m as PokemonHeldItemModifier).pokemonId === leadPokemonId, true) as PokemonHeldItemModifier[];
const charcoal = leadPokemonItems.find(i => i.type.name === "Charcoal");
expect(charcoal).toBeDefined;
}, 100000000);
});
});
describe("Option 2 - Suffer the weather", () => {
beforeEach(async () => {
game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT);
});
it("should have the correct properties", () => {
const option1 = FieryFalloutEncounter.options[1];
expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT);
@ -235,10 +227,6 @@ describe("Fiery Fallout - Mystery Encounter", () => {
});
describe("Option 3 - use FIRE types", () => {
beforeEach(async () => {
game.override.mysteryEncounter(MysteryEncounterType.FIERY_FALLOUT);
});
it("should have the correct properties", () => {
const option1 = FieryFalloutEncounter.options[2];
expect(option1.optionMode).toBe(EncounterOptionMode.DISABLED_OR_SPECIAL);

View File

@ -32,7 +32,6 @@ describe("Lost at Sea - Mystery Encounter", () => {
game.override.mysteryEncounterChance(100);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
game.override.disableTrainerWave(true);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([
@ -44,10 +43,11 @@ describe("Lost at Sea - Mystery Encounter", () => {
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA);
await game.runToMysteryEncounter(MysteryEncounterType.LOST_AT_SEA, defaultParty);
expect(LostAtSeaEncounter.encounterType).toBe(MysteryEncounterType.LOST_AT_SEA);
@ -99,10 +99,6 @@ describe("Lost at Sea - Mystery Encounter", () => {
});
describe("Option 1 - Surf", () => {
beforeEach(async () => {
game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA);
});
it("should have the correct properties", () => {
const option1 = LostAtSeaEncounter.options[0];
expect(option1.optionMode).toBe(EncounterOptionMode.DISABLED_OR_DEFAULT);
@ -149,10 +145,6 @@ describe("Lost at Sea - Mystery Encounter", () => {
});
describe("Option 2 - Fly", () => {
beforeEach(async () => {
game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA);
});
it("should have the correct properties", () => {
const option2 = LostAtSeaEncounter.options[1];
@ -202,10 +194,6 @@ describe("Lost at Sea - Mystery Encounter", () => {
});
describe("Option 3 - Wander aimlessy", () => {
beforeEach(async () => {
game.override.mysteryEncounter(MysteryEncounterType.LOST_AT_SEA);
});
it("should have the correct properties", () => {
const option3 = LostAtSeaEncounter.options[2];

View File

@ -0,0 +1,230 @@
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 Battle from "#app/battle";
import { getPokemonSpecies } from "#app/data/pokemon-species";
import * as BattleAnims from "#app/data/battle-anims";
import * as EncounterPhaseUtils from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { EncounterOptionMode } from "#app/data/mystery-encounters/mystery-encounter-option";
import { runSelectMysteryEncounterOption, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils";
import { CommandPhase, MovePhase, SelectModifierPhase } from "#app/phases";
import { Moves } from "#enums/moves";
import BattleScene from "#app/battle-scene";
import * as Modifiers from "#app/modifier/modifier";
import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter";
import { TheStrongStuffEncounter } from "#app/data/mystery-encounters/encounters/the-strong-stuff-encounter";
import { Nature } from "#app/data/nature";
import { BerryType } from "#enums/berry-type";
import { BattlerTagType } from "#enums/battler-tag-type";
import { PokemonMove } from "#app/field/pokemon";
import { Mode } from "#app/ui/ui";
import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler";
const namespace = "mysteryEncounter:theStrongStuff";
const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA];
const defaultBiome = Biome.CAVE;
const defaultWave = 45;
describe("The Strong Stuff - 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.mysteryEncounterTier(MysteryEncounterTier.COMMON);
game.override.startingWave(defaultWave);
game.override.startingBiome(defaultBiome);
vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue(
new Map<Biome, MysteryEncounterType[]>([
[Biome.CAVE, [MysteryEncounterType.THE_STRONG_STUFF]],
[Biome.MOUNTAIN, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]],
])
);
});
afterEach(() => {
game.phaseInterceptor.restoreOg();
vi.clearAllMocks();
vi.resetAllMocks();
});
it("should have the correct properties", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty);
expect(TheStrongStuffEncounter.encounterType).toBe(MysteryEncounterType.THE_STRONG_STUFF);
expect(TheStrongStuffEncounter.encounterTier).toBe(MysteryEncounterTier.COMMON);
expect(TheStrongStuffEncounter.dialogue).toBeDefined();
expect(TheStrongStuffEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}:intro` }]);
expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}:title`);
expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}:description`);
expect(TheStrongStuffEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}:query`);
expect(TheStrongStuffEncounter.options.length).toBe(2);
});
it("should not spawn outside of CAVE biome", async () => {
game.override.startingBiome(Biome.MOUNTAIN);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF);
});
it("should not run below wave 10", async () => {
game.override.startingWave(9);
await game.runToMysteryEncounter();
expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.THE_STRONG_STUFF);
});
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 () => {
vi.spyOn(scene, "currentBattle", "get").mockReturnValue({ mysteryEncounter: TheStrongStuffEncounter } as Battle);
const moveInitSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets");
const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets");
const { onInit } = TheStrongStuffEncounter;
expect(TheStrongStuffEncounter.onInit).toBeDefined();
const onInitResult = onInit(scene);
expect(TheStrongStuffEncounter.enemyPartyConfigs).toEqual([
{
levelAdditiveMultiplier: 1,
disableSwitch: true,
pokemonConfigs: [
{
species: getPokemonSpecies(Species.SHUCKLE),
isBoss: true,
bossSegments: 5,
spriteScale: 1.5,
nature: Nature.BOLD,
moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER],
modifierTypes: expect.any(Array),
tags: [BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON],
mysteryEncounterBattleEffects: expect.any(Function)
}
],
}
]);
await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled());
await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled());
expect(onInitResult).toBe(true);
});
describe("Option 1 - Power Swap BSTs", () => {
it("should have the correct properties", () => {
const option1 = TheStrongStuffEncounter.options[0];
expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT);
expect(option1.dialogue).toBeDefined();
expect(option1.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option:1:label`,
buttonTooltip: `${namespace}:option:1:tooltip`,
selected: [
{
text: `${namespace}:option:1:selected`,
},
],
});
});
it("should lower stats of highest BST and raise stats for rest of party", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty);
const bstsPrior = scene.getParty().map(p => p.getSpeciesForm().getBaseStatTotal());
await runSelectMysteryEncounterOption(game, 1);
const bstsAfter = scene.getParty().map(p => {
return p.getSpeciesForm().getBaseStatTotal();
});
expect(bstsAfter[0]).toEqual(bstsPrior[0] - 20 * 6);
expect(bstsAfter[1]).toEqual(bstsPrior[1] + 10 * 6);
expect(bstsAfter[2]).toEqual(bstsPrior[2] + 10 * 6);
});
it("should leave encounter without battle", async () => {
const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle");
await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty);
await runSelectMysteryEncounterOption(game, 1);
expect(leaveEncounterWithoutBattleSpy).toBeCalled();
});
});
describe("Option 2 - battle the Shuckle", () => {
it("should have the correct properties", () => {
const option1 = TheStrongStuffEncounter.options[1];
expect(option1.optionMode).toBe(EncounterOptionMode.DEFAULT);
expect(option1.dialogue).toBeDefined();
expect(option1.dialogue).toStrictEqual({
buttonLabel: `${namespace}:option:2:label`,
buttonTooltip: `${namespace}:option:2:tooltip`,
selected: [
{
text: `${namespace}:option:2:selected`,
},
],
});
});
it("should start battle against Shuckle", async () => {
const phaseSpy = vi.spyOn(scene, "pushPhase");
await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty);
await runSelectMysteryEncounterOption(game, 2, true);
const enemyField = scene.getEnemyField();
expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name);
expect(enemyField.length).toBe(1);
expect(enemyField[0].species.speciesId).toBe(Species.SHUCKLE);
expect(enemyField[0].summonData.battleStats).toEqual([0, 2, 0, 2, 0, 0, 0]);
const shuckleItems = scene.getModifiers(Modifiers.BerryModifier, false);
expect(shuckleItems.length).toBe(4);
expect(shuckleItems.find(m => m.berryType === BerryType.SITRUS)?.stackCount).toBe(1);
expect(shuckleItems.find(m => m.berryType === BerryType.GANLON)?.stackCount).toBe(1);
expect(shuckleItems.find(m => m.berryType === BerryType.APICOT)?.stackCount).toBe(1);
expect(shuckleItems.find(m => m.berryType === BerryType.LUM)?.stackCount).toBe(2);
expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.INFESTATION), new PokemonMove(Moves.SALT_CURE), new PokemonMove(Moves.GASTRO_ACID), new PokemonMove(Moves.HEAL_ORDER)]);
// Should have used moves pre-battle
const movePhases = phaseSpy.mock.calls.filter(p => p[0] instanceof MovePhase).map(p => p[0]);
expect(movePhases.length).toBe(2);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.GASTRO_ACID).length).toBe(1);
expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.STEALTH_ROCK).length).toBe(1);
});
it("should have Soul Dew in rewards", async () => {
await game.runToMysteryEncounter(MysteryEncounterType.THE_STRONG_STUFF, defaultParty);
await runSelectMysteryEncounterOption(game, 2, true);
await skipBattleRunMysteryEncounterRewardsPhase(game);
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(3);
expect(modifierSelectHandler.options[0].modifierTypeOption.type.id).toEqual("SOUL_DEW");
});
});
});

View File

@ -243,7 +243,7 @@ describe("Mystery Encounter Utils", () => {
arceus.hp = 100;
expect(arceus.isAllowedInBattle()).toBe(true);
koPlayerPokemon(arceus);
koPlayerPokemon(scene, arceus);
expect(arceus.isAllowedInBattle()).toBe(false);
});
});

View File

@ -23,8 +23,6 @@ describe("Mystery Encounters", () => {
game = new GameManager(phaserGame);
game.override.startingWave(11);
game.override.mysteryEncounterChance(100);
game.override.mysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
game.override.disableTrainerWave(true);
});
it("Spawns a mystery encounter", async () => {

View File

@ -28,7 +28,6 @@ describe("Mystery Encounter Phases", () => {
game = new GameManager(phaserGame);
game.override.startingWave(11);
game.override.mysteryEncounterChance(100);
game.override.mysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS);
// Seed guarantees wild encounter to be replaced by ME
game.override.seed("test");
});

View File

@ -38,6 +38,7 @@ import {MysteryEncounterPhase} from "#app/phases/mystery-encounter-phases";
import { OverridesHelper } from "./overridesHelper";
import { expect } from "vitest";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import { isNullOrUndefined } from "#app/utils";
/**
* Class to manage the game state and transitions between phases.
@ -151,6 +152,11 @@ export default class GameManager {
* @returns A promise that resolves when the EncounterPhase ends.
*/
async runToMysteryEncounter(encounterType?: MysteryEncounterType, species?: Species[]) {
if (!isNullOrUndefined(encounterType)) {
this.override.disableTrainerWaves(true);
this.override.mysteryEncounter(encounterType);
}
await this.runToTitle();
this.onNextPrompt("TitlePhase", Mode.TITLE, () => {
@ -167,7 +173,7 @@ export default class GameManager {
}, () => this.isCurrentPhase(MysteryEncounterPhase), true);
await this.phaseInterceptor.run(EncounterPhase);
if (encounterType) {
if (!isNullOrUndefined(encounterType)) {
expect(this.scene.currentBattle?.mysteryEncounter?.encounterType).toBe(encounterType);
}
}

View File

@ -6,6 +6,8 @@ import GameManager from "#test/utils/gameManager";
import { MysteryEncounterType } from "#enums/mystery-encounter-type";
import * as overrides from "#app/overrides";
import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter";
import * as GameMode from "#app/game-mode";
import { GameModes, getGameMode } from "#app/game-mode";
/**
* Helper to handle overrides in tests
@ -77,8 +79,13 @@ export class OverridesHelper {
* @returns spy instance
* @param disable - true
*/
disableTrainerWave(disable: boolean): MockInstance {
const spy = vi.spyOn(this.game.scene.gameMode, "isWaveTrainer").mockReturnValue(!disable);
disableTrainerWaves(disable: boolean): MockInstance {
const realFn = getGameMode;
const spy = vi.spyOn(GameMode, "getGameMode").mockImplementation((gameMode: GameModes) => {
const mode = realFn(gameMode);
mode.hasTrainers = !disable;
return mode;
});
this.log(`Standard trainer waves are ${disable? "disabled" : "enabled"}!`);
return spy;
}

View File

@ -32,7 +32,8 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler {
const charVarMap = new Map<integer, string>();
const delayMap = new Map<integer, integer>();
const soundMap = new Map<integer, string>();
const actionPattern = /@(c|d|s)\{(.*?)\}/;
const fadeMap = new Map<integer, integer>();
const actionPattern = /@(c|d|s|f)\{(.*?)\}/;
let actionMatch: RegExpExecArray;
while ((actionMatch = actionPattern.exec(text))) {
switch (actionMatch[1]) {
@ -45,6 +46,9 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler {
case "s":
soundMap.set(actionMatch.index, actionMatch[2]);
break;
case "f":
fadeMap.set(actionMatch.index, parseInt(actionMatch[2]));
break;
}
text = text.slice(0, actionMatch.index) + text.slice(actionMatch.index + actionMatch[2].length + 4);
}
@ -103,6 +107,7 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler {
const charVar = charVarMap.get(charIndex);
const charSound = soundMap.get(charIndex);
const charDelay = delayMap.get(charIndex);
const charFade = fadeMap.get(charIndex);
this.message.setText(text.slice(0, charIndex));
const advance = () => {
if (charVar) {
@ -134,6 +139,19 @@ export default abstract class MessageUiHandler extends AwaitableUiHandler {
advance();
}
});
} else if (charFade) {
this.textTimer.paused = true;
this.scene.time.delayedCall(150, () => {
this.scene.ui.fadeOut(750).then(() => {
const delay = Utils.getFrameMs(charFade);
this.scene.time.delayedCall(delay, () => {
this.scene.ui.fadeIn(500).then(() => {
this.textTimer.paused = false;
advance();
});
});
});
});
} else {
advance();
}

View File

@ -12,6 +12,7 @@ import { isNullOrUndefined } from "../utils";
import { getPokeballAtlasKey } from "../data/pokeball";
import { OptionSelectSettings } from "#app/data/mystery-encounters/utils/encounter-phase-utils";
import { getEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils";
import { MysteryEncounterTier } from "#app/data/mystery-encounters/mystery-encounter";
export default class MysteryEncounterUiHandler extends UiHandler {
private cursorContainer: Phaser.GameObjects.Container;
@ -135,7 +136,8 @@ export default class MysteryEncounterUiHandler extends UiHandler {
// TODO: If we need to handle cancel option? Maybe default logic to leave/run from encounter idk
}
} else {
switch (this.optionsContainer.list.length) {
switch (this.optionsContainer.getAll()?.length) {
default:
case 3:
success = this.handleTwoOptionMoveInput(button);
break;
@ -284,7 +286,7 @@ export default class MysteryEncounterUiHandler extends UiHandler {
this.cursor = cursor;
}
this.viewPartyIndex = this.optionsContainer.length - 1;
this.viewPartyIndex = this.optionsContainer.getAll()?.length - 1;
if (!this.cursorObj) {
this.cursorObj = this.scene.add.image(0, 0, "cursor");
@ -293,11 +295,11 @@ export default class MysteryEncounterUiHandler extends UiHandler {
if (cursor === this.viewPartyIndex) {
this.cursorObj.setPosition(246, -17);
} else if (this.optionsContainer.length === 3) { // 2 Options
} else if (this.optionsContainer.getAll()?.length === 3) { // 2 Options
this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 15);
} else if (this.optionsContainer.length === 4) { // 3 Options
} else if (this.optionsContainer.getAll()?.length === 4) { // 3 Options
this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0));
} else if (this.optionsContainer.length === 5) { // 4 Options
} else if (this.optionsContainer.getAll()?.length === 5) { // 4 Options
this.cursorObj.setPosition(-10.5 + (cursor % 2 === 1 ? 100 : 0), 7 + (cursor > 1 ? 16 : 0));
}
@ -368,7 +370,11 @@ export default class MysteryEncounterUiHandler extends UiHandler {
titleTextObject.setPosition(72 - titleTextObject.displayWidth / 2, 5.5);
// Rarity of encounter
const ballType = getPokeballAtlasKey(mysteryEncounter.encounterTier as number);
const index = mysteryEncounter.encounterTier === MysteryEncounterTier.COMMON ? 0 :
mysteryEncounter.encounterTier === MysteryEncounterTier.GREAT ? 1 :
mysteryEncounter.encounterTier === MysteryEncounterTier.ULTRA ? 2 :
mysteryEncounter.encounterTier === MysteryEncounterTier.ROGUE ? 3 : 4;
const ballType = getPokeballAtlasKey(index);
this.rarityBall.setTexture("pb", ballType);
const descriptionTextObject = addBBCodeTextObject(this.scene, 6, 25, descriptionText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } });