From de8a5b8987ffef2e590350627e0d974bda5f7ac0 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Sat, 3 Aug 2024 13:38:06 -0400 Subject: [PATCH 1/7] start clowning around encounter --- src/battle-scene.ts | 2 +- .../encounters/clowing-around-encounter.ts | 181 ++++++++++++++++++ .../mystery-encounters/mystery-encounters.ts | 7 +- .../utils/encounter-phase-utils.ts | 14 +- src/enums/mystery-encounter-type.ts | 3 +- src/locales/en/mystery-encounter.ts | 4 +- .../clowning-around-dialogue.ts | 33 ++++ src/overrides.ts | 4 +- 8 files changed, 239 insertions(+), 9 deletions(-) create mode 100644 src/data/mystery-encounters/encounters/clowing-around-encounter.ts create mode 100644 src/locales/en/mystery-encounters/clowning-around-dialogue.ts diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 2b01b777a64..249ddebdd60 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -1430,7 +1430,7 @@ export default class BattleScene extends SceneBase { const wave = waveIndex || this.currentBattle?.waveIndex || 0; this.waveSeed = Utils.shiftCharCodes(this.seed, wave); Phaser.Math.RND.sow([ this.waveSeed ]); - // console.log("Wave Seed:", this.waveSeed, wave); + console.log("Wave Seed:", this.waveSeed, wave); this.rngCounter = 0; } diff --git a/src/data/mystery-encounters/encounters/clowing-around-encounter.ts b/src/data/mystery-encounters/encounters/clowing-around-encounter.ts new file mode 100644 index 00000000000..a3f1d78da89 --- /dev/null +++ b/src/data/mystery-encounters/encounters/clowing-around-encounter.ts @@ -0,0 +1,181 @@ +import { + EnemyPartyConfig, + initBattleWithEnemyConfig, + setEncounterRewards, +} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { + trainerConfigs, + TrainerPartyCompoundTemplate, + TrainerPartyTemplate, +} from "#app/data/trainer-config"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { modifierTypes } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PartyMemberStrength } from "#enums/party-member-strength"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { Species } from "#enums/species"; +import { TrainerType } from "#enums/trainer-type"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:clowningAround"; + +/** + * Clowning Around encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/69 | GitHub Issue #69} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ClowningAroundEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.CLOWNING_AROUND) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 + .withIntroSpriteConfigs([ + { + spriteKey: Species.MR_MIME.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: -25, + tint: 0.3, + y: -3, + yShadow: -3 + }, + { + spriteKey: Species.BLACEPHALON.toString(), + fileRoot: "pokemon/exp", + hasShadow: true, + repeat: true, + x: 25, + tint: 0.3, + y: -3, + yShadow: -3 + }, + { + spriteKey: "harlequin", + fileRoot: "trainer", + hasShadow: true, + x: 0 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker` + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + + // Clown trainer is pulled from pool of boss trainers (gym leaders) for the biome + // They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons + const clownTrainerType = TrainerType.HARLEQUIN; + const clownPartyTemplate = new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONG), + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER), + new TrainerPartyTemplate(1, PartyMemberStrength.STRONG)); + const clownConfig = trainerConfigs[clownTrainerType].copy(); + clownConfig.setPartyTemplates(clownPartyTemplate); + clownConfig.partyTemplateFunc = null; // Overrides party template func + + encounter.enemyPartyConfigs.push({ + trainerConfig: clownConfig, + pokemonConfigs: [ // Overrides first 2 pokemon to be Mr. Mime and Blacephalon + { + species: getPokemonSpecies(Species.MR_MIME), + isBoss: false + }, + { + species: getPokemonSpecies(Species.BLACEPHALON), + isBoss: true + }, + ] + }); + + 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.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + // Spawn battle + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true }); + await initBattleWithEnemyConfig(scene, config); + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + // Spawn hard fight with ULTRA/GREAT reward (can improve with luck) + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; + + 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 + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 100); + return ret; + } + ) + .withSimpleOption( + { + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }, + async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + // Spawn brutal fight with ROGUE/ULTRA/GREAT reward (can improve with luck) + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[2]; + + // To avoid player level snowballing from picking this option + encounter.expMultiplier = 0.9; + + 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 + let ret; + scene.executeWithSeedOffset(() => { + ret = initBattleWithEnemyConfig(scene, config); + }, scene.currentBattle.waveIndex * 1000); + return ret; + } + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 6d49e6d014f..53cf834dcad 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -21,6 +21,7 @@ import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounter import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/a-trainers-test-encounter"; import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter"; import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter"; +import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowing-around-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -141,7 +142,7 @@ const humanTransitableBiomeEncounters: MysteryEncounterType[] = [ MysteryEncounterType.MYSTERIOUS_CHALLENGERS, MysteryEncounterType.SHADY_VITAMIN_DEALER, MysteryEncounterType.THE_POKEMON_SALESMAN, - MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE + MysteryEncounterType.AN_OFFER_YOU_CANT_REFUSE, ]; const civilizationBiomeEncounters: MysteryEncounterType[] = [ @@ -159,7 +160,8 @@ const anyBiomeEncounters: MysteryEncounterType[] = [ MysteryEncounterType.DELIBIRDY, MysteryEncounterType.A_TRAINERS_TEST, MysteryEncounterType.TRASH_TO_TREASURE, - MysteryEncounterType.BERRIES_ABOUND + MysteryEncounterType.BERRIES_ABOUND, + MysteryEncounterType.CLOWNING_AROUND ]; /** @@ -249,6 +251,7 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.A_TRAINERS_TEST] = ATrainersTestEncounter; allMysteryEncounters[MysteryEncounterType.TRASH_TO_TREASURE] = TrashToTreasureEncounter; allMysteryEncounters[MysteryEncounterType.BERRIES_ABOUND] = BerriesAboundEncounter; + allMysteryEncounters[MysteryEncounterType.CLOWNING_AROUND] = ClowningAroundEncounter; // Add extreme encounters to biome map extremeBiomeEncounters.forEach(encounter => { diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index ed050f6eb77..c166f287030 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -12,6 +12,7 @@ import PokemonData from "#app/system/pokemon-data"; import { OptionSelectConfig, OptionSelectItem } from "#app/ui/abstact-option-select-ui-handler"; import { PartyOption, PartyUiMode } from "#app/ui/party-ui-handler"; import { Mode } from "#app/ui/ui"; +import * as Utils from "#app/utils"; import { isNullOrUndefined } from "#app/utils"; import { BattlerTagType } from "#enums/battler-tag-type"; import { Biome } from "#enums/biome"; @@ -19,7 +20,6 @@ import { TrainerType } from "#enums/trainer-type"; import i18next from "i18next"; import BattleScene from "#app/battle-scene"; import Trainer, { TrainerVariant } from "#app/field/trainer"; -import * as Utils from "#app/utils"; import { Gender } from "#app/data/gender"; import { Nature } from "#app/data/nature"; import { Moves } from "#enums/moves"; @@ -152,7 +152,17 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: let isBoss = false; if (!loaded) { if (trainerType || trainerConfig) { - battle.enemyParty[e] = battle.trainer.genPartyMember(e); + // Allows overriding a trainer's pokemon to use specific species/data + if (e < partyConfig?.pokemonConfigs?.length) { + const config = partyConfig?.pokemonConfigs?.[e]; + level = config.level ? config.level : level; + dataSource = config.dataSource; + enemySpecies = config.species; + isBoss = config.isBoss; + battle.enemyParty[e] = scene.addEnemyPokemon(enemySpecies, level, TrainerSlot.TRAINER, isBoss, dataSource); + } else { + battle.enemyParty[e] = battle.trainer.genPartyMember(e); + } } else { if (e < partyConfig?.pokemonConfigs?.length) { const config = partyConfig?.pokemonConfigs?.[e]; diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index 6cf2c1b4fb2..d82a0075d46 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -18,5 +18,6 @@ export enum MysteryEncounterType { ABSOLUTE_AVARICE, A_TRAINERS_TEST, TRASH_TO_TREASURE, - BERRIES_ABOUND + BERRIES_ABOUND, + CLOWNING_AROUND } diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index 099802a5f53..11e6887b3e0 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -18,6 +18,7 @@ import { absoluteAvariceDialogue } from "#app/locales/en/mystery-encounters/abso import { aTrainersTestDialogue } from "#app/locales/en/mystery-encounters/a-trainers-test-dialogue"; import { trashToTreasureDialogue } from "#app/locales/en/mystery-encounters/trash-to-treasure-dialogue"; import { berriesAboundDialogue } from "#app/locales/en/mystery-encounters/berries-abound-dialogue"; +import { clowningAroundDialogue } from "#app/locales/en/mystery-encounters/clowning-around-dialogue"; /** * Patterns that can be used: @@ -60,5 +61,6 @@ export const mysteryEncounter = { absoluteAvarice: absoluteAvariceDialogue, aTrainersTest: aTrainersTestDialogue, trashToTreasure: trashToTreasureDialogue, - berriesAbound: berriesAboundDialogue + berriesAbound: berriesAboundDialogue, + clowningAround: clowningAroundDialogue } as const; diff --git a/src/locales/en/mystery-encounters/clowning-around-dialogue.ts b/src/locales/en/mystery-encounters/clowning-around-dialogue.ts new file mode 100644 index 00000000000..4b70c39eefb --- /dev/null +++ b/src/locales/en/mystery-encounters/clowning-around-dialogue.ts @@ -0,0 +1,33 @@ +export const clowningAroundDialogue = { + intro: "It's...@d{64} a clown?", + speaker: "Clown", + intro_dialogue: `Bumbling buffoon,\nbrace for a brilliant battle! + $You’ll be beaten by this brawling busker!\nBring it!`, + title: "Clowning Around", + description: "The clown seems eager to goad you into a battle, but to what end?\n\nSomething is off about this encounter.", + query: "What will you do?", + option: { + 1: { + label: "Battle the Clown", + tooltip: "(-) Strange Battle\n(?) Affects Pokémon Abilities", + selected: "Your pitiful Pokémon are poised for a pathetic performance!" + }, + 2: { + label: "Remain Unprovoked", + tooltip: "(-) Upsets the Clown\n(?) Affects Pokémon Items", + selected: "Dismal dodger, you deny a delightful duel?\nFeel my fury!", + selected_2: `The clown's Blacephalon uses Trick! + All of your {{switchPokemon}}'s items were randomly swapped!`, + selected_3: "Flustered fool, fall for my flawless deception!", + }, + 3: { + label: "Return the Insults", + tooltip: "(-) Upsets the Clown\n(?) Affects Pokémon Types", + selected: "I'm appalled at your absurd antics!\nTaste my temper!", + selected_2: `The clown's Blacephalon uses\na move you've never seen before! + All of your team's types were randomly swapped!`, + selected_3: "Flustered fool, fall for my flawless deception!", + }, + }, + outro: "The clown and his cohorts\ndisappear in a puff of smoke." +}; diff --git a/src/overrides.ts b/src/overrides.ts index 1196797fac0..8e83692925d 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -127,9 +127,9 @@ class DefaultOverrides { // ------------------------- // 1 to 256, set to null to ignore - readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; + readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; - readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; + readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.CLOWNING_AROUND; // ------------------------- // MODIFIER / ITEM OVERRIDES From afe101509404b2100f3457a5dda659cf2d526d37 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Mon, 5 Aug 2024 16:14:05 -0400 Subject: [PATCH 2/7] first implementation pass at clowning around --- .../battle-anims/encounter-smokescreen.json | 1694 +++++++++++++++++ src/data/battle-anims.ts | 9 +- .../an-offer-you-cant-refuse-encounter.ts | 2 +- .../encounters/clowing-around-encounter.ts | 181 -- .../encounters/clowning-around-encounter.ts | 469 +++++ .../encounters/dark-deal-encounter.ts | 2 +- .../encounters/field-trip-encounter.ts | 6 +- .../encounters/fiery-fallout-encounter.ts | 4 +- .../encounters/mysterious-chest-encounter.ts | 2 +- .../the-pokemon-salesman-encounter.ts | 2 +- .../encounters/the-strong-stuff-encounter.ts | 5 +- .../mystery-encounter-pokemon-data.ts | 16 + .../mystery-encounters/mystery-encounter.ts | 8 +- .../mystery-encounters/mystery-encounters.ts | 2 +- .../utils/encounter-phase-utils.ts | 28 +- .../utils/encounter-pokemon-utils.ts | 14 +- src/enums/battler-tag-type.ts | 2 +- src/field/pokemon.ts | 16 +- .../clowning-around-dialogue.ts | 19 +- src/overrides.ts | 4 +- src/phases.ts | 2 +- src/system/pokemon-data.ts | 11 + 22 files changed, 2265 insertions(+), 233 deletions(-) create mode 100644 public/battle-anims/encounter-smokescreen.json delete mode 100644 src/data/mystery-encounters/encounters/clowing-around-encounter.ts create mode 100644 src/data/mystery-encounters/encounters/clowning-around-encounter.ts create mode 100644 src/data/mystery-encounters/mystery-encounter-pokemon-data.ts diff --git a/public/battle-anims/encounter-smokescreen.json b/public/battle-anims/encounter-smokescreen.json new file mode 100644 index 00000000000..00e552dc503 --- /dev/null +++ b/public/battle-anims/encounter-smokescreen.json @@ -0,0 +1,1694 @@ +{ + "graphic": "PRAS- Smokescreen", + "frames": [ + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": 12.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": 8.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -4, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -3.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -4, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 21.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 50, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -7.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -8, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 17.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -11.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -12, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 13.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 21, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -15.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -16, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 5.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 17, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -19.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -20, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 13, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": 8.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 50, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 15.5, + "y": -23.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -24, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -11, + "y": -2.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 9, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": 4.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 100, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -11, + "y": -6.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -28, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": 0.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 8, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 23, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 50, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -11, + "y": -10.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -32, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": 1, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": -3.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 7, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 19, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 100, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -11, + "y": -14.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 0, + "y": -36, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": -3, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": -7.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 15, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 0, + "opacity": 150, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -11, + "y": -18.5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": -7, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": -12.5, + "y": -11.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 6, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 7, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 1, + "opacity": 150, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -12.5, + "y": -15.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 150, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": -11, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": 3, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -12.5, + "y": -19.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 100, + "priority": -1, + "focus": 2 + }, + { + "x": 11, + "y": -15, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": -1, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 2, + "opacity": 150, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": -12.5, + "y": -23.5, + "zoomX": 100, + "zoomY": 100, + "mirror": true, + "visible": true, + "target": 2, + "graphicFrame": 5, + "opacity": 50, + "priority": -1, + "focus": 2 + }, + { + "x": 4.5, + "y": -5, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 3, + "opacity": 150, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 4.5, + "y": -9, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 150, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 4.5, + "y": -13, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 100, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 4.5, + "y": -17, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 2, + "graphicFrame": 4, + "opacity": 50, + "priority": -1, + "focus": 2 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 3 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ], + [ + { + "x": 0, + "y": 0, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 0, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 2 + }, + { + "x": 128, + "y": -64, + "zoomX": 100, + "zoomY": 100, + "visible": true, + "target": 1, + "graphicFrame": 0, + "opacity": 255, + "locked": true, + "priority": -1, + "focus": 1 + } + ] + ], + "frameTimedEvents": { + "0": [ + { + "frameIndex": 0, + "resourceName": "PRSFX- Haze.wav", + "volume": 100, + "pitch": 85, + "eventType": "AnimTimedSoundEvent" + }, + { + "frameIndex": 0, + "resourceName": "Explosion1.m4a", + "volume": 100, + "pitch": 85, + "eventType": "AnimTimedSoundEvent" + } + ] + }, + "position": 2, + "hue": 0 +} \ No newline at end of file diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 6f2f293d099..6af8fedae8a 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -110,7 +110,8 @@ export enum CommonAnim { */ export enum EncounterAnim { MAGMA_BG, - MAGMA_SPOUT + MAGMA_SPOUT, + SMOKESCREEN } export class AnimConfig { @@ -533,16 +534,16 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { export async function initEncounterAnims(scene: BattleScene, encounterAnim: EncounterAnim | EncounterAnim[]): Promise { const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim]; const encounterAnimNames = Utils.getEnumKeys(EncounterAnim); - const encounterAnimIds = Utils.getEnumValues(EncounterAnim); + // const encounterAnimIds = Utils.getEnumValues(EncounterAnim); const encounterAnimFetches = []; for (const anim of anims) { if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { continue; } - const encounterAnimId = encounterAnimIds[anim]; + // const encounterAnimId = encounterAnimIds[anim]; encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`) .then(response => response.json()) - .then(cas => encounterAnims.set(encounterAnimId, new AnimConfig(cas)))); + .then(cas => encounterAnims.set(anim, new AnimConfig(cas)))); } await Promise.allSettled(encounterAnimFetches); } diff --git a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts index 0bf3145ea7e..8eabbbd8d05 100644 --- a/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts +++ b/src/data/mystery-encounters/encounters/an-offer-you-cant-refuse-encounter.ts @@ -62,7 +62,7 @@ export const AnOfferYouCantRefuseEncounter: IMysteryEncounter = const pokemon = getHighestStatTotalPlayerPokemon(scene, false); const price = scene.getWaveMoneyAmount(10); - encounter.setDialogueToken("strongestPokemon", pokemon.name); + encounter.setDialogueToken("strongestPokemon", pokemon.getNameToRender()); encounter.setDialogueToken("price", price.toString()); // Store pokemon and price diff --git a/src/data/mystery-encounters/encounters/clowing-around-encounter.ts b/src/data/mystery-encounters/encounters/clowing-around-encounter.ts deleted file mode 100644 index a3f1d78da89..00000000000 --- a/src/data/mystery-encounters/encounters/clowing-around-encounter.ts +++ /dev/null @@ -1,181 +0,0 @@ -import { - EnemyPartyConfig, - initBattleWithEnemyConfig, - setEncounterRewards, -} from "#app/data/mystery-encounters/utils/encounter-phase-utils"; -import { - trainerConfigs, - TrainerPartyCompoundTemplate, - TrainerPartyTemplate, -} from "#app/data/trainer-config"; -import { ModifierTier } from "#app/modifier/modifier-tier"; -import { modifierTypes } from "#app/modifier/modifier-type"; -import { MysteryEncounterType } from "#enums/mystery-encounter-type"; -import { PartyMemberStrength } from "#enums/party-member-strength"; -import BattleScene from "#app/battle-scene"; -import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; -import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; -import { Species } from "#enums/species"; -import { TrainerType } from "#enums/trainer-type"; -import { getPokemonSpecies } from "#app/data/pokemon-species"; - -/** the i18n namespace for the encounter */ -const namespace = "mysteryEncounter:clowningAround"; - -/** - * Clowning Around encounter. - * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/69 | GitHub Issue #69} - * @see For biome requirements check {@linkcode mysteryEncountersByBiome} - */ -export const ClowningAroundEncounter: IMysteryEncounter = - MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.CLOWNING_AROUND) - .withEncounterTier(MysteryEncounterTier.ULTRA) - .withSceneWaveRangeRequirement(10, 180) // waves 10 to 180 - .withIntroSpriteConfigs([ - { - spriteKey: Species.MR_MIME.toString(), - fileRoot: "pokemon", - hasShadow: true, - repeat: true, - x: -25, - tint: 0.3, - y: -3, - yShadow: -3 - }, - { - spriteKey: Species.BLACEPHALON.toString(), - fileRoot: "pokemon/exp", - hasShadow: true, - repeat: true, - x: 25, - tint: 0.3, - y: -3, - yShadow: -3 - }, - { - spriteKey: "harlequin", - fileRoot: "trainer", - hasShadow: true, - x: 0 - }, - ]) - .withIntroDialogue([ - { - text: `${namespace}.intro`, - }, - { - text: `${namespace}.intro_dialogue`, - speaker: `${namespace}.speaker` - }, - ]) - .withOnInit((scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - - // Clown trainer is pulled from pool of boss trainers (gym leaders) for the biome - // They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons - const clownTrainerType = TrainerType.HARLEQUIN; - const clownPartyTemplate = new TrainerPartyCompoundTemplate( - new TrainerPartyTemplate(1, PartyMemberStrength.STRONG), - new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER), - new TrainerPartyTemplate(1, PartyMemberStrength.STRONG)); - const clownConfig = trainerConfigs[clownTrainerType].copy(); - clownConfig.setPartyTemplates(clownPartyTemplate); - clownConfig.partyTemplateFunc = null; // Overrides party template func - - encounter.enemyPartyConfigs.push({ - trainerConfig: clownConfig, - pokemonConfigs: [ // Overrides first 2 pokemon to be Mr. Mime and Blacephalon - { - species: getPokemonSpecies(Species.MR_MIME), - isBoss: false - }, - { - species: getPokemonSpecies(Species.BLACEPHALON), - isBoss: true - }, - ] - }); - - 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.selected`, - }, - ], - }, - async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - // Spawn battle - const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; - - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true }); - await initBattleWithEnemyConfig(scene, config); - } - ) - .withSimpleOption( - { - buttonLabel: `${namespace}.option.2.label`, - buttonTooltip: `${namespace}.option.2.tooltip`, - selected: [ - { - text: `${namespace}.option.selected`, - }, - ], - }, - async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - // Spawn hard fight with ULTRA/GREAT reward (can improve with luck) - const config: EnemyPartyConfig = encounter.enemyPartyConfigs[1]; - - 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 - let ret; - scene.executeWithSeedOffset(() => { - ret = initBattleWithEnemyConfig(scene, config); - }, scene.currentBattle.waveIndex * 100); - return ret; - } - ) - .withSimpleOption( - { - buttonLabel: `${namespace}.option.3.label`, - buttonTooltip: `${namespace}.option.3.tooltip`, - selected: [ - { - text: `${namespace}.option.selected`, - }, - ], - }, - async (scene: BattleScene) => { - const encounter = scene.currentBattle.mysteryEncounter; - // Spawn brutal fight with ROGUE/ULTRA/GREAT reward (can improve with luck) - const config: EnemyPartyConfig = encounter.enemyPartyConfigs[2]; - - // To avoid player level snowballing from picking this option - encounter.expMultiplier = 0.9; - - 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 - let ret; - scene.executeWithSeedOffset(() => { - ret = initBattleWithEnemyConfig(scene, config); - }, scene.currentBattle.waveIndex * 1000); - return ret; - } - ) - .withOutroDialogue([ - { - text: `${namespace}.outro`, - }, - ]) - .build(); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts new file mode 100644 index 00000000000..7f8f622c41f --- /dev/null +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -0,0 +1,469 @@ +import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { BerryModifierType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import { PartyMemberStrength } from "#enums/party-member-strength"; +import BattleScene from "#app/battle-scene"; +import IMysteryEncounter, { MysteryEncounterBuilder } from "../mystery-encounter"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { Species } from "#enums/species"; +import { TrainerType } from "#enums/trainer-type"; +import { getPokemonSpecies } from "#app/data/pokemon-species"; +import { Abilities } from "#enums/abilities"; +import { applyModifierTypeToPlayerPokemon } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { Type } from "#app/data/type"; +import { MysteryEncounterOptionBuilder } from "#app/data/mystery-encounters/mystery-encounter-option"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { randSeedInt, randSeedShuffle } from "#app/utils"; +import { showEncounterDialogue, showEncounterText } from "#app/data/mystery-encounters/utils/encounter-dialogue-utils"; +import { Mode } from "#app/ui/ui"; +import i18next from "i18next"; +import { OptionSelectConfig } from "#app/ui/abstact-option-select-ui-handler"; +import { PlayerPokemon, PokemonMove } from "#app/field/pokemon"; +import { Ability } from "#app/data/ability"; +import { BerryModifier } from "#app/modifier/modifier"; +import { BerryType } from "#enums/berry-type"; +import { BattlerIndex } from "#app/battle"; +import { Moves } from "#enums/moves"; +import { EncounterAnim, EncounterBattleAnim } from "#app/data/battle-anims"; +import { MoveCategory } from "#app/data/move"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; + +/** the i18n namespace for the encounter */ +const namespace = "mysteryEncounter:clowningAround"; + +const RANDOM_ABILITY_POOL = [ + Abilities.STURDY, + Abilities.PICKUP, + Abilities.INTIMIDATE, + Abilities.GUTS, + Abilities.DROUGHT, + Abilities.DRIZZLE, + Abilities.SNOW_WARNING, + Abilities.SAND_STREAM, + Abilities.ELECTRIC_SURGE, + Abilities.PSYCHIC_SURGE, + Abilities.GRASSY_SURGE, + Abilities.MISTY_SURGE, + Abilities.MAGICIAN, + Abilities.SHEER_FORCE, + Abilities.PRANKSTER +]; + +/** + * Clowning Around encounter. + * @see {@link https://github.com/AsdarDevelops/PokeRogue-Events/issues/69 | GitHub Issue #69} + * @see For biome requirements check {@linkcode mysteryEncountersByBiome} + */ +export const ClowningAroundEncounter: IMysteryEncounter = + MysteryEncounterBuilder.withEncounterType(MysteryEncounterType.CLOWNING_AROUND) + .withEncounterTier(MysteryEncounterTier.ULTRA) + .withSceneWaveRangeRequirement(80, 180) + .withAnimations(EncounterAnim.SMOKESCREEN) + .withAutoHideIntroVisuals(false) + .withIntroSpriteConfigs([ + { + spriteKey: Species.MR_MIME.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: -25, + tint: 0.3, + y: -3, + yShadow: -3 + }, + { + spriteKey: Species.BLACEPHALON.toString(), + fileRoot: "pokemon/exp", + hasShadow: true, + repeat: true, + x: 25, + tint: 0.3, + y: -3, + yShadow: -3 + }, + { + spriteKey: "harlequin", + fileRoot: "trainer", + hasShadow: true, + x: 0, + y: 2, + yShadow: 2 + }, + ]) + .withIntroDialogue([ + { + text: `${namespace}.intro`, + }, + { + text: `${namespace}.intro_dialogue`, + speaker: `${namespace}.speaker` + }, + ]) + .withOnInit((scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + + // Clown trainer is pulled from pool of boss trainers (gym leaders) for the biome + // They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons + const clownTrainerType = TrainerType.HARLEQUIN; + const clownPartyTemplate = new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONG), + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER)); + const clownConfig = trainerConfigs[clownTrainerType].copy(); + clownConfig.setPartyTemplates(clownPartyTemplate); + clownConfig.setDoubleOnly(); + clownConfig.partyTemplateFunc = null; // Overrides party template func + + // Generate random ability for Blacephalon from pool + const ability = RANDOM_ABILITY_POOL[randSeedInt(RANDOM_ABILITY_POOL.length)]; + encounter.setDialogueToken("ability", new Ability(ability, 3).name); + encounter.misc = { ability }; + + encounter.enemyPartyConfigs.push({ + trainerConfig: clownConfig, + pokemonConfigs: [ // Overrides first 2 pokemon to be Mr. Mime and Blacephalon + { + species: getPokemonSpecies(Species.MR_MIME), + isBoss: true, + moveSet: [Moves.TEETER_DANCE, Moves.ALLY_SWITCH, Moves.DAZZLING_GLEAM, Moves.PSYCHIC] + }, + { // Blacephalon has the random ability from pool, and 2 entirely random types to fit with the theme of the encounter + species: getPokemonSpecies(Species.BLACEPHALON), + ability: ability, + mysteryEncounterData: new MysteryEncounterPokemonData(null, null, null, [randSeedInt(18), randSeedInt(18)]), + isBoss: true, + moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN] + }, + ], + doubleBattle: true + }); + + // Load animations/sfx for start of fight moves + loadCustomMovesForEncounter(scene, [Moves.ROLE_PLAY, Moves.TAUNT]); + + // These have to be defined at runtime so that modifierTypes exist + encounter.misc.RANDOM_ULTRA_POOL = [ + modifierTypes.REVIVER_SEED, + modifierTypes.GOLDEN_PUNCH, + modifierTypes.ATTACK_TYPE_BOOSTER, + modifierTypes.QUICK_CLAW, + modifierTypes.WIDE_LENS, + modifierTypes.WHITE_HERB + ]; + + encounter.misc.RANDOM_ROGUE_POOL = [ + modifierTypes.LEFTOVERS, + modifierTypes.SHELL_BELL, + modifierTypes.SOUL_DEW, + modifierTypes.SOOTHE_BELL, + modifierTypes.SCOPE_LENS, + modifierTypes.BATON, + modifierTypes.FOCUS_BAND, + modifierTypes.KINGS_ROCK, + modifierTypes.GRIP_CLAW + ]; + + return true; + }) + .withTitle(`${namespace}.title`) + .withDescription(`${namespace}.description`) + .withQuery(`${namespace}.query`) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.1.selected`, + speaker: `${namespace}.speaker` + }, + ], + }) + .withOptionPhase(async (scene: BattleScene) => { + const encounter = scene.currentBattle.mysteryEncounter; + // Spawn battle + const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; + + setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true }); + + // TODO: when Magic Room and Wonder Room are implemented, add those to start of battle + encounter.startOfBattleEffects.push( + { // Mr. Mime copies the Blacephalon's random ability + sourceBattlerIndex: BattlerIndex.ENEMY, + targets: [BattlerIndex.ENEMY_2], + move: new PokemonMove(Moves.ROLE_PLAY), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.PLAYER], + move: new PokemonMove(Moves.TAUNT), + ignorePp: true + }, + { + sourceBattlerIndex: BattlerIndex.ENEMY_2, + targets: [BattlerIndex.PLAYER_2], + move: new PokemonMove(Moves.TAUNT), + ignorePp: true + }); + + await transitionMysteryEncounterIntroVisuals(scene); + await initBattleWithEnemyConfig(scene, config); + }) + .withPostOptionPhase(async (scene: BattleScene): Promise => { + // After the battle, offer the player the opportunity to permanently swap ability + const abilityWasSwapped = await handleSwapAbility(scene); + if (abilityWasSwapped) { + await scene.ui.setMode(Mode.MESSAGE); + await showEncounterText(scene, `${namespace}.option.1.ability_gained`); + } + + // Play animations once ability swap is complete + // Trainer sprite that is shown at end of battle is not the same as mystery encounter intro visuals + scene.tweens.add({ + targets: scene.currentBattle.trainer, + x: "+=16", + y: "-=16", + alpha: 0, + ease: "Sine.easeInOut", + duration: 250 + }); + const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon(), scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 230, 40, 2); + return true; + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.2.selected`, + speaker: `${namespace}.speaker` + }, + { + text: `${namespace}.option.2.selected_2`, + }, + { + text: `${namespace}.option.2.selected_3`, + speaker: `${namespace}.speaker` + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Swap player's items on pokemon with the most items + // Item comparisons look at whichever Pokemon has the greatest number of TRANSFERABLE, non-berry items + // So Vitamins, form change items, etc. are not included + const encounter = scene.currentBattle.mysteryEncounter; + + const party = scene.getParty(); + let mostHeldItemsPokemon = party[0]; + let count = mostHeldItemsPokemon.getHeldItems() + .filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + .reduce((v, m) => v + m.stackCount, 0); + + party.forEach(pokemon => { + const nextCount = pokemon.getHeldItems() + .filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + .reduce((v, m) => v + m.stackCount, 0); + if (nextCount > count) { + mostHeldItemsPokemon = pokemon; + count = nextCount; + } + }); + + encounter.setDialogueToken("switchPokemon", mostHeldItemsPokemon.getNameToRender()); + + const items = mostHeldItemsPokemon.getHeldItems(); + + // Shuffles Berries (if they have any) + const berries = items.filter(m => m instanceof BerryModifier); + + berries.forEach(berry => { + const stackCount = berry.stackCount; + scene.removeModifier(berry); + const newBerry = generateModifierTypeOption(scene, modifierTypes.BERRY, [randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType]).type as BerryModifierType; + for (let i = 0; i < stackCount; i++) { + applyModifierTypeToPlayerPokemon(scene, mostHeldItemsPokemon, newBerry); + } + }); + + // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) + const transferableItems = items.filter(m => m.isTransferrable && !(m instanceof BerryModifier)); + + transferableItems.forEach(transferableItem => { + const stackCount = transferableItem.stackCount; + transferableItem.type.withTierFromPool(); + + // Lucky Eggs and other items that do not appear in item pools are treated as Ultra rarity + const tier = transferableItem.type.tier ?? ModifierTier.ULTRA; + + if (tier === ModifierTier.ULTRA) { + scene.removeModifier(transferableItem); + for (let i = 0; i < stackCount; i++) { + const newItemType = encounter.misc.RANDOM_ULTRA_POOL[randSeedInt(encounter.misc.RANDOM_ULTRA_POOL.length)]; + const newMod = generateModifierTypeOption(scene, newItemType).type as PokemonHeldItemModifierType; + applyModifierTypeToPlayerPokemon(scene, mostHeldItemsPokemon, newMod); + } + } else if (tier === ModifierTier.ROGUE) { + scene.removeModifier(transferableItem); + for (let i = 0; i < stackCount; i++) { + const newItemType = encounter.misc.RANDOM_ROGUE_POOL[randSeedInt(encounter.misc.RANDOM_ROGUE_POOL.length)]; + const newMod = generateModifierTypeOption(scene, newItemType).type as PokemonHeldItemModifierType; + applyModifierTypeToPlayerPokemon(scene, mostHeldItemsPokemon, newMod); + } + } + }); + + }) + .withOptionPhase(async (scene: BattleScene) => { + leaveEncounterWithoutBattle(scene, true); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Play animations + const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon(), scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 230, 40, 2); + await transitionMysteryEncounterIntroVisuals(scene); + }) + .build() + ) + .withOption( + new MysteryEncounterOptionBuilder() + .withOptionMode(MysteryEncounterOptionMode.DEFAULT) + .withDialogue({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.3.selected`, + speaker: `${namespace}.speaker` + }, + { + text: `${namespace}.option.3.selected_2`, + }, + { + text: `${namespace}.option.3.selected_3`, + speaker: `${namespace}.speaker` + }, + ], + }) + .withPreOptionPhase(async (scene: BattleScene) => { + // Swap player's types on all party pokemon + // If a Pokemon had a single type prior, they will still have a single type after + for (const pokemon of scene.getParty()) { + const originalTypes = pokemon.getTypes(false, false, true); + + // If the Pokemon has non-status moves that don't match the Pokemon's type, prioritizes those as the new type + // Makes the "randomness" of the shuffle slightly less punishing + let priorityTypes = pokemon.moveset + .filter(move => !originalTypes.includes(move.getMove().type) && move.getMove().category !== MoveCategory.STATUS) + .map(move => move.getMove().type); + if (priorityTypes?.length > 0) { + priorityTypes = [...new Set(priorityTypes)]; + randSeedShuffle(priorityTypes); + } + + let newTypes; + if (!originalTypes || originalTypes.length < 1) { + newTypes = priorityTypes?.length > 0 ? [priorityTypes.pop()] : [(randSeedInt(18) as Type)]; + } else { + newTypes = originalTypes.map(m => { + if (priorityTypes?.length > 0) { + const ret = priorityTypes.pop(); + randSeedShuffle(priorityTypes); + return ret; + } + + return randSeedInt(18) as Type; + }); + } + + if (!pokemon.mysteryEncounterData) { + pokemon.mysteryEncounterData = new MysteryEncounterPokemonData(null, null, null, newTypes); + } else { + pokemon.mysteryEncounterData.types = newTypes; + } + } + }) + .withOptionPhase(async (scene: BattleScene) => { + leaveEncounterWithoutBattle(scene, true); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Play animations + const background = new EncounterBattleAnim(EncounterAnim.SMOKESCREEN, scene.getPlayerPokemon(), scene.getPlayerPokemon()); + background.playWithoutTargets(scene, 230, 40, 2); + await transitionMysteryEncounterIntroVisuals(scene); + }) + .build() + ) + .withOutroDialogue([ + { + text: `${namespace}.outro`, + }, + ]) + .build(); + +async function handleSwapAbility(scene: BattleScene) { + return new Promise(async resolve => { + await showEncounterDialogue(scene, `${namespace}.option.1.apply_ability_dialogue`, `${namespace}.speaker`); + await showEncounterText(scene, `${namespace}.option.1.apply_ability_message`); + + scene.ui.setMode(Mode.MESSAGE).then(() => { + displayYesNoOptions(scene, resolve); + }); + }); +} + +function displayYesNoOptions(scene: BattleScene, resolve) { + showEncounterText(scene, `${namespace}.option.1.ability_prompt`, 500, false); + const fullOptions = [ + { + label: i18next.t("menu:yes"), + handler: () => { + onYesAbilitySwap(scene, resolve); + return true; + } + }, + { + label: i18next.t("menu:no"), + handler: () => { + resolve(false); + return true; + } + } + ]; + + const config: OptionSelectConfig = { + options: fullOptions, + maxOptions: 7, + yOffset: 0 + }; + scene.ui.setModeWithoutClear(Mode.OPTION_SELECT, config, null, true); +} + +function onYesAbilitySwap(scene: BattleScene, resolve) { + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Do ability swap + if (!pokemon.mysteryEncounterData) { + pokemon.mysteryEncounterData = new MysteryEncounterPokemonData(null, Abilities.AERILATE); + } + pokemon.mysteryEncounterData.ability = scene.currentBattle.mysteryEncounter.misc.ability; + scene.currentBattle.mysteryEncounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); + resolve(true); + }; + + const onPokemonNotSelected = () => { + scene.ui.setMode(Mode.MESSAGE).then(() => { + displayYesNoOptions(scene, resolve); + }); + }; + + selectPokemonForOption(scene, onPokemonSelected, onPokemonNotSelected); +} diff --git a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts index e8a8272df0e..fde28a2ab00 100644 --- a/src/data/mystery-encounters/encounters/dark-deal-encounter.ts +++ b/src/data/mystery-encounters/encounters/dark-deal-encounter.ts @@ -127,7 +127,7 @@ export const DarkDealEncounter: IMysteryEncounter = const removedPokemon = getRandomPlayerPokemon(scene, false, true); scene.removePokemonFromPlayerParty(removedPokemon); - scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", removedPokemon.name); + scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", removedPokemon.getNameToRender()); // Store removed pokemon types scene.currentBattle.mysteryEncounter.misc = [ diff --git a/src/data/mystery-encounters/encounters/field-trip-encounter.ts b/src/data/mystery-encounters/encounters/field-trip-encounter.ts index e67aaa1ffad..93e2d71aa1a 100644 --- a/src/data/mystery-encounters/encounters/field-trip-encounter.ts +++ b/src/data/mystery-encounters/encounters/field-trip-encounter.ts @@ -95,7 +95,7 @@ export const FieldTripEncounter: IMysteryEncounter = ]; setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); } else { - encounter.setDialogueToken("pokeName", pokemon.name); + encounter.setDialogueToken("pokeName", pokemon.getNameToRender()); encounter.setDialogueToken("move", move.getName()); encounter.options[0].dialogue.selected = [ { @@ -187,7 +187,7 @@ export const FieldTripEncounter: IMysteryEncounter = ]; setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); } else { - encounter.setDialogueToken("pokeName", pokemon.name); + encounter.setDialogueToken("pokeName", pokemon.getNameToRender()); encounter.setDialogueToken("move", move.getName()); encounter.options[1].dialogue.selected = [ { @@ -273,7 +273,7 @@ export const FieldTripEncounter: IMysteryEncounter = ]; setEncounterExp(scene, scene.getParty().map((p) => p.id), 50); } else { - encounter.setDialogueToken("pokeName", pokemon.name); + encounter.setDialogueToken("pokeName", pokemon.getNameToRender()); encounter.setDialogueToken("move", move.getName()); encounter.options[2].dialogue.selected = [ { diff --git a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts index 71311481ed1..5b4b582da01 100644 --- a/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts +++ b/src/data/mystery-encounters/encounters/fiery-fallout-encounter.ts @@ -191,7 +191,7 @@ export const FieryFalloutEncounter: IMysteryEncounter = const chosenPokemon = burnable[roll]; if (chosenPokemon.trySetStatus(StatusEffect.BURN)) { // Burn applied - encounter.setDialogueToken("burnedPokemon", chosenPokemon.name); + encounter.setDialogueToken("burnedPokemon", chosenPokemon.getNameToRender()); queueEncounterMessage(scene, `${namespace}.option.2.target_burned`); } } @@ -245,7 +245,7 @@ function giveLeadPokemonCharcoal(scene: BattleScene) { if (leadPokemon) { const charcoal = generateModifierTypeOption(scene, modifierTypes.ATTACK_TYPE_BOOSTER, [Type.FIRE]).type as AttackTypeBoosterModifierType; applyModifierTypeToPlayerPokemon(scene, leadPokemon, charcoal); - scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.name); + scene.currentBattle.mysteryEncounter.setDialogueToken("leadPokemon", leadPokemon.getNameToRender()); queueEncounterMessage(scene, `${namespace}.found_charcoal`); } } diff --git a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts index 5f6f235efc2..f0a96356eef 100644 --- a/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts +++ b/src/data/mystery-encounters/encounters/mysterious-chest-encounter.ts @@ -113,7 +113,7 @@ export const MysteriousChestEncounter: IMysteryEncounter = ); koPlayerPokemon(scene, highestLevelPokemon); - scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.name); + scene.currentBattle.mysteryEncounter.setDialogueToken("pokeName", highestLevelPokemon.getNameToRender()); // Show which Pokemon was KOed, then leave encounter with no rewards // Does this synchronously so that game over doesn't happen over result message await showEncounterText(scene, `${namespace}.option.1.bad`).then(() => { diff --git a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts index 26ddf86719d..80b7354b747 100644 --- a/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-pokemon-salesman-encounter.ts @@ -92,7 +92,7 @@ export const ThePokemonSalesmanEncounter: IMysteryEncounter = encounter.options[0].dialogue.buttonTooltip = `${namespace}.option.1.tooltip_shiny`; } const price = scene.getWaveMoneyAmount(priceMultiplier); - encounter.setDialogueToken("purchasePokemon", pokemon.name); + encounter.setDialogueToken("purchasePokemon", pokemon.getNameToRender()); encounter.setDialogueToken("price", price.toString()); encounter.misc = { price: price, diff --git a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts index e3e410b5b93..36ac31f8046 100644 --- a/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts +++ b/src/data/mystery-encounters/encounters/the-strong-stuff-encounter.ts @@ -16,6 +16,7 @@ import { BattleStat } from "#app/data/battle-stat"; import { BattlerTagType } from "#enums/battler-tag-type"; import { BerryType } from "#enums/berry-type"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; /** the i18n namespace for the encounter */ const namespace = "mysteryEncounter:theStrongStuff"; @@ -70,7 +71,7 @@ export const TheStrongStuffEncounter: IMysteryEncounter = species: getPokemonSpecies(Species.SHUCKLE), isBoss: true, bossSegments: 5, - spriteScale: 1.5, + mysteryEncounterData: new MysteryEncounterPokemonData(1.5), nature: Nature.BOLD, moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], modifierTypes: [ @@ -147,7 +148,7 @@ export const TheStrongStuffEncounter: IMysteryEncounter = modifyPlayerPokemonBST(pokemon, 10); } - encounter.setDialogueToken("highBstPokemon", highestBst.name); + encounter.setDialogueToken("highBstPokemon", highestBst.getNameToRender()); await showEncounterText(scene, `${namespace}.option.1.selected_2`, null, true); setEncounterRewards(scene, { fillRemaining: true }); diff --git a/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts b/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts new file mode 100644 index 00000000000..cab6d1ce82f --- /dev/null +++ b/src/data/mystery-encounters/mystery-encounter-pokemon-data.ts @@ -0,0 +1,16 @@ +import { Abilities } from "#enums/abilities"; +import { Type } from "#app/data/type"; + +export class MysteryEncounterPokemonData { + public spriteScale: number; + public ability: Abilities; + public passive: Abilities; + public types: Type[] = []; + + constructor(spriteScale?: number, ability?: Abilities, passive?: Abilities, types?: Type[]) { + this.spriteScale = spriteScale; + this.ability = ability; + this.passive = passive; + this.types = types; + } +} diff --git a/src/data/mystery-encounters/mystery-encounter.ts b/src/data/mystery-encounters/mystery-encounter.ts index be496fe1133..b1b47cef8ef 100644 --- a/src/data/mystery-encounters/mystery-encounter.ts +++ b/src/data/mystery-encounters/mystery-encounter.ts @@ -305,7 +305,7 @@ export default class IMysteryEncounter implements IMysteryEncounter { } } if (this.primaryPokemon?.length > 0) { - this.setDialogueToken("primaryName", this.primaryPokemon.name); + this.setDialogueToken("primaryName", this.primaryPokemon.getNameToRender()); for (const req of this.primaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, this.primaryPokemon); @@ -316,7 +316,7 @@ export default class IMysteryEncounter implements IMysteryEncounter { } } if (this.secondaryPokemonRequirements?.length > 0 && this.secondaryPokemon?.length > 0) { - this.setDialogueToken("secondaryName", this.secondaryPokemon[0].name); + this.setDialogueToken("secondaryName", this.secondaryPokemon[0].getNameToRender()); for (const req of this.secondaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, this.secondaryPokemon[0]); @@ -342,7 +342,7 @@ export default class IMysteryEncounter implements IMysteryEncounter { } } if (opt.primaryPokemonRequirements?.length > 0 && opt.primaryPokemon?.length > 0) { - this.setDialogueToken("option" + j + "PrimaryName", opt.primaryPokemon.name); + this.setDialogueToken("option" + j + "PrimaryName", opt.primaryPokemon.getNameToRender()); for (const req of opt.primaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, opt.primaryPokemon); @@ -353,7 +353,7 @@ export default class IMysteryEncounter implements IMysteryEncounter { } } if (opt.secondaryPokemonRequirements?.length > 0 && opt.secondaryPokemon?.length > 0) { - this.setDialogueToken("option" + j + "SecondaryName", opt.secondaryPokemon[0].name); + this.setDialogueToken("option" + j + "SecondaryName", opt.secondaryPokemon[0].getNameToRender()); for (const req of opt.secondaryPokemonRequirements) { if (!req.invertQuery) { const value = req.getDialogueToken(scene, opt.secondaryPokemon[0]); diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 53cf834dcad..b1c4ada12e7 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -21,7 +21,7 @@ import { AbsoluteAvariceEncounter } from "#app/data/mystery-encounters/encounter import { ATrainersTestEncounter } from "#app/data/mystery-encounters/encounters/a-trainers-test-encounter"; import { TrashToTreasureEncounter } from "#app/data/mystery-encounters/encounters/trash-to-treasure-encounter"; import { BerriesAboundEncounter } from "#app/data/mystery-encounters/encounters/berries-abound-encounter"; -import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowing-around-encounter"; +import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index c166f287030..675e864f976 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -30,6 +30,8 @@ import { TrainerConfig, trainerConfigs, TrainerSlot } from "#app/data/trainer-co import PokemonSpecies from "#app/data/pokemon-species"; import Overrides from "#app/overrides"; import { Egg, IEggOptions } from "#app/data/egg"; +import { Abilities } from "#enums/abilities"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; /** * Animates exclamation sprite over trainer's head at start of encounter @@ -63,7 +65,7 @@ export interface EnemyPokemonConfig { isBoss: boolean; bossSegments?: number; bossSegmentModifier?: number; // Additive to the determined segment number - spriteScale?: number; + mysteryEncounterData?: MysteryEncounterPokemonData; formIndex?: number; level?: number; gender?: Gender; @@ -71,6 +73,8 @@ export interface EnemyPokemonConfig { moveSet?: Moves[]; nature?: Nature; ivs?: [integer, integer, integer, integer, integer, integer]; + ability?: Abilities; + shiny?: boolean; /** Can set just the status, or pass a timer on the status turns */ status?: StatusEffect | [StatusEffect, number]; mysteryEncounterBattleEffects?: (pokemon: Pokemon) => void; @@ -210,11 +214,14 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: enemyPokemon.formIndex = config.formIndex; } - // Set scale - if (!isNullOrUndefined(config.spriteScale)) { - enemyPokemon.mysteryEncounterData = { - spriteScale: config.spriteScale - }; + // Set shiny + if (!isNullOrUndefined(config.shiny)) { + enemyPokemon.shiny = config.shiny; + } + + // Set custom mystery encounter data fields (such as sprite scale, custom abilities, types, etc.) + if (!isNullOrUndefined(config.mysteryEncounterData)) { + enemyPokemon.mysteryEncounterData = config.mysteryEncounterData; } // Set Boss @@ -252,6 +259,11 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: // Set summon data fields + // Set ability + if (!isNullOrUndefined(config.ability)) { + enemyPokemon.summonData.ability = config.ability; + } + // Set gender if (!isNullOrUndefined(config.gender)) { enemyPokemon.gender = config.gender; @@ -381,7 +393,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p const pokemon = scene.getParty()[slotIndex]; const secondaryOptions = onPokemonSelected(pokemon); if (!secondaryOptions) { - scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.name); + scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); resolve(true); return; } @@ -395,7 +407,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p const onSelect = option.handler; option.handler = () => { onSelect(); - scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.name); + scene.currentBattle.mysteryEncounter.setDialogueToken("selectedPokemon", pokemon.getNameToRender()); resolve(true); return true; }; diff --git a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts index e3dd3106aeb..03066314a5b 100644 --- a/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-pokemon-utils.ts @@ -20,10 +20,6 @@ import { getPokemonNameWithAffix } from "#app/messages"; import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { Gender } from "#app/data/gender"; -export interface MysteryEncounterPokemonData { - spriteScale?: number -} - export function getSpriteKeysFromSpecies(species: Species, female?: boolean, formIndex?: integer, shiny?: boolean, variant?: integer): { spriteKey: string, fileRoot: string } { const spriteKey = getPokemonSpecies(species).getSpriteKey(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); const fileRoot = getPokemonSpecies(species).getSpriteAtlasPath(female ?? false, formIndex ?? 0, shiny ?? false, variant ?? 0); @@ -447,7 +443,7 @@ function failCatch(scene: BattleScene, pokemon: EnemyPokemon, originalY: number, scene.currentBattle.lastUsedPokeball = pokeballType; removePb(scene, pokeball); - scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.name }), null, () => resolve(), null, true); + scene.ui.showText(i18next.t("battle:pokemonBrokeFree", { pokemonName: pokemon.getNameToRender() }), null, () => resolve(), null, true); }); } @@ -516,7 +512,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po Promise.all([pokemon.hideInfo(), scene.gameData.setPokemonCaught(pokemon)]).then(() => { if (scene.getParty().length === 6) { const promptRelease = () => { - scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.name }), null, () => { + scene.ui.showText(i18next.t("battle:partyFull", { pokemonName: pokemon.getNameToRender() }), null, () => { scene.pokemonInfoContainer.makeRoomForConfirmUi(); scene.ui.setMode(Mode.CONFIRM, () => { scene.ui.setMode(Mode.PARTY, PartyUiMode.RELEASE, 0, (slotIndex: integer, _option: PartyOption) => { @@ -544,7 +540,7 @@ export async function catchPokemon(scene: BattleScene, pokemon: EnemyPokemon, po }; if (showCatchObtainMessage) { - scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.name }), null, doPokemonCatchMenu, 0, true); + scene.ui.showText(i18next.t(isObtain ? "battle:pokemonObtained" : "battle:pokemonCaught", { pokemonName: pokemon.getNameToRender() }), null, doPokemonCatchMenu, 0, true); } else { doPokemonCatchMenu(); } @@ -581,7 +577,7 @@ export async function doPokemonFlee(scene: BattleScene, pokemon: EnemyPokemon): onComplete: () => { pokemon.setVisible(false); scene.field.remove(pokemon, true); - showEncounterText(scene, i18next.t("battle:pokemonFled", { pokemonName: pokemon.name }), 600, false) + showEncounterText(scene, i18next.t("battle:pokemonFled", { pokemonName: pokemon.getNameToRender() }), 600, false) .then(() => { resolve(); }); @@ -604,7 +600,7 @@ export function doPlayerFlee(scene: BattleScene, pokemon: EnemyPokemon): Promise onComplete: () => { pokemon.setVisible(false); scene.field.remove(pokemon, true); - showEncounterText(scene, i18next.t("battle:playerFled", { pokemonName: pokemon.name }), 600, false) + showEncounterText(scene, i18next.t("battle:playerFled", { pokemonName: pokemon.getNameToRender() }), 600, false) .then(() => { resolve(); }); diff --git a/src/enums/battler-tag-type.ts b/src/enums/battler-tag-type.ts index c6e638c2426..b50cc4f7f10 100644 --- a/src/enums/battler-tag-type.ts +++ b/src/enums/battler-tag-type.ts @@ -64,5 +64,5 @@ export enum BattlerTagType { STOCKPILING = "STOCKPILING", RECEIVE_DOUBLE_DAMAGE = "RECEIVE_DOUBLE_DAMAGE", ALWAYS_GET_HIT = "ALWAYS_GET_HIT", - MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON" // Provides effects on post-summon for MEs + MYSTERY_ENCOUNTER_POST_SUMMON = "MYSTERY_ENCOUNTER_POST_SUMMON", // Provides effects on post-summon for MEs } diff --git a/src/field/pokemon.ts b/src/field/pokemon.ts index e812d47fe1a..feca608038a 100644 --- a/src/field/pokemon.ts +++ b/src/field/pokemon.ts @@ -51,7 +51,7 @@ import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; import { getPokemonNameWithAffix } from "#app/messages.js"; -import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/utils/encounter-pokemon-utils"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; export enum FieldPosition { CENTER, @@ -187,6 +187,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { this.fusionVariant = dataSource.fusionVariant || 0; this.fusionGender = dataSource.fusionGender; this.fusionLuck = dataSource.fusionLuck; + this.mysteryEncounterData = dataSource.mysteryEncounterData; } else { this.id = Utils.randSeedInt(4294967296); this.ivs = ivs || Utils.getIvsFromId(this.id); @@ -233,6 +234,7 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } this.luck = (this.shiny ? this.variant + 1 : 0) + (this.fusionShiny ? this.fusionVariant + 1 : 0); this.fusionLuck = this.luck; + this.mysteryEncounterData = new MysteryEncounterPokemonData(); } this.generateName(); @@ -927,7 +929,10 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { } if (!types.length || !includeTeraType) { - if (!ignoreOverride && this.summonData?.types) { + if (this.mysteryEncounterData?.types?.length > 0) { + // "Permanent" override for a Pokemon's normal types, currently only used by Mystery Encounters + this.mysteryEncounterData.types.forEach(t => types.push(t)); + } else if (!ignoreOverride && this.summonData?.types) { this.summonData.types.forEach(t => types.push(t)); } else { const speciesForm = this.getSpeciesForm(ignoreOverride); @@ -994,6 +999,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.OPP_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_ABILITY_OVERRIDE]; } + if (this.mysteryEncounterData?.ability) { + return allAbilities[this.mysteryEncounterData.ability]; + } if (this.isFusion()) { return allAbilities[this.getFusionSpeciesForm(ignoreOverride).getAbility(this.fusionAbilityIndex)]; } @@ -1018,6 +1026,9 @@ export default abstract class Pokemon extends Phaser.GameObjects.Container { if (Overrides.OPP_PASSIVE_ABILITY_OVERRIDE && !this.isPlayer()) { return allAbilities[Overrides.OPP_PASSIVE_ABILITY_OVERRIDE]; } + if (this.mysteryEncounterData?.passive) { + return allAbilities[this.mysteryEncounterData.passive]; + } let starterSpeciesId = this.species.speciesId; while (pokemonPrevolutions.hasOwnProperty(starterSpeciesId)) { @@ -4059,6 +4070,7 @@ export class PokemonSummonData { public speciesForm: PokemonSpeciesForm; public fusionSpeciesForm: PokemonSpeciesForm; public ability: Abilities = Abilities.NONE; + public passiveAbility: Abilities = Abilities.NONE; public gender: Gender; public fusionGender: Gender; public stats: integer[]; diff --git a/src/locales/en/mystery-encounters/clowning-around-dialogue.ts b/src/locales/en/mystery-encounters/clowning-around-dialogue.ts index 4b70c39eefb..dd7797a3035 100644 --- a/src/locales/en/mystery-encounters/clowning-around-dialogue.ts +++ b/src/locales/en/mystery-encounters/clowning-around-dialogue.ts @@ -1,31 +1,32 @@ export const clowningAroundDialogue = { intro: "It's...@d{64} a clown?", speaker: "Clown", - intro_dialogue: `Bumbling buffoon,\nbrace for a brilliant battle! - $You’ll be beaten by this brawling busker!\nBring it!`, + intro_dialogue: "Bumbling buffoon, brace for a brilliant battle!\nYou’ll be beaten by this brawling busker!", title: "Clowning Around", - description: "The clown seems eager to goad you into a battle, but to what end?\n\nSomething is off about this encounter.", + description: "Something is off about this encounter. The clown seems eager to goad you into a battle, but to what end?\n\nThe Blacephalon is especially strange, like it has @[TOOLTIP_TITLE]{weird types and ability.}", query: "What will you do?", option: { 1: { label: "Battle the Clown", tooltip: "(-) Strange Battle\n(?) Affects Pokémon Abilities", - selected: "Your pitiful Pokémon are poised for a pathetic performance!" + selected: "Your pitiful Pokémon are poised for a pathetic performance!", + apply_ability_dialogue: "A sensational showcase!\nYour savvy suits a sensational skill as spoils!", + apply_ability_message: "The clown is offering to permanently Skill Swap one of your Pokémon's ability to {{ability}}!", + ability_prompt: "Would you like to permanently teach a Pokémon the {{ability}} ability?", + ability_gained: "@s{level_up_fanfare}{{chosenPokemon}} gained the {{ability}} ability!" }, 2: { label: "Remain Unprovoked", tooltip: "(-) Upsets the Clown\n(?) Affects Pokémon Items", selected: "Dismal dodger, you deny a delightful duel?\nFeel my fury!", - selected_2: `The clown's Blacephalon uses Trick! - All of your {{switchPokemon}}'s items were randomly swapped!`, + selected_2: "The clown's Blacephalon uses Trick!\nAll of your {{switchPokemon}}'s items were randomly swapped!", selected_3: "Flustered fool, fall for my flawless deception!", }, 3: { label: "Return the Insults", tooltip: "(-) Upsets the Clown\n(?) Affects Pokémon Types", - selected: "I'm appalled at your absurd antics!\nTaste my temper!", - selected_2: `The clown's Blacephalon uses\na move you've never seen before! - All of your team's types were randomly swapped!`, + selected: "Dismal dodger, you deny a delightful duel?\nFeel my fury!", + selected_2: "The clown's Blacephalon uses a strange move!\nAll of your team's types were randomly swapped!", selected_3: "Flustered fool, fall for my flawless deception!", }, }, diff --git a/src/overrides.ts b/src/overrides.ts index 8e83692925d..1196797fac0 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -127,9 +127,9 @@ class DefaultOverrides { // ------------------------- // 1 to 256, set to null to ignore - readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; + readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; - readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.CLOWNING_AROUND; + readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; // ------------------------- // MODIFIER / ITEM OVERRIDES diff --git a/src/phases.ts b/src/phases.ts index b0bfc739ae8..56206a97f1a 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -1369,7 +1369,7 @@ export class PostSummonPhase extends PokemonPhase { } this.scene.arena.applyTags(ArenaTrapTag, pokemon); - // If this is fight or flight mystery encounter and is enemy pokemon summon phase, add enraged tag + // If this is mystery encounter and has post summon phase tag, apply post summon effects if (pokemon.findTags(t => t instanceof MysteryEncounterPostSummonTag)) { pokemon.lapseTag(BattlerTagType.MYSTERY_ENCOUNTER_POST_SUMMON); } diff --git a/src/system/pokemon-data.ts b/src/system/pokemon-data.ts index 7e8f1e21c07..0601e883878 100644 --- a/src/system/pokemon-data.ts +++ b/src/system/pokemon-data.ts @@ -12,6 +12,7 @@ import { loadBattlerTag } from "../data/battler-tags"; import { Biome } from "#enums/biome"; import { Moves } from "#enums/moves"; import { Species } from "#enums/species"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; export default class PokemonData { public id: integer; @@ -54,6 +55,7 @@ export default class PokemonData { public bossSegments?: integer; public summonData: PokemonSummonData; + public mysteryEncounterData: MysteryEncounterPokemonData; constructor(source: Pokemon | any, forHistory: boolean = false) { const sourcePokemon = source instanceof Pokemon ? source : null; @@ -108,6 +110,7 @@ export default class PokemonData { this.status = sourcePokemon.status; if (this.player) { this.summonData = sourcePokemon.summonData; + this.mysteryEncounterData = sourcePokemon.mysteryEncounterData; } } } else { @@ -137,6 +140,14 @@ export default class PokemonData { this.summonData.tags = []; } } + + this.mysteryEncounterData = new MysteryEncounterPokemonData(); + if (!forHistory && source.mysteryEncounterData) { + this.mysteryEncounterData.spriteScale = source.mysteryEncounterData.spriteScale; + this.mysteryEncounterData.ability = source.mysteryEncounterData.ability; + this.mysteryEncounterData.passive = source.mysteryEncounterData.passive; + this.mysteryEncounterData.types = source.mysteryEncounterData.types; + } } } From 42eadcb36cc64bee9b9c5d84446fb3659d8bb19b Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Wed, 7 Aug 2024 22:52:12 -0400 Subject: [PATCH 3/7] add unit tests for clowning around --- .../mystery-encounters/encounter_radar.json | 41 -- public/images/mystery-encounters/exclaim.json | 41 -- src/data/battle-anims.ts | 2 - .../encounters/clowning-around-encounter.ts | 154 +++++--- .../utils/encounter-phase-utils.ts | 8 +- src/field/mystery-encounter-intro.ts | 3 - src/loading-scene.ts | 3 + src/overrides.ts | 4 +- src/phases.ts | 2 +- .../clowning-around-encounter.test.ts | 374 ++++++++++++++++++ .../the-strong-stuff-encounter.test.ts | 3 +- src/ui/mystery-encounter-ui-handler.ts | 4 +- 12 files changed, 480 insertions(+), 159 deletions(-) delete mode 100644 public/images/mystery-encounters/encounter_radar.json delete mode 100644 public/images/mystery-encounters/exclaim.json create mode 100644 src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts diff --git a/public/images/mystery-encounters/encounter_radar.json b/public/images/mystery-encounters/encounter_radar.json deleted file mode 100644 index 82f16af59f6..00000000000 --- a/public/images/mystery-encounters/encounter_radar.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "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$" - } -} diff --git a/public/images/mystery-encounters/exclaim.json b/public/images/mystery-encounters/exclaim.json deleted file mode 100644 index 31231910097..00000000000 --- a/public/images/mystery-encounters/exclaim.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "textures": [ - { - "image": "exclaim.png", - "format": "RGBA8888", - "size": { - "w": 32, - "h": 32 - }, - "scale": 1, - "frames": [ - { - "filename": "0001.png", - "rotated": false, - "trimmed": false, - "sourceSize": { - "w": 32, - "h": 32 - }, - "spriteSourceSize": { - "x": 0, - "y": 0, - "w": 32, - "h": 32 - }, - "frame": { - "x": 0, - "y": 0, - "w": 32, - "h": 32 - } - } - ] - } - ], - "meta": { - "app": "https://www.codeandweb.com/texturepacker", - "version": "3.0", - "smartupdate": "$TexturePacker:SmartUpdate:895f0a79b89fa0fb44167f4584fd9a22:357b46953b7e17c6b2f43a62d52855d8:cc1ed0e4f90aaa9dcf1b39a0af1283b0$" - } -} diff --git a/src/data/battle-anims.ts b/src/data/battle-anims.ts index 6af8fedae8a..ffc39efedac 100644 --- a/src/data/battle-anims.ts +++ b/src/data/battle-anims.ts @@ -534,13 +534,11 @@ export function initMoveAnim(scene: BattleScene, move: Moves): Promise { export async function initEncounterAnims(scene: BattleScene, encounterAnim: EncounterAnim | EncounterAnim[]): Promise { const anims = Array.isArray(encounterAnim) ? encounterAnim : [encounterAnim]; const encounterAnimNames = Utils.getEnumKeys(EncounterAnim); - // const encounterAnimIds = Utils.getEnumValues(EncounterAnim); const encounterAnimFetches = []; for (const anim of anims) { if (encounterAnims.has(anim) && !isNullOrUndefined(encounterAnims.get(anim))) { continue; } - // const encounterAnimId = encounterAnimIds[anim]; encounterAnimFetches.push(scene.cachedFetch(`./battle-anims/encounter-${encounterAnimNames[anim].toLowerCase().replace(/\_/g, "-")}.json`) .then(response => response.json()) .then(cas => encounterAnims.set(anim, new AnimConfig(cas)))); diff --git a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts index 7f8f622c41f..c03508a5700 100644 --- a/src/data/mystery-encounters/encounters/clowning-around-encounter.ts +++ b/src/data/mystery-encounters/encounters/clowning-around-encounter.ts @@ -1,7 +1,7 @@ import { EnemyPartyConfig, generateModifierTypeOption, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, loadCustomMovesForEncounter, selectPokemonForOption, setEncounterRewards, transitionMysteryEncounterIntroVisuals, } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; import { trainerConfigs, TrainerPartyCompoundTemplate, TrainerPartyTemplate, } from "#app/data/trainer-config"; import { ModifierTier } from "#app/modifier/modifier-tier"; -import { BerryModifierType, modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; import { PartyMemberStrength } from "#enums/party-member-strength"; import BattleScene from "#app/battle-scene"; @@ -104,16 +104,14 @@ export const ClowningAroundEncounter: IMysteryEncounter = .withOnInit((scene: BattleScene) => { const encounter = scene.currentBattle.mysteryEncounter; - // Clown trainer is pulled from pool of boss trainers (gym leaders) for the biome - // They are given an E4 template team, so will be stronger than usual boss encounter and always have 6 mons const clownTrainerType = TrainerType.HARLEQUIN; + const clownConfig = trainerConfigs[clownTrainerType].copy(); const clownPartyTemplate = new TrainerPartyCompoundTemplate( new TrainerPartyTemplate(1, PartyMemberStrength.STRONG), new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER)); - const clownConfig = trainerConfigs[clownTrainerType].copy(); clownConfig.setPartyTemplates(clownPartyTemplate); clownConfig.setDoubleOnly(); - clownConfig.partyTemplateFunc = null; // Overrides party template func + clownConfig.partyTemplateFunc = null; // Overrides party template func if it exists // Generate random ability for Blacephalon from pool const ability = RANDOM_ABILITY_POOL[randSeedInt(RANDOM_ABILITY_POOL.length)]; @@ -142,28 +140,6 @@ export const ClowningAroundEncounter: IMysteryEncounter = // Load animations/sfx for start of fight moves loadCustomMovesForEncounter(scene, [Moves.ROLE_PLAY, Moves.TAUNT]); - // These have to be defined at runtime so that modifierTypes exist - encounter.misc.RANDOM_ULTRA_POOL = [ - modifierTypes.REVIVER_SEED, - modifierTypes.GOLDEN_PUNCH, - modifierTypes.ATTACK_TYPE_BOOSTER, - modifierTypes.QUICK_CLAW, - modifierTypes.WIDE_LENS, - modifierTypes.WHITE_HERB - ]; - - encounter.misc.RANDOM_ROGUE_POOL = [ - modifierTypes.LEFTOVERS, - modifierTypes.SHELL_BELL, - modifierTypes.SOUL_DEW, - modifierTypes.SOOTHE_BELL, - modifierTypes.SCOPE_LENS, - modifierTypes.BATON, - modifierTypes.FOCUS_BAND, - modifierTypes.KINGS_ROCK, - modifierTypes.GRIP_CLAW - ]; - return true; }) .withTitle(`${namespace}.title`) @@ -187,7 +163,7 @@ export const ClowningAroundEncounter: IMysteryEncounter = // Spawn battle const config: EnemyPartyConfig = encounter.enemyPartyConfigs[0]; - setEncounterRewards(scene, { guaranteedModifierTypeFuncs: [modifierTypes.TM_COMMON, modifierTypes.TM_GREAT, modifierTypes.MEMORY_MUSHROOM], fillRemaining: true }); + setEncounterRewards(scene, { fillRemaining: true }); // TODO: when Magic Room and Wonder Room are implemented, add those to start of battle encounter.startOfBattleEffects.push( @@ -217,7 +193,6 @@ export const ClowningAroundEncounter: IMysteryEncounter = // After the battle, offer the player the opportunity to permanently swap ability const abilityWasSwapped = await handleSwapAbility(scene); if (abilityWasSwapped) { - await scene.ui.setMode(Mode.MESSAGE); await showEncounterText(scene, `${namespace}.option.1.ability_gained`); } @@ -284,44 +259,33 @@ export const ClowningAroundEncounter: IMysteryEncounter = const items = mostHeldItemsPokemon.getHeldItems(); // Shuffles Berries (if they have any) - const berries = items.filter(m => m instanceof BerryModifier); + let numBerries = 0; + items.filter(m => m instanceof BerryModifier) + .forEach(m => { + numBerries += m.stackCount; + scene.removeModifier(m); + }); - berries.forEach(berry => { - const stackCount = berry.stackCount; - scene.removeModifier(berry); - const newBerry = generateModifierTypeOption(scene, modifierTypes.BERRY, [randSeedInt(Object.keys(BerryType).filter(s => !isNaN(Number(s))).length) as BerryType]).type as BerryModifierType; - for (let i = 0; i < stackCount; i++) { - applyModifierTypeToPlayerPokemon(scene, mostHeldItemsPokemon, newBerry); - } - }); + generateItemsOfTier(scene, mostHeldItemsPokemon, numBerries, "Berries"); // Shuffle Transferable held items in the same tier (only shuffles Ultra and Rogue atm) - const transferableItems = items.filter(m => m.isTransferrable && !(m instanceof BerryModifier)); - - transferableItems.forEach(transferableItem => { - const stackCount = transferableItem.stackCount; - transferableItem.type.withTierFromPool(); - - // Lucky Eggs and other items that do not appear in item pools are treated as Ultra rarity - const tier = transferableItem.type.tier ?? ModifierTier.ULTRA; - - if (tier === ModifierTier.ULTRA) { - scene.removeModifier(transferableItem); - for (let i = 0; i < stackCount; i++) { - const newItemType = encounter.misc.RANDOM_ULTRA_POOL[randSeedInt(encounter.misc.RANDOM_ULTRA_POOL.length)]; - const newMod = generateModifierTypeOption(scene, newItemType).type as PokemonHeldItemModifierType; - applyModifierTypeToPlayerPokemon(scene, mostHeldItemsPokemon, newMod); + let numUltra = 0; + let numRogue = 0; + items.filter(m => m.isTransferrable && !(m instanceof BerryModifier)) + .forEach(m => { + const type = m.type.withTierFromPool(); + const tier = type.tier ?? ModifierTier.ULTRA; + if (type.id === "LUCKY_EGG" || tier === ModifierTier.ULTRA) { + numUltra += m.stackCount; + scene.removeModifier(m); + } else if (type.id === "GOLDEN_EGG" || tier === ModifierTier.ROGUE) { + numRogue += m.stackCount; + scene.removeModifier(m); } - } else if (tier === ModifierTier.ROGUE) { - scene.removeModifier(transferableItem); - for (let i = 0; i < stackCount; i++) { - const newItemType = encounter.misc.RANDOM_ROGUE_POOL[randSeedInt(encounter.misc.RANDOM_ROGUE_POOL.length)]; - const newMod = generateModifierTypeOption(scene, newItemType).type as PokemonHeldItemModifierType; - applyModifierTypeToPlayerPokemon(scene, mostHeldItemsPokemon, newMod); - } - } - }); + }); + generateItemsOfTier(scene, mostHeldItemsPokemon, numUltra, ModifierTier.ULTRA); + generateItemsOfTier(scene, mostHeldItemsPokemon, numRogue, ModifierTier.ROGUE); }) .withOptionPhase(async (scene: BattleScene) => { leaveEncounterWithoutBattle(scene, true); @@ -456,7 +420,7 @@ function onYesAbilitySwap(scene: BattleScene, resolve) { } pokemon.mysteryEncounterData.ability = scene.currentBattle.mysteryEncounter.misc.ability; scene.currentBattle.mysteryEncounter.setDialogueToken("chosenPokemon", pokemon.getNameToRender()); - resolve(true); + scene.ui.setMode(Mode.MESSAGE).then(() => resolve(true)); }; const onPokemonNotSelected = () => { @@ -467,3 +431,67 @@ function onYesAbilitySwap(scene: BattleScene, resolve) { selectPokemonForOption(scene, onPokemonSelected, onPokemonNotSelected); } + +function generateItemsOfTier(scene: BattleScene, pokemon: PlayerPokemon, numItems: integer, tier: ModifierTier | "Berries") { + // These pools have to be defined at runtime so that modifierTypes exist + // Pools have instances of the modifier type equal to the max stacks that modifier can be applied to any one pokemon + // This is to prevent "over-generating" a random item of a certain type during item swaps + const ultraPool = [ + [modifierTypes.REVIVER_SEED, 1], + [modifierTypes.GOLDEN_PUNCH, 5], + [modifierTypes.ATTACK_TYPE_BOOSTER, 99], + [modifierTypes.QUICK_CLAW, 3], + [modifierTypes.WIDE_LENS, 3], + [modifierTypes.WHITE_HERB, 2] + ]; + + const roguePool = [ + [modifierTypes.LEFTOVERS, 4], + [modifierTypes.SHELL_BELL, 4], + [modifierTypes.SOUL_DEW, 10], + [modifierTypes.SOOTHE_BELL, 3], + [modifierTypes.SCOPE_LENS, 5], + [modifierTypes.BATON, 1], + [modifierTypes.FOCUS_BAND, 5], + [modifierTypes.KINGS_ROCK, 3], + [modifierTypes.GRIP_CLAW, 5] + ]; + + const berryPool = [ + [BerryType.APICOT, 3], + [BerryType.ENIGMA, 2], + [BerryType.GANLON, 3], + [BerryType.LANSAT, 3], + [BerryType.LEPPA, 2], + [BerryType.LIECHI, 3], + [BerryType.LUM, 2], + [BerryType.PETAYA, 3], + [BerryType.SALAC, 2], + [BerryType.SITRUS, 2], + [BerryType.STARF, 3] + ]; + + let pool: any[]; + if (tier === "Berries") { + pool = berryPool; + } else { + pool = tier === ModifierTier.ULTRA ? ultraPool : roguePool; + } + + for (let i = 0; i < numItems; i++) { + const randIndex = randSeedInt(pool.length); + const newItemType = pool[randIndex]; + let newMod; + if (tier === "Berries") { + newMod = generateModifierTypeOption(scene, modifierTypes.BERRY, [newItemType[0]]).type as PokemonHeldItemModifierType; + } else { + newMod = generateModifierTypeOption(scene, newItemType[0]).type as PokemonHeldItemModifierType; + } + applyModifierTypeToPlayerPokemon(scene, pokemon, newMod); + // Decrement max stacks and remove from pool if at max + newItemType[1]--; + if (newItemType[1] <= 0) { + pool.splice(randIndex, 1); + } + } +} diff --git a/src/data/mystery-encounters/utils/encounter-phase-utils.ts b/src/data/mystery-encounters/utils/encounter-phase-utils.ts index 675e864f976..05ddf07d1e8 100644 --- a/src/data/mystery-encounters/utils/encounter-phase-utils.ts +++ b/src/data/mystery-encounters/utils/encounter-phase-utils.ts @@ -38,7 +38,7 @@ import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/myster * @param scene */ export function doTrainerExclamation(scene: BattleScene) { - const exclamationSprite = scene.addFieldSprite(0, 0, "exclaim"); + const exclamationSprite = scene.add.sprite(0, 0, "exclaim"); exclamationSprite.setName("exclamation"); scene.field.add(exclamationSprite); scene.field.moveTo(exclamationSprite, scene.field.getAll().length - 1); @@ -386,10 +386,12 @@ export function generateModifierTypeOption(scene: BattleScene, modifier: () => M */ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void, selectablePokemonFilter?: (pokemon: PlayerPokemon) => string): Promise { return new Promise(resolve => { + const modeToSetOnExit = scene.ui.getMode(); + // Open party screen to choose pokemon to train scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, option: PartyOption) => { if (slotIndex < scene.getParty().length) { - scene.ui.setMode(Mode.MYSTERY_ENCOUNTER).then(() => { + scene.ui.setMode(modeToSetOnExit).then(() => { const pokemon = scene.getParty()[slotIndex]; const secondaryOptions = onPokemonSelected(pokemon); if (!secondaryOptions) { @@ -443,7 +445,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p }); }); } else { - scene.ui.setMode(Mode.MYSTERY_ENCOUNTER).then(() => { + scene.ui.setMode(modeToSetOnExit).then(() => { if (onPokemonNotSelected) { onPokemonNotSelected(); } diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 7055d29d45c..0f13f486eb2 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -190,9 +190,6 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con } }); - // Load dex progress icon - this.scene.loadAtlas("encounter_radar", "mystery-encounters"); - this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => { this.spriteConfigs.every((config) => { if (config.isItem) { diff --git a/src/loading-scene.ts b/src/loading-scene.ts index 3d62248f693..54acc2dd0d3 100644 --- a/src/loading-scene.ts +++ b/src/loading-scene.ts @@ -275,6 +275,9 @@ export class LoadingScene extends SceneBase { } } + // Load Mystery Encounter dex progress icon + this.loadImage("encounter_radar", "mystery-encounters"); + this.loadAtlas("dualshock", "inputs"); this.loadAtlas("xbox", "inputs"); this.loadAtlas("keyboard", "inputs"); diff --git a/src/overrides.ts b/src/overrides.ts index 1196797fac0..6632dfeab6f 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -127,9 +127,9 @@ class DefaultOverrides { // ------------------------- // 1 to 256, set to null to ignore - readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; + readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; - readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; + readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.TRAINING_SESSION; // ------------------------- // MODIFIER / ITEM OVERRIDES diff --git a/src/phases.ts b/src/phases.ts index 56206a97f1a..c6b238c5d79 100644 --- a/src/phases.ts +++ b/src/phases.ts @@ -915,7 +915,7 @@ export class EncounterPhase extends BattlePhase { // Load Mystery Encounter Exclamation bubble and sfx loadEnemyAssets.push(new Promise(resolve => { this.scene.loadSe("GEN8- Exclaim", "battle_anims", "GEN8- Exclaim.wav"); - this.scene.loadAtlas("exclaim", "mystery-encounters"); + this.scene.loadImage("exclaim", "mystery-encounters"); this.scene.load.once(Phaser.Loader.Events.COMPLETE, () => resolve()); if (!this.scene.load.isLoading()) { this.scene.load.start(); diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts new file mode 100644 index 00000000000..1f2c02cf03e --- /dev/null +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -0,0 +1,374 @@ +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 { 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 { generateModifierTypeOption } from "#app/data/mystery-encounters/utils/encounter-phase-utils"; +import { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; +import { CommandPhase, MovePhase, NewBattlePhase, SelectModifierPhase } from "#app/phases"; +import { Moves } from "#enums/moves"; +import BattleScene from "#app/battle-scene"; +import Pokemon, { PokemonMove } from "#app/field/pokemon"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { ClowningAroundEncounter } from "#app/data/mystery-encounters/encounters/clowning-around-encounter"; +import { TrainerType } from "#enums/trainer-type"; +import { Abilities } from "#enums/abilities"; +import { PostMysteryEncounterPhase } from "#app/phases/mystery-encounter-phases"; +import { Button } from "#enums/buttons"; +import PartyUiHandler from "#app/ui/party-ui-handler"; +import OptionSelectUiHandler from "#app/ui/settings/option-select-ui-handler"; +import { modifierTypes, PokemonHeldItemModifierType } from "#app/modifier/modifier-type"; +import { BerryType } from "#enums/berry-type"; +import { PokemonHeldItemModifier } from "#app/modifier/modifier"; +import { Type } from "#app/data/type"; + +const namespace = "mysteryEncounter:clowningAround"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Clowning Around - 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.CAVE, [MysteryEncounterType.CLOWNING_AROUND]], + ]) + ); + }); + + afterEach(() => { + game.phaseInterceptor.restoreOg(); + vi.clearAllMocks(); + vi.resetAllMocks(); + }); + + it("should have the correct properties", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + + expect(ClowningAroundEncounter.encounterType).toBe(MysteryEncounterType.CLOWNING_AROUND); + expect(ClowningAroundEncounter.encounterTier).toBe(MysteryEncounterTier.ULTRA); + expect(ClowningAroundEncounter.dialogue).toBeDefined(); + expect(ClowningAroundEncounter.dialogue.intro).toStrictEqual([ + { text: `${namespace}.intro` }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.intro_dialogue`, + }, + ]); + expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`); + expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`); + expect(ClowningAroundEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`); + expect(ClowningAroundEncounter.options.length).toBe(3); + }); + + it("should not run below wave 80", async () => { + game.override.startingWave(79); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.CLOWNING_AROUND); + }); + + it("should not run above wave 179", async () => { + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = ClowningAroundEncounter; + const moveInitSpy = vi.spyOn(BattleAnims, "initMoveAnim"); + const moveLoadSpy = vi.spyOn(BattleAnims, "loadMoveAnimAssets"); + + const { onInit } = ClowningAroundEncounter; + + expect(ClowningAroundEncounter.onInit).toBeDefined(); + + ClowningAroundEncounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit(scene); + const config = ClowningAroundEncounter.enemyPartyConfigs[0]; + + expect(config.doubleBattle).toBe(true); + expect(config.trainerConfig.trainerType).toBe(TrainerType.HARLEQUIN); + expect(config.pokemonConfigs[0]).toEqual({ + species: getPokemonSpecies(Species.MR_MIME), + isBoss: true, + moveSet: [Moves.TEETER_DANCE, Moves.ALLY_SWITCH, Moves.DAZZLING_GLEAM, Moves.PSYCHIC] + }); + expect(config.pokemonConfigs[1]).toEqual({ + species: getPokemonSpecies(Species.BLACEPHALON), + ability: expect.any(Number), + mysteryEncounterData: expect.anything(), + isBoss: true, + moveSet: [Moves.TRICK, Moves.HYPNOSIS, Moves.SHADOW_BALL, Moves.MIND_BLOWN] + }); + expect(config.pokemonConfigs[1].mysteryEncounterData.types.length).toBe(2); + expect([ + Abilities.STURDY, + Abilities.PICKUP, + Abilities.INTIMIDATE, + Abilities.GUTS, + Abilities.DROUGHT, + Abilities.DRIZZLE, + Abilities.SNOW_WARNING, + Abilities.SAND_STREAM, + Abilities.ELECTRIC_SURGE, + Abilities.PSYCHIC_SURGE, + Abilities.GRASSY_SURGE, + Abilities.MISTY_SURGE, + Abilities.MAGICIAN, + Abilities.SHEER_FORCE, + Abilities.PRANKSTER + ]).toContain(config.pokemonConfigs[1].ability); + expect(ClowningAroundEncounter.misc.ability).toBe(config.pokemonConfigs[1].ability); + await vi.waitFor(() => expect(moveInitSpy).toHaveBeenCalled()); + await vi.waitFor(() => expect(moveLoadSpy).toHaveBeenCalled()); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Battle the Clown", () => { + it("should have the correct properties", () => { + const option = ClowningAroundEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.1.selected`, + }, + ], + }); + }); + + it("should start double battle against the clown", async () => { + const phaseSpy = vi.spyOn(scene, "pushPhase"); + + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, null, true); + + const enemyField = scene.getEnemyField(); + expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + expect(enemyField.length).toBe(2); + expect(enemyField[0].species.speciesId).toBe(Species.MR_MIME); + expect(enemyField[0].moveset).toEqual([new PokemonMove(Moves.TEETER_DANCE), new PokemonMove(Moves.ALLY_SWITCH), new PokemonMove(Moves.DAZZLING_GLEAM), new PokemonMove(Moves.PSYCHIC)]); + expect(enemyField[1].species.speciesId).toBe(Species.BLACEPHALON); + expect(enemyField[1].moveset).toEqual([new PokemonMove(Moves.TRICK), new PokemonMove(Moves.HYPNOSIS), new PokemonMove(Moves.SHADOW_BALL), new PokemonMove(Moves.MIND_BLOWN)]); + + // 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(3); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.ROLE_PLAY).length).toBe(1); + expect(movePhases.filter(p => (p as MovePhase).move.moveId === Moves.TAUNT).length).toBe(2); + }); + + it("should let the player gain the ability after battle completion", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 1, null, true); + await skipBattleRunMysteryEncounterRewardsPhase(game); + await game.phaseInterceptor.to(SelectModifierPhase, false); + expect(scene.getCurrentPhase().constructor.name).toBe(SelectModifierPhase.name); + await game.phaseInterceptor.run(SelectModifierPhase); + const abilityToTrain = scene.currentBattle.mysteryEncounter.misc.ability; + + game.onNextPrompt("PostMysteryEncounterPhase", Mode.MESSAGE, () => { + game.scene.ui.getHandler().processInput(Button.ACTION); + }); + + // Run to ability train option selection + const optionSelectUiHandler = game.scene.ui.handlers[Mode.OPTION_SELECT] as OptionSelectUiHandler; + vi.spyOn(optionSelectUiHandler, "show"); + const partyUiHandler = game.scene.ui.handlers[Mode.PARTY] as PartyUiHandler; + vi.spyOn(partyUiHandler, "show"); + game.endPhase(); + await game.phaseInterceptor.to(PostMysteryEncounterPhase); + expect(scene.getCurrentPhase().constructor.name).toBe(PostMysteryEncounterPhase.name); + + // Wait for Yes/No confirmation to appear + await vi.waitFor(() => expect(optionSelectUiHandler.show).toHaveBeenCalled()); + // Select "Yes" on train ability + optionSelectUiHandler.processInput(Button.ACTION); + // Select first pokemon in party to train + await vi.waitFor(() => expect(partyUiHandler.show).toHaveBeenCalled()); + partyUiHandler.processInput(Button.ACTION); + // Click "Select" on Pokemon + partyUiHandler.processInput(Button.ACTION); + // Stop next battle before it runs + await game.phaseInterceptor.to(NewBattlePhase, false); + + const leadPokemon = scene.getParty()[0]; + expect(leadPokemon.mysteryEncounterData.ability).toBe(abilityToTrain); + }); + }); + + describe("Option 2 - Remain Unprovoked", () => { + it("should have the correct properties", () => { + const option = ClowningAroundEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected`, + }, + { + text: `${namespace}.option.2.selected_2`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.2.selected_3`, + }, + ], + }); + }); + + it("should randomize held items of the Pokemon with the most items, and not the held items of other pokemon", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + + // 2 Sitrus Berries on lead + scene.modifiers = []; + let itemType = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.SITRUS]).type as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + // 2 Ganlon Berries on lead + itemType = generateModifierTypeOption(scene, modifierTypes.BERRY, [BerryType.GANLON]).type as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + // 5 Golden Punch on lead (ultra) + itemType = generateModifierTypeOption(scene, modifierTypes.GOLDEN_PUNCH).type as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + // 5 Lucky Egg on lead (ultra) + itemType = generateModifierTypeOption(scene, modifierTypes.LUCKY_EGG).type as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + // 5 Soul Dew on lead (rogue) + itemType = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 5, itemType); + // 2 Golden Egg on lead (rogue) + itemType = generateModifierTypeOption(scene, modifierTypes.GOLDEN_EGG).type as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[0], 2, itemType); + + // 5 Soul Dew on second party pokemon (these should not change) + itemType = generateModifierTypeOption(scene, modifierTypes.SOUL_DEW).type as PokemonHeldItemModifierType; + await addItemToPokemon(scene, scene.getParty()[1], 5, itemType); + + await runMysteryEncounterToEnd(game, 2); + + const leadItemsAfter = scene.getParty()[0].getHeldItems(); + const ultraCountAfter = leadItemsAfter + .filter(m => m.type.tier === ModifierTier.ULTRA) + .reduce((a, b) => a + b.stackCount, 0); + const rogueCountAfter = leadItemsAfter + .filter(m => m.type.tier === ModifierTier.ROGUE) + .reduce((a, b) => a + b.stackCount, 0); + expect(ultraCountAfter).toBe(10); + expect(rogueCountAfter).toBe(7); + + const secondItemsAfter = scene.getParty()[1].getHeldItems(); + expect(secondItemsAfter.length).toBe(1); + expect(secondItemsAfter[0].type.id).toBe("SOUL_DEW"); + expect(secondItemsAfter[0].stackCount).toBe(5); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 2); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); + + describe("Option 3 - Return the Insults", () => { + it("should have the correct properties", () => { + const option = ClowningAroundEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected`, + }, + { + text: `${namespace}.option.3.selected_2`, + }, + { + speaker: `${namespace}.speaker`, + text: `${namespace}.option.3.selected_3`, + }, + ], + }); + }); + + it("should randomize the pokemon types of the party", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + + // Same type moves on lead + scene.getParty()[0].moveset = [new PokemonMove(Moves.ICE_BEAM), new PokemonMove(Moves.SURF)]; + // Different type moves on second + scene.getParty()[1].moveset = [new PokemonMove(Moves.GRASS_KNOT), new PokemonMove(Moves.ELECTRO_BALL)]; + // No moves on third + scene.getParty()[2].moveset = []; + await runMysteryEncounterToEnd(game, 3); + + const leadTypesAfter = scene.getParty()[0].mysteryEncounterData.types; + const secondaryTypesAfter = scene.getParty()[1].mysteryEncounterData.types; + const thirdTypesAfter = scene.getParty()[2].mysteryEncounterData.types; + + expect(leadTypesAfter.length).toBe(2); + expect(leadTypesAfter).not.toBe([Type.ICE, Type.WATER]); + expect(secondaryTypesAfter.length).toBe(2); + expect(secondaryTypesAfter.includes(Type.GRASS)).toBeTruthy(); + expect(secondaryTypesAfter.includes(Type.ELECTRIC)).toBeTruthy(); + expect(thirdTypesAfter.length).toBe(1); + }); + + it("should leave encounter without battle", async () => { + const leaveEncounterWithoutBattleSpy = vi.spyOn(EncounterPhaseUtils, "leaveEncounterWithoutBattle"); + + await game.runToMysteryEncounter(MysteryEncounterType.CLOWNING_AROUND, defaultParty); + await runMysteryEncounterToEnd(game, 3); + + expect(leaveEncounterWithoutBattleSpy).toBeCalled(); + }); + }); +}); + +async function addItemToPokemon(scene: BattleScene, pokemon: Pokemon, stackCount: integer, itemType: PokemonHeldItemModifierType) { + const itemMod = itemType.newModifier(pokemon) as PokemonHeldItemModifier; + itemMod.stackCount = stackCount; + await scene.addModifier(itemMod, true, false, false, true); + await scene.updateModifiers(true); +} diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index 105fbb3fd6b..db89e15b33f 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -23,6 +23,7 @@ import { PokemonBaseStatTotalModifier } from "#app/modifier/modifier"; import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { MysteryEncounterPokemonData } from "#app/data/mystery-encounters/mystery-encounter-pokemon-data"; const namespace = "mysteryEncounter:theStrongStuff"; const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; @@ -118,7 +119,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { species: getPokemonSpecies(Species.SHUCKLE), isBoss: true, bossSegments: 5, - spriteScale: 1.5, + mysteryEncounterData: new MysteryEncounterPokemonData(1.5), nature: Nature.BOLD, moveSet: [Moves.INFESTATION, Moves.SALT_CURE, Moves.GASTRO_ACID, Moves.HEAL_ORDER], modifierTypes: expect.any(Array), diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index 3d89afcd784..04642bec37b 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -81,8 +81,8 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.rarityBall.setScale(0.75); this.descriptionContainer.add(this.rarityBall); - const dexProgressIndicator = this.scene.add.sprite(12, 9, "encounter_radar"); - dexProgressIndicator.setScale(0.85); + const dexProgressIndicator = this.scene.add.sprite(12, 10, "encounter_radar"); + dexProgressIndicator.setScale(0.80); this.dexProgressContainer.add(dexProgressIndicator); this.dexProgressContainer.setInteractive(new Phaser.Geom.Rectangle(0, 0, 24, 28), Phaser.Geom.Rectangle.Contains); this.dexProgressContainer.on("pointerover", () => { From 9f4a0d24758cc39da34a20375657a5d96bd893b3 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Wed, 7 Aug 2024 22:55:39 -0400 Subject: [PATCH 4/7] add unit tests for clowning around --- src/overrides.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/overrides.ts b/src/overrides.ts index 6632dfeab6f..1196797fac0 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -127,9 +127,9 @@ class DefaultOverrides { // ------------------------- // 1 to 256, set to null to ignore - readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; + readonly MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; readonly MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; - readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.TRAINING_SESSION; + readonly MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; // ------------------------- // MODIFIER / ITEM OVERRIDES From db2fb6e54d0d856e1b2d40ce2f19c6510032a50c Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Thu, 8 Aug 2024 09:24:07 -0400 Subject: [PATCH 5/7] clean up ME unit tests --- .../encounters/a-trainers-test-encounter.test.ts | 2 +- .../encounters/absolute-avarice-encounter.test.ts | 4 ++-- .../encounters/an-offer-you-cant-refuse-encounter.test.ts | 2 +- .../encounters/berries-abound-encounter.test.ts | 2 +- .../encounters/clowning-around-encounter.test.ts | 2 +- .../mystery-encounter/encounters/delibirdy-encounter.test.ts | 2 +- .../encounters/department-store-sale-encounter.test.ts | 1 - .../encounters/fiery-fallout-encounter.test.ts | 2 +- .../encounters/fight-or-flight-encounter.test.ts | 2 +- .../encounters/lost-at-sea-encounter.test.ts | 1 + .../encounters/the-pokemon-salesman-encounter.test.ts | 2 +- .../encounters/the-strong-stuff-encounter.test.ts | 2 +- .../encounters/trash-to-treasure-encounter.test.ts | 2 +- 13 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts b/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts index 083d6c6985f..74effa3b29d 100644 --- a/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/a-trainers-test-encounter.test.ts @@ -33,9 +33,9 @@ describe("A Trainer's Test - Mystery Encounter", () => { 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.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index 7b0c7a2dd7e..e80b5821dbf 100644 --- a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -33,9 +33,9 @@ describe("Absolute Avarice - Mystery Encounter", () => { 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); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ @@ -106,7 +106,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { it("should remove all player's berries at the start of the encounter", async () => { game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); - await game.runToMysteryEncounter(); + await game.runToMysteryEncounter(MysteryEncounterType.ABSOLUTE_AVARICE, defaultParty); expect(scene.currentBattle?.mysteryEncounter?.encounterType).toBe(MysteryEncounterType.ABSOLUTE_AVARICE); expect(scene.modifiers?.length).toBe(0); diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index fe7e5b676a3..6b4405a779b 100644 --- a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -36,9 +36,9 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { 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.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], diff --git a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts index 9bd17702f74..6164c32418c 100644 --- a/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/berries-abound-encounter.test.ts @@ -39,9 +39,9 @@ describe("Berries Abound - Mystery Encounter", () => { 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); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ diff --git a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts index 1f2c02cf03e..3acd8d1ef99 100644 --- a/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/clowning-around-encounter.test.ts @@ -48,9 +48,9 @@ describe("Clowning Around - Mystery Encounter", () => { 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); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ diff --git a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts index 0da778d793c..8934dbb5f5c 100644 --- a/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/delibirdy-encounter.test.ts @@ -35,9 +35,9 @@ describe("Delibird-y - Mystery Encounter", () => { 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); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ diff --git a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts index 2eb1b6176bc..016dbcee964 100644 --- a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -33,7 +33,6 @@ describe("Department Store Sale - Mystery Encounter", () => { 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); diff --git a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts index 00ae3a37139..31f41a32701 100644 --- a/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fiery-fallout-encounter.test.ts @@ -40,9 +40,9 @@ describe("Fiery Fallout - Mystery Encounter", () => { 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); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ diff --git a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts index 70978c90834..b44acc6d027 100644 --- a/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/fight-or-flight-encounter.test.ts @@ -38,9 +38,9 @@ describe("Fight or Flight - Mystery Encounter", () => { 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); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index dd892ee8a9d..0e4d56c9fc1 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -36,6 +36,7 @@ describe("Lost at Sea - Mystery Encounter", () => { game.override.mysteryEncounterChance(100); game.override.startingWave(defaultWave); game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(true); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index 93686ecc64b..966d9763440 100644 --- a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -33,9 +33,9 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { 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.VOLCANO, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]], diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index db89e15b33f..bcfdad48ad3 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -43,9 +43,9 @@ describe("The Strong Stuff - Mystery Encounter", () => { 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); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ diff --git a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts index df3a3471548..3d4cd8855cd 100644 --- a/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/trash-to-treasure-encounter.test.ts @@ -39,9 +39,9 @@ describe("Trash to Treasure - Mystery Encounter", () => { 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); vi.spyOn(MysteryEncounters, "mysteryEncountersByBiome", "get").mockReturnValue( new Map([ From ba637e80e280250766f33261c8276d9f9a4353c0 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Thu, 8 Aug 2024 16:04:14 -0400 Subject: [PATCH 6/7] clean up unit tests --- .../mysterious-challengers-encounter.test.ts | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts diff --git a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts new file mode 100644 index 00000000000..94a7161a9cd --- /dev/null +++ b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -0,0 +1,269 @@ +import * as MysteryEncounters from "#app/data/mystery-encounters/mystery-encounters"; +import { HUMAN_TRANSITABLE_BIOMES } 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 { runMysteryEncounterToEnd, skipBattleRunMysteryEncounterRewardsPhase } from "#test/mystery-encounter/encounterTestUtils"; +import { CommandPhase, SelectModifierPhase } from "#app/phases"; +import BattleScene from "#app/battle-scene"; +import { Mode } from "#app/ui/ui"; +import { MysteryEncounterOptionMode } from "#enums/mystery-encounter-option-mode"; +import { MysteryEncounterTier } from "#enums/mystery-encounter-tier"; +import { initSceneWithoutEncounterPhase } from "#test/utils/gameManagerUtils"; +import { ModifierTier } from "#app/modifier/modifier-tier"; +import { MysteriousChallengersEncounter } from "#app/data/mystery-encounters/encounters/mysterious-challengers-encounter"; +import { TrainerConfig, TrainerPartyCompoundTemplate, TrainerPartyTemplate } from "#app/data/trainer-config"; +import { PartyMemberStrength } from "#enums/party-member-strength"; +import { MysteryEncounterMode } from "#enums/mystery-encounter-mode"; +import ModifierSelectUiHandler from "#app/ui/modifier-select-ui-handler"; +import IMysteryEncounter from "#app/data/mystery-encounters/mystery-encounter"; + +const namespace = "mysteryEncounter:mysteriousChallengers"; +const defaultParty = [Species.LAPRAS, Species.GENGAR, Species.ABRA]; +const defaultBiome = Biome.CAVE; +const defaultWave = 45; + +describe("Mysterious Challengers - Mystery Encounter", () => { + let phaserGame: Phaser.Game; + let game: GameManager; + let scene: BattleScene; + + beforeAll(() => { + phaserGame = new Phaser.Game({ type: Phaser.HEADLESS }); + }); + + beforeEach(async () => { + game = new GameManager(phaserGame); + scene = game.scene; + game.override.mysteryEncounterChance(100); + game.override.startingWave(defaultWave); + game.override.startingBiome(defaultBiome); + game.override.disableTrainerWaves(true); + + const biomeMap = new Map([ + [Biome.VOLCANO, [MysteryEncounterType.FIGHT_OR_FLIGHT]], + ]); + HUMAN_TRANSITABLE_BIOMES.forEach(biome => { + biomeMap.set(biome, [MysteryEncounterType.MYSTERIOUS_CHALLENGERS]); + }); + 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.MYSTERIOUS_CHALLENGERS, defaultParty); + + expect(MysteriousChallengersEncounter.encounterType).toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + expect(MysteriousChallengersEncounter.encounterTier).toBe(MysteryEncounterTier.GREAT); + expect(MysteriousChallengersEncounter.dialogue).toBeDefined(); + expect(MysteriousChallengersEncounter.dialogue.intro).toStrictEqual([{ text: `${namespace}.intro` }]); + expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue.title).toBe(`${namespace}.title`); + expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue.description).toBe(`${namespace}.description`); + expect(MysteriousChallengersEncounter.dialogue.encounterOptionsDialogue.query).toBe(`${namespace}.query`); + expect(MysteriousChallengersEncounter.options.length).toBe(3); + }); + + it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.startingBiome(Biome.VOLCANO); + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + }); + + it("should not run below wave 10", async () => { + game.override.startingWave(9); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle?.mysteryEncounter?.encounterType).not.toBe(MysteryEncounterType.MYSTERIOUS_CHALLENGERS); + }); + + it("should not run above wave 179", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); + game.override.startingWave(181); + + await game.runToMysteryEncounter(); + + expect(scene.currentBattle.mysteryEncounter).toBeUndefined(); + }); + + it("should initialize fully", async () => { + initSceneWithoutEncounterPhase(scene, defaultParty); + scene.currentBattle.mysteryEncounter = new IMysteryEncounter(MysteriousChallengersEncounter); + const encounter = scene.currentBattle.mysteryEncounter; + scene.currentBattle.waveIndex = defaultWave; + + const { onInit } = encounter; + + expect(encounter.onInit).toBeDefined(); + + encounter.populateDialogueTokensFromRequirements(scene); + const onInitResult = onInit(scene); + + expect(encounter.enemyPartyConfigs).toBeDefined(); + expect(encounter.enemyPartyConfigs.length).toBe(3); + expect(encounter.enemyPartyConfigs).toEqual([ + { + trainerConfig: expect.any(TrainerConfig), + female: expect.any(Boolean), + }, + { + trainerConfig: expect.any(TrainerConfig), + levelAdditiveMultiplier: 0.5, + female: expect.any(Boolean), + }, + { + trainerConfig: expect.any(TrainerConfig), + levelAdditiveMultiplier: 1, + female: expect.any(Boolean), + } + ]); + expect(encounter.enemyPartyConfigs[1].trainerConfig.partyTemplates[0]).toEqual(new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER, false, true), + new TrainerPartyTemplate(3, PartyMemberStrength.AVERAGE, false, true) + )); + expect(encounter.enemyPartyConfigs[2].trainerConfig.partyTemplates[0]).toEqual(new TrainerPartyCompoundTemplate( + new TrainerPartyTemplate(2, PartyMemberStrength.AVERAGE), + new TrainerPartyTemplate(3, PartyMemberStrength.STRONG), + new TrainerPartyTemplate(1, PartyMemberStrength.STRONGER)) + ); + expect(encounter.spriteConfigs).toBeDefined(); + expect(encounter.spriteConfigs.length).toBe(3); + expect(onInitResult).toBe(true); + }); + + describe("Option 1 - Normal Battle", () => { + it("should have the correct properties", () => { + const option = MysteriousChallengersEncounter.options[0]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.1.label`, + buttonTooltip: `${namespace}.option.1.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 1, null, true); + + expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + }); + + it("should have normal trainer rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 1, null, 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).toContain("TM_COMMON"); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.id).toContain("TM_GREAT"); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.id).toContain("MEMORY_MUSHROOM"); + }); + }); + + describe("Option 2 - Hard Battle", () => { + it("should have the correct properties", () => { + const option = MysteriousChallengersEncounter.options[1]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.2.label`, + buttonTooltip: `${namespace}.option.2.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 2, null, true); + + expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + }); + + it("should have hard trainer rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 2, null, 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(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT); + }); + }); + + describe("Option 3 - Brutal Battle", () => { + it("should have the correct properties", () => { + const option = MysteriousChallengersEncounter.options[2]; + expect(option.optionMode).toBe(MysteryEncounterOptionMode.DEFAULT); + expect(option.dialogue).toBeDefined(); + expect(option.dialogue).toStrictEqual({ + buttonLabel: `${namespace}.option.3.label`, + buttonTooltip: `${namespace}.option.3.tooltip`, + selected: [ + { + text: `${namespace}.option.selected`, + }, + ], + }); + }); + + it("should start battle against the trainer", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 3, null, true); + + expect(scene.getCurrentPhase().constructor.name).toBe(CommandPhase.name); + expect(scene.currentBattle.trainer).toBeDefined(); + expect(scene.currentBattle.mysteryEncounter.encounterMode).toBe(MysteryEncounterMode.TRAINER_BATTLE); + }); + + it("should have brutal trainer rewards after battle", async () => { + await game.runToMysteryEncounter(MysteryEncounterType.MYSTERIOUS_CHALLENGERS, defaultParty); + await runMysteryEncounterToEnd(game, 3, null, 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(4); + expect(modifierSelectHandler.options[0].modifierTypeOption.type.tier - modifierSelectHandler.options[0].modifierTypeOption.upgradeCount).toBe(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[1].modifierTypeOption.type.tier - modifierSelectHandler.options[1].modifierTypeOption.upgradeCount).toBe(ModifierTier.ROGUE); + expect(modifierSelectHandler.options[2].modifierTypeOption.type.tier - modifierSelectHandler.options[2].modifierTypeOption.upgradeCount).toBe(ModifierTier.ULTRA); + expect(modifierSelectHandler.options[3].modifierTypeOption.type.tier - modifierSelectHandler.options[3].modifierTypeOption.upgradeCount).toBe(ModifierTier.GREAT); + }); + }); +}); From de928e5d7ebce79dc3bf1727cfa068a676eb7c87 Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Thu, 8 Aug 2024 17:38:41 -0400 Subject: [PATCH 7/7] update unit tests --- src/battle-scene.ts | 13 +++++++++++-- .../encounters/absolute-avarice-encounter.test.ts | 2 ++ .../an-offer-you-cant-refuse-encounter.test.ts | 1 + .../department-store-sale-encounter.test.ts | 1 + .../encounters/lost-at-sea-encounter.test.ts | 1 + .../mysterious-challengers-encounter.test.ts | 2 +- .../the-pokemon-salesman-encounter.test.ts | 1 + .../encounters/the-strong-stuff-encounter.test.ts | 1 + 8 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/battle-scene.ts b/src/battle-scene.ts index 249ddebdd60..80a1ae9e8ae 100644 --- a/src/battle-scene.ts +++ b/src/battle-scene.ts @@ -2760,7 +2760,7 @@ export default class BattleScene extends SceneBase { const previousEncounter = this.mysteryEncounterData.encounteredEvents?.length > 0 ? this.mysteryEncounterData.encounteredEvents[this.mysteryEncounterData.encounteredEvents.length - 1][0] : null; const biomeMysteryEncounters = mysteryEncountersByBiome.get(this.arena.biomeType) ?? []; // If no valid encounters exist at tier, checks next tier down, continuing until there are some encounters available - while (availableEncounters.length === 0 && tier >= 0) { + while (availableEncounters.length === 0 && tier !== null) { availableEncounters = biomeMysteryEncounters .filter((encounterType) => { const encounterCandidate = allMysteryEncounters[encounterType]; @@ -2784,7 +2784,16 @@ export default class BattleScene extends SceneBase { return true; }) .map((m) => (allMysteryEncounters[m])); - tier--; + // Decrement tier + if (tier === MysteryEncounterTier.ROGUE) { + tier = MysteryEncounterTier.ULTRA; + } else if (tier === MysteryEncounterTier.ULTRA) { + tier = MysteryEncounterTier.GREAT; + } else if (tier === MysteryEncounterTier.GREAT) { + tier = MysteryEncounterTier.COMMON; + } else { + tier = null; // Ends loop + } } // If absolutely no encounters are available, spawn 0th encounter diff --git a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts index e80b5821dbf..d21421cde2b 100644 --- a/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/absolute-avarice-encounter.test.ts @@ -81,6 +81,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { }); it("should not spawn outside of proper biomes", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.startingBiome(Biome.VOLCANO); await game.runToMysteryEncounter(); @@ -96,6 +97,7 @@ describe("Absolute Avarice - Mystery Encounter", () => { }); it("should spawn if player has enough berries", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.starterHeldItems([{name: "BERRY", count: 2, type: BerryType.SITRUS}, {name: "BERRY", count: 3, type: BerryType.GANLON}]); await game.runToMysteryEncounter(); diff --git a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts index 6b4405a779b..128a4c109dc 100644 --- a/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/an-offer-you-cant-refuse-encounter.test.ts @@ -72,6 +72,7 @@ describe("An Offer You Can't Refuse - Mystery Encounter", () => { }); it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.startingBiome(Biome.VOLCANO); await game.runToMysteryEncounter(); diff --git a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts index 016dbcee964..1ffd1708523 100644 --- a/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/department-store-sale-encounter.test.ts @@ -72,6 +72,7 @@ describe("Department Store Sale - Mystery Encounter", () => { }); it("should not spawn outside of CIVILIZATION_ENCOUNTER_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); game.override.startingBiome(Biome.VOLCANO); await game.runToMysteryEncounter(); diff --git a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts index 0e4d56c9fc1..c9d2d276ecb 100644 --- a/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/lost-at-sea-encounter.test.ts @@ -66,6 +66,7 @@ describe("Lost at Sea - Mystery Encounter", () => { }); it("should not spawn outside of sea biome", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); game.override.startingBiome(Biome.MOUNTAIN); await game.runToMysteryEncounter(); diff --git a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts index 94a7161a9cd..7fe6b1a3f95 100644 --- a/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/mysterious-challengers-encounter.test.ts @@ -71,6 +71,7 @@ describe("Mysterious Challengers - Mystery Encounter", () => { }); it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.startingBiome(Biome.VOLCANO); await game.runToMysteryEncounter(); @@ -86,7 +87,6 @@ describe("Mysterious Challengers - Mystery Encounter", () => { }); it("should not run above wave 179", async () => { - game.override.mysteryEncounterTier(MysteryEncounterTier.GREAT); game.override.startingWave(181); await game.runToMysteryEncounter(); diff --git a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts index 966d9763440..833503e2636 100644 --- a/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-pokemon-salesman-encounter.test.ts @@ -69,6 +69,7 @@ describe("The Pokemon Salesman - Mystery Encounter", () => { }); it("should not spawn outside of HUMAN_TRANSITABLE_BIOMES", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.ULTRA); game.override.startingBiome(Biome.VOLCANO); await game.runToMysteryEncounter(); diff --git a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts index bcfdad48ad3..21d0678c858 100644 --- a/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts +++ b/src/test/mystery-encounter/encounters/the-strong-stuff-encounter.test.ts @@ -75,6 +75,7 @@ describe("The Strong Stuff - Mystery Encounter", () => { }); it("should not spawn outside of CAVE biome", async () => { + game.override.mysteryEncounterTier(MysteryEncounterTier.COMMON); game.override.startingBiome(Biome.MOUNTAIN); await game.runToMysteryEncounter();