diff --git a/riffy_test.mjs b/riffy_test.mjs new file mode 100644 index 0000000000..9a28630aa1 --- /dev/null +++ b/riffy_test.mjs @@ -0,0 +1,39 @@ +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); +const { Riffy } = require('riffy'); + +const nodes = [ + { host: 'lavalinkv4.serenetia.com', port: 443, password: 'https://seretia.link/discord', secure: true, name: 'Serenetia' }, + { host: 'lavalink.jirayu.net', port: 443, password: 'youshallnotpass', secure: true, name: 'Jirayu' }, + { host: 'lava-v4.millohost.my.id', port: 443, password: 'https://discord.gg/mjS5J2K3ep', secure: true, name: 'MilloHost' }, + { host: 'lavalink-v4.triniumhost.com', port: 443, password: 'free', secure: true, name: 'TriniumHost' }, +]; + +const fakeClient = { + user: { id: '123456789' }, + guilds: { cache: new Map() }, + ws: { shards: new Map([[0, { send() {} }]]) }, + on() {}, +}; + +const riffy = new Riffy(fakeClient, nodes, { + send() {}, + defaultSearchPlatform: 'ytmsearch', + restVersion: 'v4', + bypassChecks: { nodeFetchInfo: true }, +}); + +let connected = 0; +riffy.on('nodeConnect', (node) => { console.log(`CONNECTED: ${node.name}`); connected++; }); +riffy.on('nodeError', (node, err) => console.log(`ERROR ${node.name}: ${err?.message || err}`)); +riffy.on('nodeDisconnect', (node) => console.log(`DISCONNECT: ${node.name}`)); + +riffy.init('123456789'); + +setTimeout(() => { + console.log(`\n=== After 10s: ${connected} node(s) connected ===`); + for (const node of riffy.nodeMap.values()) { + console.log(` ${node.name}: connected=${node.connected}`); + } + process.exit(0); +}, 10000); diff --git a/riffy_test2.mjs b/riffy_test2.mjs new file mode 100644 index 0000000000..f1e0d03274 --- /dev/null +++ b/riffy_test2.mjs @@ -0,0 +1,35 @@ +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); +const { Riffy } = require('riffy'); + +const nodes = [ + { host: 'lava-v4.millohost.my.id', port: 443, password: 'https://discord.gg/mjS5J2K3ep', secure: true, name: 'MilloHost' }, +]; + +const fakeClient = { + user: { id: '123456789' }, + guilds: { cache: new Map() }, + ws: { shards: new Map([[0, { send() {} }]]) }, + on() {}, +}; + +const riffy = new Riffy(fakeClient, nodes, { + send() {}, + defaultSearchPlatform: 'ytmsearch', + restVersion: 'v4', + // NO bypassChecks +}); + +riffy.on('nodeConnect', (node) => console.log(`CONNECTED: ${node.name}`)); +riffy.on('nodeError', (node, err) => console.log(`ERROR ${node.name}: ${err?.message || err}`)); +riffy.on('nodeDisconnect', (node) => console.log(`DISCONNECT: ${node.name}`)); +riffy.on('nodeReconnect', (node) => console.log(`RECONNECT: ${node.name}`)); + +riffy.init('123456789'); + +setTimeout(() => { + for (const node of riffy.nodeMap.values()) { + console.log(`${node.name}: connected=${node.connected}`); + } + process.exit(0); +}, 8000); diff --git a/riffy_test3.mjs b/riffy_test3.mjs new file mode 100644 index 0000000000..9baca4651f --- /dev/null +++ b/riffy_test3.mjs @@ -0,0 +1,35 @@ +import { createRequire } from 'module'; +const require = createRequire(import.meta.url); +const { Riffy } = require('riffy'); + +const nodes = [ + { host: 'lava-v4.millohost.my.id', port: 443, password: 'https://discord.gg/mjS5J2K3ep', secure: true, name: 'MilloHost' }, +]; + +const fakeClient = { + user: { id: '123456789' }, + guilds: { cache: new Map() }, + ws: { shards: new Map([[0, { send() {} }]]) }, + on() {}, +}; + +const riffy = new Riffy(fakeClient, nodes, { + send() {}, + defaultSearchPlatform: 'ytmsearch', + restVersion: 'v4', + bypassChecks: { nodeFetchInfo: true }, +}); + +riffy.on('debug', (msg) => console.log('[DEBUG]', msg)); +riffy.on('nodeConnect', (node) => console.log(`CONNECTED: ${node.name}`)); +riffy.on('nodeError', (node, err) => console.log(`ERROR ${node.name}: ${err?.message || err}`)); +riffy.on('nodeDisconnect', (node, e) => console.log(`DISCONNECT: ${node.name} code=${e?.event?.code}`)); + +riffy.init('123456789'); + +setTimeout(() => { + for (const node of riffy.nodeMap.values()) { + console.log(`${node.name}: connected=${node.connected}, wsUrl=${node.wsUrl}`); + } + process.exit(0); +}, 8000); diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index 1a90387315..5fa66141c8 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -7,6 +7,17 @@ import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHan import { InteractionHelper } from '../../utils/interactionHelper.js'; const SHOP_ITEMS = shopItems; +const ADMIN_POTION_ID = 'admin_potion'; +// Potion ini langsung aktif saat dibeli. Admin potion TIDAK termasuk di sini: +// admin potion disimpan ke inventory dulu supaya bisa dipakai (/use) atau dikirim (/give). +const LUCK_POTION_IDS = new Set([ + 'luck_potion_1', + 'luck_potion_2', + 'luck_potion_3', + 'luck_potion_4', + 'luck_potion_5', +]); +const DEFAULT_LUCK_POTION_DURATION_MS = 5 * 60 * 1000; export default { data: new SlashCommandBuilder() @@ -136,11 +147,22 @@ export default { } else if (item.type === "upgrade") { userData.upgrades[itemId] = true; successDescription += `\n\n**✨ Your upgrade is now active!**`; + } else if (item.type === "consumable" && LUCK_POTION_IDS.has(itemId)) { + const now = Date.now(); + const potionExpiries = userData.luckPotionExpiresAt || {}; + const currentExpiry = Math.max(now, Number(potionExpiries[itemId]) || 0); + const duration = Number(item.effect?.duration) || DEFAULT_LUCK_POTION_DURATION_MS; + potionExpiries[itemId] = currentExpiry + (duration * quantity); + userData.luckPotionExpiresAt = potionExpiries; + const activeMinutes = Math.ceil((potionExpiries[itemId] - now) / 60000); + successDescription += `\n\nπŸ§ͺ **Active for ${activeMinutes} minutes. Timer started at purchase.**`; } else if (item.type === "consumable" || item.type === "tool") { userData.inventory[itemId] = (userData.inventory[itemId] || 0) + quantity; if (item.type === "tool") { successDescription += `\n\n**πŸ› οΈ ${item.name} added to your inventory!**`; + } else if (itemId === ADMIN_POTION_ID) { + successDescription += `\n\nπŸ‘‘ **Added to your inventory!** Activate it with \`/use item_id:${ADMIN_POTION_ID}\` or gift it with \`/give\`.`; } } @@ -157,4 +179,4 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: [MessageFlags.Ephemeral] }); }, { command: 'buy' }) -}; \ No newline at end of file +}; diff --git a/src/commands/Economy/daily.js b/src/commands/Economy/daily.js index 4045d23ed0..32cf3977a0 100644 --- a/src/commands/Economy/daily.js +++ b/src/commands/Economy/daily.js @@ -1,7 +1,6 @@ import { SlashCommandBuilder } from 'discord.js'; import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { getEconomyData, setEconomyData } from '../../utils/economy.js'; -import { getGuildConfig } from '../../services/config/guildConfig.js'; import { formatDuration } from '../../utils/embeds.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { logger } from '../../utils/logger.js'; @@ -10,7 +9,6 @@ import { botConfig } from '../../config/bot.js'; const DAILY_COOLDOWN = 24 * 60 * 60 * 1000; const DAILY_AMOUNT = botConfig.economy?.dailyAmount ?? 100; -const PREMIUM_BONUS_PERCENTAGE = 0.1; export default { data: new SlashCommandBuilder() @@ -50,25 +48,7 @@ export default { ); } - const guildConfig = await getGuildConfig(client, guildId); - const PREMIUM_ROLE_ID = guildConfig.premiumRoleId; - - let earned = DAILY_AMOUNT; - let bonusMessage = ""; - let hasPremiumRole = false; - - if ( - PREMIUM_ROLE_ID && - interaction.member && - interaction.member.roles.cache.has(PREMIUM_ROLE_ID) - ) { - const bonusAmount = Math.floor( - DAILY_AMOUNT * PREMIUM_BONUS_PERCENTAGE, - ); - earned += bonusAmount; - bonusMessage = `\n✨ **Premium Bonus:** +$${bonusAmount.toLocaleString()}`; - hasPremiumRole = true; - } + const earned = DAILY_AMOUNT; userData.wallet = (userData.wallet || 0) + earned; userData.lastDaily = now; @@ -80,13 +60,12 @@ export default { guildId, amount: earned, newWallet: userData.wallet, - hasPremium: hasPremiumRole, timestamp: new Date().toISOString() }); const embed = successEmbed( "βœ… Daily Claimed!", - `You have claimed your daily **$${earned.toLocaleString()}**!${bonusMessage}` + `You have claimed your daily **$${earned.toLocaleString()}**!` ) .addFields({ name: "New Cash Balance", @@ -94,11 +73,9 @@ export default { inline: true, }) .setFooter({ - text: hasPremiumRole - ? `Next claim in 24 hours. (Premium Active)` - : `Next claim in 24 hours.`, + text: `Next claim in 24 hours.`, }); await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'daily' }) -}; \ No newline at end of file +}; diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 5288f048bb..ba972374da 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -3,30 +3,140 @@ import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from ' import { getEconomyData, setEconomyData } from '../../utils/economy.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.js'; +import { getItemById } from '../../config/shop/items.js'; +import { getGuildConfig } from '../../services/config/guildConfig.js'; -const FISH_COOLDOWN = 45 * 60 * 1000; +const FISH_COOLDOWN = 0; const BASE_MIN_REWARD = 300; const BASE_MAX_REWARD = 900; -const FISHING_ROD_MULTIPLIER = 1.5; +const DEFAULT_LUCK_POTION_DURATION_MS = 5 * 60 * 1000; +const ADMIN_POTION_ID = 'admin_potion'; +const PREMIUM_FISHING_MULTIPLIER = getItemById('premium_role')?.effect?.multiplier ?? 1.1; -const FISH_TYPES = [ +// Helper: find the highest-tier fishing rod the user owns +function getBestFishingRod(inventory) { + for (let tier = 5; tier >= 1; tier--) { + const id = `fishing_rod_${tier}`; + const legacyTierFiveQuantity = tier === 5 + ? Number(inventory.fishibg_rod_5 || 0) + : 0; + if (Number(inventory[id] || 0) > 0 || legacyTierFiveQuantity > 0) { + const item = getItemById(id); + if (item) return item; + } + } + return null; +} + +// Helper: find the highest-tier luck potion with an active timer +function getBestActiveLuckPotion(potionExpiries, now) { + for (let tier = 5; tier >= 1; tier--) { + const id = `luck_potion_${tier}`; + if (Number(potionExpiries[id]) > now) { + const item = getItemById(id); + if (item) return item; + } + } + return null; +} + +export const FISH_TYPES = [ + // Common { name: 'Bass', emoji: '🐟', rarity: 'common' }, - { name: 'Salmon', emoji: '🐟', rarity: 'common' }, - { name: 'Trout', emoji: '🐟', rarity: 'common' }, - { name: 'Tuna', emoji: '🐠', rarity: 'uncommon' }, - { name: 'Swordfish', emoji: '🐠', rarity: 'uncommon' }, - { name: 'Octopus', emoji: 'πŸ™', rarity: 'rare' }, + { name: 'Salmon', emoji: '🐠', rarity: 'common' }, + { name: 'forel', emoji: '🐟', rarity: 'common' }, + { name: 'Carp', emoji: '🐟', rarity: 'common' }, + { name: 'Catfish', emoji: '🐟', rarity: 'common' }, + { name: 'Perch', emoji: '🐟', rarity: 'common' }, + { name: 'Sardine', emoji: '🐟', rarity: 'common' }, + { name: 'Mackerel', emoji: '🐟', rarity: 'common' }, + + // Uncommon + { name: 'Tuna', emoji: '🐑', rarity: 'uncommon' }, + { name: 'Swordfish', emoji: '🐟', rarity: 'uncommon' }, + { name: 'Octopus', emoji: 'πŸ™', rarity: 'uncommon' }, + { name: 'Barracuda', emoji: '🐟', rarity: 'uncommon' }, + { name: 'Pufferfish', emoji: '🐑', rarity: 'uncommon' }, + { name: 'Snapper', emoji: '🐠', rarity: 'uncommon' }, + { name: 'Mahi Mahi', emoji: '🐠', rarity: 'uncommon' }, + + // Rare { name: 'Lobster', emoji: '🦞', rarity: 'rare' }, - { name: 'Shark', emoji: '🦈', rarity: 'epic' }, - { name: 'Whale', emoji: 'πŸ‹', rarity: 'legendary' }, + { name: 'Shark', emoji: '🦈', rarity: 'rare' }, + { name: 'Manta Ray', emoji: '🐟', rarity: 'rare' }, + { name: 'Stingray', emoji: '🐟', rarity: 'rare' }, + { name: 'Anglerfish', emoji: '🐑', rarity: 'rare' }, + { name: 'Moray Eel', emoji: '🐍', rarity: 'rare' }, + { name: 'Hammerhead Shark', emoji: '🦈', rarity: 'rare' }, + + // Epic + { name: 'Golden Koi', emoji: '🐠', rarity: 'epic' }, + { name: 'Blue Marlin', emoji: '🐟', rarity: 'epic' }, + { name: 'Crystal Fish', emoji: 'πŸ’Ž', rarity: 'epic' }, + { name: 'Moonfish', emoji: 'πŸŒ™', rarity: 'epic' }, + { name: 'Electric Eel', emoji: '⚑', rarity: 'epic' }, + { name: 'Ruby Grouper', emoji: 'πŸ”΄', rarity: 'epic' }, + { name: 'Sapphire Tuna', emoji: 'πŸ”΅', rarity: 'epic' }, + + // Legendary + { name: 'Golden Shark', emoji: '🦈', rarity: 'legendary' }, + { name: 'Sea Dragon', emoji: 'πŸ‰', rarity: 'legendary' }, + { name: 'Golden Whale', emoji: 'πŸ‹', rarity: 'legendary' }, + { name: 'Storm Serpent', emoji: '🌩️', rarity: 'legendary' }, + { name: 'Aurora Whale', emoji: '🌌', rarity: 'legendary' }, + { name: 'Thunder Marlin', emoji: '⚑', rarity: 'legendary' }, + { name: 'Emerald Leviathan', emoji: 'πŸ’š', rarity: 'legendary' }, + + // Mythic + { name: 'Leviathan', emoji: 'πŸ‰', rarity: 'mythic' }, + { name: 'Megalodon', emoji: '🦈', rarity: 'mythic' }, + { name: 'Phoenix Koi', emoji: 'πŸ”₯', rarity: 'mythic' }, + { name: 'Celestial Turtle', emoji: '🐒', rarity: 'mythic' }, + { name: 'Abyssal Dragon', emoji: 'πŸ‰', rarity: 'mythic' }, + { name: 'Star Kraken', emoji: 'πŸ™', rarity: 'mythic' }, + { name: 'Obsidian Whale', emoji: 'πŸ‹', rarity: 'mythic' }, + + // Secret + { name: 'Void Whale', emoji: '🌌', rarity: 'secret' }, + { name: 'Ancient Kraken', emoji: 'πŸ™', rarity: 'secret' }, + { name: 'Abyss Serpent', emoji: '🐍', rarity: 'secret' }, + { name: 'Shadow Leviathan', emoji: 'πŸŒ‘', rarity: 'secret' }, + { name: 'Eclipse Ray', emoji: '🌘', rarity: 'secret' }, + { name: 'Genesis Eel', emoji: '⚑', rarity: 'secret' }, + { name: 'Nebula Shark', emoji: '🦈', rarity: 'secret' }, + + // Divine (Above Secret) + { name: 'Cosmic Leviathan', emoji: '🌠', rarity: 'divine' }, + { name: 'God of Ocean', emoji: 'πŸ”±', rarity: 'divine' }, + { name: 'Eternal Whale', emoji: '🌌', rarity: 'divine' }, + { name: 'Ocean Titan', emoji: '🌊', rarity: 'divine' }, + { name: 'Holy Megalodon', emoji: '✨', rarity: 'divine' }, + { name: 'Astral Dragon', emoji: 'πŸ‰', rarity: 'divine' }, + { name: 'Solar Koi', emoji: 'β˜€οΈ', rarity: 'divine' }, + + // Transcendent (Above Divine) + { name: 'Chronos Whale', emoji: '⏳', rarity: 'transcendent' }, + { name: 'Omnipotent Kraken', emoji: 'πŸ‘‘', rarity: 'transcendent' }, + { name: 'Primordial Serpent', emoji: '🐍', rarity: 'transcendent' }, + { name: 'Infinity Koi', emoji: '♾️', rarity: 'transcendent' }, + { name: 'Genesis Leviathan', emoji: '🌟', rarity: 'transcendent' }, + { name: 'Celestia Dragon', emoji: '🐲', rarity: 'transcendent' }, + + // Celestial (Above Transcendent) β€” Kitsune, peluang 2x lebih susah dari transcendent + { name: 'Kitsune', emoji: '🦊', rarity: 'celestial' } ]; const CATCH_MESSAGES = [ - "You cast your line into the crystal clear waters...", - "You wait patiently as your bobber floats...", - "After a few minutes of waiting, you feel a tug...", - "The water ripples as something takes your bait...", - "You reel in your catch with expert precision...", + "Kamu melemparkan kailmu ke dalam air yang sangat jernih...", + "Kamu menunggu dengan sabar saat pelampung pancingmu mengapung...", + "Setelah beberapa menit menunggu, kamu merasakan tarikan pada kailmu...", + "Air mulai beriak saat sesuatu memakan umpanmu...", + "Kamu menarik hasil tangkapanmu dengan sangat ahli...", + "Pelampungmu tenggelam! Sepertinya ada ikan yang menyambar...", + "Air di sekitarmu mulai bergejolak...", + "Tarikan semakin kuat, sepertinya ikan ini cukup besar...", + "Kailmu bergerak liar, tangkapanmu berusaha melarikan diri...", + "Setelah perjuangan panjang, akhirnya kamu berhasil mendapatkan ikan...", ]; export default { @@ -44,7 +154,29 @@ export default { const userData = await getEconomyData(client, guildId, userId); const lastFish = userData.lastFish || 0; - const hasFishingRod = userData.inventory["fishing_rod"] || 0; + const inventory = userData.inventory || {}; + const potionExpiries = { ...(userData.luckPotionExpiresAt || {}) }; + + const bestRod = getBestFishingRod(inventory); + for (let tier = 1; tier <= 5; tier++) { + const potionId = `luck_potion_${tier}`; + const legacyQuantity = Number(inventory[potionId] || 0); + if (legacyQuantity > 0) { + const currentExpiry = Math.max(now, Number(potionExpiries[potionId]) || 0); + const duration = Number(getItemById(potionId)?.effect?.duration) + || DEFAULT_LUCK_POTION_DURATION_MS; + potionExpiries[potionId] = currentExpiry + (duration * legacyQuantity); + delete inventory[potionId]; + } + } + const bestPotion = getBestActiveLuckPotion(potionExpiries, now); + userData.luckPotionExpiresAt = potionExpiries; + + const guildConfig = await getGuildConfig(client, guildId); + const premiumRoleId = guildConfig.premiumRoleId; + const hasPremiumRole = Boolean( + premiumRoleId && interaction.member?.roles?.cache?.has(premiumRoleId) + ); if (now < lastFish + FISH_COOLDOWN) { const remaining = lastFish + FISH_COOLDOWN - now; @@ -61,42 +193,123 @@ export default { ); } - const rand = Math.random(); - let fishCaught; - - if (rand < 0.5) { - - fishCaught = FISH_TYPES.filter(f => f.rarity === 'common')[Math.floor(Math.random() * 3)]; - } else if (rand < 0.75) { - - fishCaught = FISH_TYPES.filter(f => f.rarity === 'uncommon')[Math.floor(Math.random() * 2)]; - } else if (rand < 0.9) { - - fishCaught = FISH_TYPES.filter(f => f.rarity === 'rare')[Math.floor(Math.random() * 2)]; - } else if (rand < 0.98) { - - fishCaught = FISH_TYPES.find(f => f.rarity === 'epic'); - } else { - - fishCaught = FISH_TYPES.find(f => f.rarity === 'legendary'); + let rand = Math.random(); + + // Potion admin punya prioritas dan efek khusus; kalau tidak ada, pakai potion luck biasa. + const adminActive = Number(potionExpiries[ADMIN_POTION_ID]) > now; + const activePotion = adminActive ? getItemById(ADMIN_POTION_ID) : bestPotion; + const adminChances = activePotion?.effect?.admin ? activePotion.effect.chances : null; + + let luckMessage = ""; + if (activePotion) { + const minutesLeft = Math.ceil( + (Number(potionExpiries[activePotion.id]) - now) / 60000 + ); + if (adminChances) { + luckMessage = `\nπŸ‘‘ **${activePotion.name} Active: Kitsune ${Math.round(adminChances.celestial * 100)}% (${minutesLeft}m left)**`; + } else { + const luckMultiplier = activePotion.effect.multiplier || 1; + // Bias halus: potion menaikkan peluang ikan langka tanpa "membanjiri" + // tier atas. Hasil tetap dibatasi 0.9998 agar potion tidak bisa + // menembus celestial (Kitsune). + rand = 1 - Math.pow(1 - rand, luckMultiplier); + if (rand > 0.9998) rand = 0.9998; + luckMessage = `\nπŸ§ͺ **${activePotion.name} Active: x${luckMultiplier} luck (${minutesLeft}m left)**`; + } } - const baseEarned = Math.floor( - Math.random() * (BASE_MAX_REWARD - BASE_MIN_REWARD + 1) - ) + BASE_MIN_REWARD; + let selectedRarity; + + // Potion admin: peluang tetap divine/transcendent/celestial; sisanya (40%) normal. + if (adminChances) { + const roll = Math.random(); + let acc = 0; + if (roll < (acc += adminChances.divine)) { + selectedRarity = 'divine'; + } else if (roll < (acc += adminChances.transcendent)) { + selectedRarity = 'transcendent'; + } else if (roll < (acc += adminChances.celestial)) { + selectedRarity = 'celestial'; + } + } + + if (!selectedRarity) { + if (rand < 0.45) { + selectedRarity = 'common'; + } else if (rand < 0.70) { + selectedRarity = 'uncommon'; + } else if (rand < 0.85) { + selectedRarity = 'rare'; + } else if (rand < 0.94) { + selectedRarity = 'epic'; + } else if (rand < 0.98) { + selectedRarity = 'legendary'; + } else if (rand < 0.995) { + selectedRarity = 'mythic'; + } else if (rand < 0.9995) { + selectedRarity = 'secret'; + } else if (rand < 0.9997) { + selectedRarity = 'divine'; + } else if (rand < 0.9999) { + selectedRarity = 'transcendent'; + } else { + selectedRarity = 'celestial'; + } + } + + const availableFish = FISH_TYPES.filter( + f => f.rarity === selectedRarity + ); + + const fishCaught = availableFish[ + Math.floor(Math.random() * availableFish.length) + ]; + const fishPrices = { + common: [200, 450], + uncommon: [450, 750], + rare: [1000, 1250], + epic: [1750, 3000], + legendary: [3750, 7500], + mythic: [15000, 25000], + secret: [30000, 50000], + divine: [62500, 175000], + transcendent: [250000, 500000], + celestial: [10000000, 20000000] + }; - let finalEarned = baseEarned; - let multiplierMessage = ""; + const [minPrice, maxPrice] = fishPrices[fishCaught.rarity]; - if (hasFishingRod > 0) { - finalEarned = Math.floor(baseEarned * FISHING_ROD_MULTIPLIER); - multiplierMessage = `\n🎣 **Fishing Rod Bonus: +50%**`; + const baseEarned = Math.floor( + Math.random() * (maxPrice - minPrice + 1) + ) + minPrice; + + let fishingMultiplier = 1; + const bonusMessages = []; + if (bestRod) { + const rodMultiplier = bestRod.effect.multiplier || 1; + fishingMultiplier *= rodMultiplier; + const boostPct = Math.round((rodMultiplier - 1) * 100); + bonusMessages.push(`🎣 **${bestRod.name} Bonus: +${boostPct}%**`); + } + if (hasPremiumRole) { + fishingMultiplier *= PREMIUM_FISHING_MULTIPLIER; + const boostPct = Math.round((PREMIUM_FISHING_MULTIPLIER - 1) * 100); + bonusMessages.push(`✨ **Premium Role Bonus: +${boostPct}%**`); } + const finalEarned = Math.floor(baseEarned * fishingMultiplier); + const multiplierMessage = bonusMessages.length + ? `\n${bonusMessages.join('\n')}` + : ""; const catchMessage = CATCH_MESSAGES[Math.floor(Math.random() * CATCH_MESSAGES.length)]; userData.wallet += finalEarned; userData.lastFish = now; + userData.fishCollection = userData.fishCollection || {}; + userData.fishCollection[fishCaught.name] = + (Number(userData.fishCollection[fishCaught.name]) || 0) + 1; + userData.inventory = inventory; + userData.luckPotionExpiresAt = potionExpiries; await setEconomyData(client, guildId, userId, userData); @@ -105,12 +318,17 @@ export default { uncommon: '#2ECC71', rare: '#3498DB', epic: '#9B59B6', - legendary: '#F1C40F' + legendary: '#F1C40F', + mythic: '#E91E63', + secret: '#FF0000', + divine: '#00FFFF', + transcendent: '#FF00FF', + celestial: '#FF69B4' }; const embed = createEmbed({ title: 'Fishing Success!', - description: `${catchMessage}\n\nYou caught a **${fishCaught.emoji} ${fishCaught.name}**! You sold it for **$${finalEarned.toLocaleString()}**!${multiplierMessage}`, + description: `${catchMessage}\n\nYou caught a **${fishCaught.emoji} ${fishCaught.name}**! You sold it for **$${finalEarned.toLocaleString()}**!${multiplierMessage}${luckMessage}`, color: rarityColors[fishCaught.rarity] }) .addFields( @@ -124,9 +342,8 @@ export default { value: fishCaught.rarity.charAt(0).toUpperCase() + fishCaught.rarity.slice(1), inline: true, } - ) - .setFooter({ text: `Next fishing trip available in 45 minutes.` }); + ); await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'fish' }) -}; \ No newline at end of file +}; diff --git a/src/commands/Economy/fishdex.js b/src/commands/Economy/fishdex.js new file mode 100644 index 0000000000..03030a328f --- /dev/null +++ b/src/commands/Economy/fishdex.js @@ -0,0 +1,162 @@ +import { + ActionRowBuilder, + ComponentType, + EmbedBuilder, + MessageFlags, + SlashCommandBuilder, + StringSelectMenuBuilder, +} from 'discord.js'; +import { FISH_TYPES } from './fish.js'; +import { getEconomyData } from '../../utils/economy.js'; +import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; +import { logger } from '../../utils/logger.js'; + +const RARITIES = [...new Set(FISH_TYPES.map(fish => fish.rarity))]; +const RARITY_COLORS = { + common: 0x95A5A6, + uncommon: 0x2ECC71, + rare: 0x3498DB, + epic: 0x9B59B6, + legendary: 0xF1C40F, + mythic: 0xE91E63, + secret: 0xFF0000, + divine: 0x00FFFF, + transcendent: 0xFF00FF, + celestial: 0xFF69B4, +}; + +const IMAGE_HINTS = { + common: 'common freshwater fish, natural underwater photography', + uncommon: 'colorful tropical fish, natural underwater photography', + rare: 'rare deep sea fish, detailed underwater art', + epic: 'magical rare fish with vivid colors, fantasy art', + legendary: 'legendary sea creature with a glowing aura, epic fantasy art', + mythic: 'mythical giant sea creature, magical glow, epic fantasy art', + secret: 'mysterious shadowy abyssal sea creature, dark fantasy art', + divine: 'divine celestial sea creature, holy golden light, epic fantasy art', + transcendent: 'cosmic sea creature among galaxies and starlight, epic fantasy art', + celestial: 'cute pink nine-tailed fox fish, glowing magical aura, fantasy art', +}; + +function createImageUrl(fish) { + const prompt = `${fish.name}, ${IMAGE_HINTS[fish.rarity]}, single subject, centered, highly detailed`; + let seed = 0; + for (let i = 0; i < fish.name.length; i++) { + seed = (seed * 31 + fish.name.charCodeAt(i)) >>> 0; + } + return `https://image.pollinations.ai/prompt/${encodeURIComponent(prompt)}?width=256&height=256&nologo=true&seed=${seed}`; +} + +function makeFishEmbeds(rarity, targetUser, fishCollection) { + const pageFish = FISH_TYPES.filter(fish => fish.rarity === rarity); + const caughtSpecies = FISH_TYPES.filter( + fish => Number(fishCollection[fish.name]) > 0 + ).length; + + return pageFish.map((fish, index) => { + const count = Number(fishCollection[fish.name]) || 0; + const embed = new EmbedBuilder() + .setColor(RARITY_COLORS[fish.rarity] ?? 0x5865F2) + .setTitle(`${fish.emoji} ${fish.name}`); + + if (count > 0) { + embed + .setDescription(`Tertangkap **${count}x**`) + .setThumbnail(createImageUrl(fish)); + } else { + embed.setDescription('Belum tertangkap'); + } + + if (index === pageFish.length - 1) { + embed.setFooter({ + text: `${targetUser.username}: ${caughtSpecies}/${FISH_TYPES.length} spesies | ${pageFish.length} ikan ${rarity}`, + iconURL: targetUser.displayAvatarURL(), + }); + } + return embed; + }); +} + +function makeComponents(selectedRarity, customIdPrefix, disabled = false) { + return [ + new ActionRowBuilder().addComponents( + new StringSelectMenuBuilder() + .setCustomId(`${customIdPrefix}_rarity`) + .setPlaceholder('Pilih rarity ikan') + .setDisabled(disabled) + .addOptions(RARITIES.map(rarity => ({ + label: rarity.charAt(0).toUpperCase() + rarity.slice(1), + value: rarity, + default: rarity === selectedRarity, + }))), + ), + ]; +} + +export default { + data: new SlashCommandBuilder() + .setName('fishdex') + .setDescription('Lihat index ikan tangkapanmu atau pengguna lain') + .addUserOption(option => + option + .setName('user') + .setDescription('Pengguna yang index ikannya ingin dilihat') + .setRequired(false) + ), + + execute: withErrorHandling(async (interaction, config, client) => { + const targetUser = interaction.options.getUser('user') || interaction.user; + if (targetUser.bot) { + throw createError( + 'Cannot view bot fishdex', + ErrorTypes.VALIDATION, + 'Bot tidak memiliki index tangkapan ikan.' + ); + } + + const userData = await getEconomyData(client, interaction.guildId, targetUser.id); + const fishCollection = userData.fishCollection || {}; + const customIdPrefix = `fishdex_${interaction.id}`; + let selectedRarity = RARITIES[0]; + + await interaction.reply({ + embeds: makeFishEmbeds(selectedRarity, targetUser, fishCollection), + components: makeComponents(selectedRarity, customIdPrefix), + }); + const message = await interaction.fetchReply(); + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.StringSelect, + time: 300000, + }); + + collector.on('collect', async selectInteraction => { + if (selectInteraction.user.id !== interaction.user.id) { + await selectInteraction.reply({ + content: 'Hanya orang yang membuka fishdex ini yang bisa memilih rarity.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + await selectInteraction.deferUpdate(); + selectedRarity = selectInteraction.values[0]; + + await message.edit({ + embeds: makeFishEmbeds(selectedRarity, targetUser, fishCollection), + components: makeComponents(selectedRarity, customIdPrefix), + }); + }); + + collector.on('end', async () => { + try { + await message.edit({ + components: makeComponents(selectedRarity, customIdPrefix, true), + }); + } catch (error) { + logger.debug('fishdex: could not disable pagination buttons', { + error: error.message, + }); + } + }); + }, { command: 'fishdex' }), +}; diff --git a/src/commands/Economy/give.js b/src/commands/Economy/give.js new file mode 100644 index 0000000000..179d80379d --- /dev/null +++ b/src/commands/Economy/give.js @@ -0,0 +1,153 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { createEmbed, successEmbed } from '../../utils/embeds.js'; +import { getItemById } from '../../config/shop/items.js'; +import { getEconomyData, setEconomyData } from '../../utils/economy.js'; +import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; +import { logger } from '../../utils/logger.js'; +import { InteractionHelper } from '../../utils/interactionHelper.js'; + +export default { + data: new SlashCommandBuilder() + .setName('give') + .setDescription('Give an item from your inventory to another user') + .addUserOption(option => + option + .setName('user') + .setDescription('User to give the item to') + .setRequired(true) + ) + .addStringOption(option => + option + .setName('item_id') + .setDescription('ID of the item to give (e.g. admin_potion)') + .setRequired(true) + ) + .addIntegerOption(option => + option + .setName('quantity') + .setDescription('Quantity to give (default: 1)') + .setRequired(false) + .setMinValue(1) + ), + + execute: withErrorHandling(async (interaction, config, client) => { + const deferred = await InteractionHelper.safeDefer(interaction); + if (!deferred) return; + + const senderId = interaction.user.id; + const receiver = interaction.options.getUser("user"); + const itemId = interaction.options.getString("item_id").toLowerCase(); + const quantity = interaction.options.getInteger("quantity") || 1; + const guildId = interaction.guildId; + + if (receiver.bot) { + throw createError( + "Cannot give to bot", + ErrorTypes.VALIDATION, + "You cannot give an item to a bot.", + { receiverId: receiver.id } + ); + } + + if (receiver.id === senderId) { + throw createError( + "Cannot give to self", + ErrorTypes.VALIDATION, + "You cannot give an item to yourself.", + { senderId } + ); + } + + const item = getItemById(itemId); + if (!item) { + throw createError( + `Item ${itemId} not found`, + ErrorTypes.VALIDATION, + `The item ID \`${itemId}\` does not exist.`, + { itemId } + ); + } + + const [senderData, receiverData] = await Promise.all([ + getEconomyData(client, guildId, senderId), + getEconomyData(client, guildId, receiver.id) + ]); + + const senderHeld = Number(senderData.inventory?.[itemId]) || 0; + if (senderHeld < quantity) { + throw createError( + "Not enough items", + ErrorTypes.VALIDATION, + `You only have **${senderHeld}x ${item.name}** in your inventory.`, + { itemId, senderHeld, quantity } + ); + } + + // Batasi kepemilikan penerima seperti aturan pembelian. + const receiverHeld = Number(receiverData.inventory?.[itemId]) || 0; + if (item.type === 'tool' && itemId !== 'bank_note' && receiverHeld > 0) { + throw createError( + "Receiver already owns item", + ErrorTypes.VALIDATION, + `**${receiver.username}** already owns a **${item.name}**.`, + { itemId, receiverId: receiver.id } + ); + } + if (item.type === 'consumable' && item.maxQuantity && receiverHeld + quantity > item.maxQuantity) { + throw createError( + "Receiver inventory limit", + ErrorTypes.VALIDATION, + `**${receiver.username}** can only hold a maximum of **${item.maxQuantity}x ${item.name}**.`, + { itemId, receiverId: receiver.id, receiverHeld, quantity, maxQuantity: item.maxQuantity } + ); + } + + if (senderHeld - quantity > 0) { + senderData.inventory[itemId] = senderHeld - quantity; + } else { + delete senderData.inventory[itemId]; + } + + receiverData.inventory[itemId] = receiverHeld + quantity; + + await Promise.all([ + setEconomyData(client, guildId, senderId, senderData), + setEconomyData(client, guildId, receiver.id, receiverData) + ]); + + const embed = successEmbed( + "🎁 Item Gifted", + `You gave **${quantity}x ${item.name}** to **${receiver.username}**!` + ).addFields( + { + name: "Item", + value: `${item.name} x${quantity}`, + inline: true, + }, + { + name: "Remaining", + value: `${Number(senderData.inventory[itemId]) || 0}x`, + inline: true, + }, + ); + + await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); + + logger.info(`[ECONOMY] Item gifted`, { + senderId, + receiverId: receiver.id, + itemId, + quantity, + }); + + try { + const receiverEmbed = createEmbed({ + title: "🎁 You received an item!", + description: `${interaction.user.username} gave you **${quantity}x ${item.name}**.` + }); + await receiver.send({ embeds: [receiverEmbed] }); + } catch (e) { + logger.warn(`Could not DM user ${receiver.id}: ${e.message}`); + } + }, { command: 'give' }) +}; diff --git a/src/commands/Economy/inventory.js b/src/commands/Economy/inventory.js index b7efcfec80..f5bd537a29 100644 --- a/src/commands/Economy/inventory.js +++ b/src/commands/Economy/inventory.js @@ -8,6 +8,13 @@ import { InteractionHelper } from '../../utils/interactionHelper.js'; const SHOP_ITEMS = shopItems; +function getShopItem(itemId) { + return SHOP_ITEMS.find(item => item.id === itemId) + || (itemId === 'fishibg_rod_5' + ? SHOP_ITEMS.find(item => item.id === 'fishing_rod_5') + : null); +} + export default { data: new SlashCommandBuilder() .setName('inventory') @@ -34,25 +41,26 @@ export default { } const inventory = userData.inventory || {}; - - let inventoryDescription = "Your inventory is currently empty."; - - if (Object.keys(inventory).length > 0) { - inventoryDescription = Object.entries(inventory) - .filter( - ([itemId, quantity]) => { - const item = SHOP_ITEMS.find(i => i.id === itemId); - return quantity > 0 && item; - } - ) - .map( - ([itemId, quantity]) => { - const item = SHOP_ITEMS.find(i => i.id === itemId); - return `**${item.name}:** ${quantity}x`; - } - ) - .join("\n"); - } + const inventoryLines = Object.entries(inventory) + .filter(([itemId, quantity]) => { + const item = getShopItem(itemId); + return Number(quantity) > 0 && item; + }) + .map(([itemId, quantity]) => { + const item = getShopItem(itemId); + return `**${item.name}:** ${quantity}x`; + }); + const potionLines = Object.entries(userData.luckPotionExpiresAt || {}) + .filter(([, expiresAt]) => Number(expiresAt) > Date.now()) + .map(([itemId, expiresAt]) => { + const item = getShopItem(itemId); + if (!item) return null; + const minutesLeft = Math.ceil((Number(expiresAt) - Date.now()) / 60000); + return `πŸ§ͺ **${item.name}:** Active (${minutesLeft}m left)`; + }) + .filter(Boolean); + const inventoryDescription = [...inventoryLines, ...potionLines].join("\n") + || "Your inventory is currently empty."; logger.info(`[ECONOMY] Inventory retrieved`, { userId, @@ -67,4 +75,4 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'inventory' }) -}; \ No newline at end of file +}; diff --git a/src/commands/Economy/use.js b/src/commands/Economy/use.js new file mode 100644 index 0000000000..a94beac6f9 --- /dev/null +++ b/src/commands/Economy/use.js @@ -0,0 +1,88 @@ +import { SlashCommandBuilder, MessageFlags } from 'discord.js'; +import { successEmbed } from '../../utils/embeds.js'; +import { getItemById } from '../../config/shop/items.js'; +import { getEconomyData, setEconomyData } from '../../utils/economy.js'; +import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; +import { InteractionHelper } from '../../utils/interactionHelper.js'; + +const ADMIN_POTION_ID = 'admin_potion'; +const DEFAULT_LUCK_POTION_DURATION_MS = 5 * 60 * 1000; + +export default { + data: new SlashCommandBuilder() + .setName('use') + .setDescription('Use a consumable item from your inventory (e.g. Admin Potion)') + .addStringOption(option => + option + .setName('item_id') + .setDescription('ID of the item to use (e.g. admin_potion)') + .setRequired(true) + ), + + execute: withErrorHandling(async (interaction, config, client) => { + const deferred = await InteractionHelper.safeDefer(interaction); + if (!deferred) return; + + const userId = interaction.user.id; + const guildId = interaction.guildId; + const itemId = interaction.options.getString("item_id").toLowerCase(); + + const item = getItemById(itemId); + if (!item) { + throw createError( + `Item ${itemId} not found`, + ErrorTypes.VALIDATION, + `The item ID \`${itemId}\` does not exist.`, + { itemId } + ); + } + + const userData = await getEconomyData(client, guildId, userId); + const held = Number(userData.inventory?.[itemId]) || 0; + + if (held <= 0) { + throw createError( + "Item not in inventory", + ErrorTypes.VALIDATION, + `You don't have **${item.name}** in your inventory.`, + { itemId } + ); + } + + if (item.effect?.type !== 'fishing_luck') { + throw createError( + "Item not usable", + ErrorTypes.VALIDATION, + `**${item.name}** cannot be used right now.`, + { itemId } + ); + } + + const now = Date.now(); + const potionExpiries = userData.luckPotionExpiresAt || {}; + + const duration = Number(item.effect?.duration) || DEFAULT_LUCK_POTION_DURATION_MS; + const currentExpiry = Math.max(now, Number(potionExpiries[itemId]) || 0); + potionExpiries[itemId] = currentExpiry + duration; + userData.luckPotionExpiresAt = potionExpiries; + + const remaining = held - 1; + if (remaining > 0) { + userData.inventory[itemId] = remaining; + } else { + delete userData.inventory[itemId]; + } + + await setEconomyData(client, guildId, userId, userData); + + const activeSeconds = Math.ceil((potionExpiries[itemId] - now) / 1000); + let description = `You used **${item.name}**! It is now active for **${activeSeconds}s**.`; + if (itemId === ADMIN_POTION_ID) { + description += `\n\nπŸ‘‘ **Luck: Divine 30% / Transcendent 20% / Kitsune 10%**`; + } + + const embed = successEmbed("✨ Item Used", description); + + await InteractionHelper.safeEditReply(interaction, { embeds: [embed], flags: [MessageFlags.Ephemeral] }); + }, { command: 'use' }) +}; diff --git a/src/config/bot.js b/src/config/bot.js index 86dc861cc3..de9489bc04 100644 --- a/src/config/bot.js +++ b/src/config/bot.js @@ -24,7 +24,7 @@ export const botConfig = { activities: [ { name: "Custom Status", // required by Discord API, not shown in the client - state: "stalking", // this is what people actually see + state: "hari hari mau eek", // this is what people actually see type: 4, // Custom }, ], @@ -97,7 +97,7 @@ export const botConfig = { secondary: "#2F3136", // Standard status colors for success/error/warning/info messages. - success: "#57F287", + success: "#ff88d7", error: "#ED4245", warning: "#FEE75C", info: "#3498DB", @@ -141,7 +141,7 @@ export const botConfig = { }, footer: { // Default footer text used in bot embeds. - text: "Titan Bot", + text: "MannNayla", // Footer icon URL (null = no icon). icon: null, }, @@ -648,4 +648,4 @@ export function getRandomColor() { return colors[Math.floor(Math.random() * colors.length)]; } -export default botConfig; \ No newline at end of file +export default botConfig; diff --git a/src/config/commands/commandAliases.js b/src/config/commands/commandAliases.js index b23023a143..10b2086234 100644 --- a/src/config/commands/commandAliases.js +++ b/src/config/commands/commandAliases.js @@ -17,7 +17,6 @@ export const commandAliases = { 'rob': 'rob', 'crime': 'crime', 'pay': 'pay', - 'give': 'pay', 'send': 'pay', 'ping': 'ping', diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 5f47eaeaf5..8cedd4bded 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -2,7 +2,7 @@ export const shopItems = [ { id: 'extra_work', name: 'Extra Work Shift', - price: 5000, + price: 300000, description: 'Allows 1 extra use of the `/work` command.', type: 'consumable', maxQuantity: 5, @@ -16,7 +16,7 @@ cooldown: 86400000, { id: 'bank_upgrade_1', name: 'Bank Upgrade I', - price: 15000, + price: 150000, description: 'Increases bank capacity and allows more funds to be deposited.', type: 'upgrade', maxLevel: 5, @@ -40,19 +40,19 @@ cooldown: 86400000, { id: 'premium_role', name: 'Premium Server Role', - price: 15000, - description: 'A special role granting a fancy color and a 10% daily bonus.', + price: 1500000, + description: 'A special role granting a fancy color and a 10% fishing earnings bonus.', type: 'role', roleId: null, effect: { - type: 'daily_bonus', - multiplier: 1.1 + type: 'fishing_yield', + multiplier: 4 } }, { id: 'lucky_clover', name: 'Lucky Clover', - price: 10000, + price: 100000, description: 'Increases the chance of winning a higher payout on `/gamble` once.', type: 'consumable', maxQuantity: 10, @@ -62,22 +62,162 @@ roleId: null, uses: 1 } }, + // ===== Fishing Rods (Tiers I - V) ===== { - id: 'fishing_rod', - name: '🎣 Fishing Rod', + id: 'fishing_rod_1', + name: '🎣 Fishing Rod I', price: 5000, - description: 'Used for fishing commands', + description: 'A basic fishing rod. Boosts fishing earnings by 10%.', type: 'tool', durability: 100, effect: { type: 'fishing_yield', - multiplier: 1.0 + multiplier: 1.1, + tier: 1 + } + }, + { + id: 'fishing_rod_2', + name: '🎣 Fishing Rod II', + price: 25000, + description: 'A sturdy fishing rod. Boosts fishing earnings by 25%.', + type: 'tool', + durability: 150, + effect: { + type: 'fishing_yield', + multiplier: 1.25, + tier: 2 + } + }, + { + id: 'fishing_rod_3', + name: '🎣 Fishing Rod III', + price: 75000, + description: 'A reinforced fishing rod. Boosts fishing earnings by 50%.', + type: 'tool', + durability: 200, + effect: { + type: 'fishing_yield', + multiplier: 1.5, + tier: 3 + } + }, + { + id: 'fishing_rod_4', + name: '🎣 Fishing Rod IV', + price: 200000, + description: 'A premium fishing rod. Boosts fishing earnings by 75%.', + type: 'tool', + durability: 250, + effect: { + type: 'fishing_yield', + multiplier: 1.75, + tier: 4 + } + }, + { + id: 'fishing_rod_5', + name: '🎣 Fishing Rod V', + price: 500000, + description: 'A legendary fishing rod. Boosts fishing earnings by 100%.', + type: 'tool', + durability: 300, + effect: { + type: 'fishing_yield', + multiplier: 2.0, + tier: 5 + } + }, + + // ===== Luck Potions (Tiers I - V) ===== + { + id: 'luck_potion_1', + name: 'πŸ§ͺ Luck Potion I', + price: 1000, + description: 'Boosts your fishing luck slightly for 5 minutes. Higher chance of rarer fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 1.15, + duration: 300000 + } + }, + { + id: 'luck_potion_2', + name: 'πŸ§ͺ Luck Potion II', + price: 7000, + description: 'Boosts your fishing luck for 5 minutes. Noticeably higher chance of rarer fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 1.3, + duration: 300000 + } + }, + { + id: 'luck_potion_3', + name: 'πŸ§ͺ Luck Potion III', + price: 50000, + description: 'Boosts your fishing luck for 5 minutes. Good chance of rarer fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 1.5, + duration: 300000 + } + }, + { + id: 'luck_potion_4', + name: 'πŸ§ͺ Luck Potion IV', + price: 100000, + description: 'Boosts your fishing luck for 5 minutes. Great chance of rarer fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 1.8, + duration: 300000 + } + }, + { + id: 'luck_potion_5', + name: 'πŸ§ͺ Luck Potion V', + price: 150000, + description: 'Maximizes your fishing luck for 5 minutes. Best chance of legendary+ fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 20000, + duration: 2.1 + } + }, + + // ===== Admin Potion (disimpan di inventory, dipakai dengan /use, bisa dikirim dengan /give) ===== + { + id: 'admin_potion', + name: 'πŸ‘‘ Admin Potion', + price: 100, + description: 'Admin potion. Activate with `/use` for 5 minutes of great luck: Divine 30%, Transcendent 20%, Kitsune 10%. Can be gifted with `/give`.', + type: 'consumable', + effect: { + type: 'fishing_luck', + admin: true, + chances: { + divine: 0.3, + transcendent: 0.2, + celestial: 0.1 + }, + duration: 300000 } }, { id: 'pickaxe', name: '⛏️ Pickaxe', - price: 7500, + price: 75000, description: 'Used for mining commands', type: 'tool', durability: 100, @@ -89,7 +229,7 @@ roleId: null, { id: 'laptop', name: 'πŸ’» Laptop', - price: 15000, + price: 500000, description: 'Increases work earnings', type: 'tool', durability: 200, @@ -101,10 +241,10 @@ roleId: null, { id: 'lucky_charm', name: 'πŸ€ Lucky Charm', - price: 10000, + price: 100000, description: 'Increases luck for gambling. Has 3 uses before being consumed.', type: 'consumable', - maxQuantity: 10, + maxQuantity: 100, effect: { type: 'gamble_boost', multiplier: 1.3, @@ -114,7 +254,7 @@ roleId: null, { id: 'bank_note', name: 'πŸ“œ Bank Note', - price: 25000, + price: 250000, description: 'Increases bank capacity by 10,000. Can be purchased multiple times.', type: 'tool', durability: null, @@ -126,7 +266,7 @@ roleId: null, { id: 'personal_safe', name: 'πŸ”’ Personal Safe', - price: 30000, + price: 300000, description: 'Protects your money from theft. Prevents others from robbing you.', type: 'tool', durability: null, @@ -200,4 +340,4 @@ export function validatePurchase(itemId, userData) { } return { valid: true }; -} \ No newline at end of file +} diff --git a/src/utils/constants.js b/src/utils/constants.js index 18382d6225..bd1b1f10a0 100644 --- a/src/utils/constants.js +++ b/src/utils/constants.js @@ -18,6 +18,7 @@ export const DEFAULT_ECONOMY_DATA = { lastDeposit: 0, lastWithdraw: 0, inventory: {}, + fishCollection: {}, upgrades: {}, cooldowns: {} }; @@ -94,4 +95,4 @@ export default { DEFAULTS, ERROR_DEFAULTS, TIME -}; \ No newline at end of file +}; diff --git a/src/utils/schemas.js b/src/utils/schemas.js index 1ecc1af4bf..9778ded3f4 100644 --- a/src/utils/schemas.js +++ b/src/utils/schemas.js @@ -96,6 +96,7 @@ export const EconomyDataSchema = z xp: z.number().int().nonnegative().default(0), level: z.number().int().nonnegative().default(1), inventory: z.record(z.any()).default({}), + fishCollection: z.record(z.number().int().nonnegative()).default({}), cooldowns: z.record(z.number().int().nonnegative()).default({}) }) .passthrough();