From 7b35efe95eecec42f9b5dcd827f990a45b38f07b Mon Sep 17 00:00:00 2001 From: ImperialSympathizer Date: Mon, 8 Jul 2024 17:30:24 -0400 Subject: [PATCH] adds 2 new encounters and colorable text options --- .../images/mystery-encounters/b2w2_lady.json | 734 ++++++++++++++++ .../images/mystery-encounters/b2w2_lady.png | Bin 0 -> 23208 bytes .../mystery-encounters/b2w2_veteran_m.json | 797 ++++++++++++++++++ .../mystery-encounters/b2w2_veteran_m.png | Bin 0 -> 19744 bytes src/data/mystery-encounter-requirements.ts | 92 +- src/data/mystery-encounter.ts | 25 +- .../department-store-sale.ts | 120 +++ .../department-store-sale-dialogue.ts | 36 + .../dialogue/mystery-encounter-dialogue.ts | 7 + .../dialogue/shady-vitamin-dealer.ts | 42 + .../mystery-encounters/fight-or-flight.ts | 47 +- .../mystery-encounter-utils.ts | 82 +- .../mystery-encounters/mystery-encounters.ts | 26 +- .../shady-vitamin-dealer.ts | 148 ++++ .../mystery-encounters/sleeping-snorlax.ts | 6 +- .../mystery-encounters/training-session.ts | 8 +- src/enums/mystery-encounter-type.ts | 4 +- src/field/mystery-encounter-intro.ts | 8 +- src/locales/en/mystery-encounter.ts | 87 +- src/overrides.ts | 4 +- src/phases/mystery-encounter-phase.ts | 10 +- .../mystery-encounter-utils.test.ts | 18 +- .../phases/mystery-encounter-phase.test.ts | 2 +- src/ui/mystery-encounter-ui-handler.ts | 53 +- 24 files changed, 2235 insertions(+), 121 deletions(-) create mode 100644 public/images/mystery-encounters/b2w2_lady.json create mode 100644 public/images/mystery-encounters/b2w2_lady.png create mode 100644 public/images/mystery-encounters/b2w2_veteran_m.json create mode 100644 public/images/mystery-encounters/b2w2_veteran_m.png create mode 100644 src/data/mystery-encounters/department-store-sale.ts create mode 100644 src/data/mystery-encounters/dialogue/department-store-sale-dialogue.ts create mode 100644 src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts create mode 100644 src/data/mystery-encounters/shady-vitamin-dealer.ts diff --git a/public/images/mystery-encounters/b2w2_lady.json b/public/images/mystery-encounters/b2w2_lady.json new file mode 100644 index 00000000000..e143086e157 --- /dev/null +++ b/public/images/mystery-encounters/b2w2_lady.json @@ -0,0 +1,734 @@ +{ + "textures": [ + { + "image": "b2w2_lady.png", + "format": "RGBA8888", + "size": { + "w": 399, + "h": 360 + }, + "scale": 1, + "frames": [ + { + "filename": "0000.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 0, + "y": 0, + "w": 56, + "h": 72 + } + }, + { + "filename": "0001.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 57, + "y": 0, + "w": 56, + "h": 72 + } + }, + { + "filename": "0002.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 114, + "y": 0, + "w": 56, + "h": 72 + } + }, + { + "filename": "0003.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 55, + "h": 72 + }, + "frame": { + "x": 171, + "y": 0, + "w": 55, + "h": 72 + } + }, + { + "filename": "0004.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 11, + "y": 8, + "w": 54, + "h": 72 + }, + "frame": { + "x": 228, + "y": 0, + "w": 54, + "h": 72 + } + }, + { + "filename": "0005.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 11, + "y": 8, + "w": 54, + "h": 72 + }, + "frame": { + "x": 285, + "y": 0, + "w": 54, + "h": 72 + } + }, + { + "filename": "0006.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 14, + "y": 8, + "w": 52, + "h": 72 + }, + "frame": { + "x": 342, + "y": 0, + "w": 52, + "h": 72 + } + }, + { + "filename": "0007.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 20, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 0, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0008.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 47, + "h": 72 + }, + "frame": { + "x": 57, + "y": 72, + "w": 47, + "h": 72 + } + }, + { + "filename": "0009.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 47, + "h": 72 + }, + "frame": { + "x": 114, + "y": 72, + "w": 47, + "h": 72 + } + }, + { + "filename": "0010.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 171, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0011.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 228, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0012.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 285, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0013.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 342, + "y": 72, + "w": 48, + "h": 72 + } + }, + { + "filename": "0014.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 0, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0015.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 57, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0016.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 114, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0017.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 49, + "h": 72 + }, + "frame": { + "x": 171, + "y": 144, + "w": 49, + "h": 72 + } + }, + { + "filename": "0018.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 228, + "y": 144, + "w": 48, + "h": 72 + } + }, + { + "filename": "0019.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 285, + "y": 144, + "w": 48, + "h": 72 + } + }, + { + "filename": "0020.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 342, + "y": 144, + "w": 48, + "h": 72 + } + }, + { + "filename": "0021.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 22, + "y": 8, + "w": 48, + "h": 72 + }, + "frame": { + "x": 0, + "y": 216, + "w": 48, + "h": 72 + } + }, + { + "filename": "0022.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 20, + "y": 8, + "w": 50, + "h": 72 + }, + "frame": { + "x": 57, + "y": 216, + "w": 50, + "h": 72 + } + }, + { + "filename": "0023.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 51, + "h": 72 + }, + "frame": { + "x": 114, + "y": 216, + "w": 51, + "h": 72 + } + }, + { + "filename": "0024.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 18, + "y": 8, + "w": 51, + "h": 72 + }, + "frame": { + "x": 171, + "y": 216, + "w": 51, + "h": 72 + } + }, + { + "filename": "0025.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 15, + "y": 8, + "w": 53, + "h": 72 + }, + "frame": { + "x": 228, + "y": 216, + "w": 53, + "h": 72 + } + }, + { + "filename": "0026.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 57, + "h": 72 + }, + "frame": { + "x": 285, + "y": 216, + "w": 57, + "h": 72 + } + }, + { + "filename": "0027.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 342, + "y": 216, + "w": 56, + "h": 72 + } + }, + { + "filename": "0028.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 10, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 0, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0029.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 9, + "y": 8, + "w": 55, + "h": 72 + }, + "frame": { + "x": 57, + "y": 288, + "w": 55, + "h": 72 + } + }, + { + "filename": "0030.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 114, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0031.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 171, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0032.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 228, + "y": 288, + "w": 56, + "h": 72 + } + }, + { + "filename": "0033.png", + "rotated": false, + "trimmed": true, + "sourceSize": { + "w": 80, + "h": 80 + }, + "spriteSourceSize": { + "x": 8, + "y": 8, + "w": 56, + "h": 72 + }, + "frame": { + "x": 285, + "y": 288, + "w": 56, + "h": 72 + } + } + ] + } + ], + "meta": { + "app": "https://www.codeandweb.com/texturepacker", + "version": "3.0", + "smartupdate": "$TexturePacker:SmartUpdate:e7f062304401dbd7b3ec79512f0ff4cb:0136dac01331f88892a3df26aeab78f5:1ed1e22abb9b55d76337a5a599835c06$" + } +} diff --git a/public/images/mystery-encounters/b2w2_lady.png b/public/images/mystery-encounters/b2w2_lady.png new file mode 100644 index 0000000000000000000000000000000000000000..9dcc1281c9ebe1a378503fb54ee4e25447413cd3 GIT binary patch literal 23208 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 0 && this.requiredMoney > money) { + if (isNullOrUndefined(money)) { return false; } - return true; + + if (this?.scalingMultiplier > 0) { + this.requiredMoney = scene.getWaveMoneyAmount(this.scalingMultiplier); + } + return !(this?.requiredMoney > 0 && this.requiredMoney > money); } getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - return ["money", "₽" + scene.money.toString()]; + const value = this?.scalingMultiplier > 0 ? scene.getWaveMoneyAmount(this.scalingMultiplier).toString() : this.requiredMoney.toString(); + // Colors money text + return ["money", "@ecCol[MONEY]{₽" + value + "}"]; } } @@ -399,9 +407,9 @@ export class MoveRequirement extends EncounterPokemonRequirement { } getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { - const includedMoves = this.requiredMoves.filter((reqMove) => pokemon.moveset.filter((move) => move.moveId === reqMove).length > 0); + const includedMoves = pokemon.moveset.filter((move) => this.requiredMoves.includes(move.moveId)); if (includedMoves.length > 0) { - return ["move", Moves[includedMoves[0]].replace("_", " ")]; + return ["move", includedMoves[0].getName()]; } return null; } @@ -552,15 +560,15 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { minNumberOfPokemon:number; invertQuery:boolean; - constructor(StatusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { + constructor(statusEffect: StatusEffect | StatusEffect[], minNumberOfPokemon: number = 1, invertQuery: boolean = false) { super(); this.minNumberOfPokemon = minNumberOfPokemon; this.invertQuery = invertQuery; - if (StatusEffect instanceof Array) { - this.requiredStatusEffect = StatusEffect; + if (statusEffect instanceof Array) { + this.requiredStatusEffect = statusEffect; } else { this.requiredStatusEffect = []; - this.requiredStatusEffect.push(StatusEffect); + this.requiredStatusEffect.push(statusEffect); } } @@ -576,16 +584,38 @@ export class StatusEffectRequirement extends EncounterPokemonRequirement { queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { if (!this.invertQuery) { - return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((StatusEffect) => pokemon.status?.effect === StatusEffect).length > 0); + return partyPokemon.filter((pokemon) => { + return this.requiredStatusEffect.some((statusEffect) => { + if (statusEffect === StatusEffect.NONE) { + // StatusEffect.NONE also checks for null or undefined status + return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status.effect) || pokemon.status?.effect === statusEffect; + } else { + return pokemon.status?.effect === statusEffect; + } + }); + }); } else { // for an inverted query, we only want to get the pokemon that don't have ANY of the listed StatusEffects - return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((StatusEffect) => pokemon.status?.effect === StatusEffect).length === 0); + // return partyPokemon.filter((pokemon) => this.requiredStatusEffect.filter((statusEffect) => pokemon.status?.effect === statusEffect).length === 0); + return partyPokemon.filter((pokemon) => { + return !this.requiredStatusEffect.some((statusEffect) => { + if (statusEffect === StatusEffect.NONE) { + // StatusEffect.NONE also checks for null or undefined status + return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status.effect) || pokemon.status?.effect === statusEffect; + } else { + return pokemon.status?.effect === statusEffect; + } + }); + }); } } getDialogueToken(scene: BattleScene, pokemon?: PlayerPokemon): [string, string] { const reqStatus = this.requiredStatusEffect.filter((a) => { - pokemon.status?.effect ===(a); + if (a === StatusEffect.NONE) { + return isNullOrUndefined(pokemon.status) || isNullOrUndefined(pokemon.status.effect) || pokemon.status?.effect === a; + } + return pokemon.status?.effect === a; }); if (reqStatus.length > 0) { return ["status", StatusEffect[reqStatus[0]]]; @@ -863,7 +893,9 @@ export class HealthRatioRequirement extends EncounterPokemonRequirement { queryParty(partyPokemon: PlayerPokemon[]): PlayerPokemon[] { if (!this.invertQuery) { - return partyPokemon.filter((pokemon) => pokemon.getHpRatio() >= this.requiredHealthRange[0] && pokemon.getHpRatio() <= this.requiredHealthRange[1]); + return partyPokemon.filter((pokemon) => { + return pokemon.getHpRatio() >= this.requiredHealthRange[0] && pokemon.getHpRatio() <= this.requiredHealthRange[1]; + }); } else { // for an inverted query, we only want to get the pokemon that don't have ANY of the listed requiredHealthRanges return partyPokemon.filter((pokemon) => pokemon.getHpRatio() < this.requiredHealthRange[0] || pokemon.getHpRatio() > this.requiredHealthRange[1]); diff --git a/src/data/mystery-encounter.ts b/src/data/mystery-encounter.ts index 8325a26d29d..d768a672632 100644 --- a/src/data/mystery-encounter.ts +++ b/src/data/mystery-encounter.ts @@ -5,10 +5,13 @@ import MysteryEncounterDialogue, { allMysteryEncounterDialogue } from "./mystery-encounters/dialogue/mystery-encounter-dialogue"; import MysteryEncounterOption from "./mystery-encounter-option"; -import { EncounterPokemonRequirement, EncounterSceneRequirement } from "./mystery-encounter-requirements"; +import { + EncounterPokemonRequirement, + EncounterSceneRequirement +} from "./mystery-encounter-requirements"; import * as Utils from "../utils"; import {EnemyPartyConfig} from "#app/data/mystery-encounters/mystery-encounter-utils"; -import { PlayerPokemon } from "#app/field/pokemon"; +import Pokemon, { PlayerPokemon } from "#app/field/pokemon"; import {isNullOrUndefined} from "../utils"; export enum MysteryEncounterVariant { @@ -167,6 +170,10 @@ export default class MysteryEncounter implements MysteryEncounter { return sceneReq && secReqs && priReqs; } + pokemonMeetsPrimaryRequirements?(scene: BattleScene, pokemon: Pokemon) { + return !this.primaryPokemonRequirements.some(req => !req.queryParty(scene.getParty()).map(p => p.id).includes(pokemon.id)); + } + private meetsPrimaryRequirementAndPrimaryPokemonSelected?(scene: BattleScene) { if (this.primaryPokemonRequirements.length === 0) { const activeMon = scene.getParty().filter(p => p.isActive(true)); @@ -263,6 +270,12 @@ export default class MysteryEncounter implements MysteryEncounter { * For multiple support pokemon in the dialogue token, it will have to be overridden. */ populateDialogueTokensFromRequirements?(scene: BattleScene) { + if (this.requirements?.length > 0) { + for (const req of this.requirements) { + const dialogueToken = req.getDialogueToken(scene); + this.setDialogueToken(...dialogueToken); + } + } if (this.primaryPokemon?.length > 0) { this.setDialogueToken("primaryName", this.primaryPokemon.name); for (const req of this.primaryPokemonRequirements) { @@ -281,9 +294,17 @@ export default class MysteryEncounter implements MysteryEncounter { } } } + + // Dialogue tokens for options for (let i = 0; i < this.options.length; i++) { const opt = this.options[i]; const j = i + 1; + if (opt.requirements?.length > 0) { + for (const req of opt.requirements) { + const dialogueToken = req.getDialogueToken(scene); + this.setDialogueToken("option" + j + this.capitalizeFirstLetter(dialogueToken[0]), dialogueToken[1]); + } + } if (opt.primaryPokemonRequirements?.length > 0 && opt.primaryPokemon?.length > 0) { this.setDialogueToken("option" + j + "PrimaryName", opt.primaryPokemon.name); for (const req of opt.primaryPokemonRequirements) { diff --git a/src/data/mystery-encounters/department-store-sale.ts b/src/data/mystery-encounters/department-store-sale.ts new file mode 100644 index 00000000000..3837e94af60 --- /dev/null +++ b/src/data/mystery-encounters/department-store-sale.ts @@ -0,0 +1,120 @@ +import BattleScene from "../../battle-scene"; +import { + leaveEncounterWithoutBattle, + setCustomEncounterRewards, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; +import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import {WaveCountRequirement} from "../mystery-encounter-requirements"; +import { MysteryEncounterOptionBuilder } from "../mystery-encounter-option"; +import {modifierTypes} from "#app/modifier/modifier-type"; +import {Species} from "#enums/species"; +import {randSeedInt} from "#app/utils"; + +export const DepartmentStoreSaleEncounter: MysteryEncounter = new MysteryEncounterBuilder() + .withEncounterType(MysteryEncounterType.DEPARTMENT_STORE_SALE) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withIntroSpriteConfigs([ + { + spriteKey: "b2w2_lady", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -20 + }, + { + spriteKey: Species.FURFROU.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: 30 + } + ]) + // .withHideIntroVisuals(false) + .withSceneRequirement(new WaveCountRequirement([10, 100])) + .withOption(new MysteryEncounterOptionBuilder() + .withOptionPhase(async (scene: BattleScene) => { + // Choose TMs + const modifiers = []; + let i = 0; + while (i < 4) { + // 2/2/1 weight on TM rarity + const roll = randSeedInt(5); + if (roll < 2) { + modifiers.push(modifierTypes.TM_COMMON); + } else if (roll < 4) { + modifiers.push(modifierTypes.TM_GREAT); + } else { + modifiers.push(modifierTypes.TM_ULTRA); + } + i++; + } + + setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + leaveEncounterWithoutBattle(scene); + }) + .build()) + .withOption(new MysteryEncounterOptionBuilder() + .withOptionPhase(async (scene: BattleScene) => { + // Choose Vitamins + const modifiers = []; + let i = 0; + while (i < 3) { + // 2/1 weight on base stat booster vs PP Up + const roll = randSeedInt(3); + if (roll === 0) { + modifiers.push(modifierTypes.PP_UP); + } else { + modifiers.push(modifierTypes.BASE_STAT_BOOSTER); + } + i++; + } + + setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + leaveEncounterWithoutBattle(scene); + }) + .build()) + .withOption(new MysteryEncounterOptionBuilder() + .withOptionPhase(async (scene: BattleScene) => { + // Choose X Items + const modifiers = []; + let i = 0; + while (i < 5) { + // 4/1 weight on base stat booster vs Dire Hit + const roll = randSeedInt(5); + if (roll === 0) { + modifiers.push(modifierTypes.DIRE_HIT); + } else { + modifiers.push(modifierTypes.TEMP_STAT_BOOSTER); + } + i++; + } + + setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + leaveEncounterWithoutBattle(scene); + }) + .build()) + .withOption(new MysteryEncounterOptionBuilder() + .withOptionPhase(async (scene: BattleScene) => { + // Choose Pokeballs + const modifiers = []; + let i = 0; + while (i < 4) { + // 10/30/20/5 weight on pokeballs + const roll = randSeedInt(65); + if (roll < 10) { + modifiers.push(modifierTypes.POKEBALL); + } else if (roll < 40) { + modifiers.push(modifierTypes.GREAT_BALL); + } else if (roll < 60) { + modifiers.push(modifierTypes.ULTRA_BALL); + } else { + modifiers.push(modifierTypes.ROGUE_BALL); + } + i++; + } + + setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + leaveEncounterWithoutBattle(scene); + }) + .build()) + .build(); diff --git a/src/data/mystery-encounters/dialogue/department-store-sale-dialogue.ts b/src/data/mystery-encounters/dialogue/department-store-sale-dialogue.ts new file mode 100644 index 00000000000..3a9b75b9f83 --- /dev/null +++ b/src/data/mystery-encounters/dialogue/department-store-sale-dialogue.ts @@ -0,0 +1,36 @@ +import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue"; + +export const DepartmentStoreSaleDialogue: MysteryEncounterDialogue = { + intro: [ + { + text: "mysteryEncounter:department_store_sale_intro_message" + }, + { + text: "mysteryEncounter:department_store_sale_intro_dialogue", + speaker: "mysteryEncounter:department_store_sale_speaker" + } + ], + encounterOptionsDialogue: { + title: "mysteryEncounter:department_store_sale_title", + description: "mysteryEncounter:department_store_sale_description", + query: "mysteryEncounter:department_store_sale_query", + options: [ + { + buttonLabel: "mysteryEncounter:department_store_sale_option_1_label", + buttonTooltip: "mysteryEncounter:department_store_sale_option_1_tooltip" + }, + { + buttonLabel: "mysteryEncounter:department_store_sale_option_2_label", + buttonTooltip: "mysteryEncounter:department_store_sale_option_2_tooltip" + }, + { + buttonLabel: "mysteryEncounter:department_store_sale_option_3_label", + buttonTooltip: "mysteryEncounter:department_store_sale_option_3_tooltip" + }, + { + buttonLabel: "mysteryEncounter:department_store_sale_option_4_label", + buttonTooltip: "mysteryEncounter:department_store_sale_option_4_tooltip" + } + ] + } +}; diff --git a/src/data/mystery-encounters/dialogue/mystery-encounter-dialogue.ts b/src/data/mystery-encounters/dialogue/mystery-encounter-dialogue.ts index e390b0e706a..c64d9627388 100644 --- a/src/data/mystery-encounters/dialogue/mystery-encounter-dialogue.ts +++ b/src/data/mystery-encounters/dialogue/mystery-encounter-dialogue.ts @@ -5,10 +5,14 @@ import {DarkDealDialogue} from "#app/data/mystery-encounters/dialogue/dark-deal- import {FightOrFlightDialogue} from "#app/data/mystery-encounters/dialogue/fight-or-flight-dialogue"; import {TrainingSessionDialogue} from "#app/data/mystery-encounters/dialogue/training-session-dialogue"; import { SleepingSnorlaxDialogue } from "./sleeping-snorlax-dialogue"; +import {DepartmentStoreSaleDialogue} from "#app/data/mystery-encounters/dialogue/department-store-sale-dialogue"; +import {ShadyVitaminDealerDialogue} from "#app/data/mystery-encounters/dialogue/shady-vitamin-dealer"; +import {TextStyle} from "#app/ui/text"; export class TextDisplay { speaker?: TemplateStringsArray | `mysteryEncounter:${string}`; text: TemplateStringsArray | `mysteryEncounter:${string}`; + style?: TextStyle; } export class OptionTextDisplay { @@ -17,6 +21,7 @@ export class OptionTextDisplay { disabledTooltip?: TemplateStringsArray | `mysteryEncounter:${string}`; secondOptionPrompt?: TemplateStringsArray | `mysteryEncounter:${string}`; selected?: TextDisplay[]; + style?: TextStyle; } export class EncounterOptionsDialogue { @@ -91,4 +96,6 @@ export function initMysteryEncounterDialogue() { allMysteryEncounterDialogue[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightDialogue; allMysteryEncounterDialogue[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionDialogue; allMysteryEncounterDialogue[MysteryEncounterType.SLEEPING_SNORLAX] = SleepingSnorlaxDialogue; + allMysteryEncounterDialogue[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleDialogue; + allMysteryEncounterDialogue[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerDialogue; } diff --git a/src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts b/src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts new file mode 100644 index 00000000000..4a9d48913c5 --- /dev/null +++ b/src/data/mystery-encounters/dialogue/shady-vitamin-dealer.ts @@ -0,0 +1,42 @@ +import MysteryEncounterDialogue from "#app/data/mystery-encounters/dialogue/mystery-encounter-dialogue"; + +export const ShadyVitaminDealerDialogue: MysteryEncounterDialogue = { + intro: [ + { + text: "mysteryEncounter:shady_vitamin_dealer_intro_message" + }, + { + text: "mysteryEncounter:shady_vitamin_dealer_intro_dialogue", + speaker: "mysteryEncounter:shady_vitamin_dealer_speaker" + } + ], + encounterOptionsDialogue: { + title: "mysteryEncounter:shady_vitamin_dealer_title", + description: "mysteryEncounter:shady_vitamin_dealer_description", + query: "mysteryEncounter:shady_vitamin_dealer_query", + options: [ + { + buttonLabel: "mysteryEncounter:shady_vitamin_dealer_option_1_label", + buttonTooltip: "mysteryEncounter:shady_vitamin_dealer_option_1_tooltip", + selected: [ + { + text: "mysteryEncounter:shady_vitamin_dealer_option_selected" + }, + ] + }, + { + buttonLabel: "mysteryEncounter:shady_vitamin_dealer_option_2_label", + buttonTooltip: "mysteryEncounter:shady_vitamin_dealer_option_2_tooltip", + selected: [ + { + text: "mysteryEncounter:shady_vitamin_dealer_option_selected" + }, + ] + }, + { + buttonLabel: "mysteryEncounter:shady_vitamin_dealer_option_3_label", + buttonTooltip: "mysteryEncounter:shady_vitamin_dealer_option_3_tooltip" + } + ] + } +}; diff --git a/src/data/mystery-encounters/fight-or-flight.ts b/src/data/mystery-encounters/fight-or-flight.ts index 90cc57bbc13..9ee4166662e 100644 --- a/src/data/mystery-encounters/fight-or-flight.ts +++ b/src/data/mystery-encounters/fight-or-flight.ts @@ -9,7 +9,7 @@ import { } from "#app/data/mystery-encounters/mystery-encounter-utils"; import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; import {MysteryEncounterType} from "#enums/mystery-encounter-type"; -import {WaveCountRequirement} from "../mystery-encounter-requirements"; +import {MoveRequirement, WaveCountRequirement} from "../mystery-encounter-requirements"; import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option"; import { getPartyLuckValue, @@ -23,6 +23,18 @@ import {StatChangePhase} from "#app/phases"; import {BattleStat} from "#app/data/battle-stat"; import Pokemon from "#app/field/pokemon"; import {randSeedInt} from "#app/utils"; +import {Moves} from "#enums/moves"; +import {TextStyle} from "#app/ui/text"; + +const validMovesForSteal = [ + Moves.PLUCK, + Moves.COVET, + Moves.FAKE_OUT, + Moves.THIEF, + Moves.TRICK, + Moves.SWITCHEROO, + Moves.GIGA_DRAIN +]; export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuilder() .withEncounterType(MysteryEncounterType.FIGHT_OR_FLIGHT) @@ -70,6 +82,21 @@ export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuil } ]; + // If player has a stealing move, they succeed automatically + const moveRequirement = new MoveRequirement(validMovesForSteal); + const validPokemon = moveRequirement.queryParty(scene.getParty()); + if (validPokemon?.length > 0) { + // Use first valid pokemon to execute the theivery + const pokemon = validPokemon[0]; + encounter.setDialogueToken("thiefPokemon", pokemon.name); + encounter.setDialogueToken(...moveRequirement.getDialogueToken(scene, pokemon)); + encounter.dialogue.encounterOptionsDialogue.options[1].buttonTooltip = "mysteryEncounter:fight_or_flight_option_2_steal_tooltip"; + encounter.dialogue.encounterOptionsDialogue.options[1].style = TextStyle.SUMMARY_GREEN; + } else { + encounter.dialogue.encounterOptionsDialogue.options[1].buttonTooltip = "mysteryEncounter:fight_or_flight_option_2_tooltip"; + encounter.dialogue.encounterOptionsDialogue.options[1].style = null; + } + return true; }) .withOption(new MysteryEncounterOptionBuilder() @@ -83,9 +110,23 @@ export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuil .withOption(new MysteryEncounterOptionBuilder() .withOptionPhase(async (scene: BattleScene) => { // Pick steal + const encounter = scene.currentBattle.mysteryEncounter; const item = scene.currentBattle.mysteryEncounter.misc as ModifierTypeOption; setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [item], fillRemaining: false}); + // If player has a stealing move, they succeed automatically + const moveRequirement = new MoveRequirement(validMovesForSteal); + const validPokemon = moveRequirement.queryParty(scene.getParty()); + if (validPokemon?.length > 0) { + // Use first valid pokemon to execute the theivery + const pokemon = validPokemon[0]; + encounter.setDialogueToken("thiefPokemon", pokemon.name); + encounter.setDialogueToken(...moveRequirement.getDialogueToken(scene, pokemon)); + await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_steal_result"); + leaveEncounterWithoutBattle(scene); + return; + } + const roll = randSeedInt(16); if (roll > 6) { // Noticed and attacked by boss, gets +1 to all stats at start of fight (62.5%) @@ -101,8 +142,8 @@ export const FightOrFlightEncounter: MysteryEncounter = new MysteryEncounterBuil } else { // Steal item (37.5%) // Display result message then proceed to rewards - await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_good_result") - .then(() => leaveEncounterWithoutBattle(scene)); + await showEncounterText(scene, "mysteryEncounter:fight_or_flight_option_2_good_result"); + leaveEncounterWithoutBattle(scene); } }) .build()) diff --git a/src/data/mystery-encounters/mystery-encounter-utils.ts b/src/data/mystery-encounters/mystery-encounter-utils.ts index 8cd4963daa1..dcbcc753d32 100644 --- a/src/data/mystery-encounters/mystery-encounter-utils.ts +++ b/src/data/mystery-encounters/mystery-encounter-utils.ts @@ -10,8 +10,12 @@ import Trainer, {TrainerVariant} from "../../field/trainer"; import {PokemonExpBoosterModifier} from "#app/modifier/modifier"; import { CustomModifierSettings, + getModifierPoolForType, ModifierPoolType, + ModifierType, ModifierTypeFunc, + ModifierTypeGenerator, + modifierTypes, PokemonHeldItemModifierType, regenerateModifierPoolThresholds } from "#app/modifier/modifier-type"; @@ -31,6 +35,7 @@ import {Mode} from "#app/ui/ui"; import {PartyOption, PartyUiMode} from "#app/ui/party-ui-handler"; import {OptionSelectConfig, OptionSelectItem} from "#app/ui/abstact-option-select-ui-handler"; import {WIGHT_INCREMENT_ON_SPAWN_MISS} from "#app/data/mystery-encounters/mystery-encounters"; +import {getBBCodeFrag, TextStyle} from "#app/ui/text"; /** * @@ -162,21 +167,35 @@ export function koPlayerPokemon(pokemon: PlayerPokemon) { pokemon.updateInfo(); } -export function getTextWithEncounterDialogueTokens(scene: BattleScene, textKey: TemplateStringsArray | `mysteryEncounter:${string}`): string { +export function getTextWithEncounterDialogueTokensAndColor(scene: BattleScene, textKey: TemplateStringsArray | `mysteryEncounter:${string}`, primaryStyle: TextStyle = TextStyle.MESSAGE): string { if (isNullOrUndefined(textKey)) { return null; } let textString: string = i18next.t(textKey); - const dialogueTokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens; + // Apply primary styling before anything else, if it exists + textString = getBBCodeFrag(textString, primaryStyle) + "[/color][/shadow]"; + const primaryStyleString = [...textString.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))][0]; + // Apply dialogue tokens + const dialogueTokens = scene.currentBattle?.mysteryEncounter?.dialogueTokens; if (dialogueTokens) { dialogueTokens.forEach((value) => { textString = textString.replace(value[0], value[1]); }); } + // Set custom colors + // Looks for any pattern like this: @ecCol[SUMMARY_BLUE]{my text to color} + // Resulting in: "my text to color" string with TextStyle.SUMMARY_BLUE + textString = textString.replace(/@ecCol\[([^{]*)\]{([^}]*)}/gi, (substring, textStyle: string, textToColor: string) => { + return "[/color][/shadow]" + getBBCodeFrag(textToColor, TextStyle[textStyle]) + "[/color][/shadow]" + primaryStyleString; + }); + + // Remove extra style block at the end + textString = textString.replace(/\[color=[^\[]*\]\[shadow=[^\[]*\]\[\/color\]\[\/shadow\]/gi, ""); + return textString; } @@ -186,7 +205,7 @@ export function getTextWithEncounterDialogueTokens(scene: BattleScene, textKey: * @param contentKey */ export function queueEncounterMessage(scene: BattleScene, contentKey: TemplateStringsArray | `mysteryEncounter:${string}`): void { - const text: string = getTextWithEncounterDialogueTokens(scene, contentKey); + const text: string = getTextWithEncounterDialogueTokensAndColor(scene, contentKey, TextStyle.MESSAGE); scene.queueMessage(text, null, true); } @@ -197,7 +216,7 @@ export function queueEncounterMessage(scene: BattleScene, contentKey: TemplateSt */ export function showEncounterText(scene: BattleScene, contentKey: TemplateStringsArray | `mysteryEncounter:${string}`): Promise { return new Promise(resolve => { - const text: string = getTextWithEncounterDialogueTokens(scene, contentKey); + const text: string = getTextWithEncounterDialogueTokensAndColor(scene, contentKey, TextStyle.MESSAGE); scene.ui.showText(text, null, () => resolve(), 0, true); }); } @@ -210,8 +229,8 @@ export function showEncounterText(scene: BattleScene, contentKey: TemplateString * @param callback */ export function showEncounterDialogue(scene: BattleScene, textContentKey: TemplateStringsArray | `mysteryEncounter:${string}`, speakerContentKey: TemplateStringsArray | `mysteryEncounter:${string}`, callback?: Function) { - const text: string = getTextWithEncounterDialogueTokens(scene, textContentKey); - const speaker: string = getTextWithEncounterDialogueTokens(scene, speakerContentKey); + const text: string = getTextWithEncounterDialogueTokensAndColor(scene, textContentKey, TextStyle.MESSAGE); + const speaker: string = getTextWithEncounterDialogueTokensAndColor(scene, speakerContentKey); scene.ui.showDialogue(text, speaker, null, callback, 0, 0); } @@ -399,6 +418,50 @@ export async function initBattleWithEnemyConfig(scene: BattleScene, partyConfig: } } +/** + * Will update player money, and animate change (sound optional) + * @param scene - Battle Scene + * @param changeValue + * @param playSound + */ +export function updatePlayerMoney(scene: BattleScene, changeValue: number, playSound: boolean = true) { + scene.money += changeValue; + scene.updateMoneyText(); + scene.animateMoneyChanged(false); + if (playSound) { + scene.playSound("buy"); + } +} + +/** + * Converts modifier bullshit to an actual item + * @param scene - Battle Scene + * @param modifier + * @param pregenArgs - can specify BerryType for berries, TM for TMs, AttackBoostType for item, etc. + */ +export function generateModifierType(scene: BattleScene, modifier: () => ModifierType, pregenArgs?: any[]): ModifierType { + const modifierId = Object.keys(modifierTypes).find(k => modifierTypes[k] === modifier); + let result: ModifierType = modifierTypes[modifierId]?.(); + + // Gets tier of item by checking player item pool + const modifierPool = getModifierPoolForType(ModifierPoolType.PLAYER); + Object.keys(modifierPool).every(modifierTier => { + const modType = modifierPool[modifierTier].find(m => { + if (m.modifierType.id === modifierId) { + return m; + } + }); + if (modType) { + result = modType.modifierType; + return false; + } + return true; + }); + + result = result instanceof ModifierTypeGenerator ? result.generateType(scene.getParty(), pregenArgs) : result; + return result; +} + /** * Will initialize reward phases to follow the mystery encounter * Can have shop displayed or skipped @@ -439,8 +502,9 @@ export function setCustomEncounterRewards(scene: BattleScene, customShopRewards? * @param onPokemonSelected - Any logic that needs to be performed when Pokemon is chosen * If a second option needs to be selected, onPokemonSelected should return a OptionSelectItem[] object * @param onPokemonNotSelected - Any logic that needs to be performed if no Pokemon is chosen + * @param selectablePokemonFilter */ -export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void): Promise { +export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (pokemon: PlayerPokemon) => void | OptionSelectItem[], onPokemonNotSelected?: () => void, selectablePokemonFilter?: (pokemon: PlayerPokemon) => string): Promise { return new Promise(resolve => { // Open party screen to choose pokemon to train scene.ui.setMode(Mode.PARTY, PartyUiMode.SELECT, -1, (slotIndex: integer, option: PartyOption) => { @@ -493,7 +557,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p if (!textPromptKey) { displayOptions(); } else { - const secondOptionSelectPrompt = getTextWithEncounterDialogueTokens(scene, textPromptKey); + const secondOptionSelectPrompt = getTextWithEncounterDialogueTokensAndColor(scene, textPromptKey, TextStyle.MESSAGE); scene.ui.showText(secondOptionSelectPrompt, null, displayOptions, null, true); } }); @@ -506,7 +570,7 @@ export function selectPokemonForOption(scene: BattleScene, onPokemonSelected: (p resolve(false); }); } - }); + }, selectablePokemonFilter); }); } diff --git a/src/data/mystery-encounters/mystery-encounters.ts b/src/data/mystery-encounters/mystery-encounters.ts index 918868d3d2c..003b544ce50 100644 --- a/src/data/mystery-encounters/mystery-encounters.ts +++ b/src/data/mystery-encounters/mystery-encounters.ts @@ -7,6 +7,8 @@ import {TrainingSessionEncounter} from "#app/data/mystery-encounters/training-se import { Biome } from "#app/enums/biome"; import { SleepingSnorlaxEncounter } from "./sleeping-snorlax"; import { MysteryEncounterType } from "#enums/mystery-encounter-type"; +import {DepartmentStoreSaleEncounter} from "#app/data/mystery-encounters/department-store-sale"; +import {ShadyVitaminDealerEncounter} from "#app/data/mystery-encounters/shady-vitamin-dealer"; // Spawn chance: (BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT + WIGHT_INCREMENT_ON_SPAWN_MISS * ) / 256 export const BASE_MYSTERY_ENCOUNTER_SPAWN_WEIGHT = 1; @@ -19,18 +21,20 @@ export const allMysteryEncounters : {[encounterType:string]: MysteryEncounter} = // To enable an encounter in all biomes, do not add to this map export const mysteryEncountersByBiome = new Map([ [Biome.TOWN, [ + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.PLAINS,[ - + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.GRASS, [ - MysteryEncounterType.SLEEPING_SNORLAX + MysteryEncounterType.SLEEPING_SNORLAX, + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.TALL_GRASS, [ - + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.METROPOLIS, [ - + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.FOREST, [ MysteryEncounterType.SLEEPING_SNORLAX @@ -43,7 +47,7 @@ export const mysteryEncountersByBiome = new Map([ ]], [Biome.BEACH, [ - + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.LAKE, [ @@ -67,10 +71,10 @@ export const mysteryEncountersByBiome = new Map([ ]], [Biome.MEADOW, [ - + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.POWER_PLANT, [ - + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.VOLCANO, [ @@ -82,7 +86,7 @@ export const mysteryEncountersByBiome = new Map([ ]], [Biome.FACTORY, [ - + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.RUINS, [ @@ -97,7 +101,7 @@ export const mysteryEncountersByBiome = new Map([ ]], [Biome.CONSTRUCTION_SITE, [ - + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.JUNGLE, [ @@ -109,7 +113,7 @@ export const mysteryEncountersByBiome = new Map([ ]], [Biome.SLUM, [ - + MysteryEncounterType.DEPARTMENT_STORE_SALE ]], [Biome.SNOWY_FOREST, [ @@ -131,6 +135,8 @@ export function initMysteryEncounters() { allMysteryEncounters[MysteryEncounterType.FIGHT_OR_FLIGHT] = FightOrFlightEncounter; allMysteryEncounters[MysteryEncounterType.TRAINING_SESSION] = TrainingSessionEncounter; allMysteryEncounters[MysteryEncounterType.SLEEPING_SNORLAX] = SleepingSnorlaxEncounter; + allMysteryEncounters[MysteryEncounterType.DEPARTMENT_STORE_SALE] = DepartmentStoreSaleEncounter; + allMysteryEncounters[MysteryEncounterType.SHADY_VITAMIN_DEALER] = ShadyVitaminDealerEncounter; // Append encounters that can occur in any biome to biome map const anyBiomeEncounters: MysteryEncounterType[] = Object.keys(MysteryEncounterType).filter(e => !isNaN(Number(e))).map(k => Number(k) as MysteryEncounterType); diff --git a/src/data/mystery-encounters/shady-vitamin-dealer.ts b/src/data/mystery-encounters/shady-vitamin-dealer.ts new file mode 100644 index 00000000000..bd2e6094748 --- /dev/null +++ b/src/data/mystery-encounters/shady-vitamin-dealer.ts @@ -0,0 +1,148 @@ +import BattleScene from "../../battle-scene"; +import { + generateModifierType, + leaveEncounterWithoutBattle, + queueEncounterMessage, + selectPokemonForOption, + setCustomEncounterRewards, + updatePlayerMoney, +} from "#app/data/mystery-encounters/mystery-encounter-utils"; +import MysteryEncounter, {MysteryEncounterBuilder, MysteryEncounterTier} from "../mystery-encounter"; +import {MysteryEncounterType} from "#enums/mystery-encounter-type"; +import { + HealthRatioRequirement, + MoneyRequirement, + StatusEffectRequirement, + WaveCountRequirement +} from "../mystery-encounter-requirements"; +import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option"; +import {modifierTypes} from "#app/modifier/modifier-type"; +import {Species} from "#enums/species"; +import {randSeedInt} from "#app/utils"; +import Pokemon, {PlayerPokemon} from "#app/field/pokemon"; +import {StatusEffect} from "#app/data/status-effect"; + +export const ShadyVitaminDealerEncounter: MysteryEncounter = new MysteryEncounterBuilder() + .withEncounterType(MysteryEncounterType.SHADY_VITAMIN_DEALER) + .withEncounterTier(MysteryEncounterTier.COMMON) + .withIntroSpriteConfigs([ + { + spriteKey: Species.KROOKODILE.toString(), + fileRoot: "pokemon", + hasShadow: true, + repeat: true, + x: 10, + y: -1 + }, + { + spriteKey: "b2w2_veteran_m", + fileRoot: "mystery-encounters", + hasShadow: true, + x: -10, + y: 2 + } + ]) + .withSceneRequirement(new WaveCountRequirement([10, 180])) + .withPrimaryPokemonRequirement(new StatusEffectRequirement([StatusEffect.NONE])) // Pokemon must not have status + .withPrimaryPokemonRequirement(new HealthRatioRequirement([0.34, 1])) // Pokemon must have above 1/3rd HP + .withOption(new MysteryEncounterOptionBuilder() + .withSceneRequirement(new MoneyRequirement(0, 2)) // Wave scaling multiplier of 2 for cost + .withPreOptionPhase(async (scene: BattleScene): Promise => { + const encounter = scene.currentBattle.mysteryEncounter; + const onPokemonSelected = (pokemon: PlayerPokemon) => { + // Update money + updatePlayerMoney(scene, -(encounter.options[0].requirements[0] as MoneyRequirement).requiredMoney); + // Calculate modifiers and dialogue tokens + const modifiers = [ + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER), + generateModifierType(scene, modifierTypes.BASE_STAT_BOOSTER) + ]; + encounter.setDialogueToken("boost1", modifiers[0].name); + encounter.setDialogueToken("boost2", modifiers[1].name); + encounter.misc = { + chosenPokemon: pokemon, + modifiers: modifiers + }; + }; + + // Only Pokemon that can gain benefits are unfainted with no status + const selectableFilter = (pokemon: Pokemon) => { + // If pokemon meets primary pokemon reqs, it can be selected + const meetsReqs = encounter.pokemonMeetsPrimaryRequirements(scene, pokemon); + if (!meetsReqs) { + return "Pokémon must be healthy enough."; + } + + return null; + }; + + return selectPokemonForOption(scene, onPokemonSelected, null, selectableFilter); + }) + .withOptionPhase(async (scene: BattleScene) => { + // Choose Cheap Option + const encounter = scene.currentBattle.mysteryEncounter; + const chosenPokemon = encounter.misc.chosenPokemon; + const modifiers = encounter.misc.modifiers; + + for (const modType of modifiers) { + const modifier = modType.newModifier(chosenPokemon); + await scene.addModifier(modifier, true, false, false, true); + } + scene.updateModifiers(true); + + leaveEncounterWithoutBattle(scene); + }) + .withPostOptionPhase(async (scene: BattleScene) => { + // Damage and status applied after dealer leaves (to make thematic sense) + const encounter = scene.currentBattle.mysteryEncounter; + const chosenPokemon = encounter.misc.chosenPokemon; + + // Pokemon takes 1/3 max HP damage + const damage = Math.round(chosenPokemon.getMaxHp() / 3); + chosenPokemon.hp = Math.max(chosenPokemon.hp - damage, 0); + + // Roll for poison (80%) + if (randSeedInt(10) < 10) { + if (chosenPokemon.trySetStatus(StatusEffect.TOXIC)) { + // Toxic applied + queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_bad_poison"); + } else { + // Pokemon immune or something else prevents status + queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_damage_only"); + } + } else { + queueEncounterMessage(scene, "mysteryEncounter:shady_vitamin_dealer_damage_only"); + } + + chosenPokemon.updateInfo(); + }) + .build()) + .withOption(new MysteryEncounterOptionBuilder() + .withSceneRequirement(new MoneyRequirement(0, 5)) // Wave scaling multiplier of 2 for cost + .withOptionPhase(async (scene: BattleScene) => { + // Choose Expensive Option + const modifiers = []; + let i = 0; + while (i < 3) { + // 2/1 weight on base stat booster vs PP Up + const roll = randSeedInt(3); + if (roll === 0) { + modifiers.push(modifierTypes.PP_UP); + } else { + + } + i++; + } + + setCustomEncounterRewards(scene, { guaranteedModifierTypeFuncs: modifiers, fillRemaining: false}); + leaveEncounterWithoutBattle(scene); + }) + .build()) + .withOption(new MysteryEncounterOptionBuilder() + .withOptionPhase(async (scene: BattleScene) => { + // Leave encounter with no rewards or exp + leaveEncounterWithoutBattle(scene, true); + return true; + }) + .build()) + .build(); diff --git a/src/data/mystery-encounters/sleeping-snorlax.ts b/src/data/mystery-encounters/sleeping-snorlax.ts index 56baa4ea1e7..bf53ad72593 100644 --- a/src/data/mystery-encounters/sleeping-snorlax.ts +++ b/src/data/mystery-encounters/sleeping-snorlax.ts @@ -1,7 +1,7 @@ import BattleScene from "../../battle-scene"; import { EnemyPartyConfig, - EnemyPokemonConfig, + EnemyPokemonConfig, generateModifierType, initBattleWithEnemyConfig, leaveEncounterWithoutBattle, queueEncounterMessage, setCustomEncounterRewards @@ -12,7 +12,6 @@ import {MysteryEncounterType} from "#enums/mystery-encounter-type"; import {MoveRequirement, WaveCountRequirement} from "../mystery-encounter-requirements"; import {MysteryEncounterOptionBuilder} from "../mystery-encounter-option"; import { - ModifierTypeGenerator, ModifierTypeOption, modifierTypes } from "#app/modifier/modifier-type"; @@ -76,7 +75,8 @@ export const SleepingSnorlaxEncounter: MysteryEncounter = new MysteryEncounterBu const p = instance.primaryPokemon; p.status = new Status(StatusEffect.SLEEP, 0, 3); p.updateInfo(true); - const sitrus = (modifierTypes.BERRY?.() as ModifierTypeGenerator).generateType(scene.getParty(), [BerryType.SITRUS]); + // const sitrus = (modifierTypes.BERRY?.() as ModifierTypeGenerator).generateType(scene.getParty(), [BerryType.SITRUS]); + const sitrus = generateModifierType(scene, modifierTypes.BERRY, [BerryType.SITRUS]); setCustomEncounterRewards(scene, { guaranteedModifierTypeOptions: [new ModifierTypeOption(sitrus, 0)], fillRemaining: false}); queueEncounterMessage(scene, "mysteryEncounter:sleeping_snorlax_option_2_bad_result"); diff --git a/src/data/mystery-encounters/training-session.ts b/src/data/mystery-encounters/training-session.ts index 9866a2785a1..c833c37c06f 100644 --- a/src/data/mystery-encounters/training-session.ts +++ b/src/data/mystery-encounters/training-session.ts @@ -1,7 +1,7 @@ import BattleScene from "../../battle-scene"; import { EnemyPartyConfig, - getTextWithEncounterDialogueTokens, + getTextWithEncounterDialogueTokensAndColor, initBattleWithEnemyConfig, selectPokemonForOption, setCustomEncounterRewards @@ -128,7 +128,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu scene.addModifier(mod, true, false, false, true); } scene.updateModifiers(true); - scene.queueMessage(getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:training_session_battle_finished_1"), null, true); + scene.queueMessage(getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:training_session_battle_finished_1"), null, true); }; setCustomEncounterRewards(scene, { fillRemaining: true }, null, onBeforeRewardsPhase); @@ -174,7 +174,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { - scene.queueMessage(getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:training_session_battle_finished_2"), null, true); + scene.queueMessage(getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:training_session_battle_finished_2"), null, true); // Add the pokemon back to party with Nature change playerPokemon.setNature(encounter.misc.chosenNature); scene.gameData.setPokemonCaught(playerPokemon, false); @@ -237,7 +237,7 @@ export const TrainingSessionEncounter: MysteryEncounter = new MysteryEncounterBu scene.removePokemonFromPlayerParty(playerPokemon, false); const onBeforeRewardsPhase = () => { - scene.queueMessage(getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:training_session_battle_finished_3"), null, true); + scene.queueMessage(getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:training_session_battle_finished_3"), null, true); // Add the pokemon back to party with ability change const abilityIndex = encounter.misc.abilityIndex; if (!!playerPokemon.getFusionSpeciesForm()) { diff --git a/src/enums/mystery-encounter-type.ts b/src/enums/mystery-encounter-type.ts index a29a409103d..6e2815babca 100644 --- a/src/enums/mystery-encounter-type.ts +++ b/src/enums/mystery-encounter-type.ts @@ -4,5 +4,7 @@ export enum MysteryEncounterType { DARK_DEAL, FIGHT_OR_FLIGHT, SLEEPING_SNORLAX, - TRAINING_SESSION + TRAINING_SESSION, + DEPARTMENT_STORE_SALE, + SHADY_VITAMIN_DEALER } diff --git a/src/field/mystery-encounter-intro.ts b/src/field/mystery-encounter-intro.ts index 82e9bb49f89..8fdbd27ac9b 100644 --- a/src/field/mystery-encounter-intro.ts +++ b/src/field/mystery-encounter-intro.ts @@ -79,12 +79,12 @@ export default class MysteryEncounterIntroVisuals extends Phaser.GameObjects.Con // Sprite offset from origin if (config.x || config.y) { if (config.x) { - sprite.x = origin + config.x; - tintSprite.x = origin + config.x; + sprite.setPosition(origin + config.x, sprite.y); + tintSprite.setPosition(origin + config.x, tintSprite.y); } if (config.y) { - sprite.y = origin + config.y; - tintSprite.y = origin + config.y; + sprite.setPosition(sprite.x, config.y); + tintSprite.setPosition(tintSprite.x, config.y); } } else { // Single sprite diff --git a/src/locales/en/mystery-encounter.ts b/src/locales/en/mystery-encounter.ts index f6b3a80e0ec..9fe63b33f58 100644 --- a/src/locales/en/mystery-encounter.ts +++ b/src/locales/en/mystery-encounter.ts @@ -1,17 +1,28 @@ import {SimpleTranslationEntries} from "#app/interfaces/locales"; +/** + * Patterns that can be used: + * '$' will be treated as a new line for Message and Dialogue strings + * '@d{}' will add a time delay to text animation for Message and Dialogue strings + * + * '@ec{}' will auto-inject the matching token value for the specified Encounter + * + * '@ecCol[]{}' will auto-color the given text to a specified TextStyle (e.g. TextStyle.SUMMARY_GREEN) + * + * Any '(+)' or '(-)' type of tooltip will auto-color to green/blue respectively. THIS ONLY OCCURS FOR OPTION TOOLTIPS, NOWHERE ELSE + * Other types of '(...)' tooltips will have to specify the text color manually by using '@ecCol[SUMMARY_GREEN]{}' pattern + */ export const mysteryEncounter: SimpleTranslationEntries = { // DO NOT REMOVE "unit_test_dialogue": "@ec{test}@ec{test} @ec{test@ec{test}} @ec{test1} @ec{test\} @ec{test\\} @ec{test\\\} {test}", - // Mysterious Encounters -- Common Tier - + // Mystery Encounters -- Common Tier "mysterious_chest_intro_message": "You found...@d{32} a chest?", "mysterious_chest_title": "The Mysterious Chest", "mysterious_chest_description": "A beautifully ornamented chest stands on the ground. There must be something good inside... right?", "mysterious_chest_query": "Will you open it?", "mysterious_chest_option_1_label": "Open it", - "mysterious_chest_option_1_tooltip": "(35%) Something terrible\n(40%) Okay Rewards\n(20%) Good Rewards\n(4%) Great Rewards\n(1%) Amazing Rewards", + "mysterious_chest_option_1_tooltip": "@ecCol[SUMMARY_BLUE]{(35%) Something terrible}\n@ecCol[SUMMARY_GREEN]{(40%) Okay Rewards}\n@ecCol[SUMMARY_GREEN]{(20%) Good Rewards}\n@ecCol[SUMMARY_GREEN]{(4%) Great Rewards}\n@ecCol[SUMMARY_GREEN]{(1%) Amazing Rewards}", "mysterious_chest_option_2_label": "It's too risky, leave", "mysterious_chest_option_2_tooltip": "(-) No Rewards", "mysterious_chest_option_1_selected_message": "You open the chest to find...", @@ -27,36 +38,82 @@ export const mysteryEncounter: SimpleTranslationEntries = { "fight_or_flight_title": "Fight or Flight", "fight_or_flight_description": "It looks like there's a strong Pokémon guarding an item. Battling is the straightforward approach, but this Pokémon looks strong. You could also try to sneak around, though the Pokémon might catch you.", "fight_or_flight_query": "What will you do?", - "fight_or_flight_option_1_label": "Battle it", - "fight_or_flight_option_1_tooltip": "(+) Hard Battle\n(+) New Item", - "fight_or_flight_option_2_label": "Sneak around", - "fight_or_flight_option_2_tooltip": "(35%) Steal Item\n(65%) Harder Battle", + "fight_or_flight_option_1_label": "Battle the Pokémon", + "fight_or_flight_option_1_tooltip": "(-) Hard Battle\n(+) New Item", + "fight_or_flight_option_2_label": "Steal the item", + "fight_or_flight_option_2_tooltip": "@ecCol[SUMMARY_GREEN]{(35%) Steal Item}\n@ecCol[SUMMARY_BLUE]{(65%) Harder Battle}", + "fight_or_flight_option_2_steal_tooltip": "@ecCol[SUMMARY_GREEN]{(?) Use a Pokémon Move}", "fight_or_flight_option_3_label": "Leave", "fight_or_flight_option_3_tooltip": "(-) No Rewards", "fight_or_flight_option_1_selected_message": "You approach the\nPokémon without fear.", "fight_or_flight_option_2_good_result": `.@d{32}.@d{32}.@d{32} $You manage to sneak your way\npast and grab the item!`, + "fight_or_flight_option_2_steal_result": `.@d{32}.@d{32}.@d{32} + $Your @ec{thiefPokemon} helps you out and uses @ec{move}! + $ You nabbed the item!`, "fight_or_flight_option_2_bad_result": `.@d{32}.@d{32}.@d{32} $The Pokémon catches you\nas you try to sneak around!`, "fight_or_flight_boss_enraged": "The opposing @ec{enemyPokemon} has become enraged!", "fight_or_flight_option_3_selected": "You leave the strong Pokémon\nwith its prize and continue on.", - // Mysterious Encounters -- Uncommon Tier + "department_store_sale_intro_message": "It's a lady with a ton of shopping bags.", + "department_store_sale_speaker": "Shopper", + "department_store_sale_intro_dialogue": `Hello! Are you here for\nthe amazing sales too? + $There's a special coupon that you can\nredeem for a free item during the sale! + $I have an extra one. Here you go!`, + "department_store_sale_title": "Department Store Sale", + "department_store_sale_description": "There is merchandise in every direction! It looks like there are 4 counters where you can redeem the coupon for various items. The possibilities are endless!", + "department_store_sale_query": "Which counter will you go to?", + "department_store_sale_option_1_label": "TM Counter", + "department_store_sale_option_1_tooltip": "(+) TM Shop", + "department_store_sale_option_2_label": "Vitamin Counter", + "department_store_sale_option_2_tooltip": "(+) Vitamin Shop", + "department_store_sale_option_3_label": "Battle Item Counter", + "department_store_sale_option_3_tooltip": "(+) X Item Shop", + "department_store_sale_option_4_label": "Pokéball Counter", + "department_store_sale_option_4_tooltip": "(+) Pokéball Shop", + "department_store_sale_outro": "What a deal! You should shop there more often.", + + "shady_vitamin_dealer_intro_message": "A man in a dark coat approaches you.", + "shady_vitamin_dealer_speaker": "Shady Salesman", + "shady_vitamin_dealer_intro_dialogue": `.@d{16}.@d{16}.@d{16} + $I've got the goods if you've got the money. + $Make sure your Pokémon can handle it though.`, + "shady_vitamin_dealer_title": "The Vitamin Dealer", + "shady_vitamin_dealer_description": "The man opens his jacket to reveal some Pokémon vitamins. The numbers he quotes seem like a really good deal. Almost too good...\nHe offers two package deals to choose from.", + "shady_vitamin_dealer_query": "Which deal will choose?", + "shady_vitamin_dealer_option_1_label": "The Cheap Deal", + "shady_vitamin_dealer_option_1_tooltip": "(-) Pay @ec{option1Money}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", + "shady_vitamin_dealer_option_2_label": "The Pricey Deal", + "shady_vitamin_dealer_option_2_tooltip": "(-) Pay @ec{option2Money}\n(-) Side Effects?\n(+) Chosen Pokémon Gains 2 Random Vitamins", + "shady_vitamin_dealer_option_selected": `The man hands you two bottles and quickly disappears. + $@ec{selectedPokemon} gained @ec{boost1} and @ec{boost2} boosts!`, + "shady_vitamin_dealer_damage_only": `But the medicine had some side effects! + $Your @ec{selectedPokemon} takes some damage...`, + "shady_vitamin_dealer_bad_poison": `But the medicine had some side effects! + $Your @ec{selectedPokemon} takes some damage\nand becomes badly poisoned...`, + "shady_vitamin_dealer_poison": `But the medicine had some side effects! + $Your @ec{selectedPokemon} becomes poisoned...`, + "shady_vitamin_dealer_option_3_label": "Leave", + "shady_vitamin_dealer_option_3_tooltip": "(-) No Rewards", + "shady_vitamin_dealer_outro_good": "Looks like there were no side-effects this time.", + + // Mystery Encounters -- Uncommon Tier "mysterious_challengers_intro_message": "Mysterious challengers have appeared!", "mysterious_challengers_title": "Mysterious Challengers", "mysterious_challengers_description": "If you defeat a challenger, you might impress them enough to receive a boon. But some look tough, are you up to the challenge?", "mysterious_challengers_query": "Who will you battle?", "mysterious_challengers_option_1_label": "A clever, mindful foe", - "mysterious_challengers_option_1_tooltip": "(+) Standard Battle\n(+) Move Item Rewards", + "mysterious_challengers_option_1_tooltip": "(-) Standard Battle\n(+) Move Item Rewards", "mysterious_challengers_option_2_label": "A strong foe", - "mysterious_challengers_option_2_tooltip": "(+) Hard Battle\n(+) Good Rewards", + "mysterious_challengers_option_2_tooltip": "(-) Hard Battle\n(+) Good Rewards", "mysterious_challengers_option_3_label": "The mightiest foe", - "mysterious_challengers_option_3_tooltip": "(+) Brutal Battle\n(+) Great Rewards", + "mysterious_challengers_option_3_tooltip": "(-) Brutal Battle\n(+) Great Rewards", "mysterious_challengers_option_selected_message": "The trainer steps forward...", "mysterious_challengers_outro_win": "The mysterious challenger was defeated!", - // Mysterious Encounters -- Rare Tier + // Mystery Encounters -- Rare Tier "training_session_intro_message": "You've come across some\ntraining tools and supplies.", "training_session_title": "Training Session", "training_session_description": "These supplies look like they could be used to train a member of your party! There are a few ways you could train your Pokémon, by battling against it with the rest of your team.", @@ -78,7 +135,7 @@ export const mysteryEncounter: SimpleTranslationEntries = { $Its ability was changed to @ec{ability}!`, "training_session_outro_win": "That was a successful training session!", - // Mysterious Encounters -- Super Rare Tier + // Mystery Encounters -- Super Rare Tier "dark_deal_intro_message": "A strange man in a tattered coat\nstands in your way...", "dark_deal_speaker": "Shady Guy", @@ -108,9 +165,9 @@ export const mysteryEncounter: SimpleTranslationEntries = { "sleeping_snorlax_description": "You could attack it to try and get it to move, or simply wait for it to wake up.", "sleeping_snorlax_query": "What will you do?", "sleeping_snorlax_option_1_label": "Fight it", - "sleeping_snorlax_option_1_tooltip": "(+) Fight Sleeping Snorlax", + "sleeping_snorlax_option_1_tooltip": "(-) Fight Sleeping Snorlax", "sleeping_snorlax_option_2_label": "Wait for it to move", - "sleeping_snorlax_option_2_tooltip": "(75%) Wait a short time\n(25%) Wait a long time", + "sleeping_snorlax_option_2_tooltip": "@ecCol[SUMMARY_BLUE]{(75%) Wait a short time}\n@ecCol[SUMMARY_BLUE]{(25%) Wait a long time}", "sleeping_snorlax_option_3_label": "Steal", "sleeping_snorlax_option_3_tooltip": "(+) Leftovers", "sleeping_snorlax_option_3_disabled_tooltip": "Your Pokémon need to know certain moves to choose this", diff --git a/src/overrides.ts b/src/overrides.ts index 6634ca32788..f3ac35e2075 100644 --- a/src/overrides.ts +++ b/src/overrides.ts @@ -118,9 +118,9 @@ export const EGG_GACHA_PULL_COUNT_OVERRIDE: number = 0; */ // 1 to 256, set to null to ignore -export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = null; +export const MYSTERY_ENCOUNTER_RATE_OVERRIDE: number = 256; export const MYSTERY_ENCOUNTER_TIER_OVERRIDE: MysteryEncounterTier = null; -export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = null; +export const MYSTERY_ENCOUNTER_OVERRIDE: MysteryEncounterType = MysteryEncounterType.SHADY_VITAMIN_DEALER; /** * MODIFIER / ITEM OVERRIDES diff --git a/src/phases/mystery-encounter-phase.ts b/src/phases/mystery-encounter-phase.ts index 1cd04339b84..b928d98834c 100644 --- a/src/phases/mystery-encounter-phase.ts +++ b/src/phases/mystery-encounter-phase.ts @@ -3,7 +3,7 @@ import BattleScene from "../battle-scene"; import { Phase } from "../phase"; import { Mode } from "../ui/ui"; import { - getTextWithEncounterDialogueTokens + getTextWithEncounterDialogueTokensAndColor } from "../data/mystery-encounters/mystery-encounter-utils"; import { CheckSwitchPhase, NewBattlePhase, PostSummonPhase, ReturnPhase, ScanIvsPhase, SummonPhase, ToggleDoublePositionPhase } from "../phases"; import MysteryEncounterOption from "../data/mystery-encounter-option"; @@ -89,9 +89,9 @@ export class MysteryEncounterPhase extends Phase { const nextAction = i === selectedDialogue.length - 1 ? endDialogueAndContinueEncounter : showNextDialogue; const dialogue = selectedDialogue[i]; let title: string = null; - const text: string = getTextWithEncounterDialogueTokens(this.scene, dialogue.text); + const text: string = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.text); if (dialogue.speaker) { - title = getTextWithEncounterDialogueTokens(this.scene, dialogue.speaker); + title = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.speaker); } if (title) { @@ -451,9 +451,9 @@ export class PostMysteryEncounterPhase extends Phase { const nextAction = i === outroDialogue.length - 1 ? endPhase : showNextDialogue; const dialogue = outroDialogue[i]; let title: string = null; - const text: string = getTextWithEncounterDialogueTokens(this.scene, dialogue.text); + const text: string = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.text); if (dialogue.speaker) { - title = getTextWithEncounterDialogueTokens(this.scene, dialogue.speaker); + title = getTextWithEncounterDialogueTokensAndColor(this.scene, dialogue.speaker); } this.scene.ui.setMode(Mode.MESSAGE); diff --git a/src/test/mystery-encounter/mystery-encounter-utils.test.ts b/src/test/mystery-encounter/mystery-encounter-utils.test.ts index 6f1bccd359c..63cbd83d975 100644 --- a/src/test/mystery-encounter/mystery-encounter-utils.test.ts +++ b/src/test/mystery-encounter/mystery-encounter-utils.test.ts @@ -3,7 +3,7 @@ import GameManager from "#app/test/utils/gameManager"; import Phaser from "phaser"; import { getHighestLevelPlayerPokemon, getLowestLevelPlayerPokemon, - getRandomPlayerPokemon, getRandomSpeciesByStarterTier, getTextWithEncounterDialogueTokens, + getRandomPlayerPokemon, getRandomSpeciesByStarterTier, getTextWithEncounterDialogueTokensAndColor, koPlayerPokemon, queueEncounterMessage, showEncounterDialogue, showEncounterText, } from "#app/data/mystery-encounters/mystery-encounter-utils"; import {initSceneWithoutEncounterPhase} from "#test/utils/gameManagerUtils"; @@ -272,12 +272,12 @@ describe("Mystery Encounter Utils", () => { }); describe("getTextWithEncounterDialogueTokens", () => { - it("injects dialogue tokens", () => { + it("injects dialogue tokens and color styling", () => { scene.currentBattle.mysteryEncounter = new MysteryEncounter(null); scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); - const result = getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:unit_test_dialogue"); - expect(result).toEqual("valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}"); + const result = getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:unit_test_dialogue"); + expect(result).toEqual("[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]"); }); it("can perform nested dialogue token injection", () => { @@ -285,8 +285,8 @@ describe("Mystery Encounter Utils", () => { scene.currentBattle.mysteryEncounter.setDialogueToken("test", "value"); scene.currentBattle.mysteryEncounter.setDialogueToken("testvalue", "new"); - const result = getTextWithEncounterDialogueTokens(scene, "mysteryEncounter:unit_test_dialogue"); - expect(result).toEqual("valuevalue new @ec{test1} value @ec{test\\} @ec{test\\} {test}"); + const result = getTextWithEncounterDialogueTokensAndColor(scene, "mysteryEncounter:unit_test_dialogue"); + expect(result).toEqual("[color=#f8f8f8][shadow=#6b5a73]valuevalue new @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]"); }); }); @@ -298,7 +298,7 @@ describe("Mystery Encounter Utils", () => { const phaseSpy = vi.spyOn(game.scene, "unshiftPhase"); queueEncounterMessage(scene, "mysteryEncounter:unit_test_dialogue"); - expect(spy).toHaveBeenCalledWith("valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}", null, true); + expect(spy).toHaveBeenCalledWith("[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]", null, true); expect(phaseSpy).toHaveBeenCalledWith(expect.any(MessagePhase)); }); }); @@ -310,7 +310,7 @@ describe("Mystery Encounter Utils", () => { const spy = vi.spyOn(game.scene.ui, "showText"); showEncounterText(scene, "mysteryEncounter:unit_test_dialogue"); - expect(spy).toHaveBeenCalledWith("valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}", null, expect.any(Function), 0, true); + expect(spy).toHaveBeenCalledWith("[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]", null, expect.any(Function), 0, true); }); }); @@ -321,7 +321,7 @@ describe("Mystery Encounter Utils", () => { const spy = vi.spyOn(game.scene.ui, "showDialogue"); showEncounterDialogue(scene, "mysteryEncounter:unit_test_dialogue", "mysteryEncounter:unit_test_dialogue"); - expect(spy).toHaveBeenCalledWith("valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}", "valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}", null, undefined, 0, 0); + expect(spy).toHaveBeenCalledWith("[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]", "[color=#f8f8f8][shadow=#6b5a73]valuevalue @ec{testvalue} @ec{test1} value @ec{test\\} @ec{test\\} {test}[/color][/shadow]", null, undefined, 0, 0); }); }); diff --git a/src/test/phases/mystery-encounter-phase.test.ts b/src/test/phases/mystery-encounter-phase.test.ts index 8d8bac79b5a..4374fc60bd6 100644 --- a/src/test/phases/mystery-encounter-phase.test.ts +++ b/src/test/phases/mystery-encounter-phase.test.ts @@ -84,7 +84,7 @@ describe("Mystery Encounter Phases", () => { expect(messageSpy).toHaveBeenCalledTimes(2); expect(dialogueSpy).toHaveBeenCalledWith("What's this?", "???", null, expect.any(Function)); expect(messageSpy).toHaveBeenCalledWith("Mysterious challengers have appeared!", null, expect.any(Function), 750, true); - expect(messageSpy).toHaveBeenCalledWith("The trainer steps forward...", null, expect.any(Function), 750, true); + expect(messageSpy).toHaveBeenCalledWith("[color=#f8f8f8][shadow=#6b5a73]The trainer steps forward...[/color][/shadow]", null, expect.any(Function), 750, true); }); }); diff --git a/src/ui/mystery-encounter-ui-handler.ts b/src/ui/mystery-encounter-ui-handler.ts index ce49a9f280d..862fa155bf4 100644 --- a/src/ui/mystery-encounter-ui-handler.ts +++ b/src/ui/mystery-encounter-ui-handler.ts @@ -1,16 +1,16 @@ import BattleScene from "../battle-scene"; -import { addTextObject, TextStyle } from "./text"; -import { Mode } from "./ui"; +import {addBBCodeTextObject, getBBCodeFrag, TextStyle} from "./text"; +import {Mode} from "./ui"; import UiHandler from "./ui-handler"; -import { Button } from "#enums/buttons"; -import { addWindow, WindowVariant } from "./ui-theme"; -import i18next from "i18next"; -import { MysteryEncounterPhase } from "../phases/mystery-encounter-phase"; -import { PartyUiMode } from "./party-ui-handler"; +import {Button} from "#enums/buttons"; +import {addWindow, WindowVariant} from "./ui-theme"; +import {MysteryEncounterPhase} from "../phases/mystery-encounter-phase"; +import {PartyUiMode} from "./party-ui-handler"; import MysteryEncounterOption from "../data/mystery-encounter-option"; import * as Utils from "../utils"; -import { getPokeballAtlasKey } from "../data/pokeball"; import {isNullOrUndefined} from "../utils"; +import {getPokeballAtlasKey} from "../data/pokeball"; +import {getTextWithEncounterDialogueTokensAndColor} from "#app/data/mystery-encounters/mystery-encounter-utils"; export default class MysteryEncounterUiHandler extends UiHandler { private cursorContainer: Phaser.GameObjects.Container; @@ -298,9 +298,9 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.filteredEncounterOptions = mysteryEncounter.options; this.optionsMeetsReqs = []; - const titleText: string = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.title); - const descriptionText: string = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.description); - const queryText: string = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.query); + const titleText: string = getTextWithEncounterDialogueTokensAndColor(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.title, TextStyle.TOOLTIP_TITLE); + const descriptionText: string = getTextWithEncounterDialogueTokensAndColor(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.description, TextStyle.TOOLTIP_CONTENT); + const queryText: string = getTextWithEncounterDialogueTokensAndColor(this.scene, mysteryEncounter.dialogue.encounterOptionsDialogue.query, TextStyle.TOOLTIP_CONTENT); // Clear options container (except cursor) this.optionsContainer.removeAll(); @@ -310,16 +310,17 @@ export default class MysteryEncounterUiHandler extends UiHandler { let optionText; switch (this.filteredEncounterOptions.length) { case 2: - optionText = addTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, 8, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); break; case 3: - optionText = addTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); break; case 4: - optionText = addTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); + optionText = addBBCodeTextObject(this.scene, i % 2 === 0 ? 0 : 100, i < 2 ? 0 : 16, "-", TextStyle.WINDOW, { wordWrap: { width: 558 }, fontSize: "80px", lineSpacing: -8 }); break; } - const text = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.options[i].buttonLabel); + const option = mysteryEncounter.dialogue.encounterOptionsDialogue.options[i]; + const text = getTextWithEncounterDialogueTokensAndColor(this.scene, option.buttonLabel, option.style ? option.style : TextStyle.WINDOW); if (text) { optionText.setText(text); } @@ -336,11 +337,11 @@ export default class MysteryEncounterUiHandler extends UiHandler { } // View Party Button - const viewPartyText = addTextObject(this.scene, 256, -24, "View Party", TextStyle.PARTY); + const viewPartyText = addBBCodeTextObject(this.scene, 256, -24, getBBCodeFrag("View Party", TextStyle.PARTY), TextStyle.PARTY); this.optionsContainer.add(viewPartyText); // Description Window - const titleTextObject = addTextObject(this.scene, 0, 0, titleText, TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 }); + const titleTextObject = addBBCodeTextObject(this.scene, 0, 0, titleText, TextStyle.TOOLTIP_TITLE, { wordWrap: { width: 750 }, align: "center", lineSpacing: -8 }); this.descriptionContainer.add(titleTextObject); titleTextObject.setPosition(72 - titleTextObject.displayWidth / 2, 5.5); @@ -348,7 +349,7 @@ export default class MysteryEncounterUiHandler extends UiHandler { const ballType = getPokeballAtlasKey(mysteryEncounter.encounterTier as number); this.rarityBall.setTexture("pb", ballType); - const descriptionTextObject = addTextObject(this.scene, 6, 25, descriptionText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } }); + const descriptionTextObject = addBBCodeTextObject(this.scene, 6, 25, descriptionText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } }); // Sets up the mask that hides the description text to give an illusion of scrolling const descriptionTextMaskRect = this.scene.make.graphics({}); @@ -382,8 +383,9 @@ export default class MysteryEncounterUiHandler extends UiHandler { this.descriptionContainer.add(descriptionTextObject); - const queryTextObject = addTextObject(this.scene, 65 - (queryText.length), 90, queryText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } }); + const queryTextObject = addBBCodeTextObject(this.scene, 0, 0, queryText, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 830 } }); this.descriptionContainer.add(queryTextObject); + queryTextObject.setPosition(75 - queryTextObject.displayWidth / 2, 90); // Slide in description container if (slideInDescription) { @@ -412,15 +414,20 @@ export default class MysteryEncounterUiHandler extends UiHandler { const mysteryEncounter = this.scene.currentBattle.mysteryEncounter; let text; - if (!this.optionsMeetsReqs[cursor] && mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor].disabledTooltip) { - text = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor].disabledTooltip); + const option = mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor]; + if (!this.optionsMeetsReqs[cursor] && option.disabledTooltip) { + text = getTextWithEncounterDialogueTokensAndColor(this.scene, option.disabledTooltip, TextStyle.TOOLTIP_CONTENT); } else { - text = i18next.t(mysteryEncounter.dialogue.encounterOptionsDialogue.options[cursor].buttonTooltip); + text = getTextWithEncounterDialogueTokensAndColor(this.scene, option.buttonTooltip, TextStyle.TOOLTIP_CONTENT); } + // Auto-color options green/blue for good/bad by looking for (+)/(-) + const primaryStyleString = [...text.match(new RegExp(/\[color=[^\[]*\]\[shadow=[^\[]*\]/i))][0]; + text = text.replace(/(\([^\(]*\+\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_GREEN) + "[/color][/shadow]" + primaryStyleString); + text = text.replace(/(\([^\(]*\-\)[^\(\[]*)/gi, substring => "[/color][/shadow]" + getBBCodeFrag(substring, TextStyle.SUMMARY_BLUE) + "[/color][/shadow]" + primaryStyleString); if (text) { - const tooltipTextObject = addTextObject(this.scene, 6, 7, text, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 600 }, fontSize: "72px" }); + const tooltipTextObject = addBBCodeTextObject(this.scene, 6, 7, text, TextStyle.TOOLTIP_CONTENT, { wordWrap: { width: 600 }, fontSize: "72px" }); this.tooltipContainer.add(tooltipTextObject); // Sets up the mask that hides the description text to give an illusion of scrolling