diff --git a/changelog.md b/changelog.md index 3d0172cf..da1878d5 100644 --- a/changelog.md +++ b/changelog.md @@ -21,6 +21,7 @@ TODO: - Make party add/remove easier - Add Pokemon Emojis wherever possible - Fix spawn type bug +- Fix search with apostrophe **Stretch** diff --git a/scripts/database/admin/resetUserDaily.js b/scripts/database/admin/resetUserDaily.js new file mode 100644 index 00000000..1e5cdb38 --- /dev/null +++ b/scripts/database/admin/resetUserDaily.js @@ -0,0 +1,32 @@ +/* eslint-disable no-console */ +require("dotenv").config(); +const { updateDocuments } = require("../../../src/database/mongoHandler"); +const { collectionNames } = require("../../../src/config/databaseConfig"); + +const tutorialUserId = "638163104236175427"; +const userId = process.argv[2] || tutorialUserId; + +// reset the specified player's dailys + +const resetPlayerDaily = async () => { + const res = await updateDocuments( + collectionNames.USERS, + { + userId, + }, + { + $set: { + lastCorrected: 0, + }, + } + ); + return res; +}; + +resetPlayerDaily() + .then((res) => { + console.log(res); + }) + .catch((error) => { + console.log(error); + }); diff --git a/src/battle/data/abilities.js b/src/battle/data/abilities.js index b3e86199..d2256fee 100644 --- a/src/battle/data/abilities.js +++ b/src/battle/data/abilities.js @@ -174,6 +174,17 @@ const abilitiesToRegister = Object.freeze({ battle.unregisterListener(properties.listenerId); }, }), + [abilityIdEnum.SERENE_GRACE]: new Ability({ + id: abilityIdEnum.SERENE_GRACE, + name: "Serene Grace", + description: + "Most moves have twice the chance to apply effects and status.", + // effect is hard-coded in moves.js > genericApplySingleStatus and genericApplySingleEffect + abilityAdd() { + return {}; + }, + abilityRemove() {}, + }), [abilityIdEnum.ANGER_POINT]: new Ability({ id: abilityIdEnum.ANGER_POINT, name: "Anger Point", diff --git a/src/battle/data/effects.js b/src/battle/data/effects.js index 7276ab80..73a8b27c 100644 --- a/src/battle/data/effects.js +++ b/src/battle/data/effects.js @@ -1,8 +1,13 @@ /* eslint-disable no-param-reassign */ const { effectTypes, statToBattleStat } = require("../../config/battleConfig"); -const { effectIdEnum, battleEventEnum } = require("../../enums/battleEnums"); +const { + effectIdEnum, + battleEventEnum, + moveIdEnum, +} = require("../../enums/battleEnums"); const { getIsTargetPokemonCallback } = require("../engine/eventConditions"); const { getEffect } = require("./effectRegistry"); +const { getMove } = require("./moveRegistry"); /** * @template T @@ -157,6 +162,39 @@ const effectsToRegister = Object.freeze({ target[statToIncrease] -= baseStatValue; }, }), + [effectIdEnum.DOOM_DESIRE]: new Effect({ + id: effectIdEnum.DOOM_DESIRE, + name: "Doom Desire", + description: "The target will take damage when the effect is removed.", + type: effectTypes.DEBUFF, + dispellable: false, + /** + * @param {EffectAddBasicArgs & {initialArgs: any}} args + */ + effectAdd({ battle, target, source }) { + battle.addToLog( + `${source.name} is foreseeing an attack against ${target.name}!` + ); + return {}; + }, + effectRemove({ battle, target, source }) { + battle.addToLog( + `${target.name} was hit by ${source.name}'s Doom Desire!` + ); + const damageToDeal = source.calculateMoveDamage({ + move: getMove(moveIdEnum.DOOM_DESIRE), + target, + primaryTarget: target, + allTargets: [target], + offTargetDamageMultiplier: 1, + backTargetDamageMultiplier: 1, + }); + source.dealDamage(damageToDeal, target, { + type: "move", + moveId: moveIdEnum.DOOM_DESIRE, + }); + }, + }), }); module.exports = { diff --git a/src/battle/data/moves.js b/src/battle/data/moves.js index 26bef313..48836b9f 100644 --- a/src/battle/data/moves.js +++ b/src/battle/data/moves.js @@ -8,7 +8,11 @@ const { statusConditions, } = require("../../config/battleConfig"); const { getMove } = require("./moveRegistry"); -const { moveIdEnum } = require("../../enums/battleEnums"); +const { + moveIdEnum, + abilityIdEnum, + effectIdEnum, +} = require("../../enums/battleEnums"); const { drawIterable } = require("../../utils/gachaUtils"); class Move { @@ -128,7 +132,21 @@ class Move { options, probablity = 1, }) { - if (!missedTargets.includes(target) && Math.random() < probablity) { + let shouldApplyStatus = false; + if (!missedTargets.includes(target)) { + const roll = Math.random(); + if (roll < probablity) { + shouldApplyStatus = true; + } else if ( + source.hasAbility(abilityIdEnum.SERENE_GRACE) && + roll < 2 * probablity + ) { + source.battle.addToLog(`${source.name}'s Serene Grace activates!`); + shouldApplyStatus = true; + } + } + + if (shouldApplyStatus) { return target.applyStatus(statusId, source, options); } return false; @@ -166,6 +184,78 @@ class Move { }); } } + + // eslint-disable-next-line class-methods-use-this + genericApplySingleEffect({ + source, + target, + // eslint-disable-next-line no-unused-vars + primaryTarget, + // eslint-disable-next-line no-unused-vars + allTargets, + missedTargets = [], + effectId, + duration, + initialArgs = {}, + probablity = 1, + }) { + let shouldApplyEffect = false; + if (!missedTargets.includes(target)) { + const roll = Math.random(); + if (roll < probablity) { + shouldApplyEffect = true; + } else if ( + source.hasAbility(abilityIdEnum.SERENE_GRACE) && + roll < 2 * probablity + ) { + source.battle.addToLog(`${source.name}'s Serene Grace activates!`); + shouldApplyEffect = true; + } + } + + if (shouldApplyEffect) { + return target.applyEffect(effectId, duration, source, initialArgs); + } + return false; + } + + /** + * @template {EffectIdEnum} K + * @param {object} param0 + * @param {BattlePokemon} param0.source + * @param {BattlePokemon} param0.primaryTarget + * @param {Array} param0.allTargets + * @param {Array=} param0.missedTargets + * @param {K} param0.effectId + * @param {number} param0.duration + * @param {EffectInitialArgsTypeFromId=} param0.initialArgs + * @param {number=} param0.probablity + */ + genericApplyAllEffects({ + source, + primaryTarget, + allTargets, + missedTargets = [], + effectId, + duration, + // @ts-ignore + initialArgs = {}, + probablity = 1, + }) { + for (const target of allTargets) { + this.genericApplySingleEffect({ + source, + target, + primaryTarget, + allTargets, + missedTargets, + effectId, + duration, + initialArgs, + probablity, + }); + } + } } const movesToRegister = Object.freeze({ @@ -222,6 +312,106 @@ const movesToRegister = Object.freeze({ }); }, }), + [moveIdEnum.CONFUSION]: new Move({ + id: moveIdEnum.CONFUSION, + name: "Confusion", + type: pokemonTypes.PSYCHIC, + power: 50, + accuracy: 100, + cooldown: 0, + targetType: targetTypes.ENEMY, + targetPosition: targetPositions.FRONT, + targetPattern: targetPatterns.SINGLE, + tier: moveTiers.BASIC, + damageType: damageTypes.SPECIAL, + description: + "The target is hit by a weak telekinetic force. This has a 25% chance to confuse the target for 1 turn.", + execute(args) { + this.genericDealAllDamage(args); + this.genericApplyAllEffects({ + ...args, + effectId: "confused", + duration: 1, + probablity: 0.25, + }); + }, + }), + [moveIdEnum.PSYCHIC]: new Move({ + id: moveIdEnum.PSYCHIC, + name: "Psychic", + type: pokemonTypes.PSYCHIC, + power: 65, + accuracy: 90, + cooldown: 3, + targetType: targetTypes.ENEMY, + targetPosition: targetPositions.FRONT, + targetPattern: targetPatterns.ROW, + tier: moveTiers.POWER, + damageType: damageTypes.SPECIAL, + description: + "The target is hit by a strong telekinetic force. This has a 60% chance to lower the targets' Special Defense for 2 turns.", + execute(args) { + this.genericDealAllDamage(args); + this.genericApplyAllEffects({ + ...args, + effectId: "spdDown", + duration: 2, + probablity: 0.6, + }); + }, + }), + [moveIdEnum.DOOM_DESIRE]: new Move({ + id: moveIdEnum.DOOM_DESIRE, + name: "Doom Desire", + type: pokemonTypes.STEEL, + power: 120, + accuracy: 100, + cooldown: 5, + targetType: targetTypes.ENEMY, + targetPosition: targetPositions.ANY, + targetPattern: targetPatterns.SQUARE, + tier: moveTiers.ULTIMATE, + damageType: damageTypes.SPECIAL, + description: + "Two turns after this move is used, the user's strikes the target with a concentrated bundle of light (undispellable). This move also has a 10% chance to apply Perish Song.", + execute(args) { + this.genericApplyAllEffects({ + ...args, + effectId: effectIdEnum.DOOM_DESIRE, + duration: 2, + }); + this.genericApplyAllEffects({ + ...args, + effectId: "perishSong", + duration: 3, + probablity: 0.1, + }); + }, + }), + [moveIdEnum.IRON_HEAD]: new Move({ + id: moveIdEnum.IRON_HEAD, + name: "Iron Head", + type: pokemonTypes.STEEL, + power: 90, + accuracy: 100, + cooldown: 3, + targetType: targetTypes.ENEMY, + targetPosition: targetPositions.FRONT, + targetPattern: targetPatterns.SINGLE, + tier: moveTiers.POWER, + damageType: damageTypes.PHYSICAL, + description: + "The target is struck with a hard head made of iron. This has a 50% chance to flinch.", + execute(args) { + this.genericDealAllDamage(args); + this.genericApplyAllEffects({ + ...args, + effectId: "flinched", + duration: 1, + probablity: 0.5, + }); + }, + }), [moveIdEnum.AQUA_IMPACT]: new Move({ id: moveIdEnum.AQUA_IMPACT, name: "Aqua Impact", diff --git a/src/battle/engine/BattlePokemon.js b/src/battle/engine/BattlePokemon.js index 6dd44896..faabf485 100644 --- a/src/battle/engine/BattlePokemon.js +++ b/src/battle/engine/BattlePokemon.js @@ -1245,6 +1245,10 @@ class BattlePokemon { } } + hasAbility(abilityId) { + return this.ability?.abilityId === abilityId; + } + /** * @param {number} heal * @param {BattlePokemon} target @@ -1506,7 +1510,8 @@ class BattlePokemon { */ removeEffect(effectId) { // if effect doesn't exist, do nothing - if (!this.effectIds[effectId]) { + const effectInstance = this.getEffectInstance(effectId); + if (!effectInstance) { return false; } const effect = getEffect(effectId); @@ -1519,28 +1524,30 @@ class BattlePokemon { // @ts-ignore effect.effectRemove({ battle: this.battle, + source: effectInstance.source, + duration: effectInstance.duration, target: this, - properties: this.effectIds[effectId].args, - initialArgs: this.effectIds[effectId].initialArgs, + properties: effectInstance.args, + initialArgs: effectInstance.initialArgs, }); } else { const legacyEffect = /** @type {any} */ (effect); legacyEffect.effectRemove( this.battle, this, - this.effectIds[effectId].args, - this.effectIds[effectId].initialArgs + effectInstance.args, + effectInstance.initialArgs ); } if (this.effectIds[effectId] !== undefined) { const afterRemoveArgs = { target: this, - source: this.effectIds[effectId].source, + source: effectInstance.source, effectId, - duration: this.effectIds[effectId].duration, - initialArgs: this.effectIds[effectId].initialArgs, - args: this.effectIds[effectId].args, + duration: effectInstance.duration, + initialArgs: effectInstance.initialArgs, + args: effectInstance.args, }; this.battle.eventHandler.emit( battleEventEnum.AFTER_EFFECT_REMOVE, diff --git a/src/battle/types.js b/src/battle/types.js index 6f3c96e7..a91e959c 100644 --- a/src/battle/types.js +++ b/src/battle/types.js @@ -116,6 +116,8 @@ * @param {object} param0 * @param {Battle} param0.battle * @param {BattlePokemon} param0.target + * @param {BattlePokemon} param0.source + * @param {number} param0.duration * @param {T} param0.initialArgs * @param {U} param0.properties */ diff --git a/src/commands/heartbeat/give.js b/src/commands/heartbeat/give.js index 46177cbd..30f34308 100644 --- a/src/commands/heartbeat/give.js +++ b/src/commands/heartbeat/give.js @@ -1,10 +1,14 @@ const { getTrainer } = require("../../services/trainer"); const { giveNewPokemons } = require("../../services/gacha"); const { buildNewPokemonEmbed } = require("../../embeds/pokemonEmbeds"); +const { stageNames } = require("../../config/stageConfig"); // give.js is used to give a user a pokemon of any level within the limits and any equipment. Anyone can use in Alpha, no one can use in beta onwards. const give = async (user, pokemonId, level, equipmentLevel) => { + if (process.env.STAGE !== stageNames.ALPHA) { + return { send: null, err: "This command is not available yet." }; + } // TODO: restrict users who can use? // restrict level diff --git a/src/commands/heartbeat/giveItem.js b/src/commands/heartbeat/giveItem.js new file mode 100644 index 00000000..d35ae4cd --- /dev/null +++ b/src/commands/heartbeat/giveItem.js @@ -0,0 +1,53 @@ +const { getTrainer, updateTrainer } = require("../../services/trainer"); +const { stageNames } = require("../../config/stageConfig"); +const { backpackItems } = require("../../config/backpackConfig"); +const { addItems } = require("../../utils/trainerUtils"); + +// giveItem.js is used to give items to the current user. Only useable in Alpha + +const giveItem = async (user, itemId, quantity) => { + if (process.env.STAGE !== stageNames.ALPHA) { + return { send: null, err: "This command is not available yet." }; + } + // TODO: restrict users who can use? + if (Object.values(backpackItems).indexOf(itemId) === -1) { + return { send: null, err: "Invalid item" }; + } + if (!quantity || quantity < 1) { + return { send: null, err: "Invalid quantity" }; + } + + const trainer = await getTrainer(user); + if (trainer.err) { + return { send: null, err: trainer.err }; + } + + addItems(trainer.data, itemId, quantity); + + const updateRes = await updateTrainer(trainer.data); + if (updateRes.err) { + return { send: null, err: updateRes.err }; + } + + return { send: "Added items successfully", err: null }; +}; + +const giveItemMessageCommand = async () => ({ + err: "Use the slash command srr", +}); + +const giveItemSlashCommand = async (interaction) => { + const itemId = interaction.options.getString("itemid"); + const quantity = interaction.options.getInteger("quantity") || 1; + const { send, err } = await giveItem(interaction.user, itemId, quantity); + if (err) { + await interaction.reply(`${err}`); + return { err }; + } + await interaction.reply(send); +}; + +module.exports = { + message: giveItemMessageCommand, + slash: giveItemSlashCommand, +}; diff --git a/src/commands/pokemon/evolve.js b/src/commands/pokemon/evolve.js index 96ad408a..c58e9173 100644 --- a/src/commands/pokemon/evolve.js +++ b/src/commands/pokemon/evolve.js @@ -11,7 +11,7 @@ const { setState } = require("../../services/state"); const { buildIdConfigSelectRow: buildSpeciesSelectRow, } = require("../../components/idConfigSelectRow"); -const { buildPokemonEmbed } = require("../../embeds/pokemonEmbeds"); +const { DEPRECATEDbuildPokemonEmbed } = require("../../embeds/pokemonEmbeds"); const { eventNames } = require("../../config/eventConfig"); const { buildSpeciesEvolutionString } = require("../../utils/pokemonUtils"); @@ -64,7 +64,7 @@ const evolve = async (user, pokemonId) => { } // build pokemon embed - const embed = buildPokemonEmbed(trainer.data, pokemon.data); + const embed = DEPRECATEDbuildPokemonEmbed(trainer.data, pokemon.data); // build selection list of pokemon to evolve to const stateId = setState( diff --git a/src/commands/pokemon/jirachi.js b/src/commands/pokemon/jirachi.js new file mode 100644 index 00000000..f8f6df28 --- /dev/null +++ b/src/commands/pokemon/jirachi.js @@ -0,0 +1,28 @@ +/** + * @file + * @author Elvis Wei + * + * jirachi.js jirachi. + */ +const { createRoot } = require("../../deact/deact"); +const Jirachi = require("../../elements/pokemon/Jirachi"); +const { getUserFromInteraction } = require("../../utils/utils"); + +const jirachi = async (interaction) => + await createRoot( + Jirachi, + { + user: getUserFromInteraction(interaction), + }, + interaction, + { ttl: 180 } + ); + +const jirachiMessageCommand = async (message) => await jirachi(message); + +const jirachiSlashCommand = async (interaction) => await jirachi(interaction); + +module.exports = { + message: jirachiMessageCommand, + slash: jirachiSlashCommand, +}; diff --git a/src/commands/trainer/daily.js b/src/commands/trainer/daily.js index 2262834f..03912b46 100644 --- a/src/commands/trainer/daily.js +++ b/src/commands/trainer/daily.js @@ -10,7 +10,7 @@ const { User } = require("discord.js"); const { drawDaily } = require("../../services/gacha"); const { getTrainer } = require("../../services/trainer"); const { - getRewardsString, + getFlattenedRewardsString, getBackpackItemsString, } = require("../../utils/trainerUtils"); const { formatMoney } = require("../../utils/utils"); @@ -36,7 +36,7 @@ const daily = async (user) => { const { money } = rewards.data; // build itemized rewards string - let rewardsString = getRewardsString(rewards.data); + let rewardsString = getFlattenedRewardsString(rewards.data); rewardsString += "\n\n**You now own:**"; if (money) { rewardsString += `\n${formatMoney(trainer.data.money)}`; diff --git a/src/config/backpackConfig.js b/src/config/backpackConfig.js index 50a6adbb..e6c0962c 100644 --- a/src/config/backpackConfig.js +++ b/src/config/backpackConfig.js @@ -16,6 +16,7 @@ const backpackItems = Object.freeze({ WILLPOWER_SHARD: "6", MINT: "7", RAID_PASS: "8", + STAR_PIECE: "9", }); const backpackCategoryConfig = Object.freeze({ @@ -28,7 +29,7 @@ const backpackCategoryConfig = Object.freeze({ [backpackCategories.MATERIALS]: { name: "Materials", emoji: "<:materials:1112557472759160852>", - description: "Used to upgrade Pokemon and equipment!", + description: "Used to upgrade Pokemon, craft items, and as currency!", }, [backpackCategories.CONSUMABLES]: { name: "Consumables", @@ -90,6 +91,12 @@ const backpackItemConfig = Object.freeze({ description: "Used to change a Pokemon's nature!", category: backpackCategories.MATERIALS, }, + [backpackItems.STAR_PIECE]: { + name: "Star Piece", + emoji: "<:starpiece:1329630164526829709>", + description: "Obtained from raids; used to make wishes!", + category: backpackCategories.MATERIALS, + }, [backpackItems.RAID_PASS]: { name: "Raid Pass", emoji: "<:raidpass:1150161526297206824>", diff --git a/src/config/battleConfig.js b/src/config/battleConfig.js index fcb5dd83..4593d1e6 100644 --- a/src/config/battleConfig.js +++ b/src/config/battleConfig.js @@ -3150,34 +3150,7 @@ const moveConfig = Object.freeze({ description: "A move that leaves the targets badly poisoned. Its poison damage worsens every turn. If the user is Poison type and the target isn't Steel type, ignore miss on the primary target.", }, - m93: { - name: "Confusion", - type: pokemonTypes.PSYCHIC, - power: 50, - accuracy: 100, - cooldown: 0, - targetType: targetTypes.ENEMY, - targetPosition: targetPositions.FRONT, - targetPattern: targetPatterns.SINGLE, - tier: moveTiers.BASIC, - damageType: damageTypes.SPECIAL, - description: - "The target is hit by a weak telekinetic force. This has a 25% chance to confuse the target for 1 turn.", - }, - m94: { - name: "Psychic", - type: pokemonTypes.PSYCHIC, - power: 65, - accuracy: 90, - cooldown: 3, - targetType: targetTypes.ENEMY, - targetPosition: targetPositions.FRONT, - targetPattern: targetPatterns.ROW, - tier: moveTiers.POWER, - damageType: damageTypes.SPECIAL, - description: - "The target is hit by a strong telekinetic force. This has a 60% chance to lower the targets' Special Defense for 2 turns.", - }, + m97: { name: "Agility", type: pokemonTypes.PSYCHIC, @@ -7399,40 +7372,6 @@ const moveExecutes = { } } }, - m93(_battle, source, _primaryTarget, allTargets, missedTargets) { - const moveId = "m93"; - const moveData = getMove(moveId); - for (const target of allTargets) { - const miss = missedTargets.includes(target); - const damageToDeal = calculateDamage(moveData, source, target, miss); - source.dealDamage(damageToDeal, target, { - type: "move", - moveId, - }); - - // if not miss, 25% to confuse - if (!miss && Math.random() < 0.25) { - target.applyEffect("confused", 1, source); - } - } - }, - m94(_battle, source, _primaryTarget, allTargets, missedTargets) { - const moveId = "m94"; - const moveData = getMove(moveId); - for (const target of allTargets) { - const miss = missedTargets.includes(target); - const damageToDeal = calculateDamage(moveData, source, target, miss); - source.dealDamage(damageToDeal, target, { - type: "move", - moveId, - }); - - // if not miss, 60% chance to spd down 2 turn - if (!miss && Math.random() < 0.6) { - target.applyEffect("spdDown", 2, source); - } - } - }, m97(_battle, source, _primaryTarget, allTargets) { for (const target of allTargets) { // apply greaterSpeUp buff diff --git a/src/config/commandConfig.js b/src/config/commandConfig.js index a0c02af2..e3f4802d 100644 --- a/src/config/commandConfig.js +++ b/src/config/commandConfig.js @@ -1,3 +1,4 @@ +const { logger } = require("../log"); const { stageNames, stageConfig } = require("./stageConfig"); const { prefix } = stageConfig[process.env.STAGE]; @@ -52,6 +53,7 @@ const commandCategoryConfigRaw = { "mew", "celebi", "deoxys", + "jirachi", // "togglespawn", ], }, @@ -111,7 +113,7 @@ const commandCategoryConfigRaw = { name: "Heartbeat", description: "Basic heartbeat commands; intended for testing", folder: "heartbeat", - commands: ["ping", "echo", "give", "test"], + commands: ["ping", "echo", "give", "giveitem", "test"], }, }; /** @type {Record} */ @@ -957,7 +959,7 @@ const commandConfigRaw = { name: "Mythic", aliases: ["mythic"], description: "Entry point for Mythical Pokemon", - subcommands: ["mew", "celebi", "deoxys"], + subcommands: ["mew", "celebi", "deoxys", "jirachi"], args: {}, stages: [stageNames.ALPHA, stageNames.BETA, stageNames.PROD], }, @@ -999,6 +1001,19 @@ const commandConfigRaw = { money: 10, parent: "mythic", }, + jirachi: { + name: "Jirachi", + aliases: ["jirachi"], + description: "View your Jirachi and make a wish!", + longDescription: + "View your Jirachi and make a wish! Jirachi is a special Pokemon that can grant wishes and increases your shiny Pokemon odds!", + execute: "jirachi.js", + args: {}, + stages: [stageNames.ALPHA, stageNames.BETA, stageNames.PROD], + exp: 5, + money: 10, + parent: "mythic", + }, tutorial: { name: "Tutorial", aliases: ["tutorial"], @@ -1122,6 +1137,30 @@ const commandConfigRaw = { exp: 0, money: 0, }, + giveitem: { + name: "Give Item", + aliases: ["giveitem"], + description: "Give a Item to self", + longDescription: "Give a Item to self.", + execute: "giveItem.js", + args: { + itemid: { + type: "string", + description: "ID for Item to give to self", + optional: false, + variable: false, + }, + quantity: { + type: "int", + description: "quantity to give Item", + optional: true, + variable: false, + }, + }, + stages: [stageNames.ALPHA], + exp: 0, + money: 0, + }, test: { name: "Test", aliases: ["test"], @@ -1139,6 +1178,13 @@ const commandConfigRaw = { }, }; +// if detect a capital letter in any command ID, warn +for (const commandId of Object.keys(commandConfigRaw)) { + if (commandId !== commandId.toLowerCase()) { + logger.warn(`Command ID ${commandId} contains a capital letter!`); + } +} + /** @type {Record} */ const commandConfig = Object.freeze(commandConfigRaw); diff --git a/src/config/npcConfig.js b/src/config/npcConfig.js index c02c596e..4d259455 100644 --- a/src/config/npcConfig.js +++ b/src/config/npcConfig.js @@ -2649,8 +2649,35 @@ const raids = Object.freeze({ const RAID_SHINY_CHANCE = 0.0033; // process.env.STAGE === stageNames.ALPHA ? 0.8 : 0.0033; const BASE_RAID_MONEY = 500; +const BASE_STAR_PIECE = 1; -const raidConfig = Object.freeze({ +/** + * @typedef {object} RaidConfigData + * @property {string} name + * @property {string} sprite + * @property {string} emoji + * @property {string} description + * @property {string} boss + * @property {string[]} shinyRewards + * @property {PartialRecord>; + * ttl: number; + * }>} difficulties + */ + +/** + * @type {Record} + */ +const raidConfigRaw = { [raids.ARMORED_MEWTWO]: { name: "Armored Mewtwo", sprite: @@ -2698,6 +2725,11 @@ const raidConfig = Object.freeze({ ], shinyChance: RAID_SHINY_CHANCE, moneyPerPercent: BASE_RAID_MONEY, + backpackPerPercent: { + [backpackCategories.MATERIALS]: { + [backpackItems.STAR_PIECE]: BASE_STAR_PIECE, + }, + }, ttl: 1000 * 60 * 60 * 2, }, }, @@ -2749,6 +2781,11 @@ const raidConfig = Object.freeze({ ], shinyChance: RAID_SHINY_CHANCE, moneyPerPercent: BASE_RAID_MONEY, + backpackPerPercent: { + [backpackCategories.MATERIALS]: { + [backpackItems.STAR_PIECE]: BASE_STAR_PIECE, + }, + }, ttl: 1000 * 60 * 60 * 2, }, }, @@ -2805,6 +2842,11 @@ const raidConfig = Object.freeze({ ], shinyChance: RAID_SHINY_CHANCE * 2, moneyPerPercent: BASE_RAID_MONEY * 1.25 * 2, + backpackPerPercent: { + [backpackCategories.MATERIALS]: { + [backpackItems.STAR_PIECE]: BASE_STAR_PIECE, + }, + }, ttl: 1000 * 60 * 60 * 2, }, }, @@ -2862,11 +2904,17 @@ const raidConfig = Object.freeze({ ], shinyChance: RAID_SHINY_CHANCE * 2, moneyPerPercent: BASE_RAID_MONEY * 1.25 * 2, + backpackPerPercent: { + [backpackCategories.MATERIALS]: { + [backpackItems.STAR_PIECE]: BASE_STAR_PIECE * 1.25 * 2, + }, + }, ttl: 1000 * 60 * 60 * 2, }, }, }, -}); +}; +const raidConfig = Object.freeze(raidConfigRaw); const difficultyConfig = Object.freeze({ [difficulties.VERY_EASY]: { diff --git a/src/config/pokemonConfig.js b/src/config/pokemonConfig.js index 2b466fe5..4f52bd87 100644 --- a/src/config/pokemonConfig.js +++ b/src/config/pokemonConfig.js @@ -1,5 +1,6 @@ const { moveIdEnum, abilityIdEnum } = require("../enums/battleEnums"); const { pokemonIdEnum } = require("../enums/pokemonEnums"); +const { formatMoney } = require("../utils/utils"); /** @typedef {Enum} PokemonTypeEnum */ const types = Object.freeze({ @@ -7321,6 +7322,61 @@ const pokemonConfigRaw = { growthRate: growthRates.SLOW, noGacha: true, }, + [pokemonIdEnum.JIRACHI]: { + name: "Jirachi", + emoji: "<:385:1132497393431105588>", + description: + "Jirachi will awaken from its sleep of a thousand years if you sing to it in a voice of purity. It is said to make true any wish that people desire.", + type: [types.STEEL, types.PSYCHIC], + baseStats: [100, 100, 100, 100, 100, 100], + sprite: + "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/385.png", + shinySprite: + "https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/shiny/385.png", + abilities: { + [abilityIdEnum.SERENE_GRACE]: 1, + }, + moveIds: [ + moveIdEnum.CONFUSION, + moveIdEnum.PSYCHIC, + moveIdEnum.IRON_HEAD, + moveIdEnum.DOOM_DESIRE, + ], + battleEligible: true, + rarity: rarities.MYTHICAL, + growthRate: growthRates.SLOW, + noGacha: true, + mythicConfig: { + wishes: { + power: { + name: "Power", + description: + "Sets a random IV stat of a selected Pokemon to 31. Will select a stat that does not already have 31 IVs.", + starPieceCost: 100, + }, + rebirth: { + name: "Rebirth", + description: + "Rerolls a selected Pokemon's ability, guaranteeing a new ability. Will not work if the Pokemon has only one ability.", + starPieceCost: 150, + }, + allies: { + name: "Allies", + description: + "Grants 50 random Pokeballs, with odds equal to `/daily` and the Pokemart.", + starPieceCost: 200, + }, + wealth: { + name: "Wealth", + description: `Grants ${formatMoney( + 100000 + )}, 400 of each Equipment Shard, and 5 mints.`, + starPieceCost: 200, + }, + }, + shinyChanceMultiplier: 2, + }, + }, 386: { name: "Deoxys", emoji: "<:386:1132497394739712010>", diff --git a/src/config/trainerConfig.js b/src/config/trainerConfig.js index db63e90c..8d9c0f55 100644 --- a/src/config/trainerConfig.js +++ b/src/config/trainerConfig.js @@ -407,15 +407,25 @@ const trainerFields = { pokemonIds: [], }, }, + // TODO: move mythics to its own nested object? hasCelebi: { type: "boolean", default: false, }, + hasJirachi: { + type: "boolean", + default: false, + }, usedTimeTravel: { type: "boolean", default: false, refreshInterval: timeEnum.DAY, }, + usedWish: { + type: "boolean", + default: false, + refreshInterval: timeEnum.WEEK, + }, lastTowerStage: { type: "number", default: 0, diff --git a/src/config/types.js b/src/config/types.js index 2ccd3750..6acc843e 100644 --- a/src/config/types.js +++ b/src/config/types.js @@ -11,6 +11,7 @@ /** @typedef {import("./npcConfig").NpcEnum} NpcEnum */ /** @typedef {import("./npcConfig").DungeonEnum} DungeonEnum */ /** @typedef {import("./npcConfig").RaidEnum} RaidEnum */ +/** @typedef {import("./npcConfig").RaidConfigData} RaidConfigData */ /** @typedef {import("./npcConfig").NpcDifficultyEnum} NpcDifficultyEnum */ /** @typedef {import("./backpackConfig").BackpackItemEnum} BackpackItemEnum */ /** @typedef {import("./backpackConfig").BackpackCategoryEnum} BackpackCategoryEnum */ diff --git a/src/deact/DeactElement.js b/src/deact/DeactElement.js index e6939f69..a146d922 100644 --- a/src/deact/DeactElement.js +++ b/src/deact/DeactElement.js @@ -129,6 +129,7 @@ class DeactElement { // compose embeds if (this.res.embeds !== undefined) { + rv.embeds = rv.embeds ?? []; for (const embed of this.res.embeds) { const elementRes = await this.getElementResWithArrayKey( embed, diff --git a/src/deact/utils.js b/src/deact/utils.js index 98cf84e4..a2337aab 100644 --- a/src/deact/utils.js +++ b/src/deact/utils.js @@ -4,7 +4,7 @@ const { StringSelectMenuBuilder, } = require("discord.js"); -const isDeactCreateElement = (element) => element.isDeactCreateElement; +const isDeactCreateElement = (element) => element?.isDeactCreateElement; const isActionRowBuilder = (element) => element instanceof ActionRowBuilder; const isButtonBuilder = (element) => element instanceof ButtonBuilder; diff --git a/src/elements/foundation/YesNoButtons.js b/src/elements/foundation/YesNoButtons.js new file mode 100644 index 00000000..ef8350c2 --- /dev/null +++ b/src/elements/foundation/YesNoButtons.js @@ -0,0 +1,51 @@ +const { ButtonStyle } = require("discord.js"); +const { createElement } = require("../../deact/deact"); +const Button = require("../../deact/elements/Button"); + +/** + * + * @param {DeactElement} ref + * @param {object} param1 + * @param {string=} param1.onYesPressedKey + * @param {string=} param1.onNoPressedKey + * @param {string=} param1.onPresssedKey + * @param {boolean=} param1.isYesDisabled + * @param {boolean=} param1.isNoDisabled + * @param {string=} param1.yesLabel + * @param {string=} param1.noLabel + */ +const YesNoButtons = async ( + ref, + { + onYesPressedKey = undefined, + onNoPressedKey = undefined, + onPresssedKey = undefined, + isYesDisabled = false, + isNoDisabled = false, + yesLabel = "Yes", + noLabel = "No", + } +) => ({ + components: [ + [ + createElement(Button, { + emoji: "✅", + label: yesLabel, + style: ButtonStyle.Success, + callbackBindingKey: onYesPressedKey || onPresssedKey, + disabled: isYesDisabled, + data: { action: "yes" }, + }), + createElement(Button, { + emoji: "✖️", + label: noLabel, + style: ButtonStyle.Danger, + callbackBindingKey: onNoPressedKey || onPresssedKey, + disabled: isNoDisabled, + data: { action: "no" }, + }), + ], + ], +}); + +module.exports = YesNoButtons; diff --git a/src/elements/pokemon/Jirachi.js b/src/elements/pokemon/Jirachi.js new file mode 100644 index 00000000..35223f1c --- /dev/null +++ b/src/elements/pokemon/Jirachi.js @@ -0,0 +1,214 @@ +const { + buildJirachiAbilityEmbed, + buildPokemonEmbed, +} = require("../../embeds/pokemonEmbeds"); +const { + useAwaitedMemo, + useCallbackBinding, + createElement, + useState, + useCallback, + createModal, + useModalSubmitCallbackBinding, +} = require("../../deact/deact"); +const useTrainer = require("../../hooks/useTrainer"); +const { + getJirachi, + canTrainerUseWish, + useWish, +} = require("../../services/mythic"); +const { pokemonConfig } = require("../../config/pokemonConfig"); +const { pokemonIdEnum } = require("../../enums/pokemonEnums"); +const Button = require("../../deact/elements/Button"); +const { + backpackItemConfig, + backpackItems, +} = require("../../config/backpackConfig"); +const YesNoButtons = require("../foundation/YesNoButtons"); +const { buildPokemonIdSearchModal } = require("../../modals/pokemonModals"); +const { getPokemon } = require("../../services/pokemon"); +const { formatItemQuantity } = require("../../utils/itemUtils"); + +const starPieceEmoji = backpackItemConfig[backpackItems.STAR_PIECE].emoji; +const { mythicConfig } = pokemonConfig[pokemonIdEnum.JIRACHI]; + +/** + * @param {DeactElement} ref + * @param {object} param1 + * @param {DiscordUser} param1.user + * @param {() => Promise} param1.refreshTrainer + * @param {string} param1.wishId + * @param {Function} param1.goBack + * @param {WithId?=} param1.selectedPokemon + * @returns {Promise} + */ +const ConfirmWish = async ( + ref, + { user, refreshTrainer, wishId, goBack, selectedPokemon } +) => { + const [content, setContent] = useState("", ref); + const [shouldShowResults, setShouldShowResults] = useState(false, ref); + + // confirm buttons + const currentWishData = mythicConfig.wishes[wishId]; + const selectedPokemonString = selectedPokemon + ? `on **${selectedPokemon.name}** ` + : ""; + const confirmContent = `Are you sure you want to make **Wish for ${ + currentWishData.name + }** ${selectedPokemonString}for ${formatItemQuantity( + backpackItems.STAR_PIECE, + currentWishData.starPieceCost + )}?\n\n${currentWishData.description}`; + const confirmButtonPressedKey = useCallbackBinding(async () => { + const { result: useWishResult, err } = await useWish(user, { + wishId, + pokemon: selectedPokemon, + }); + if (err) { + await refreshTrainer(); + goBack(err); + return; + } + + setShouldShowResults(true); + setContent(useWishResult); + }, ref); + const denyButtonPressedKey = useCallbackBinding(() => { + goBack(); + }, ref); + + return { + contents: [content || confirmContent], + embeds: + selectedPokemon && !shouldShowResults + ? [buildPokemonEmbed(user, selectedPokemon, "info")] + : [], + components: shouldShowResults + ? [] + : [ + [ + createElement(YesNoButtons, { + onYesPressedKey: confirmButtonPressedKey, + onNoPressedKey: denyButtonPressedKey, + }), + ], + ], + }; +}; + +/** + * @param {DeactElement} ref + * @param {object} param1 + * @param {DiscordUser} param1.user + * @returns {Promise} + */ +const Jirachi = async (ref, { user }) => { + const [content, setContent] = useState("", ref); + const [shouldShowConfirm, setShouldShowConfirm] = useState(false, ref); + const [selectedWishId, setSelectedWishId] = useState("", ref); + const [selectedPokemon, setSelectedPokemon] = useState(null, ref); + const { + trainer, + err: trainerErr, + refreshTrainer, + } = await useTrainer(user, ref); + if (trainerErr) { + return { err: trainerErr }; + } + + const { data: jirachi, err: jirachiErr } = await useAwaitedMemo( + async () => getJirachi(trainer), + [], + ref + ); + if (jirachiErr) { + return { err: jirachiErr }; + } + + // pokemon ID input + const idSubmittedActionBinding = useModalSubmitCallbackBinding( + async (interaction) => { + const pokemonIdInput = + interaction.fields.getTextInputValue("pokemonIdInput"); + const { data: pokemon, err } = await getPokemon(trainer, pokemonIdInput); + if (err) { + setContent( + "**ERROR:** Pokemon not found. Make sure you enter its full exact ID!" + ); + setSelectedWishId(""); + return; + } + + setSelectedPokemon(pokemon); + setShouldShowConfirm(true); + }, + ref + ); + + // wish buttons + const wishButtonPressedKey = useCallbackBinding( + async (interaction, data) => { + const { wishId } = data; + if (wishId === "power" || wishId === "rebirth") { + await createModal( + buildPokemonIdSearchModal, + {}, + idSubmittedActionBinding, + interaction, + ref + ); + } else { + setShouldShowConfirm(true); + } + setSelectedWishId(wishId); + }, + ref, + { defer: false } + ); + const wishButtons = Object.entries(mythicConfig.wishes).map( + ([wishId, wish]) => { + const canUseWish = canTrainerUseWish(trainer, { wishId }); + return createElement(Button, { + label: `x${wish.starPieceCost} [${wish.name}]`, + emoji: starPieceEmoji, + disabled: !!canUseWish.err, + callbackBindingKey: wishButtonPressedKey, + data: { wishId }, + }); + } + ); + const goBack = useCallback( + (err = "") => { + setSelectedPokemon(null); + setShouldShowConfirm(false); + setSelectedWishId(""); + setContent(`**ERROR:** ${err}`); + }, + [setShouldShowConfirm, setSelectedWishId, setContent], + ref + ); + + if (shouldShowConfirm) { + return { + elements: [ + createElement(ConfirmWish, { + user, + refreshTrainer, + wishId: selectedWishId, + goBack, + selectedPokemon, + }), + ], + }; + } + return { + contents: [content || jirachi._id.toString()], + embeds: (content ? [] : [buildPokemonEmbed(user, jirachi, "info")]).concat([ + buildJirachiAbilityEmbed(trainer), + ]), + components: [wishButtons], + }; +}; + +module.exports = Jirachi; diff --git a/src/elements/pokemon/PokemonList.js b/src/elements/pokemon/PokemonList.js index 28c3a099..cd8c73ca 100644 --- a/src/elements/pokemon/PokemonList.js +++ b/src/elements/pokemon/PokemonList.js @@ -180,6 +180,7 @@ module.exports = async ( // get list of pokemon const pokemonsRes = await useAwaitedMemo( () => + // @ts-ignore listPokemons(trainer, computeListOptions(page, filter, sort)).then( (res) => { if (res.data && !res.err) { diff --git a/src/elements/quest/TutorialList.js b/src/elements/quest/TutorialList.js index c44ff45d..94c3badd 100644 --- a/src/elements/quest/TutorialList.js +++ b/src/elements/quest/TutorialList.js @@ -1,16 +1,12 @@ -const { - useAwaitedMemo, - useState, - createElement, -} = require("../../deact/deact"); +const { createElement } = require("../../deact/deact"); const usePaginationAndSelection = require("../../hooks/usePaginationAndSelection"); const { newTutorialStages, newTutorialConfig, } = require("../../config/questConfig"); const { buildTutorialListEmbed } = require("../../embeds/questEmbeds"); -const { getTrainer } = require("../../services/trainer"); const TutorialStage = require("./TutorialStage"); +const useTrainer = require("../../hooks/useTrainer"); const PAGE_SIZE = 10; @@ -24,11 +20,7 @@ const PAGE_SIZE = 10; */ module.exports = async (ref, { user, initialStagePage }) => { // TODO: got to current tutorial stage - const { data: initialTrainer, err } = await useAwaitedMemo( - async () => getTrainer(user), - [], - ref - ); + const { trainer, setTrainer, err } = await useTrainer(user, ref); if (err) { return { err }; } @@ -47,7 +39,7 @@ module.exports = async (ref, { user, initialStagePage }) => { initialItem: /** @type {TutorialStageEnum} */ ( initialStagePage && newTutorialStages[initialStagePage - 1] ? newTutorialStages[initialStagePage - 1] - : initialTrainer.tutorialData.currentTutorialStage + : trainer.tutorialData.currentTutorialStage ), selectionPlaceholder: "Select a tutorial stage", itemConfig: newTutorialConfig, @@ -58,8 +50,6 @@ module.exports = async (ref, { user, initialStagePage }) => { ref ); - const [trainer, setTrainer] = useState(initialTrainer, ref); - if (currentStage) { return { elements: [ diff --git a/src/elements/quest/TutorialStage.js b/src/elements/quest/TutorialStage.js index 04762e63..e9a334f2 100644 --- a/src/elements/quest/TutorialStage.js +++ b/src/elements/quest/TutorialStage.js @@ -21,7 +21,7 @@ const { const { getInteractionInstance } = require("../../deact/interactions"); const Button = require("../../deact/elements/Button"); const { - getRewardsString, + getFlattenedRewardsString, flattenRewards, } = require("../../utils/trainerUtils"); const { getTrainer } = require("../../services/trainer"); @@ -68,7 +68,9 @@ module.exports = async ( goToNext(); await interactionInstance.reply({ element: { - content: getRewardsString(flattenRewards(tutorialStageData.rewards)), + content: getFlattenedRewardsString( + flattenRewards(tutorialStageData.rewards) + ), }, }); }, diff --git a/src/embeds/battleEmbeds.js b/src/embeds/battleEmbeds.js index 5fb6a413..4772c8f3 100644 --- a/src/embeds/battleEmbeds.js +++ b/src/embeds/battleEmbeds.js @@ -38,9 +38,11 @@ const { const { pokemonConfig } = require("../config/pokemonConfig"); const { getFullUsername, - getRewardsString, + getFlattenedRewardsString, flattenRewards, + flattenCategories, } = require("../utils/trainerUtils"); +const { backpackItemConfig } = require("../config/backpackConfig"); /** * Handles building the party embedded instructions for building a party. @@ -535,7 +537,7 @@ const buildBattleTowerEmbed = (towerStage) => { const bossData = pokemonConfig[npcDifficultyData.aceId]; const bossString = `**Boss**: ${bossData.emoji} #${npcDifficultyData.aceId} **${bossData.name}** \`/pokedex ${npcDifficultyData.aceId}\``; difficultyString += `${bossString}\n`; - difficultyString += `**Rewards:** ${getRewardsString( + difficultyString += `**Rewards:** ${getFlattenedRewardsString( flattenRewards(battleTowerData.rewards), false )}`; @@ -693,13 +695,19 @@ const buildRaidWinEmbed = (raid, rewards) => { const participantRewards = rewards[participantId]; if (!participantRewards) continue; const { money, backpack, shiny } = participantRewards; + const backpackRewards = flattenCategories(backpack); const rewardsForTrainer = []; if (money) { rewardsForTrainer.push(formatMoney(money)); } - if (backpack) { - // TODO + if (Object.keys(backpackRewards)) { + let backpackString = ""; + for (const itemId in backpackRewards) { + const itemData = backpackItemConfig[itemId]; + backpackString += `${itemData.emoji} x${backpackRewards[itemId]} `; + } + rewardsForTrainer.push(backpackString); } if (shiny) { const shinyData = pokemonConfig[shiny]; diff --git a/src/embeds/pokemonEmbeds.js b/src/embeds/pokemonEmbeds.js index c828093a..672679cf 100644 --- a/src/embeds/pokemonEmbeds.js +++ b/src/embeds/pokemonEmbeds.js @@ -22,6 +22,7 @@ const { setTwoInline, getOrSetDefault, formatMoney, + getNextTimeIntervalDate, } = require("../utils/utils"); const { getPokemonExpNeeded, @@ -57,6 +58,9 @@ const { equipmentConfig, SWAP_COST, } = require("../config/equipmentConfig"); +const { pokemonIdEnum } = require("../enums/pokemonEnums"); +const { timeEnum } = require("../enums/miscEnums"); +const { formatItemQuantityFromBackpack } = require("../utils/itemUtils"); /** * @@ -304,15 +308,15 @@ const buildPokemonListEmbed = (trainer, pokemons, page) => { }; /** - * @param {Trainer} trainer - * @param {Pokemon} pokemon - * @param {string=} tab + * @param {CompactUser | DiscordUser} user + * @param {WithId} pokemon + * @param {("info" | "battle" | "equipment" | "all")=} tab * @param {Pokemon=} oldPokemon * @param {string=} originalOwnerId * @returns {EmbedBuilder} */ const buildPokemonEmbed = ( - trainer, + user, pokemon, tab = "all", oldPokemon = null, @@ -362,7 +366,7 @@ const buildPokemonEmbed = ( // TODO: display original owner? const embed = new EmbedBuilder(); - embed.setTitle(`${trainer.user.username}'s ${pokemon.name}`); + embed.setTitle(`${user.username}'s ${pokemon.name}`); embed.setDescription( `${pokemon.shiny ? "✨" : ""}**[Lv. ${pokemon.level}]** ${ speciesData.name @@ -475,6 +479,23 @@ const buildPokemonEmbed = ( return embed; }; +/** + * @param {Trainer} trainer + * @param {WithId} pokemon + * @param {("info" | "battle" | "equipment" | "all")=} tab + * @param {Pokemon=} oldPokemon + * @param {string=} originalOwnerId + * @returns {EmbedBuilder} + */ +// eslint-disable-next-line camelcase +const DEPRECATEDbuildPokemonEmbed = ( + trainer, + pokemon, + tab, + oldPokemon, + originalOwnerId +) => buildPokemonEmbed(trainer.user, pokemon, tab, oldPokemon, originalOwnerId); + /** * @param {Pokemon} pokemon * @param {Pokemon} oldPokemon @@ -921,12 +942,59 @@ const buildCelebiAbilityEmbed = (trainer) => { return embed; }; +/** + * @param {Trainer} trainer + */ +const buildJirachiAbilityEmbed = (trainer) => { + const { mythicConfig } = pokemonConfig[pokemonIdEnum.JIRACHI]; + const embed = new EmbedBuilder(); + embed.setTitle(`Jirachi's Abilities`); + embed.setColor("#FFFFFF"); + embed.setDescription( + "Jirachi has one passive ability and a special Wish ability!" + ); + + embed.addFields({ + name: "Passive: Serene Luck", + value: `Jirachi calls upon the stars to grant you improved luck! **You are ${mythicConfig.shinyChanceMultiplier}x more likely to find Shiny Pokemon** from most methods (spawning excluded).`, + inline: false, + }); + + const nextWeek = getNextTimeIntervalDate(timeEnum.WEEK); + const remainingTimeString = trainer.usedWish + ? `(next Wish: ) ` + : ""; + let wishString = `Wish upon a star **once a week** ${remainingTimeString}for one of the following powerful effects:\n`; + for (const wish of Object.values(mythicConfig.wishes)) { + wishString += `* **[${ + backpackItemConfig[backpackItems.STAR_PIECE].emoji + } x${wish.starPieceCost}] Wish for ${wish.name}:** ${wish.description}\n`; + } + wishString += `\nYou currently have ${formatItemQuantityFromBackpack( + backpackItems.STAR_PIECE, + trainer.backpack + )}.`; + embed.addFields({ + name: "Active: Wishmaker", + value: wishString, + inline: false, + }); + + embed.setImage( + "https://i.pinimg.com/originals/e3/70/fe/e370fea53723b0e260c60d1de31e3a39.jpg" + ); + + return embed; +}; + module.exports = { buildBannerEmbed, buildPokemonSpawnEmbed, buildNewPokemonEmbed, buildNewPokemonListEmbed, buildPokemonListEmbed, + // eslint-disable-next-line camelcase + DEPRECATEDbuildPokemonEmbed, buildPokemonEmbed, buildEquipmentEmbed, buildEquipmentUpgradeEmbed, @@ -936,4 +1004,5 @@ module.exports = { buildSpeciesDexEmbed, buildGachaInfoString, buildCelebiAbilityEmbed, + buildJirachiAbilityEmbed, }; diff --git a/src/embeds/questEmbeds.js b/src/embeds/questEmbeds.js index 2eb7bf53..f6c60fc7 100644 --- a/src/embeds/questEmbeds.js +++ b/src/embeds/questEmbeds.js @@ -3,7 +3,10 @@ const { newTutorialConfig, newTutorialStages, } = require("../config/questConfig"); -const { getRewardsString, flattenRewards } = require("../utils/trainerUtils"); +const { + getFlattenedRewardsString, + flattenRewards, +} = require("../utils/trainerUtils"); /** * @param {object} param0 @@ -65,7 +68,10 @@ const buildTutorialStageEmbed = ({ stage, userTutorialData, page = 1 }) => { }, { name: "Rewards", - value: getRewardsString(flattenRewards(stageData.rewards), false), + value: getFlattenedRewardsString( + flattenRewards(stageData.rewards), + false + ), inline: false, }, ]); diff --git a/src/enums/battleEnums.js b/src/enums/battleEnums.js index edc1fb8b..632715f8 100644 --- a/src/enums/battleEnums.js +++ b/src/enums/battleEnums.js @@ -9,6 +9,7 @@ const effectIdEnum = Object.freeze({ SHIELD: "shield", DEBUFF_IMMUNITY: "debuffImmunity", AQUA_BLESSING: "aquaBlessing", + DOOM_DESIRE: "doomDesire", }); /** @@ -21,6 +22,10 @@ const moveIdEnum = Object.freeze({ TEST_MOVE2: "998", FIRE_PUNCH: "m7", VINE_WHIP: "m22", + CONFUSION: "m93", + PSYCHIC: "m94", + DOOM_DESIRE: "m353", + IRON_HEAD: "m442", AQUA_IMPACT: "m618-1", MAGMA_IMPACT: "m619-1", FLAME_BALL: "m780-1", @@ -34,6 +39,7 @@ const moveIdEnum = Object.freeze({ const abilityIdEnum = Object.freeze({ TEST_ABILITY: "testAbility", AQUA_POWER: "2-1", + SERENE_GRACE: "32", MAGMA_POWER: "70-1", ANGER_POINT: "83", REGENERATOR: "144", diff --git a/src/enums/pokemonEnums.js b/src/enums/pokemonEnums.js index f38c15ce..3d7ad180 100644 --- a/src/enums/pokemonEnums.js +++ b/src/enums/pokemonEnums.js @@ -269,6 +269,7 @@ const pokemonIdEnum = Object.freeze({ KYOGRE: "382", GROUDON: "383", RAYQUAZA: "384", + JIRACHI: "385", DEOXYS: "386", GARYS_BLASTOISE: "9-1", AAABAAAJSS: "18-1", diff --git a/src/events/pokemon/pokemonEvolveConfirm.js b/src/events/pokemon/pokemonEvolveConfirm.js index 7a16a71f..60c6190a 100644 --- a/src/events/pokemon/pokemonEvolveConfirm.js +++ b/src/events/pokemon/pokemonEvolveConfirm.js @@ -9,7 +9,7 @@ const { getState, deleteState } = require("../../services/state"); const { getTrainer } = require("../../services/trainer"); const { getPokemon, evolvePokemon } = require("../../services/pokemon"); -const { buildPokemonEmbed } = require("../../embeds/pokemonEmbeds"); +const { DEPRECATEDbuildPokemonEmbed } = require("../../embeds/pokemonEmbeds"); const pokemonEvolveConfirm = async (interaction, data) => { // get state @@ -52,7 +52,7 @@ const pokemonEvolveConfirm = async (interaction, data) => { const { pokemon: evolvedPokemon, species: newName } = evolveResult.data; // update embed to selected evolution - const embed = buildPokemonEmbed(trainer.data, evolvedPokemon); + const embed = DEPRECATEDbuildPokemonEmbed(trainer.data, evolvedPokemon); deleteState(data.stateId); diff --git a/src/events/pokemon/pokemonEvolveSelect.js b/src/events/pokemon/pokemonEvolveSelect.js index e7b6830b..21c2a130 100644 --- a/src/events/pokemon/pokemonEvolveSelect.js +++ b/src/events/pokemon/pokemonEvolveSelect.js @@ -10,7 +10,7 @@ const { getPokemon, getEvolvedPokemon } = require("../../services/pokemon"); const { getState } = require("../../services/state"); const { getTrainer } = require("../../services/trainer"); -const { buildPokemonEmbed } = require("../../embeds/pokemonEmbeds"); +const { DEPRECATEDbuildPokemonEmbed } = require("../../embeds/pokemonEmbeds"); const { buildButtonActionRow } = require("../../components/buttonActionRow"); const { eventNames } = require("../../config/eventConfig"); @@ -45,7 +45,7 @@ const pokemonEvolveSelect = async (interaction, data) => { const speciesId = interaction.values[0]; state.speciesId = speciesId; const evolvedPokemon = getEvolvedPokemon(pokemon.data, speciesId); - const embed = buildPokemonEmbed(trainer.data, evolvedPokemon); + const embed = DEPRECATEDbuildPokemonEmbed(trainer.data, evolvedPokemon); // get confirm button const rowData = { diff --git a/src/hooks/useTrainer.js b/src/hooks/useTrainer.js new file mode 100644 index 00000000..f548083a --- /dev/null +++ b/src/hooks/useTrainer.js @@ -0,0 +1,40 @@ +const { useAwaitedMemo, useState, useCallback } = require("../deact/deact"); +const { getTrainer } = require("../services/trainer"); + +/** + * @param {DiscordUser} user + * @param {DeactElement} ref + * @returns {Promise<{ + * trainer: WithId, + * setTrainer: (trainer: Trainer) => void, + * refreshTrainer: () => Promise<{ trainer?: WithId, err?: string }>, + * err?: string + * }>} + */ +const useTrainer = async (user, ref) => { + // TODO: got to current tutorial stage + const { data: initialTrainer, err } = await useAwaitedMemo( + async () => getTrainer(user), + [], + ref + ); + const [trainer, setTrainer] = useState(initialTrainer, ref); + + const refreshTrainer = useCallback( + async () => { + const { data: newTrainer, err: newErr } = await getTrainer(user); + if (newErr) { + return { err: newErr }; + } + + setTrainer(newTrainer); + return { trainer: newTrainer }; + }, + [user, setTrainer], + ref + ); + + return { trainer, setTrainer, err, refreshTrainer }; +}; + +module.exports = useTrainer; diff --git a/src/modals/pokemonModals.js b/src/modals/pokemonModals.js index c9fb4330..65300269 100644 --- a/src/modals/pokemonModals.js +++ b/src/modals/pokemonModals.js @@ -27,6 +27,34 @@ const buildPokemonSearchModal = ({ required, }); +/** + * Builds a simple Pokemon ID search modal + * pokemonIdInput is the id for the text + * @param {object} param0 + * @param {string} param0.id + * @param {string=} param0.title + * @param {string=} param0.placeholder + * @param {string=} param0.value + * @param {boolean=} param0.required + */ +const buildPokemonIdSearchModal = ({ + id, + title = "Select Pokemon", + placeholder = "Enter a Pokemon's exact ID", + value, + required = true, +}) => + buildGenericTextInputModal({ + id, + textInputId: "pokemonIdInput", + title, + label: "Pokemon ID", + placeholder, + value, + required, + }); + module.exports = { buildPokemonSearchModal, + buildPokemonIdSearchModal, }; diff --git a/src/services/battle.js b/src/services/battle.js index bca23eac..11a14f42 100644 --- a/src/services/battle.js +++ b/src/services/battle.js @@ -50,7 +50,7 @@ const { buildIdConfigSelectRow } = require("../components/idConfigSelectRow"); const { validateParty } = require("./party"); const { addRewards, - getRewardsString, + getFlattenedRewardsString, getUserSelectedDevice, } = require("../utils/trainerUtils"); const { getIdFromTowerStage } = require("../utils/battleUtils"); @@ -262,7 +262,10 @@ const getStartTurnSend = async (battle, stateId) => { content += `\n**${rewardRecipients .map((r) => r.username) .join(", ")} received rewards for their victory:**`; - content += getRewardsString(rewardRecipients[0].rewards, false); + content += getFlattenedRewardsString( + rewardRecipients[0].rewards, + false + ); } } } else if (battle.loseCallback) { diff --git a/src/services/gacha.js b/src/services/gacha.js index e994402c..05425151 100644 --- a/src/services/gacha.js +++ b/src/services/gacha.js @@ -56,6 +56,7 @@ const { eventNames } = require("../config/eventConfig"); const { buildButtonActionRow } = require("../components/buttonActionRow"); const { addItems } = require("../utils/trainerUtils"); const { equipmentConfig } = require("../config/equipmentConfig"); +const { pokemonIdEnum } = require("../enums/pokemonEnums"); const DAILY_MONEY = process.env.STAGE === stageNames.ALPHA ? 100000 : 300; @@ -158,6 +159,8 @@ const generateRandomEquipments = (equipmentLevel = 1) => { return equipments; }; +const BASE_SHINY_CHANCE = process.env.STAGE === stageNames.ALPHA ? 2 : 1024; + /** * @param {string} userId * @param {string} pokemonId @@ -165,6 +168,7 @@ const generateRandomEquipments = (equipmentLevel = 1) => { * @param {object} options * @param {number?=} options.equipmentLevel * @param {boolean?=} options.isShiny + * @param {number=} options.shinyChance * @param {boolean?=} options.betterIvs * @returns {Pokemon} */ @@ -172,7 +176,12 @@ const generateRandomPokemon = ( userId, pokemonId, level = 5, - { equipmentLevel = 1, isShiny = null, betterIvs = false } = {} + { + equipmentLevel = 1, + shinyChance = BASE_SHINY_CHANCE, + isShiny = null, + betterIvs = false, + } = {} ) => { const speciesData = pokemonConfig[pokemonId]; @@ -196,9 +205,8 @@ const generateRandomPokemon = ( } } - const shinyChance = process.env.STAGE === stageNames.ALPHA ? 1 : 1024; isShiny = - isShiny === null ? drawUniform(0, shinyChance, 1)[0] === 0 : isShiny; + isShiny === null ? drawUniform(0, shinyChance - 1, 1)[0] === 0 : isShiny; const shouldLock = process.env.STAGE !== stageNames.ALPHA && (isShiny || speciesData.rarity === rarities.LEGENDARY); @@ -247,12 +255,20 @@ const giveNewPokemons = async ( options = undefined ) => { const pokemons = []; + const newOptions = options || {}; + newOptions.shinyChance = newOptions.shinyChance || BASE_SHINY_CHANCE; + if (trainer.hasJirachi) { + newOptions.shinyChance = Math.floor( + newOptions.shinyChance / + pokemonConfig[pokemonIdEnum.JIRACHI].mythicConfig.shinyChanceMultiplier + ); + } for (const pokemonId of pokemonIds) { const pokemon = generateRandomPokemon( trainer.userId, pokemonId, level, - options + newOptions ); pokemons.push(pokemon); } diff --git a/src/services/mythic.js b/src/services/mythic.js index b5441fe4..d71b68ab 100644 --- a/src/services/mythic.js +++ b/src/services/mythic.js @@ -1,9 +1,8 @@ +/* eslint-disable no-case-declarations */ /* eslint-disable no-param-reassign */ /** * @file * @author Elvis Wei - * @date 2023 - * @section Description * * mythic.js Creates all mythic pokemon information, moves etc. */ @@ -12,10 +11,11 @@ const { buildIdConfigSelectRow } = require("../components/idConfigSelectRow"); const { backpackItemConfig, backpackItems, + backpackCategories, } = require("../config/backpackConfig"); const { collectionNames } = require("../config/databaseConfig"); const { eventNames } = require("../config/eventConfig"); -const { getCelebiPool } = require("../config/gachaConfig"); +const { getCelebiPool, dailyRewardChances } = require("../config/gachaConfig"); const { locations } = require("../config/locationConfig"); const { dungeons } = require("../config/npcConfig"); const { @@ -26,14 +26,19 @@ const { const { stageNames } = require("../config/stageConfig"); const { QueryBuilder } = require("../database/mongoHandler"); const { - buildPokemonEmbed, + DEPRECATEDbuildPokemonEmbed, buildCelebiAbilityEmbed, buildNewPokemonEmbed, } = require("../embeds/pokemonEmbeds"); const { logger } = require("../log"); const { getIdFromTowerStage } = require("../utils/battleUtils"); const { drawDiscrete, drawIterable } = require("../utils/gachaUtils"); -const { getItems, removeItems } = require("../utils/trainerUtils"); +const { + getItems, + removeItems, + addRewards, + getRewardsString, +} = require("../utils/trainerUtils"); const { generateRandomPokemon, giveNewPokemons } = require("./gacha"); const { listPokemons, @@ -41,9 +46,13 @@ const { calculatePokemonStats, calculateAndUpdatePokemonStats, checkNumPokemon, + updatePokemon, } = require("./pokemon"); const { getTrainer, updateTrainer } = require("./trainer"); const { getMoves } = require("../battle/data/moveRegistry"); +const { pokemonIdEnum } = require("../enums/pokemonEnums"); +const { statToBattleStat } = require("../config/battleConfig"); +const { getAbilityName } = require("../utils/pokemonUtils"); /** * @param {Trainer} trainer @@ -83,6 +92,52 @@ const getMythic = async (trainer, speciesId) => { return { data: pokemon.data }; }; +/** + * @param {Trainer} trainer + * @param {PokemonIdEnum} speciesId + */ +const generateMythic = (trainer, speciesId) => { + const speciesData = pokemonConfig[speciesId]; + const mythic = generateRandomPokemon(trainer.userId, speciesId, 1); + // set ivs to 31 + mythic.ivs = [31, 31, 31, 31, 31, 31]; + // set shiny to false + mythic.shiny = false; + // set locked to true + mythic.locked = true; + // recalculate stats + calculatePokemonStats(mythic, speciesData); + + return mythic; +}; + +/** + * @param {Trainer} trainer + * @param {Pokemon} mythic + * @returns {Promise<{err?: string, id?: any}>} + */ +const upsertMythic = async (trainer, mythic) => { + const mythicData = pokemonConfig[mythic.speciesId]; + try { + const query = new QueryBuilder(collectionNames.USER_POKEMON) + .setFilter({ userId: mythic.userId, speciesId: mythic.speciesId }) + .setUpsert({ $set: mythic }); + const res = await query.upsertOne(); + + if (res.upsertedCount !== 1) { + logger.warn( + `Error updating ${mythicData.name} for ${trainer.user.username}` + ); + } else { + logger.info(`Updated ${mythicData.name} for ${trainer.user.username}`); + } + return { id: res.upsertedId }; + } catch (err) { + logger.error(err); + return { err: `Error updating ${mythicData.name}` }; + } +}; + const validateMewMoves = (mew, mewData) => { const { mythicConfig } = mewData; @@ -138,15 +193,7 @@ const getMew = async (trainer) => { }; } - mew = generateRandomPokemon(trainer.userId, speciesId, 1); - // set ivs to 31 - mew.ivs = [31, 31, 31, 31, 31, 31]; - // set shiny to false - mew.shiny = false; - // set locked to true - mew.locked = true; - // recalculate stats - calculatePokemonStats(mew, mewData); + mew = generateMythic(trainer, speciesId); modified = true; } @@ -158,24 +205,11 @@ const getMew = async (trainer) => { // update mew if modified if (modified) { - try { - const query = new QueryBuilder(collectionNames.USER_POKEMON) - .setFilter({ userId: mew.userId, speciesId }) - .setUpsert({ $set: mew }); - const res = await query.upsertOne(); - - if (res.upsertedCount !== 1) { - logger.warn(`Error updating Mew for ${trainer.user.username}`); - // return { err: "Error updating Mew" }; - } - if (res.upsertedId) { - mew._id = res.upsertedId; - } - logger.info(`Updated Mew for ${trainer.user.username}`); - } catch (err) { - logger.error(err); - return { err: "Error updating Mew" }; + const { err, id } = await upsertMythic(trainer, mew); + if (err) { + return { err }; } + mew._id = id || mew._id; } return { data: mew }; @@ -297,7 +331,7 @@ const buildMewSend = async ({ user = null, tab = "basic" } = {}) => { }; // build pokemon embed - const embed = buildPokemonEmbed(trainer, mew, "all"); + const embed = DEPRECATEDbuildPokemonEmbed(trainer, mew, "all"); send.embeds.push(embed); // build tab buttons @@ -372,7 +406,6 @@ const buildMewSend = async ({ user = null, tab = "basic" } = {}) => { const getCelebi = async (trainer) => { const speciesId = "251"; - const celebiData = pokemonConfig[speciesId]; const celebiRes = await getMythic(trainer, speciesId); if (celebiRes.err) { @@ -397,17 +430,7 @@ const getCelebi = async (trainer) => { }; } - celebi = generateRandomPokemon(trainer.userId, speciesId, 1); - // set ivs to 31 - celebi.ivs = [31, 31, 31, 31, 31, 31]; - // set shiny to false - celebi.shiny = false; - // set locked to true - celebi.locked = true; - // set nature to 0 - celebi.natureId = 0; - // recalculate stats - calculatePokemonStats(celebi, celebiData); + celebi = generateMythic(trainer, speciesId); modified = true; } @@ -421,23 +444,11 @@ const getCelebi = async (trainer) => { // update celebi if modified if (modified) { - try { - const query = new QueryBuilder(collectionNames.USER_POKEMON) - .setFilter({ userId: celebi.userId, speciesId }) - .setUpsert({ $set: celebi }); - const res = await query.upsertOne(); - - if (res.upsertedCount !== 1) { - logger.warn(`Error updating Celebi for ${trainer.user.username}`); - } - if (res.upsertedId) { - celebi._id = res.upsertedId; - } - logger.info(`Updated Celebi for ${trainer.user.username}`); - } catch (err) { - logger.error(err); - return { err: "Error updating Celebi" }; + const { err, id } = await upsertMythic(trainer, celebi); + if (err) { + return { err }; } + celebi._id = id || celebi._id; } return { data: celebi }; @@ -480,7 +491,7 @@ const buildCelebiSend = async (user) => { }; // build pokemon embed - const embed = buildPokemonEmbed(trainer, celebi, "info"); + const embed = DEPRECATEDbuildPokemonEmbed(trainer, celebi, "info"); send.embeds.push(embed); const abilityEmbed = buildCelebiAbilityEmbed(trainer); send.embeds.push(abilityEmbed); @@ -574,7 +585,6 @@ const getDeoxys = async (trainer) => { let modified = false; const speciesId = (deoxys && deoxys.speciesId) || DEOXYS_SPECIES_IDS[0]; - const deoxysData = pokemonConfig[speciesId]; if (!deoxys) { // check if trainer has beat battle tower 20 if (!trainer.defeatedNPCs[getIdFromTowerStage(20)]) { @@ -583,39 +593,17 @@ const getDeoxys = async (trainer) => { }; } - deoxys = generateRandomPokemon(trainer.userId, speciesId, 1); - // set ivs to 31 - deoxys.ivs = [31, 31, 31, 31, 31, 31]; - // set shiny to false - deoxys.shiny = false; - // set locked to true - deoxys.locked = true; - // set nature to 0 - deoxys.natureId = 0; - // recalculate stats - calculatePokemonStats(deoxys, deoxysData); + deoxys = generateMythic(trainer, speciesId); modified = true; } // update deoxys if modified if (modified) { - try { - const query = new QueryBuilder(collectionNames.USER_POKEMON) - .setFilter({ userId: deoxys.userId, speciesId }) - .setUpsert({ $set: deoxys }); - const res = await query.upsertOne(); - - if (res.upsertedCount !== 1) { - logger.warn(`Error updating Deoxys for ${trainer.user.username}`); - } - if (res.upsertedId) { - deoxys._id = res.upsertedId; - } - logger.info(`Updated Deoxys for ${trainer.user.username}`); - } catch (err) { - logger.error(err); - return { err: "Error updating Deoxys" }; + const { err, id } = await upsertMythic(trainer, deoxys); + if (err) { + return { err }; } + deoxys._id = id || deoxys._id; } return { data: deoxys }; @@ -642,7 +630,7 @@ const buildDeoxysSend = async (user) => { }; // build pokemon embed - const embed = buildPokemonEmbed(trainer, deoxys, "all"); + const embed = DEPRECATEDbuildPokemonEmbed(trainer, deoxys, "all"); send.embeds.push(embed); // build tab buttons @@ -707,6 +695,224 @@ const onFormSelect = async (user, speciesId) => { return { err: null }; }; +/** + * @param {WithId} trainer + * @returns {Promise<{err?: string, data?: WithId}>} + */ +const getJirachi = async (trainer) => { + const speciesId = pokemonIdEnum.JIRACHI; + + const jirachiRes = await getMythic(trainer, speciesId); + if (jirachiRes.err) { + return { err: jirachiRes.err }; + } + + let jirachi = jirachiRes.data; + let modified = false; + if (!jirachi) { + let metRequirements = true; + // check star piece + if (getItems(trainer, backpackItems.STAR_PIECE) < 200) { + metRequirements = false; + } + // check for non-original-trainer pokemon + const pokemonsRes = await listPokemons(trainer, { + pageSize: 3, + filter: { originalOwner: { $ne: trainer.userId } }, + allowNone: true, + }); + if (pokemonsRes.err) { + return { err: pokemonsRes.err }; + } + if (pokemonsRes.data.length < 3) { + metRequirements = false; + } + if (!metRequirements) { + return { + err: `Jirachi wants you to work with others before granting your wishes! You must obtain 200x ${ + backpackItemConfig[backpackItems.STAR_PIECE].emoji + } Star Pieces from raids, and have at least 3 traded Pokemon!`, + }; + } + + // @ts-ignore + jirachi = generateMythic(trainer, speciesId); + modified = true; + } + + if (!trainer.hasJirachi) { + trainer.hasJirachi = true; + const trainerRes = await updateTrainer(trainer); + if (trainerRes.err) { + return { err: trainerRes.err }; + } + } + + // update jirachi if modified + if (modified) { + const { err, id } = await upsertMythic(trainer, jirachi); + if (err) { + return { err }; + } + jirachi._id = id || jirachi._id; + } + + return { data: jirachi }; +}; + +/** + * @param {Trainer} trainer + * @param {object} param1 + * @param {string} param1.wishId + * @param {Pokemon?=} param1.pokemon + * @returns {{err?: string}} + */ +const canTrainerUseWish = (trainer, { wishId, pokemon }) => { + const wishData = + pokemonConfig[pokemonIdEnum.JIRACHI].mythicConfig.wishes[wishId]; + if (!wishData) { + return { err: "Invalid wish" }; + } + if (trainer.usedWish) { + return { err: "You have already used your wish this week!" }; + } + const { starPieceCost } = wishData; + if (getItems(trainer, backpackItems.STAR_PIECE) < starPieceCost) { + return { err: "Not enough Star Pieces" }; + } + + if (pokemon && wishId === "power") { + // check that an IV exists that is below 31 + if (pokemon.ivs.every((iv) => iv === 31)) { + return { err: "All IVs are already at their maximum value!" }; + } + } else if (pokemon && wishId === "rebirth") { + // check that the pokemon may learn at least 2 abilites + if (Object.keys(pokemonConfig[pokemon.speciesId].abilities).length < 2) { + return { err: "This Pokemon may only have one ability!" }; + } + } + + return { err: null }; +}; + +/** + * @param {DiscordUser} user + * @param {object} param1 + * @param {string} param1.wishId + * @param {WithId?=} param1.pokemon + * @returns {Promise<{result?: string, err?: string}>} + */ +const useWish = async (user, { wishId, pokemon }) => { + const { data: trainer, err: trainerErr } = await getTrainer(user); + if (trainerErr) { + return { err: trainerErr }; + } + const { err: canUseWishErr } = canTrainerUseWish(trainer, { + wishId, + pokemon, + }); + if (canUseWishErr) { + return { err: canUseWishErr }; + } + + // use wish + const wishData = + pokemonConfig[pokemonIdEnum.JIRACHI].mythicConfig.wishes[wishId]; + const { starPieceCost } = wishData; + // remove star pieces and set usedWish to true + removeItems(trainer, backpackItems.STAR_PIECE, starPieceCost); + trainer.usedWish = true; + // make wish happen + let result = ""; + let rewards = {}; + switch (wishId) { + case "power": + // set a random non-31 IV to 31 + const non31IvIndices = pokemon.ivs.reduce((acc, iv, index) => { + if (iv !== 31) { + return [...acc, index]; + } + return acc; + }, []); + const randomIndex = drawIterable(non31IvIndices, 1)[0]; + pokemon.ivs[randomIndex] = 31; + + // recalculate stats and update pokemon + const updateStatsRes = await calculateAndUpdatePokemonStats( + pokemon, + pokemonConfig[pokemon.speciesId], + true + ); + if (updateStatsRes.err) { + return { err: updateStatsRes.err }; + } + result = `**${pokemon.name}'s ${statToBattleStat[ + randomIndex + ].toUpperCase()}** IV was set to 31!`; + break; + case "rebirth": + // reroll the Pokemon's ability ID, don't keep current ability + const speciesData = pokemonConfig[pokemon.speciesId]; + const abilityProbabilities = { + ...speciesData.abilities, + }; + // remove current ability from probabilities + delete abilityProbabilities[pokemon.abilityId]; + const newAbilityId = drawDiscrete(abilityProbabilities, 1)[0]; + pokemon.abilityId = newAbilityId; + const updateRes = await updatePokemon(pokemon); + if (updateRes.err) { + return { err: updateRes.err }; + } + result = `**${pokemon.name}'s** ability was changed to **${getAbilityName( + newAbilityId + )}**!`; + break; + case "allies": + // give 50 random Pokeballs + const pokeballResults = drawDiscrete(dailyRewardChances, 50); + const backpackRewards = pokeballResults.reduce( + (acc, curr) => { + acc[backpackCategories.POKEBALLS][curr] = + (acc[backpackCategories.POKEBALLS][curr] || 0) + 1; + return acc; + }, + { + [backpackCategories.POKEBALLS]: {}, + } + ); + rewards = { backpack: backpackRewards }; + addRewards(trainer, rewards); + result = getRewardsString(rewards); + break; + case "wealth": + rewards = { + money: 100000, + backpack: { + [backpackCategories.MATERIALS]: { + [backpackItems.EMOTION_SHARD]: 400, + [backpackItems.KNOWLEDGE_SHARD]: 400, + [backpackItems.WILLPOWER_SHARD]: 400, + [backpackItems.MINT]: 5, + }, + }, + }; + addRewards(trainer, rewards); + result = getRewardsString(rewards); + break; + default: + return { err: "Invalid wish" }; + } + + const { err: updateTrainerErr } = await updateTrainer(trainer); + if (updateTrainerErr) { + return { err: updateTrainerErr }; + } + + return { result, err: null }; +}; + module.exports = { getMew, updateMew, @@ -717,4 +923,7 @@ module.exports = { getDeoxys, buildDeoxysSend, onFormSelect, + getJirachi, + canTrainerUseWish, + useWish, }; diff --git a/src/services/pokemon.js b/src/services/pokemon.js index 3b3a10eb..b9a9bb02 100644 --- a/src/services/pokemon.js +++ b/src/services/pokemon.js @@ -41,7 +41,7 @@ const { locations, locationConfig } = require("../config/locationConfig"); const { buildSpeciesDexEmbed, buildPokemonListEmbed, - buildPokemonEmbed, + DEPRECATEDbuildPokemonEmbed, buildEquipmentEmbed, buildEquipmentUpgradeEmbed, buildDexListEmbed, @@ -981,7 +981,7 @@ const buildPokemonAllInfoSend = async ({ } // build pokemon embed - const embed = buildPokemonEmbed(trainer.data, pokemon.data, "all"); + const embed = DEPRECATEDbuildPokemonEmbed(trainer.data, pokemon.data, "all"); send.embeds.push(embed); return { send, err: null }; @@ -1030,7 +1030,7 @@ const buildPokemonInfoSend = async ({ } // build pokemon embed - const embed = buildPokemonEmbed( + const embed = DEPRECATEDbuildPokemonEmbed( trainer.data, pokemon.data, tab, @@ -1377,7 +1377,10 @@ const canRelease = async (trainer, pokemonIds) => { // see if any pokemon are mythical for (const pokemon of toRelease.data) { - if (pokemon.rarity === rarities.MYTHICAL) { + if ( + pokemon.rarity === rarities.MYTHICAL && + process.env.STAGE !== stageNames.ALPHA + ) { return { err: `You can't release or trade ${pokemon.name} (${pokemon._id}) because it's mythical!`, }; @@ -1946,7 +1949,7 @@ const buildNatureSend = async ({ stateId = null, user = null } = {}) => { }; // embed - const embed = buildPokemonEmbed(trainer, pokemon, "info"); + const embed = DEPRECATEDbuildPokemonEmbed(trainer, pokemon, "info"); send.embeds.push(embed); // nature select row diff --git a/src/services/raid.js b/src/services/raid.js index e9e94802..c07a10ec 100644 --- a/src/services/raid.js +++ b/src/services/raid.js @@ -6,6 +6,7 @@ const { buildIdConfigSelectRow } = require("../components/idConfigSelectRow"); const { backpackItems, backpackItemConfig, + backpackCategories, } = require("../config/backpackConfig"); const { collectionNames } = require("../config/databaseConfig"); const { eventNames } = require("../config/eventConfig"); @@ -210,11 +211,11 @@ const onRaidStart = async ({ stateId = null, user = null } = {}) => { const onRaidWin = async (raid) => { // ensure raid is valid const { raidId } = raid; - const raidData = raidConfig[raidId]; + const raidData = /** @type {RaidConfigData?} */ (raidConfig[raidId]); if (!raidData) { return; } - const { difficulty } = raid; + const { difficulty } = /** @type {{difficulty: NpcDifficultyEnum}} */ (raid); const difficultyData = raidData.difficulties[difficulty]; if (!difficultyData) { return; @@ -235,9 +236,24 @@ const onRaidWin = async (raid) => { } const percentDamage = damage / raid.boss.stats[0]; + const backpack = {}; + for (const [backpackCategory, items] of Object.entries( + difficultyData.backpackPerPercent + )) { + backpack[backpackCategory] = {}; + for (const [itemId, count] of Object.entries(items)) { + backpack[backpackCategory][itemId] = Math.max( + Math.floor(count * percentDamage * 100), + 1 + ); + } + } const rewardsForTrainer = { - money: Math.floor(difficultyData.moneyPerPercent * percentDamage * 100), - backpack: {}, // TODO + money: Math.max( + Math.floor(difficultyData.moneyPerPercent * percentDamage * 100), + 1 + ), + backpack, }; addRewards(trainer.data, rewardsForTrainer); rewards[userId] = rewardsForTrainer; @@ -246,8 +262,10 @@ const onRaidWin = async (raid) => { await updateTrainer(trainer.data); // if damage >= 10%, shinyChance chance of shiny - const receivedShiny = - percentDamage >= 0.1 && Math.random() < difficultyData.shinyChance; + const shinyChance = trainer.data.hasJirachi + ? difficultyData.shinyChance * 2 + : difficultyData.shinyChance; + const receivedShiny = percentDamage >= 0.1 && Math.random() < shinyChance; if (!receivedShiny) { continue; } diff --git a/src/services/shop.js b/src/services/shop.js index ee11a310..55e50a6e 100644 --- a/src/services/shop.js +++ b/src/services/shop.js @@ -33,7 +33,7 @@ const { eventNames } = require("../config/eventConfig"); const { buildButtonActionRow } = require("../components/buttonActionRow"); const { buildBackButtonRow } = require("../components/backButtonRow"); const { - getRewardsString, + getFlattenedRewardsString, getPokeballsString, addItems, getItems, @@ -210,7 +210,7 @@ const buyItem = async (trainer, itemId, quantity) => { cost )}.\n`; // build itemized rewards string - returnString += getRewardsString({ + returnString += getFlattenedRewardsString({ backpack: reducedResults, }); returnString += "\n\n**You now own:**"; diff --git a/src/services/trade.js b/src/services/trade.js index c124fade..f4ddecff 100644 --- a/src/services/trade.js +++ b/src/services/trade.js @@ -21,7 +21,7 @@ const { getTrainer, updateTrainer } = require("./trainer"); * @returns {Promise<{data?: boolean, err: string?}>} */ const canTrade = async (trainer) => { - const levelReq = process.env.STAGE === stageNames.ALPHA ? 5 : 50; + const levelReq = process.env.STAGE === stageNames.ALPHA ? 2 : 50; if (trainer.level < levelReq) { return { data: false, diff --git a/src/services/trainer.js b/src/services/trainer.js index 5f884854..767a8481 100644 --- a/src/services/trainer.js +++ b/src/services/trainer.js @@ -26,7 +26,7 @@ const { backpackCategories, } = require("../config/backpackConfig"); const { - getRewardsString, + getFlattenedRewardsString, getPokeballsString, addRewards, getBackpackItemsString, @@ -431,7 +431,7 @@ const getLevelRewards = async (user) => { let rewardsString = `You claimed rewards for levels: ${currentClaimedLevels.join( ", " )}. **Thank you for playing Pokestar!**\n\n`; - rewardsString += getRewardsString(allRewards); + rewardsString += getFlattenedRewardsString(allRewards); rewardsString += "\n\n**You now own:**"; if (allRewards.money) { rewardsString += `\n${formatMoney(trainer.money)}`; @@ -545,7 +545,7 @@ const getVoteRewards = async (user) => { // build itemized rewards string let rewardsString = `You claimed **${rewards}** vote rewards! **Thank you for voting!** Remember to vote again in 12 hours!\n\n`; - rewardsString += getRewardsString(receivedRewards); + rewardsString += getFlattenedRewardsString(receivedRewards); rewardsString += "\n\n**You now own:**"; if (receivedRewards.money) { rewardsString += `\n${formatMoney(trainer.money)}`; diff --git a/src/utils/battleUtils.js b/src/utils/battleUtils.js index 6d81b0f3..9768ef4f 100644 --- a/src/utils/battleUtils.js +++ b/src/utils/battleUtils.js @@ -7,9 +7,14 @@ const { statusConditions, targetPatterns } = require("../config/battleConfig"); const { difficultyConfig } = require("../config/npcConfig"); const { pokemonConfig, typeConfig } = require("../config/pokemonConfig"); -const { getRewardsString, flattenRewards } = require("./trainerUtils"); +const { + getFlattenedRewardsString, + flattenRewards, + flattenCategories, +} = require("./trainerUtils"); const { getPBar, formatMoney } = require("./utils"); const { getEffect } = require("../battle/data/effectRegistry"); +const { backpackItemConfig } = require("../config/backpackConfig"); const plus = "┼"; const plusEmph = "*"; @@ -458,7 +463,7 @@ const buildNpcDifficultyString = (difficulty, npcDifficultyData) => { difficultyString += "\n"; difficultyString += `**Multipliers:** Money: ${rewardMultipliers.moneyMultiplier} | EXP: ${rewardMultipliers.expMultiplier} | Pkmn. EXP: ${rewardMultipliers.pokemonExpMultiplier}`; if (npcDifficultyData.dailyRewards) { - difficultyString += `\n**Daily Rewards:** ${getRewardsString( + difficultyString += `\n**Daily Rewards:** ${getFlattenedRewardsString( flattenRewards(npcDifficultyData.dailyRewards), false )}`; @@ -497,7 +502,7 @@ const buildDungeonDifficultyString = (difficulty, dungeonDifficultyData) => { ); difficultyString += `**Phases:** ${dungeonDifficultyData.phases.length}\n`; difficultyString += `**Pokemon:** ${pokemonEmojis.join(" ")}\n`; - difficultyString += `**Rewards:** ${getRewardsString( + difficultyString += `**Rewards:** ${getFlattenedRewardsString( flattenRewards(rewards), false )}`; @@ -510,7 +515,7 @@ const buildDungeonDifficultyString = (difficulty, dungeonDifficultyData) => { /** * @param {NpcDifficultyEnum} difficulty - * @param {any} raidDifficultyData + * @param {RaidConfigData["difficulties"][NpcDifficultyEnum]} raidDifficultyData * @returns {{difficultyHeader: string, difficultyString: string}} */ const buildRaidDifficultyString = (difficulty, raidDifficultyData) => { @@ -534,7 +539,17 @@ const buildRaidDifficultyString = (difficulty, raidDifficultyData) => { difficultyString += `**Shiny Chance:** ${shinyChance}% • **Money/%:** ${formatMoney( raidDifficultyData.moneyPerPercent - )} • **Time:** ${raidDifficultyData.ttl / (1000 * 60 * 60)} hours`; + )} • **Time:** ${raidDifficultyData.ttl / (1000 * 60 * 60)} hours\n`; + + if (raidDifficultyData.backpackPerPercent) { + difficultyString += "**Items/%: ** "; + for (const [item, itemPerPercent] of Object.entries( + flattenCategories(raidDifficultyData.backpackPerPercent) + )) { + const itemData = backpackItemConfig[item]; + difficultyString += `${itemData.emoji} x${itemPerPercent} `; + } + } return { difficultyHeader, diff --git a/src/utils/gachaUtils.js b/src/utils/gachaUtils.js index f93188bd..073795a0 100644 --- a/src/utils/gachaUtils.js +++ b/src/utils/gachaUtils.js @@ -13,16 +13,25 @@ */ const drawDiscrete = (probabilityDistribution, times) => { const results = []; + const totalProbability = Object.values(probabilityDistribution).reduce( + (acc, curr) => acc + curr, + 0 + ); for (let i = 0; i < times; i += 1) { - const rand = Math.random(); + const rand = Math.random() * totalProbability; let sum = 0; - for (const item in probabilityDistribution) { + let item; + for (item in probabilityDistribution) { sum += probabilityDistribution[item]; if (rand < sum) { results.push(item); break; } } + // if rand is greater than sum due to some floating point bs, push the last item + if (rand >= sum) { + results.push(item); + } } // @ts-ignore return results; diff --git a/src/utils/itemUtils.js b/src/utils/itemUtils.js new file mode 100644 index 00000000..df3d2f61 --- /dev/null +++ b/src/utils/itemUtils.js @@ -0,0 +1,25 @@ +const { backpackItemConfig } = require("../config/backpackConfig"); + +/** + * @param {BackpackItemEnum} itemId + * @param {number} quantity + * @returns {string} + */ +const formatItemQuantity = (itemId, quantity) => + `${backpackItemConfig[itemId].emoji} ${quantity}x ${backpackItemConfig[itemId].name}`; + +/** + * @param {BackpackItemEnum} itemId + * @param {Backpack} backpack + * @returns {string} + */ +const formatItemQuantityFromBackpack = (itemId, backpack) => { + const { category } = backpackItemConfig[itemId]; + const quantity = backpack[category][itemId]; + return formatItemQuantity(itemId, quantity); +}; + +module.exports = { + formatItemQuantity, + formatItemQuantityFromBackpack, +}; diff --git a/src/utils/trainerUtils.js b/src/utils/trainerUtils.js index ecc5c752..552fd417 100644 --- a/src/utils/trainerUtils.js +++ b/src/utils/trainerUtils.js @@ -43,7 +43,7 @@ const getBackpackItemsString = (trainer) => { * @param {boolean=} received * @returns {string} */ -const getRewardsString = (rewards, received = true) => { +const getFlattenedRewardsString = (rewards, received = true) => { let rewardsString = received ? "**You received:**" : ""; if (rewards.money) { rewardsString += `\n${formatMoney(rewards.money)}`; @@ -90,6 +90,16 @@ const flattenRewards = (rewards) => { return flattenedRewards; }; +/** + * @param {Rewards} rewards + * @param {boolean=} received + * @returns {string} + */ +const getRewardsString = (rewards, received = true) => { + const flattenedRewards = flattenRewards(rewards); + return getFlattenedRewardsString(flattenedRewards, received); +}; + /** * * @param {Trainer} trainer @@ -192,6 +202,7 @@ const getUserSelectedDevice = (_user, userSettings) => module.exports = { getPokeballsString, getBackpackItemsString, + getFlattenedRewardsString, getRewardsString, flattenCategories, flattenRewards, diff --git a/src/utils/utils.js b/src/utils/utils.js index 7cb8db56..3a0670d8 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -265,6 +265,21 @@ const fortnightToUTCTime = (fortnight) => fortnight * 86400000 * 14; return Math.floor(time / (86400000 * 30)); } */ +/** + * @param {number} interval + * @param {Date=} date + * @returns {Date} + */ +const getNextTimeIntervalDate = (interval, date = null) => { + if (!date) { + date = new Date(); + } + const currentUtcTimeInterval = getFullUTCTimeInterval(interval, date); + const nextUtcTimeInterval = currentUtcTimeInterval + 1; + const nextTimeIntervalTime = nextUtcTimeInterval * interval; + return new Date(nextTimeIntervalTime); +}; + /** * @param {Date?=} date * @returns {{hours: number, minutes: number, seconds: number}} @@ -459,6 +474,7 @@ module.exports = { getFullUTCWeek, getFullUTCFortnight, getFullUTCTimeInterval, + getNextTimeIntervalDate, fortnightToUTCTime, getTimeToNextDay, poll, diff --git a/types.js b/types.js index 19aaddc3..9c08c306 100644 --- a/types.js +++ b/types.js @@ -108,7 +108,9 @@ * * Mythic Pokemon * @property {boolean} hasCelebi + * @property {boolean} hasJirachi * @property {boolean} usedTimeTravel + * @property {boolean} usedWish * * Misc * @property {PartialRecord} upsellData