From 81b0e567340f87df0cb93b386bbaa1b30f2c2c50 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 01:52:34 +0700 Subject: [PATCH 01/48] Update bot.js --- src/config/bot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/bot.js b/src/config/bot.js index 86dc861cc3..01aea0567e 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 }, ], @@ -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; From 4e781a28fcd2884dce312bbbd0ea3852e772d880 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 01:55:13 +0700 Subject: [PATCH 02/48] Update bot.js --- src/config/bot.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/bot.js b/src/config/bot.js index 01aea0567e..de9489bc04 100644 --- a/src/config/bot.js +++ b/src/config/bot.js @@ -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, }, From 9ced70ab5e2f8dada6bb4f91a38d6bc7cbdd3dbe Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 01:57:01 +0700 Subject: [PATCH 03/48] Update fish.js --- src/commands/Economy/fish.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 5288f048bb..c9212e0042 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -4,7 +4,7 @@ import { getEconomyData, setEconomyData } from '../../utils/economy.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; import { InteractionHelper } from '../../utils/interactionHelper.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; @@ -125,8 +125,8 @@ export default { 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 +}; From b8d1925c52f5fba192b4b4d9746662e00ca90ed0 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 02:31:26 +0700 Subject: [PATCH 04/48] Update fish.js --- src/commands/Economy/fish.js | 103 +++++++++++++++++++++++++++-------- 1 file changed, 79 insertions(+), 24 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index c9212e0042..ccd4af5916 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -10,15 +10,45 @@ const BASE_MAX_REWARD = 900; const FISHING_ROD_MULTIPLIER = 1.5; const FISH_TYPES = [ + // Common { name: 'Bass', emoji: '๐ŸŸ', rarity: 'common' }, - { name: 'Salmon', 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' }, + + // Uncommon + { name: 'Tuna', emoji: '๐Ÿก', rarity: 'uncommon' }, + { name: 'Swordfish', emoji: '๐ŸŸ', rarity: 'uncommon' }, + { name: 'Octopus', 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' }, + + // Epic + { name: 'Golden Koi', emoji: '๐Ÿ ', rarity: 'epic' }, + { name: 'Blue Marlin', emoji: '๐ŸŸ', rarity: 'epic' }, + { name: 'Crystal Fish', emoji: '๐Ÿ’Ž', rarity: 'epic' }, + + // Legendary + { name: 'Golden Shark', emoji: '๐Ÿฆˆ', rarity: 'legendary' }, + { name: 'Sea Dragon', emoji: '๐Ÿ‰', rarity: 'legendary' }, + { name: 'Golden Whale', emoji: '๐Ÿ‹', rarity: 'legendary' }, + + // Mythic + { name: 'Leviathan', emoji: '๐Ÿ‰', rarity: 'mythic' }, + { name: 'Megalodon', emoji: '๐Ÿฆˆ', rarity: 'mythic' }, + { name: 'Phoenix Koi', emoji: '๐Ÿ”ฅ', rarity: 'mythic' }, + + // Secret + { name: 'Void Whale', emoji: '๐ŸŒŒ', rarity: 'secret' }, + { name: 'Ancient Kraken', emoji: '๐Ÿ™', rarity: 'secret' }, + { name: 'Abyss Serpent', 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' } ]; const CATCH_MESSAGES = [ @@ -62,28 +92,50 @@ 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)]; + + let 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) { - - fishCaught = FISH_TYPES.find(f => f.rarity === 'epic'); + selectedRarity = 'legendary'; + } else if (rand < 0.995) { + selectedRarity = 'mythic'; + } else if (rand < 0.9995) { + selectedRarity = 'secret'; } else { - - fishCaught = FISH_TYPES.find(f => f.rarity === 'legendary'); + selectedRarity = 'divine'; } + const availableFish = FISH_TYPES.filter( + f => f.rarity === selectedRarity + ); + + const fishCaught = availableFish[ + Math.floor(Math.random() * availableFish.length) + ]; + const fishPrices = { + common: [300, 900], + uncommon: [1000, 2000], + rare: [2500, 4000], + epic: [5000, 8000], + legendary: [10000, 20000], + mythic: [30000, 50000], + secret: [75000, 150000], + divine: [250000, 500000] + }; + + const [minPrice, maxPrice] = fishPrices[fishCaught.rarity]; + const baseEarned = Math.floor( - Math.random() * (BASE_MAX_REWARD - BASE_MIN_REWARD + 1) - ) + BASE_MIN_REWARD; + Math.random() * (maxPrice - minPrice + 1) + ) + minPrice; let finalEarned = baseEarned; let multiplierMessage = ""; @@ -105,7 +157,10 @@ export default { uncommon: '#2ECC71', rare: '#3498DB', epic: '#9B59B6', - legendary: '#F1C40F' + legendary: '#F1C40F', + mythic: '#E91E63', + secret: '#FF0000', + divine: '#00FFFF' }; const embed = createEmbed({ From cf2a9c5613a3827a4060755fb0e55812f58f8bf4 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 03:04:29 +0700 Subject: [PATCH 05/48] Update items.js --- src/config/shop/items.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 5f47eaeaf5..85ce6ed85b 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,7 +40,7 @@ cooldown: 86400000, { id: 'premium_role', name: 'Premium Server Role', - price: 15000, + price: 1000000, description: 'A special role granting a fancy color and a 10% daily bonus.', type: 'role', roleId: null, @@ -52,7 +52,7 @@ roleId: null, { 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, @@ -77,7 +77,7 @@ roleId: null, { id: 'pickaxe', name: 'โ›๏ธ Pickaxe', - price: 7500, + price: 75000, description: 'Used for mining commands', type: 'tool', durability: 100, @@ -89,7 +89,7 @@ roleId: null, { id: 'laptop', name: '๐Ÿ’ป Laptop', - price: 15000, + price: 500000, description: 'Increases work earnings', type: 'tool', durability: 200, @@ -101,10 +101,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 +114,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 +126,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 +200,4 @@ export function validatePurchase(itemId, userData) { } return { valid: true }; -} \ No newline at end of file +} From 98fa6c446bd661a350959fb25b41477d1e1eb138 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 03:42:46 +0700 Subject: [PATCH 06/48] Update fish.js --- src/commands/Economy/fish.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index ccd4af5916..1287f2e70a 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -121,14 +121,14 @@ export default { Math.floor(Math.random() * availableFish.length) ]; const fishPrices = { - common: [300, 900], - uncommon: [1000, 2000], - rare: [2500, 4000], - epic: [5000, 8000], - legendary: [10000, 20000], + common: [300, 800], + uncommon: [900, 1500], + rare: [2000, 2500], + epic: [3500, 6000], + legendary: [7500, 15000], mythic: [30000, 50000], - secret: [75000, 150000], - divine: [250000, 500000] + secret: [60000, 100000], + divine: [125000, 350000] }; const [minPrice, maxPrice] = fishPrices[fishCaught.rarity]; From 503e9cce48004df1fc63e2e7e95b2899d0fed87b Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 10:03:56 +0000 Subject: [PATCH 07/48] feat: Buat Kode Pancingan dan Ramuan Keberuntungan Co-authored-by: traeagent --- src/commands/Economy/fish.js | 57 ++++++++++++++-- src/config/shop/items.js | 129 +++++++++++++++++++++++++++++++++-- 2 files changed, 175 insertions(+), 11 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 1287f2e70a..4d7ce4cd61 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -4,10 +4,35 @@ 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'; + const FISH_COOLDOWN = 0; const BASE_MIN_REWARD = 300; const BASE_MAX_REWARD = 900; -const FISHING_ROD_MULTIPLIER = 1.5; + +// 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}`; + if ((inventory[id] || 0) > 0) { + const item = getItemById(id); + if (item) return item; + } + } + return null; +} + +// Helper: find the highest-tier luck potion the user owns +function getBestLuckPotion(inventory) { + for (let tier = 5; tier >= 1; tier--) { + const id = `luck_potion_${tier}`; + if ((inventory[id] || 0) > 0) { + const item = getItemById(id); + if (item) return item; + } + } + return null; +} const FISH_TYPES = [ // Common @@ -74,7 +99,10 @@ 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 bestRod = getBestFishingRod(inventory); + const bestPotion = getBestLuckPotion(inventory); if (now < lastFish + FISH_COOLDOWN) { const remaining = lastFish + FISH_COOLDOWN - now; @@ -91,7 +119,19 @@ export default { ); } - const rand = Math.random(); + let rand = Math.random(); + + // Apply luck potion boost (shifts roll toward rarer fish) + let luckMultiplier = 1; + let luckMessage = ""; + if (bestPotion) { + luckMultiplier = bestPotion.effect.multiplier || 1; + rand = Math.min(rand * luckMultiplier, 0.9999); + luckMessage = `\n๐Ÿงช **${bestPotion.name} Active: x${luckMultiplier} luck**`; + // Consume one use of the potion + inventory[bestPotion.id] = (inventory[bestPotion.id] || 0) - 1; + if (inventory[bestPotion.id] <= 0) delete inventory[bestPotion.id]; + } let selectedRarity; @@ -140,15 +180,18 @@ export default { let finalEarned = baseEarned; let multiplierMessage = ""; - if (hasFishingRod > 0) { - finalEarned = Math.floor(baseEarned * FISHING_ROD_MULTIPLIER); - multiplierMessage = `\n๐ŸŽฃ **Fishing Rod Bonus: +50%**`; + if (bestRod) { + const rodMultiplier = bestRod.effect.multiplier || 1; + finalEarned = Math.floor(baseEarned * rodMultiplier); + const boostPct = Math.round((rodMultiplier - 1) * 100); + multiplierMessage = `\n๐ŸŽฃ **${bestRod.name} Bonus: +${boostPct}%**`; } const catchMessage = CATCH_MESSAGES[Math.floor(Math.random() * CATCH_MESSAGES.length)]; userData.wallet += finalEarned; userData.lastFish = now; + userData.inventory = inventory; await setEconomyData(client, guildId, userId, userData); @@ -165,7 +208,7 @@ export default { 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( diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 85ce6ed85b..6d247c1ab5 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -62,16 +62,137 @@ 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 0%.', type: 'tool', durability: 100, effect: { type: 'fishing_yield', - multiplier: 1.0 + multiplier: 1.0, + tier: 1 + } + }, + { + id: 'fishing_rod_2', + name: '๐ŸŽฃ Fishing Rod II', + price: 25000, + description: 'A sturdy fishing rod. Boosts fishing earnings by 50%.', + type: 'tool', + durability: 150, + effect: { + type: 'fishing_yield', + multiplier: 1.5, + tier: 2 + } + }, + { + id: 'fishing_rod_3', + name: '๐ŸŽฃ Fishing Rod III', + price: 75000, + description: 'A reinforced fishing rod. Boosts fishing earnings by 100%.', + type: 'tool', + durability: 200, + effect: { + type: 'fishing_yield', + multiplier: 2.0, + tier: 3 + } + }, + { + id: 'fishing_rod_4', + name: '๐ŸŽฃ Fishing Rod IV', + price: 200000, + description: 'A premium fishing rod. Boosts fishing earnings by 150%.', + type: 'tool', + durability: 250, + effect: { + type: 'fishing_yield', + multiplier: 2.5, + tier: 4 + } + }, + { + id: 'fishing_rod_5', + name: '๐ŸŽฃ Fishing Rod V', + price: 500000, + description: 'A legendary fishing rod. Boosts fishing earnings by 200%.', + type: 'tool', + durability: 300, + effect: { + type: 'fishing_yield', + multiplier: 3.0, + tier: 5 + } + }, + + // ===== Luck Potions (Tiers I - V) ===== + { + id: 'luck_potion_1', + name: '๐Ÿงช Luck Potion I', + price: 10000, + description: 'Boosts your fishing luck slightly for one catch. Higher chance of rarer fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 1.15, + uses: 1 + } + }, + { + id: 'luck_potion_2', + name: '๐Ÿงช Luck Potion II', + price: 30000, + description: 'Boosts your fishing luck for one catch. Noticeably higher chance of rarer fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 1.3, + uses: 1 + } + }, + { + id: 'luck_potion_3', + name: '๐Ÿงช Luck Potion III', + price: 75000, + description: 'Boosts your fishing luck for one catch. Good chance of rarer fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 1.5, + uses: 1 + } + }, + { + id: 'luck_potion_4', + name: '๐Ÿงช Luck Potion IV', + price: 150000, + description: 'Boosts your fishing luck for one catch. Great chance of rarer fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 1.8, + uses: 1 + } + }, + { + id: 'luck_potion_5', + name: '๐Ÿงช Luck Potion V', + price: 300000, + description: 'Maximizes your fishing luck for one catch. Best chance of legendary+ fish.', + type: 'consumable', + maxQuantity: 10, + effect: { + type: 'fishing_luck', + multiplier: 2.2, + uses: 1 } }, { From 6c18db0025bedb6874d10830f245bd35c69da29b Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 10:11:24 +0000 Subject: [PATCH 08/48] nerf: kurangi harga ikan & multiplier rod biar lebih susah --- src/commands/Economy/fish.js | 16 ++++++++-------- src/config/shop/items.js | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 4d7ce4cd61..e47e7d118a 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -161,14 +161,14 @@ export default { Math.floor(Math.random() * availableFish.length) ]; const fishPrices = { - common: [300, 800], - uncommon: [900, 1500], - rare: [2000, 2500], - epic: [3500, 6000], - legendary: [7500, 15000], - mythic: [30000, 50000], - secret: [60000, 100000], - divine: [125000, 350000] + common: [150, 400], + uncommon: [450, 750], + rare: [1000, 1250], + epic: [1750, 3000], + legendary: [3750, 7500], + mythic: [15000, 25000], + secret: [30000, 50000], + divine: [62500, 175000] }; const [minPrice, maxPrice] = fishPrices[fishCaught.rarity]; diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 6d247c1ab5..23d2a77f3e 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -80,12 +80,12 @@ roleId: null, id: 'fishing_rod_2', name: '๐ŸŽฃ Fishing Rod II', price: 25000, - description: 'A sturdy fishing rod. Boosts fishing earnings by 50%.', + description: 'A sturdy fishing rod. Boosts fishing earnings by 25%.', type: 'tool', durability: 150, effect: { type: 'fishing_yield', - multiplier: 1.5, + multiplier: 1.25, tier: 2 } }, @@ -93,12 +93,12 @@ roleId: null, id: 'fishing_rod_3', name: '๐ŸŽฃ Fishing Rod III', price: 75000, - description: 'A reinforced fishing rod. Boosts fishing earnings by 100%.', + description: 'A reinforced fishing rod. Boosts fishing earnings by 50%.', type: 'tool', durability: 200, effect: { type: 'fishing_yield', - multiplier: 2.0, + multiplier: 1.5, tier: 3 } }, @@ -106,12 +106,12 @@ roleId: null, id: 'fishing_rod_4', name: '๐ŸŽฃ Fishing Rod IV', price: 200000, - description: 'A premium fishing rod. Boosts fishing earnings by 150%.', + description: 'A premium fishing rod. Boosts fishing earnings by 75%.', type: 'tool', durability: 250, effect: { type: 'fishing_yield', - multiplier: 2.5, + multiplier: 1.75, tier: 4 } }, @@ -119,12 +119,12 @@ roleId: null, id: 'fishing_rod_5', name: '๐ŸŽฃ Fishing Rod V', price: 500000, - description: 'A legendary fishing rod. Boosts fishing earnings by 200%.', + description: 'A legendary fishing rod. Boosts fishing earnings by 100%.', type: 'tool', durability: 300, effect: { type: 'fishing_yield', - multiplier: 3.0, + multiplier: 2.0, tier: 5 } }, From 4ec18d7061d74d3938c36c50c01b1f6e93628872 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 17:50:22 +0700 Subject: [PATCH 09/48] Update fish.js --- src/commands/Economy/fish.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index e47e7d118a..f4d4e7c95b 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -161,7 +161,7 @@ export default { Math.floor(Math.random() * availableFish.length) ]; const fishPrices = { - common: [150, 400], + common: [200, 450], uncommon: [450, 750], rare: [1000, 1250], epic: [1750, 3000], From a964f14f2e096e1e0d1b4f21597db9f2cb3e18eb Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 18:26:36 +0700 Subject: [PATCH 10/48] Update items.js --- src/config/shop/items.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 23d2a77f3e..03310b382a 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -64,7 +64,7 @@ roleId: null, }, // ===== Fishing Rods (Tiers I - V) ===== { - id: 'fishing_rod_1', + id: 'rod1', name: '๐ŸŽฃ Fishing Rod I', price: 5000, description: 'A basic fishing rod. Boosts fishing earnings by 0%.', @@ -77,7 +77,7 @@ roleId: null, } }, { - id: 'fishing_rod_2', + id: 'rod2', name: '๐ŸŽฃ Fishing Rod II', price: 25000, description: 'A sturdy fishing rod. Boosts fishing earnings by 25%.', @@ -90,7 +90,7 @@ roleId: null, } }, { - id: 'fishing_rod_3', + id: 'rod3', name: '๐ŸŽฃ Fishing Rod III', price: 75000, description: 'A reinforced fishing rod. Boosts fishing earnings by 50%.', @@ -103,7 +103,7 @@ roleId: null, } }, { - id: 'fishing_rod_4', + id: 'rod4', name: '๐ŸŽฃ Fishing Rod IV', price: 200000, description: 'A premium fishing rod. Boosts fishing earnings by 75%.', @@ -116,7 +116,7 @@ roleId: null, } }, { - id: 'fishing_rod_5', + id: 'rod5', name: '๐ŸŽฃ Fishing Rod V', price: 500000, description: 'A legendary fishing rod. Boosts fishing earnings by 100%.', @@ -131,7 +131,7 @@ roleId: null, // ===== Luck Potions (Tiers I - V) ===== { - id: 'luck_potion_1', + id: 'luck1', name: '๐Ÿงช Luck Potion I', price: 10000, description: 'Boosts your fishing luck slightly for one catch. Higher chance of rarer fish.', @@ -144,7 +144,7 @@ roleId: null, } }, { - id: 'luck_potion_2', + id: 'luck2', name: '๐Ÿงช Luck Potion II', price: 30000, description: 'Boosts your fishing luck for one catch. Noticeably higher chance of rarer fish.', @@ -157,7 +157,7 @@ roleId: null, } }, { - id: 'luck_potion_3', + id: 'luck3', name: '๐Ÿงช Luck Potion III', price: 75000, description: 'Boosts your fishing luck for one catch. Good chance of rarer fish.', @@ -170,7 +170,7 @@ roleId: null, } }, { - id: 'luck_potion_4', + id: 'luck4', name: '๐Ÿงช Luck Potion IV', price: 150000, description: 'Boosts your fishing luck for one catch. Great chance of rarer fish.', @@ -183,7 +183,7 @@ roleId: null, } }, { - id: 'luck_potion_5', + id: 'luck5', name: '๐Ÿงช Luck Potion V', price: 300000, description: 'Maximizes your fishing luck for one catch. Best chance of legendary+ fish.', From 2ff2509070663637d4ad28a54c7d7df9564bd818 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 19:05:19 +0700 Subject: [PATCH 11/48] Update items.js --- src/config/shop/items.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 03310b382a..5dcbcebb39 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -64,7 +64,7 @@ roleId: null, }, // ===== Fishing Rods (Tiers I - V) ===== { - id: 'rod1', + id: 'fishing_rod_1', name: '๐ŸŽฃ Fishing Rod I', price: 5000, description: 'A basic fishing rod. Boosts fishing earnings by 0%.', @@ -77,7 +77,7 @@ roleId: null, } }, { - id: 'rod2', + id: 'fishing_rod_2', name: '๐ŸŽฃ Fishing Rod II', price: 25000, description: 'A sturdy fishing rod. Boosts fishing earnings by 25%.', @@ -90,7 +90,7 @@ roleId: null, } }, { - id: 'rod3', + id: 'fishing_rod_3', name: '๐ŸŽฃ Fishing Rod III', price: 75000, description: 'A reinforced fishing rod. Boosts fishing earnings by 50%.', @@ -103,7 +103,7 @@ roleId: null, } }, { - id: 'rod4', + id: 'fishing_rod_4', name: '๐ŸŽฃ Fishing Rod IV', price: 200000, description: 'A premium fishing rod. Boosts fishing earnings by 75%.', @@ -116,7 +116,7 @@ roleId: null, } }, { - id: 'rod5', + id: 'fishibg_rod_5', name: '๐ŸŽฃ Fishing Rod V', price: 500000, description: 'A legendary fishing rod. Boosts fishing earnings by 100%.', From 3bb0b184dc72f55d6640ec4c4bc1dfec547f9870 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 19:08:51 +0700 Subject: [PATCH 12/48] Update items.js --- src/config/shop/items.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 5dcbcebb39..3607f9f84e 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -131,7 +131,7 @@ roleId: null, // ===== Luck Potions (Tiers I - V) ===== { - id: 'luck1', + id: 'luck_potion_1', name: '๐Ÿงช Luck Potion I', price: 10000, description: 'Boosts your fishing luck slightly for one catch. Higher chance of rarer fish.', @@ -144,7 +144,7 @@ roleId: null, } }, { - id: 'luck2', + id: 'luck_potion_2', name: '๐Ÿงช Luck Potion II', price: 30000, description: 'Boosts your fishing luck for one catch. Noticeably higher chance of rarer fish.', @@ -157,7 +157,7 @@ roleId: null, } }, { - id: 'luck3', + id: 'luck_potion_3', name: '๐Ÿงช Luck Potion III', price: 75000, description: 'Boosts your fishing luck for one catch. Good chance of rarer fish.', @@ -170,7 +170,7 @@ roleId: null, } }, { - id: 'luck4', + id: 'luck_potion_4', name: '๐Ÿงช Luck Potion IV', price: 150000, description: 'Boosts your fishing luck for one catch. Great chance of rarer fish.', @@ -183,7 +183,7 @@ roleId: null, } }, { - id: 'luck5', + id: 'luck_potion_5', name: '๐Ÿงช Luck Potion V', price: 300000, description: 'Maximizes your fishing luck for one catch. Best chance of legendary+ fish.', From e9d70802ddca3fe846652c163b7640f62d0f19b2 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 19:18:22 +0700 Subject: [PATCH 13/48] Update items.js --- src/config/shop/items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 3607f9f84e..c78934103c 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -72,7 +72,7 @@ roleId: null, durability: 100, effect: { type: 'fishing_yield', - multiplier: 1.0, + multiplier: 1.1, tier: 1 } }, From fc164e2382152e4a55231f5e90995934d981f121 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 19:19:02 +0700 Subject: [PATCH 14/48] Update items.js --- src/config/shop/items.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index c78934103c..d19f9f43d4 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -40,13 +40,13 @@ cooldown: 86400000, { id: 'premium_role', name: 'Premium Server Role', - price: 1000000, + price: 1500000, description: 'A special role granting a fancy color and a 10% daily bonus.', type: 'role', roleId: null, effect: { type: 'daily_bonus', - multiplier: 1.1 + multiplier: 5 } }, { From d1f262967ebbab32139635126f0342755af4aa58 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 13:14:39 +0000 Subject: [PATCH 15/48] fix: move premium bonus to fishing --- src/commands/Economy/buy.js | 19 +++++++++- src/commands/Economy/daily.js | 31 +++------------- src/commands/Economy/fish.js | 60 +++++++++++++++++++++++-------- src/commands/Economy/inventory.js | 41 ++++++++++----------- src/config/shop/items.js | 28 +++++++-------- 5 files changed, 102 insertions(+), 77 deletions(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index 1a90387315..0781d6f52b 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -7,6 +7,14 @@ import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHan import { InteractionHelper } from '../../utils/interactionHelper.js'; const SHOP_ITEMS = shopItems; +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,6 +144,15 @@ 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; @@ -157,4 +174,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 f4d4e7c95b..a82dfc9ef2 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -3,12 +3,14 @@ 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 = 0; const BASE_MIN_REWARD = 300; const BASE_MAX_REWARD = 900; +const DEFAULT_LUCK_POTION_DURATION_MS = 5 * 60 * 1000; +const PREMIUM_FISHING_MULTIPLIER = getItemById('premium_role')?.effect?.multiplier ?? 1.1; // Helper: find the highest-tier fishing rod the user owns function getBestFishingRod(inventory) { @@ -22,11 +24,11 @@ function getBestFishingRod(inventory) { return null; } -// Helper: find the highest-tier luck potion the user owns -function getBestLuckPotion(inventory) { +// 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 ((inventory[id] || 0) > 0) { + if (Number(potionExpiries[id]) > now) { const item = getItemById(id); if (item) return item; } @@ -100,9 +102,28 @@ export default { const userData = await getEconomyData(client, guildId, userId); const lastFish = userData.lastFish || 0; const inventory = userData.inventory || {}; + const potionExpiries = { ...(userData.luckPotionExpiresAt || {}) }; const bestRod = getBestFishingRod(inventory); - const bestPotion = getBestLuckPotion(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; @@ -121,16 +142,16 @@ export default { let rand = Math.random(); - // Apply luck potion boost (shifts roll toward rarer fish) + // Terapkan potion terkuat yang masih aktif tanpa menghabiskannya. let luckMultiplier = 1; let luckMessage = ""; if (bestPotion) { luckMultiplier = bestPotion.effect.multiplier || 1; rand = Math.min(rand * luckMultiplier, 0.9999); - luckMessage = `\n๐Ÿงช **${bestPotion.name} Active: x${luckMultiplier} luck**`; - // Consume one use of the potion - inventory[bestPotion.id] = (inventory[bestPotion.id] || 0) - 1; - if (inventory[bestPotion.id] <= 0) delete inventory[bestPotion.id]; + const minutesLeft = Math.ceil( + (Number(potionExpiries[bestPotion.id]) - now) / 60000 + ); + luckMessage = `\n๐Ÿงช **${bestPotion.name} Active: x${luckMultiplier} luck (${minutesLeft}m left)**`; } let selectedRarity; @@ -177,21 +198,30 @@ export default { Math.random() * (maxPrice - minPrice + 1) ) + minPrice; - let finalEarned = baseEarned; - let multiplierMessage = ""; - + let fishingMultiplier = 1; + const bonusMessages = []; if (bestRod) { const rodMultiplier = bestRod.effect.multiplier || 1; - finalEarned = Math.floor(baseEarned * rodMultiplier); + fishingMultiplier *= rodMultiplier; const boostPct = Math.round((rodMultiplier - 1) * 100); - multiplierMessage = `\n๐ŸŽฃ **${bestRod.name} Bonus: +${boostPct}%**`; + bonusMessages.push(`๐ŸŽฃ **${bestRod.name} Bonus: +${boostPct}%**`); + } + if (hasPremiumRole) { + fishingMultiplier *= PREMIUM_FISHING_MULTIPLIER; + const boostPct = Math.round((PREMIUM_FISHING_MULTIPLIER - 1) * 100); + bonusMessages.push(`โœจ **Premium Fishing 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.inventory = inventory; + userData.luckPotionExpiresAt = potionExpiries; await setEconomyData(client, guildId, userId, userData); diff --git a/src/commands/Economy/inventory.js b/src/commands/Economy/inventory.js index b7efcfec80..8e2c41b13e 100644 --- a/src/commands/Economy/inventory.js +++ b/src/commands/Economy/inventory.js @@ -34,25 +34,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 = SHOP_ITEMS.find(i => i.id === itemId); + return Number(quantity) > 0 && item; + }) + .map(([itemId, quantity]) => { + const item = SHOP_ITEMS.find(i => i.id === itemId); + return `**${item.name}:** ${quantity}x`; + }); + const potionLines = Object.entries(userData.luckPotionExpiresAt || {}) + .filter(([, expiresAt]) => Number(expiresAt) > Date.now()) + .map(([itemId, expiresAt]) => { + const item = SHOP_ITEMS.find(i => i.id === 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 +68,4 @@ export default { await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'inventory' }) -}; \ No newline at end of file +}; diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 23d2a77f3e..f337220954 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -41,11 +41,11 @@ cooldown: 86400000, id: 'premium_role', name: 'Premium Server Role', price: 1000000, - description: 'A special role granting a fancy color and a 10% daily bonus.', + description: 'A special role granting a fancy color and a 10% fishing earnings bonus.', type: 'role', roleId: null, effect: { - type: 'daily_bonus', + type: 'fishing_yield', multiplier: 1.1 } }, @@ -67,12 +67,12 @@ roleId: null, id: 'fishing_rod_1', name: '๐ŸŽฃ Fishing Rod I', price: 5000, - description: 'A basic fishing rod. Boosts fishing earnings by 0%.', + 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 } }, @@ -134,65 +134,65 @@ roleId: null, id: 'luck_potion_1', name: '๐Ÿงช Luck Potion I', price: 10000, - description: 'Boosts your fishing luck slightly for one catch. Higher chance of rarer fish.', + 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, - uses: 1 + duration: 300000 } }, { id: 'luck_potion_2', name: '๐Ÿงช Luck Potion II', price: 30000, - description: 'Boosts your fishing luck for one catch. Noticeably higher chance of rarer fish.', + 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, - uses: 1 + duration: 300000 } }, { id: 'luck_potion_3', name: '๐Ÿงช Luck Potion III', price: 75000, - description: 'Boosts your fishing luck for one catch. Good chance of rarer fish.', + description: 'Boosts your fishing luck for 5 minutes. Good chance of rarer fish.', type: 'consumable', maxQuantity: 10, effect: { type: 'fishing_luck', multiplier: 1.5, - uses: 1 + duration: 300000 } }, { id: 'luck_potion_4', name: '๐Ÿงช Luck Potion IV', price: 150000, - description: 'Boosts your fishing luck for one catch. Great chance of rarer fish.', + description: 'Boosts your fishing luck for 5 minutes. Great chance of rarer fish.', type: 'consumable', maxQuantity: 10, effect: { type: 'fishing_luck', multiplier: 1.8, - uses: 1 + duration: 300000 } }, { id: 'luck_potion_5', name: '๐Ÿงช Luck Potion V', price: 300000, - description: 'Maximizes your fishing luck for one catch. Best chance of legendary+ fish.', + description: 'Maximizes your fishing luck for 5 minutes. Best chance of legendary+ fish.', type: 'consumable', maxQuantity: 10, effect: { type: 'fishing_luck', multiplier: 2.2, - uses: 1 + duration: 300000 } }, { From 335b5c6fe9a2e7f51e40ac0225ca6f407dc95fee Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 20:19:09 +0700 Subject: [PATCH 16/48] fix: move premium bonus to fishing (#1) Co-authored-by: mannnayla1-code --- src/commands/Economy/buy.js | 19 ++++++++- src/commands/Economy/daily.js | 31 ++------------- src/commands/Economy/fish.js | 65 +++++++++++++++++++++++-------- src/commands/Economy/inventory.js | 48 +++++++++++++---------- src/config/shop/items.js | 30 +++++++------- 5 files changed, 114 insertions(+), 79 deletions(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index 1a90387315..0781d6f52b 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -7,6 +7,14 @@ import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHan import { InteractionHelper } from '../../utils/interactionHelper.js'; const SHOP_ITEMS = shopItems; +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,6 +144,15 @@ 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; @@ -157,4 +174,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 f4d4e7c95b..d704964319 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -3,18 +3,23 @@ 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 = 0; const BASE_MIN_REWARD = 300; const BASE_MAX_REWARD = 900; +const DEFAULT_LUCK_POTION_DURATION_MS = 5 * 60 * 1000; +const PREMIUM_FISHING_MULTIPLIER = getItemById('premium_role')?.effect?.multiplier ?? 1.1; // 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}`; - if ((inventory[id] || 0) > 0) { + 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; } @@ -22,11 +27,11 @@ function getBestFishingRod(inventory) { return null; } -// Helper: find the highest-tier luck potion the user owns -function getBestLuckPotion(inventory) { +// 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 ((inventory[id] || 0) > 0) { + if (Number(potionExpiries[id]) > now) { const item = getItemById(id); if (item) return item; } @@ -100,9 +105,28 @@ export default { const userData = await getEconomyData(client, guildId, userId); const lastFish = userData.lastFish || 0; const inventory = userData.inventory || {}; + const potionExpiries = { ...(userData.luckPotionExpiresAt || {}) }; const bestRod = getBestFishingRod(inventory); - const bestPotion = getBestLuckPotion(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; @@ -121,16 +145,16 @@ export default { let rand = Math.random(); - // Apply luck potion boost (shifts roll toward rarer fish) + // Terapkan potion terkuat yang masih aktif tanpa menghabiskannya. let luckMultiplier = 1; let luckMessage = ""; if (bestPotion) { luckMultiplier = bestPotion.effect.multiplier || 1; rand = Math.min(rand * luckMultiplier, 0.9999); - luckMessage = `\n๐Ÿงช **${bestPotion.name} Active: x${luckMultiplier} luck**`; - // Consume one use of the potion - inventory[bestPotion.id] = (inventory[bestPotion.id] || 0) - 1; - if (inventory[bestPotion.id] <= 0) delete inventory[bestPotion.id]; + const minutesLeft = Math.ceil( + (Number(potionExpiries[bestPotion.id]) - now) / 60000 + ); + luckMessage = `\n๐Ÿงช **${bestPotion.name} Active: x${luckMultiplier} luck (${minutesLeft}m left)**`; } let selectedRarity; @@ -177,21 +201,30 @@ export default { Math.random() * (maxPrice - minPrice + 1) ) + minPrice; - let finalEarned = baseEarned; - let multiplierMessage = ""; - + let fishingMultiplier = 1; + const bonusMessages = []; if (bestRod) { const rodMultiplier = bestRod.effect.multiplier || 1; - finalEarned = Math.floor(baseEarned * rodMultiplier); + fishingMultiplier *= rodMultiplier; const boostPct = Math.round((rodMultiplier - 1) * 100); - multiplierMessage = `\n๐ŸŽฃ **${bestRod.name} Bonus: +${boostPct}%**`; + bonusMessages.push(`๐ŸŽฃ **${bestRod.name} Bonus: +${boostPct}%**`); + } + if (hasPremiumRole) { + fishingMultiplier *= PREMIUM_FISHING_MULTIPLIER; + const boostPct = Math.round((PREMIUM_FISHING_MULTIPLIER - 1) * 100); + bonusMessages.push(`โœจ **Premium Fishing 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.inventory = inventory; + userData.luckPotionExpiresAt = potionExpiries; await setEconomyData(client, guildId, userId, userData); 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/config/shop/items.js b/src/config/shop/items.js index d19f9f43d4..528b31bf5e 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -41,12 +41,12 @@ cooldown: 86400000, id: 'premium_role', name: 'Premium Server Role', price: 1500000, - description: 'A special role granting a fancy color and a 10% daily bonus.', + description: 'A special role granting a fancy color and a 10% fishing earnings bonus.', type: 'role', roleId: null, effect: { - type: 'daily_bonus', - multiplier: 5 + type: 'fishing_yield', + multiplier: 1.1 } }, { @@ -67,7 +67,7 @@ roleId: null, id: 'fishing_rod_1', name: '๐ŸŽฃ Fishing Rod I', price: 5000, - description: 'A basic fishing rod. Boosts fishing earnings by 0%.', + description: 'A basic fishing rod. Boosts fishing earnings by 10%.', type: 'tool', durability: 100, effect: { @@ -116,7 +116,7 @@ roleId: null, } }, { - id: 'fishibg_rod_5', + id: 'fishing_rod_5', name: '๐ŸŽฃ Fishing Rod V', price: 500000, description: 'A legendary fishing rod. Boosts fishing earnings by 100%.', @@ -134,65 +134,65 @@ roleId: null, id: 'luck_potion_1', name: '๐Ÿงช Luck Potion I', price: 10000, - description: 'Boosts your fishing luck slightly for one catch. Higher chance of rarer fish.', + 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, - uses: 1 + duration: 300000 } }, { id: 'luck_potion_2', name: '๐Ÿงช Luck Potion II', price: 30000, - description: 'Boosts your fishing luck for one catch. Noticeably higher chance of rarer fish.', + 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, - uses: 1 + duration: 300000 } }, { id: 'luck_potion_3', name: '๐Ÿงช Luck Potion III', price: 75000, - description: 'Boosts your fishing luck for one catch. Good chance of rarer fish.', + description: 'Boosts your fishing luck for 5 minutes. Good chance of rarer fish.', type: 'consumable', maxQuantity: 10, effect: { type: 'fishing_luck', multiplier: 1.5, - uses: 1 + duration: 300000 } }, { id: 'luck_potion_4', name: '๐Ÿงช Luck Potion IV', price: 150000, - description: 'Boosts your fishing luck for one catch. Great chance of rarer fish.', + description: 'Boosts your fishing luck for 5 minutes. Great chance of rarer fish.', type: 'consumable', maxQuantity: 10, effect: { type: 'fishing_luck', multiplier: 1.8, - uses: 1 + duration: 300000 } }, { id: 'luck_potion_5', name: '๐Ÿงช Luck Potion V', price: 300000, - description: 'Maximizes your fishing luck for one catch. Best chance of legendary+ fish.', + description: 'Maximizes your fishing luck for 5 minutes. Best chance of legendary+ fish.', type: 'consumable', maxQuantity: 10, effect: { type: 'fishing_luck', multiplier: 2.2, - uses: 1 + duration: 300000 } }, { From de92c60c2722b6dc499f7474613723eb3532191c Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 13:31:59 +0000 Subject: [PATCH 17/48] Update items.js --- src/config/shop/items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 528b31bf5e..9a955d75f9 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -46,7 +46,7 @@ cooldown: 86400000, roleId: null, effect: { type: 'fishing_yield', - multiplier: 1.1 + multiplier: 20 } }, { From 0fc9e0cbfdeff2e46b11fd5d759b9af5c3993d97 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 13:34:54 +0000 Subject: [PATCH 18/48] Update items.js --- src/config/shop/items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 9a955d75f9..84b96f01c2 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -46,7 +46,7 @@ cooldown: 86400000, roleId: null, effect: { type: 'fishing_yield', - multiplier: 20 + multiplier: 1000 } }, { From 37a5f72a96290e11eef070bf9e21eb7ba8d7066f Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 13:38:09 +0000 Subject: [PATCH 19/48] Update items.js --- src/config/shop/items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 84b96f01c2..fa4e7e9253 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -46,7 +46,7 @@ cooldown: 86400000, roleId: null, effect: { type: 'fishing_yield', - multiplier: 1000 + multiplier: 10 } }, { From 9fbd79dd22bd12aa48d1ca2f52e2a1e33db9e81f Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 13:44:11 +0000 Subject: [PATCH 20/48] Update items.js --- src/config/shop/items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index fa4e7e9253..6a73642a33 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -46,7 +46,7 @@ cooldown: 86400000, roleId: null, effect: { type: 'fishing_yield', - multiplier: 10 + multiplier: 10000 } }, { From 3c24ab43555dd540b18be7508a6dca35f99ec707 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 13:45:20 +0000 Subject: [PATCH 21/48] Update items.js --- src/config/shop/items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 6a73642a33..18ad2c2812 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -46,7 +46,7 @@ cooldown: 86400000, roleId: null, effect: { type: 'fishing_yield', - multiplier: 10000 + multiplier: 5 } }, { From 3f1c4751ba0d0e204fd39d4d97c3cb366d01e29a Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 13:50:39 +0000 Subject: [PATCH 22/48] Update fish.js From ef825a6c429374b89bfd4ece2224918d678d8bc0 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 13:52:42 +0000 Subject: [PATCH 23/48] Update fish.js --- src/commands/Economy/fish.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index d704964319..971cd572a6 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -212,7 +212,7 @@ export default { if (hasPremiumRole) { fishingMultiplier *= PREMIUM_FISHING_MULTIPLIER; const boostPct = Math.round((PREMIUM_FISHING_MULTIPLIER - 1) * 100); - bonusMessages.push(`โœจ **Premium Fishing Bonus: +${boostPct}%**`); + bonusMessages.push(`โœจ **Premium Fishing Bonus: +${boostPct}%**`); } const finalEarned = Math.floor(baseEarned * fishingMultiplier); const multiplierMessage = bonusMessages.length From a9f1026f1f37357a63a65fabf96ff1e12911d86c Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 14:07:03 +0000 Subject: [PATCH 24/48] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/fish.js | 53 +++++++++++++++++++++++++++++++++--- 1 file changed, 49 insertions(+), 4 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index d704964319..7dcba1a502 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -44,41 +44,82 @@ const FISH_TYPES = [ { name: 'Bass', emoji: '๐ŸŸ', rarity: 'common' }, { name: 'Salmon', emoji: '๐Ÿ ', rarity: 'common' }, { name: 'Trout', 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: '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: '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' } ]; const CATCH_MESSAGES = [ @@ -173,8 +214,10 @@ export default { selectedRarity = 'mythic'; } else if (rand < 0.9995) { selectedRarity = 'secret'; - } else { + } else if (rand < 0.9998) { selectedRarity = 'divine'; + } else { + selectedRarity = 'transcendent'; } const availableFish = FISH_TYPES.filter( @@ -192,7 +235,8 @@ export default { legendary: [3750, 7500], mythic: [15000, 25000], secret: [30000, 50000], - divine: [62500, 175000] + divine: [62500, 175000], + transcendent: [250000, 500000] }; const [minPrice, maxPrice] = fishPrices[fishCaught.rarity]; @@ -236,7 +280,8 @@ export default { legendary: '#F1C40F', mythic: '#E91E63', secret: '#FF0000', - divine: '#00FFFF' + divine: '#00FFFF', + transcendent: '#FF00FF' }; const embed = createEmbed({ From c42188b7271a28016f23f9181cb4db3a82f730a8 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 14:09:43 +0000 Subject: [PATCH 25/48] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/fish.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 7dcba1a502..600966480d 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -119,7 +119,10 @@ const FISH_TYPES = [ { name: 'Primordial Serpent', emoji: '๐Ÿ', rarity: 'transcendent' }, { name: 'Infinity Koi', emoji: 'โ™พ๏ธ', rarity: 'transcendent' }, { name: 'Genesis Leviathan', emoji: '๐ŸŒŸ', rarity: 'transcendent' }, - { name: 'Celestia Dragon', emoji: '๐Ÿฒ', rarity: 'transcendent' } + { name: 'Celestia Dragon', emoji: '๐Ÿฒ', rarity: 'transcendent' }, + + // Celestial (Above Transcendent) โ€” Kitsune sengaja TIDAK bisa dipancing + { name: 'Kitsune', emoji: '๐ŸฆŠ', rarity: 'celestial' } ]; const CATCH_MESSAGES = [ @@ -236,7 +239,8 @@ export default { mythic: [15000, 25000], secret: [30000, 50000], divine: [62500, 175000], - transcendent: [250000, 500000] + transcendent: [250000, 500000], + celestial: [1000000, 5000000] }; const [minPrice, maxPrice] = fishPrices[fishCaught.rarity]; @@ -281,7 +285,8 @@ export default { mythic: '#E91E63', secret: '#FF0000', divine: '#00FFFF', - transcendent: '#FF00FF' + transcendent: '#FF00FF', + celestial: '#FF69B4' }; const embed = createEmbed({ From a73a1b1eccfe6c403648e740d38ac644bb1fbe2d Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 14:12:03 +0000 Subject: [PATCH 26/48] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/fish.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 600966480d..2e49114fd9 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -121,7 +121,7 @@ const FISH_TYPES = [ { name: 'Genesis Leviathan', emoji: '๐ŸŒŸ', rarity: 'transcendent' }, { name: 'Celestia Dragon', emoji: '๐Ÿฒ', rarity: 'transcendent' }, - // Celestial (Above Transcendent) โ€” Kitsune sengaja TIDAK bisa dipancing + // Celestial (Above Transcendent) โ€” Kitsune, peluang 2x lebih susah dari transcendent { name: 'Kitsune', emoji: '๐ŸฆŠ', rarity: 'celestial' } ]; @@ -217,10 +217,12 @@ export default { selectedRarity = 'mythic'; } else if (rand < 0.9995) { selectedRarity = 'secret'; - } else if (rand < 0.9998) { + } else if (rand < 0.9997) { selectedRarity = 'divine'; - } else { + } else if (rand < 0.9999) { selectedRarity = 'transcendent'; + } else { + selectedRarity = 'celestial'; } const availableFish = FISH_TYPES.filter( From 3d737b934b61288b7dcbd784620bdb17611b4880 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 14:13:26 +0000 Subject: [PATCH 27/48] feat: update workspace Co-authored-by: traeagent --- src/commands/Economy/fish.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 2e49114fd9..531598401f 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -194,7 +194,9 @@ export default { let luckMessage = ""; if (bestPotion) { luckMultiplier = bestPotion.effect.multiplier || 1; - rand = Math.min(rand * luckMultiplier, 0.9999); + // Potion tidak bisa menembus celestial (Kitsune): cap di bawah ambang 0.9999, + // jadi paling tinggi hanya sampai transcendent. + rand = Math.min(rand * luckMultiplier, 0.9998); const minutesLeft = Math.ceil( (Number(potionExpiries[bestPotion.id]) - now) / 60000 ); From 68f1455335f84733a7240f7f3d3bcef29ad383e3 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 14:21:57 +0000 Subject: [PATCH 28/48] fix: smoother luck potion bias; divine & transcendent stay rare --- src/commands/Economy/fish.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 531598401f..1881f512b4 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -194,9 +194,12 @@ export default { let luckMessage = ""; if (bestPotion) { luckMultiplier = bestPotion.effect.multiplier || 1; - // Potion tidak bisa menembus celestial (Kitsune): cap di bawah ambang 0.9999, - // jadi paling tinggi hanya sampai transcendent. - rand = Math.min(rand * luckMultiplier, 0.9998); + // Bias halus: potion menaikkan peluang ikan langka tanpa "membanjiri" + // tier atas (dulu rand dikali lalu di-cap, sehingga transcendent jadi + // sangat mudah). 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; const minutesLeft = Math.ceil( (Number(potionExpiries[bestPotion.id]) - now) / 60000 ); From 8eb7206abd5efcdcaebffa45efdd0b9f4cf79e03 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 21:39:46 +0700 Subject: [PATCH 29/48] Update fish.js --- src/commands/Economy/fish.js | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 1881f512b4..0883582ad5 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -43,7 +43,7 @@ const FISH_TYPES = [ // Common { name: 'Bass', emoji: '๐ŸŸ', rarity: 'common' }, { name: 'Salmon', emoji: '๐Ÿ ', rarity: 'common' }, - { name: 'Trout', emoji: '๐ŸŸ', rarity: 'common' }, + { name: 'forel', emoji: '๐ŸŸ', rarity: 'common' }, { name: 'Carp', emoji: '๐ŸŸ', rarity: 'common' }, { name: 'Catfish', emoji: '๐ŸŸ', rarity: 'common' }, { name: 'Perch', emoji: '๐ŸŸ', rarity: 'common' }, @@ -126,11 +126,16 @@ const FISH_TYPES = [ ]; 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 { From 12d8e1904741f99c2ee75924c1ec50cf27e16303 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 14:50:16 +0000 Subject: [PATCH 30/48] feat: add Admin Potion (buy 1x, 1min active/cooldown, divine 30%/transcendent 20%/kitsune 10%) --- src/commands/Economy/buy.js | 32 +++++++++++++- src/commands/Economy/fish.js | 86 ++++++++++++++++++++++-------------- src/config/shop/items.js | 21 +++++++++ 3 files changed, 105 insertions(+), 34 deletions(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index 0781d6f52b..5ee0926c21 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -7,12 +7,14 @@ import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHan import { InteractionHelper } from '../../utils/interactionHelper.js'; const SHOP_ITEMS = shopItems; +const ADMIN_POTION_ID = 'admin_potion'; const LUCK_POTION_IDS = new Set([ 'luck_potion_1', 'luck_potion_2', 'luck_potion_3', 'luck_potion_4', 'luck_potion_5', + ADMIN_POTION_ID, ]); const DEFAULT_LUCK_POTION_DURATION_MS = 5 * 60 * 1000; @@ -107,6 +109,27 @@ export default { } } + if (itemId === ADMIN_POTION_ID) { + if (quantity > 1) { + throw createError( + "Invalid quantity for admin potion", + ErrorTypes.VALIDATION, + `You can only purchase **${item.name}** one at a time.`, + { itemId, quantity } + ); + } + const activeUntil = Number(userData.luckPotionExpiresAt?.[ADMIN_POTION_ID]) || 0; + if (activeUntil > Date.now()) { + const secondsLeft = Math.ceil((activeUntil - Date.now()) / 1000); + throw createError( + "Admin potion on cooldown", + ErrorTypes.RATE_LIMIT, + `**${item.name}** is still active. You can buy it again in **${secondsLeft}s**.`, + { itemId, secondsLeft } + ); + } + } + userData.wallet -= totalCost; let successDescription = `You successfully purchased ${quantity}x **${item.name}** for **$${totalCost.toLocaleString()}**!`; @@ -151,8 +174,13 @@ export default { 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.**`; + if (itemId === ADMIN_POTION_ID) { + const activeSeconds = Math.ceil((potionExpiries[itemId] - now) / 1000); + successDescription += `\n\n๐Ÿ‘‘ **Active for ${activeSeconds}s: Divine 30% / Transcendent 20% / Kitsune 10%. Timer started at purchase.**`; + } else { + 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; diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 1881f512b4..f5f9cfb9c0 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -10,6 +10,7 @@ const FISH_COOLDOWN = 0; const BASE_MIN_REWARD = 300; const BASE_MAX_REWARD = 900; 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; // Helper: find the highest-tier fishing rod the user owns @@ -189,45 +190,66 @@ export default { let rand = Math.random(); - // Terapkan potion terkuat yang masih aktif tanpa menghabiskannya. - let luckMultiplier = 1; + // 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 (bestPotion) { - luckMultiplier = bestPotion.effect.multiplier || 1; - // Bias halus: potion menaikkan peluang ikan langka tanpa "membanjiri" - // tier atas (dulu rand dikali lalu di-cap, sehingga transcendent jadi - // sangat mudah). 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; + if (activePotion) { const minutesLeft = Math.ceil( - (Number(potionExpiries[bestPotion.id]) - now) / 60000 + (Number(potionExpiries[activePotion.id]) - now) / 60000 ); - luckMessage = `\n๐Ÿงช **${bestPotion.name} Active: x${luckMultiplier} luck (${minutesLeft}m left)**`; + if (adminChances) { + luckMessage = `\n๐Ÿ‘‘ **${activePotion.name} Active: Divine ${Math.round(adminChances.divine * 100)}% / Transcendent ${Math.round(adminChances.transcendent * 100)}% / 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)**`; + } } let 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'; + // 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( diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 18ad2c2812..9b78da8a12 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -195,6 +195,27 @@ roleId: null, duration: 300000 } }, + + // ===== Admin Potion (bisa dibeli 1x, aktif 1 menit, cooldown 1 menit) ===== + { + id: 'admin_potion', + name: '๐Ÿ‘‘ Admin Potion', + price: 1000000, + description: 'Admin potion. For 1 minute, greatly boosts luck: Divine 30%, Transcendent 20%, Kitsune 10%. Max 1, can buy again after 1 minute.', + type: 'consumable', + maxQuantity: 1, + cooldown: 60000, + effect: { + type: 'fishing_luck', + admin: true, + chances: { + divine: 0.3, + transcendent: 0.2, + celestial: 0.1 + }, + duration: 60000 + } + }, { id: 'pickaxe', name: 'โ›๏ธ Pickaxe', From e67b97996ebf1be202bb3735f73f33acc1826883 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 21:52:44 +0700 Subject: [PATCH 31/48] Update items.js --- src/config/shop/items.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 9b78da8a12..f7d410895b 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -200,7 +200,7 @@ roleId: null, { id: 'admin_potion', name: '๐Ÿ‘‘ Admin Potion', - price: 1000000, + price: 1, description: 'Admin potion. For 1 minute, greatly boosts luck: Divine 30%, Transcendent 20%, Kitsune 10%. Max 1, can buy again after 1 minute.', type: 'consumable', maxQuantity: 1, From 580c0e167520a318d3da72309f4ab3a2e4ae6f6b Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 14:59:15 +0000 Subject: [PATCH 32/48] feat: admin potion becomes inventory item; add /use and /give for trading items - Admin Potion now stored in inventory (buy -> hold max 1 -> /use to activate 1 min) - New /use: activate fishing luck potions from inventory - New /give: transfer inventory items to another user (with max/ownership checks) - Remove misleading prefix alias give->pay --- src/commands/Economy/buy.js | 23 ++-- src/commands/Economy/give.js | 153 ++++++++++++++++++++++++++ src/commands/Economy/use.js | 101 +++++++++++++++++ src/config/commands/commandAliases.js | 1 - src/config/shop/items.js | 4 +- 5 files changed, 271 insertions(+), 11 deletions(-) create mode 100644 src/commands/Economy/give.js create mode 100644 src/commands/Economy/use.js diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index 5ee0926c21..a53b7a36dc 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -8,13 +8,14 @@ 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', - ADMIN_POTION_ID, ]); const DEFAULT_LUCK_POTION_DURATION_MS = 5 * 60 * 1000; @@ -118,6 +119,15 @@ export default { { itemId, quantity } ); } + const held = Number(userData.inventory?.[ADMIN_POTION_ID]) || 0; + if (held >= 1) { + throw createError( + "Admin potion already held", + ErrorTypes.VALIDATION, + `You already hold an **${item.name}**. Use it with \`/use\` first.`, + { itemId } + ); + } const activeUntil = Number(userData.luckPotionExpiresAt?.[ADMIN_POTION_ID]) || 0; if (activeUntil > Date.now()) { const secondsLeft = Math.ceil((activeUntil - Date.now()) / 1000); @@ -174,18 +184,15 @@ export default { const duration = Number(item.effect?.duration) || DEFAULT_LUCK_POTION_DURATION_MS; potionExpiries[itemId] = currentExpiry + (duration * quantity); userData.luckPotionExpiresAt = potionExpiries; - if (itemId === ADMIN_POTION_ID) { - const activeSeconds = Math.ceil((potionExpiries[itemId] - now) / 1000); - successDescription += `\n\n๐Ÿ‘‘ **Active for ${activeSeconds}s: Divine 30% / Transcendent 20% / Kitsune 10%. Timer started at purchase.**`; - } else { - const activeMinutes = Math.ceil((potionExpiries[itemId] - now) / 60000); - successDescription += `\n\n๐Ÿงช **Active for ${activeMinutes} minutes. Timer started at purchase.**`; - } + 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\`.`; } } 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/use.js b/src/commands/Economy/use.js new file mode 100644 index 0000000000..5cba6c0efb --- /dev/null +++ b/src/commands/Economy/use.js @@ -0,0 +1,101 @@ +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 || {}; + + if (itemId === ADMIN_POTION_ID) { + const activeUntil = Number(potionExpiries[ADMIN_POTION_ID]) || 0; + if (activeUntil > now) { + const secondsLeft = Math.ceil((activeUntil - now) / 1000); + throw createError( + "Admin potion already active", + ErrorTypes.RATE_LIMIT, + `**${item.name}** is already active for **${secondsLeft}s**.`, + { itemId, secondsLeft } + ); + } + } + + 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/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 9b78da8a12..afd6f682ce 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -196,12 +196,12 @@ roleId: null, } }, - // ===== Admin Potion (bisa dibeli 1x, aktif 1 menit, cooldown 1 menit) ===== + // ===== Admin Potion (disimpan di inventory, dipakai dengan /use, bisa dikirim dengan /give) ===== { id: 'admin_potion', name: '๐Ÿ‘‘ Admin Potion', price: 1000000, - description: 'Admin potion. For 1 minute, greatly boosts luck: Divine 30%, Transcendent 20%, Kitsune 10%. Max 1, can buy again after 1 minute.', + description: 'Admin potion. Activate with `/use` for 1 minute of great luck: Divine 30%, Transcendent 20%, Kitsune 10%. Max 1 held. Can be gifted with `/give`.', type: 'consumable', maxQuantity: 1, cooldown: 60000, From 664a0ef15e4c8e842c8191774933aafd43cc803d Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 15:03:31 +0000 Subject: [PATCH 33/48] feat(fish): show AI-generated image of the caught fish Each catch now renders an AI image (via Pollinations) based on the fish name and rarity, downloaded and attached to guarantee it displays. --- src/commands/Economy/fish.js | 44 ++++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 2c5ba9e40b..ab0796d94b 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -1,4 +1,5 @@ -import { SlashCommandBuilder } from 'discord.js'; +import { SlashCommandBuilder, AttachmentBuilder } from 'discord.js'; +import axios from 'axios'; import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { getEconomyData, setEconomyData } from '../../utils/economy.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; @@ -323,6 +324,23 @@ export default { celestial: '#FF69B4' }; + // Gaya prompt gambar per rarity (dipakai untuk generate gambar AI ikan). + const rarityImageHints = { + common: 'common freshwater fish, realistic photo', + uncommon: 'tropical fish, realistic photo', + rare: 'large deep sea fish, dramatic lighting, realistic', + epic: 'rare glowing fish, vivid colors, fantasy', + legendary: 'legendary mythical sea creature, glowing aura, epic', + mythic: 'mythical giant sea monster, magical glow, epic fantasy', + secret: 'mysterious shadowy abyssal creature, dark magic, epic', + divine: 'divine celestial sea god creature, holy golden light, epic', + transcendent: 'transcendent cosmic sea leviathan, galaxies and starlight, ultra epic', + celestial: 'kawaii pink nine tailed fox fish, glowing pink aura, magical, cute anime style' + }; + + const imagePrompt = `${fishCaught.name}, ${rarityImageHints[fishCaught.rarity] || 'fish'}, single subject, centered, highly detailed`; + const fishImageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(imagePrompt)}?width=768&height=768&nologo=true`; + const embed = createEmbed({ title: 'Fishing Success!', description: `${catchMessage}\n\nYou caught a **${fishCaught.emoji} ${fishCaught.name}**! You sold it for **$${finalEarned.toLocaleString()}**!${multiplierMessage}${luckMessage}`, @@ -339,9 +357,27 @@ export default { value: fishCaught.rarity.charAt(0).toUpperCase() + fishCaught.rarity.slice(1), inline: true, } - ) - + ); + + // Ambil gambar AI ikan, lalu lampirkan (lebih andal daripada pasang URL langsung). + const files = []; + try { + const imageResponse = await axios.get(fishImageUrl, { + responseType: 'arraybuffer', + timeout: 15000, + }); + const imageBuffer = Buffer.from(imageResponse.data); + if (imageBuffer.length > 1000) { + files.push(new AttachmentBuilder(imageBuffer, { name: 'fish.jpg' })); + embed.setImage('attachment://fish.jpg'); + } else { + embed.setImage(fishImageUrl); + } + } catch { + // Kalau gagal generate, biarkan Discord mencoba fetch URL langsung. + embed.setImage(fishImageUrl); + } - await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); + await InteractionHelper.safeEditReply(interaction, { embeds: [embed], files }); }, { command: 'fish' }) }; From 36a8c7ace737a66771672284bb409478c55b832e Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 15:05:31 +0000 Subject: [PATCH 34/48] perf(fish): reply instantly by using image URL directly (no download/wait) Use a deterministic per-fish seed so images are cached and load fast. --- src/commands/Economy/fish.js | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index ab0796d94b..56b4c03d56 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -1,5 +1,4 @@ -import { SlashCommandBuilder, AttachmentBuilder } from 'discord.js'; -import axios from 'axios'; +import { SlashCommandBuilder } from 'discord.js'; import { createEmbed, errorEmbed, successEmbed, infoEmbed, warningEmbed } from '../../utils/embeds.js'; import { getEconomyData, setEconomyData } from '../../utils/economy.js'; import { withErrorHandling, createError, ErrorTypes } from '../../utils/errorHandler.js'; @@ -339,7 +338,12 @@ export default { }; const imagePrompt = `${fishCaught.name}, ${rarityImageHints[fishCaught.rarity] || 'fish'}, single subject, centered, highly detailed`; - const fishImageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(imagePrompt)}?width=768&height=768&nologo=true`; + // Seed tetap per ikan supaya gambarnya konsisten & cepat (ter-cache). + let seed = 0; + for (let i = 0; i < fishCaught.name.length; i++) { + seed = (seed * 31 + fishCaught.name.charCodeAt(i)) >>> 0; + } + const fishImageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(imagePrompt)}?width=512&height=512&nologo=true&seed=${seed}`; const embed = createEmbed({ title: 'Fishing Success!', @@ -357,27 +361,10 @@ export default { value: fishCaught.rarity.charAt(0).toUpperCase() + fishCaught.rarity.slice(1), inline: true, } - ); - - // Ambil gambar AI ikan, lalu lampirkan (lebih andal daripada pasang URL langsung). - const files = []; - try { - const imageResponse = await axios.get(fishImageUrl, { - responseType: 'arraybuffer', - timeout: 15000, - }); - const imageBuffer = Buffer.from(imageResponse.data); - if (imageBuffer.length > 1000) { - files.push(new AttachmentBuilder(imageBuffer, { name: 'fish.jpg' })); - embed.setImage('attachment://fish.jpg'); - } else { - embed.setImage(fishImageUrl); - } - } catch { - // Kalau gagal generate, biarkan Discord mencoba fetch URL langsung. - embed.setImage(fishImageUrl); - } + ) + // Pasang URL gambar langsung: bot membalas instan, Discord yang memuat gambarnya. + .setImage(fishImageUrl); - await InteractionHelper.safeEditReply(interaction, { embeds: [embed], files }); + await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'fish' }) }; From 37fb91fc3bac5bdca0016fa0988b93bc4335b0d5 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 15:08:09 +0000 Subject: [PATCH 35/48] revert(fish): remove AI image feature (caused delay) --- src/commands/Economy/fish.js | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 56b4c03d56..c6d1d33ca0 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -323,28 +323,6 @@ export default { celestial: '#FF69B4' }; - // Gaya prompt gambar per rarity (dipakai untuk generate gambar AI ikan). - const rarityImageHints = { - common: 'common freshwater fish, realistic photo', - uncommon: 'tropical fish, realistic photo', - rare: 'large deep sea fish, dramatic lighting, realistic', - epic: 'rare glowing fish, vivid colors, fantasy', - legendary: 'legendary mythical sea creature, glowing aura, epic', - mythic: 'mythical giant sea monster, magical glow, epic fantasy', - secret: 'mysterious shadowy abyssal creature, dark magic, epic', - divine: 'divine celestial sea god creature, holy golden light, epic', - transcendent: 'transcendent cosmic sea leviathan, galaxies and starlight, ultra epic', - celestial: 'kawaii pink nine tailed fox fish, glowing pink aura, magical, cute anime style' - }; - - const imagePrompt = `${fishCaught.name}, ${rarityImageHints[fishCaught.rarity] || 'fish'}, single subject, centered, highly detailed`; - // Seed tetap per ikan supaya gambarnya konsisten & cepat (ter-cache). - let seed = 0; - for (let i = 0; i < fishCaught.name.length; i++) { - seed = (seed * 31 + fishCaught.name.charCodeAt(i)) >>> 0; - } - const fishImageUrl = `https://image.pollinations.ai/prompt/${encodeURIComponent(imagePrompt)}?width=512&height=512&nologo=true&seed=${seed}`; - const embed = createEmbed({ title: 'Fishing Success!', description: `${catchMessage}\n\nYou caught a **${fishCaught.emoji} ${fishCaught.name}**! You sold it for **$${finalEarned.toLocaleString()}**!${multiplierMessage}${luckMessage}`, @@ -361,9 +339,7 @@ export default { value: fishCaught.rarity.charAt(0).toUpperCase() + fishCaught.rarity.slice(1), inline: true, } - ) - // Pasang URL gambar langsung: bot membalas instan, Discord yang memuat gambarnya. - .setImage(fishImageUrl); + ); await InteractionHelper.safeEditReply(interaction, { embeds: [embed] }); }, { command: 'fish' }) From af8a36342f122e1018b8e1600a48302747c14560 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 15:10:17 +0000 Subject: [PATCH 36/48] feat(buy): remove admin potion purchase cooldown/time gate --- src/commands/Economy/buy.js | 10 ---------- src/config/shop/items.js | 1 - 2 files changed, 11 deletions(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index a53b7a36dc..4dd053e921 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -128,16 +128,6 @@ export default { { itemId } ); } - const activeUntil = Number(userData.luckPotionExpiresAt?.[ADMIN_POTION_ID]) || 0; - if (activeUntil > Date.now()) { - const secondsLeft = Math.ceil((activeUntil - Date.now()) / 1000); - throw createError( - "Admin potion on cooldown", - ErrorTypes.RATE_LIMIT, - `**${item.name}** is still active. You can buy it again in **${secondsLeft}s**.`, - { itemId, secondsLeft } - ); - } } userData.wallet -= totalCost; diff --git a/src/config/shop/items.js b/src/config/shop/items.js index 73b0a5efdd..adb7daac3e 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -204,7 +204,6 @@ roleId: null, description: 'Admin potion. Activate with `/use` for 1 minute of great luck: Divine 30%, Transcendent 20%, Kitsune 10%. Max 1 held. Can be gifted with `/give`.', type: 'consumable', maxQuantity: 1, - cooldown: 60000, effect: { type: 'fishing_luck', admin: true, From 4b6e597f81d6e576fdd4b667be51a81dcf94e0e6 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 15:12:53 +0000 Subject: [PATCH 37/48] feat(admin_potion): remove purchase limit, extend duration to 5 minutes - Remove maxQuantity so multiple can be bought/held - Duration 1m -> 5m (300000ms) - /use no longer blocks while active; re-using extends the timer --- src/commands/Economy/buy.js | 20 -------------------- src/commands/Economy/use.js | 13 ------------- src/config/shop/items.js | 5 ++--- 3 files changed, 2 insertions(+), 36 deletions(-) diff --git a/src/commands/Economy/buy.js b/src/commands/Economy/buy.js index 4dd053e921..5fa66141c8 100644 --- a/src/commands/Economy/buy.js +++ b/src/commands/Economy/buy.js @@ -110,26 +110,6 @@ export default { } } - if (itemId === ADMIN_POTION_ID) { - if (quantity > 1) { - throw createError( - "Invalid quantity for admin potion", - ErrorTypes.VALIDATION, - `You can only purchase **${item.name}** one at a time.`, - { itemId, quantity } - ); - } - const held = Number(userData.inventory?.[ADMIN_POTION_ID]) || 0; - if (held >= 1) { - throw createError( - "Admin potion already held", - ErrorTypes.VALIDATION, - `You already hold an **${item.name}**. Use it with \`/use\` first.`, - { itemId } - ); - } - } - userData.wallet -= totalCost; let successDescription = `You successfully purchased ${quantity}x **${item.name}** for **$${totalCost.toLocaleString()}**!`; diff --git a/src/commands/Economy/use.js b/src/commands/Economy/use.js index 5cba6c0efb..a94beac6f9 100644 --- a/src/commands/Economy/use.js +++ b/src/commands/Economy/use.js @@ -61,19 +61,6 @@ export default { const now = Date.now(); const potionExpiries = userData.luckPotionExpiresAt || {}; - if (itemId === ADMIN_POTION_ID) { - const activeUntil = Number(potionExpiries[ADMIN_POTION_ID]) || 0; - if (activeUntil > now) { - const secondsLeft = Math.ceil((activeUntil - now) / 1000); - throw createError( - "Admin potion already active", - ErrorTypes.RATE_LIMIT, - `**${item.name}** is already active for **${secondsLeft}s**.`, - { itemId, secondsLeft } - ); - } - } - const duration = Number(item.effect?.duration) || DEFAULT_LUCK_POTION_DURATION_MS; const currentExpiry = Math.max(now, Number(potionExpiries[itemId]) || 0); potionExpiries[itemId] = currentExpiry + duration; diff --git a/src/config/shop/items.js b/src/config/shop/items.js index adb7daac3e..d288a9188f 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -201,9 +201,8 @@ roleId: null, id: 'admin_potion', name: '๐Ÿ‘‘ Admin Potion', price: 1, - description: 'Admin potion. Activate with `/use` for 1 minute of great luck: Divine 30%, Transcendent 20%, Kitsune 10%. Max 1 held. Can be gifted with `/give`.', + 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', - maxQuantity: 1, effect: { type: 'fishing_luck', admin: true, @@ -212,7 +211,7 @@ roleId: null, transcendent: 0.2, celestial: 0.1 }, - duration: 60000 + duration: 300000 } }, { From 17b6b40c93bfc2f636d6286ff5a09e77dedaab4a Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 22:19:37 +0700 Subject: [PATCH 38/48] Update fish.js --- src/commands/Economy/fish.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index c6d1d33ca0..d107111f38 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -294,7 +294,7 @@ export default { if (hasPremiumRole) { fishingMultiplier *= PREMIUM_FISHING_MULTIPLIER; const boostPct = Math.round((PREMIUM_FISHING_MULTIPLIER - 1) * 100); - bonusMessages.push(`โœจ **Premium Fishing Bonus: +${boostPct}%**`); + bonusMessages.push(`โœจ **Premium Role Bonus: +${boostPct}%**`); } const finalEarned = Math.floor(baseEarned * fishingMultiplier); const multiplierMessage = bonusMessages.length From dfb06b4f14a55529ae35e2420e1fc8752c7c2cfc Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 22:20:55 +0700 Subject: [PATCH 39/48] Update fish.js --- src/commands/Economy/fish.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index d107111f38..98f621426e 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -274,7 +274,7 @@ export default { secret: [30000, 50000], divine: [62500, 175000], transcendent: [250000, 500000], - celestial: [1000000, 5000000] + celestial: [10000000, 20000000] }; const [minPrice, maxPrice] = fishPrices[fishCaught.rarity]; From 3f966c660ae410ecb54bbfac1f10996f80332ca2 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 22:23:47 +0700 Subject: [PATCH 40/48] Update items.js --- src/config/shop/items.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index d288a9188f..e66ac98097 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -46,7 +46,7 @@ cooldown: 86400000, roleId: null, effect: { type: 'fishing_yield', - multiplier: 5 + multiplier: 50000 } }, { @@ -191,7 +191,7 @@ roleId: null, maxQuantity: 10, effect: { type: 'fishing_luck', - multiplier: 2.2, + multiplier: 20000, duration: 300000 } }, From d1cb05d6072cc46a92492c6985604b7a12a5cc75 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 22:26:50 +0700 Subject: [PATCH 41/48] Update items.js --- src/config/shop/items.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/config/shop/items.js b/src/config/shop/items.js index e66ac98097..8cedd4bded 100644 --- a/src/config/shop/items.js +++ b/src/config/shop/items.js @@ -46,7 +46,7 @@ cooldown: 86400000, roleId: null, effect: { type: 'fishing_yield', - multiplier: 50000 + multiplier: 4 } }, { @@ -133,7 +133,7 @@ roleId: null, { id: 'luck_potion_1', name: '๐Ÿงช Luck Potion I', - price: 10000, + price: 1000, description: 'Boosts your fishing luck slightly for 5 minutes. Higher chance of rarer fish.', type: 'consumable', maxQuantity: 10, @@ -146,7 +146,7 @@ roleId: null, { id: 'luck_potion_2', name: '๐Ÿงช Luck Potion II', - price: 30000, + price: 7000, description: 'Boosts your fishing luck for 5 minutes. Noticeably higher chance of rarer fish.', type: 'consumable', maxQuantity: 10, @@ -159,7 +159,7 @@ roleId: null, { id: 'luck_potion_3', name: '๐Ÿงช Luck Potion III', - price: 75000, + price: 50000, description: 'Boosts your fishing luck for 5 minutes. Good chance of rarer fish.', type: 'consumable', maxQuantity: 10, @@ -172,7 +172,7 @@ roleId: null, { id: 'luck_potion_4', name: '๐Ÿงช Luck Potion IV', - price: 150000, + price: 100000, description: 'Boosts your fishing luck for 5 minutes. Great chance of rarer fish.', type: 'consumable', maxQuantity: 10, @@ -185,14 +185,14 @@ roleId: null, { id: 'luck_potion_5', name: '๐Ÿงช Luck Potion V', - price: 300000, + 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: 300000 + duration: 2.1 } }, @@ -200,7 +200,7 @@ roleId: null, { id: 'admin_potion', name: '๐Ÿ‘‘ Admin Potion', - price: 1, + 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: { From 7d1848345c1a8676a7f634ab4166281232e812ba Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 15:34:22 +0000 Subject: [PATCH 42/48] chore(fish): change admin potion display to gimmick luck text --- src/commands/Economy/fish.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index c6d1d33ca0..c6835c08d8 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -206,7 +206,7 @@ export default { (Number(potionExpiries[activePotion.id]) - now) / 60000 ); if (adminChances) { - luckMessage = `\n๐Ÿ‘‘ **${activePotion.name} Active: Divine ${Math.round(adminChances.divine * 100)}% / Transcendent ${Math.round(adminChances.transcendent * 100)}% / Kitsune ${Math.round(adminChances.celestial * 100)}% (${minutesLeft}m left)**`; + luckMessage = `\n๐Ÿ‘‘ **${activePotion.name} Active: Luck +1000000% / 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" From dea51de0fcd45e572f993a2c041022546da4df2b Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 15:37:13 +0000 Subject: [PATCH 43/48] chore(fish): remove gimmick luck percentage from potion message --- src/commands/Economy/fish.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index ef0fc39a55..11623247bb 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -206,7 +206,7 @@ export default { (Number(potionExpiries[activePotion.id]) - now) / 60000 ); if (adminChances) { - luckMessage = `\n๐Ÿ‘‘ **${activePotion.name} Active: Luck +1000000% / Kitsune ${Math.round(adminChances.celestial * 100)}% (${minutesLeft}m left)**`; + 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" From 0719b386457c0d6a7025de7f28c35013ca32f75b Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 15:47:58 +0000 Subject: [PATCH 44/48] feat(economy): add personal and public fishdex with caught fish images --- src/commands/Economy/fish.js | 5 +- src/commands/Economy/fishdex.js | 178 ++++++++++++++++++++++++++++++++ src/utils/constants.js | 3 +- src/utils/schemas.js | 1 + 4 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 src/commands/Economy/fishdex.js diff --git a/src/commands/Economy/fish.js b/src/commands/Economy/fish.js index 11623247bb..ba972374da 100644 --- a/src/commands/Economy/fish.js +++ b/src/commands/Economy/fish.js @@ -40,7 +40,7 @@ function getBestActiveLuckPotion(potionExpiries, now) { return null; } -const FISH_TYPES = [ +export const FISH_TYPES = [ // Common { name: 'Bass', emoji: '๐ŸŸ', rarity: 'common' }, { name: 'Salmon', emoji: '๐Ÿ ', rarity: 'common' }, @@ -305,6 +305,9 @@ export default { 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; diff --git a/src/commands/Economy/fishdex.js b/src/commands/Economy/fishdex.js new file mode 100644 index 0000000000..df93c2e32d --- /dev/null +++ b/src/commands/Economy/fishdex.js @@ -0,0 +1,178 @@ +import { + ActionRowBuilder, + ButtonBuilder, + ButtonStyle, + ComponentType, + EmbedBuilder, + MessageFlags, + SlashCommandBuilder, +} 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 FISH_PER_PAGE = 5; +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=512&height=512&nologo=true&seed=${seed}`; +} + +function makeFishEmbeds(page, targetUser, fishCollection, totalPages) { + const start = page * FISH_PER_PAGE; + const pageFish = FISH_TYPES.slice(start, start + FISH_PER_PAGE); + 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( + `**Rarity:** ${fish.rarity.charAt(0).toUpperCase() + fish.rarity.slice(1)}\n**Tertangkap:** ${count} kali` + ) + .setImage(createImageUrl(fish)); + } else { + embed.setDescription('Belum tertangkap'); + } + + if (index === pageFish.length - 1) { + embed.setFooter({ + text: `${targetUser.username}: ${caughtSpecies}/${FISH_TYPES.length} spesies | Halaman ${page + 1}/${totalPages}`, + iconURL: targetUser.displayAvatarURL(), + }); + } + return embed; + }); +} + +function makeComponents(page, totalPages, customIdPrefix, disabled = false) { + if (totalPages <= 1) return []; + + return [ + new ActionRowBuilder().addComponents( + new ButtonBuilder() + .setCustomId(`${customIdPrefix}_prev`) + .setLabel('Sebelumnya') + .setStyle(ButtonStyle.Secondary) + .setDisabled(disabled || page === 0), + new ButtonBuilder() + .setCustomId(`${customIdPrefix}_next`) + .setLabel('Berikutnya') + .setStyle(ButtonStyle.Secondary) + .setDisabled(disabled || page === totalPages - 1), + ), + ]; +} + +export default { + slashOnly: true, + 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 totalPages = Math.ceil(FISH_TYPES.length / FISH_PER_PAGE); + const customIdPrefix = `fishdex_${interaction.id}`; + let page = 0; + + await interaction.reply({ + embeds: makeFishEmbeds(page, targetUser, fishCollection, totalPages), + components: makeComponents(page, totalPages, customIdPrefix), + }); + const message = await interaction.fetchReply(); + const collector = message.createMessageComponentCollector({ + componentType: ComponentType.Button, + time: 300000, + }); + + collector.on('collect', async buttonInteraction => { + if (buttonInteraction.user.id !== interaction.user.id) { + await buttonInteraction.reply({ + content: 'Hanya orang yang membuka fishdex ini yang bisa memakai tombolnya.', + flags: MessageFlags.Ephemeral, + }); + return; + } + + await buttonInteraction.deferUpdate(); + if (buttonInteraction.customId === `${customIdPrefix}_prev` && page > 0) { + page--; + } else if ( + buttonInteraction.customId === `${customIdPrefix}_next` + && page < totalPages - 1 + ) { + page++; + } + + await message.edit({ + embeds: makeFishEmbeds(page, targetUser, fishCollection, totalPages), + components: makeComponents(page, totalPages, customIdPrefix), + }); + }); + + collector.on('end', async () => { + try { + await message.edit({ + components: makeComponents(page, totalPages, customIdPrefix, true), + }); + } catch (error) { + logger.debug('fishdex: could not disable pagination buttons', { + error: error.message, + }); + } + }); + }, { command: 'fishdex' }), +}; 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(); From d621141e3b182e0980fc74ff1afb6f83d4a0f4d4 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 15:54:26 +0000 Subject: [PATCH 45/48] feat(fishdex): enable prefix command support --- src/commands/Economy/fishdex.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/commands/Economy/fishdex.js b/src/commands/Economy/fishdex.js index df93c2e32d..fca545c071 100644 --- a/src/commands/Economy/fishdex.js +++ b/src/commands/Economy/fishdex.js @@ -101,7 +101,6 @@ function makeComponents(page, totalPages, customIdPrefix, disabled = false) { } export default { - slashOnly: true, data: new SlashCommandBuilder() .setName('fishdex') .setDescription('Lihat index ikan tangkapanmu atau pengguna lain') From 7d90f914ef1ac0618ea9123accc822e481b74958 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 16:01:12 +0000 Subject: [PATCH 46/48] feat(fishdex): show all fish grouped by rarity selector --- src/commands/Economy/fishdex.js | 69 +++++++++++++-------------------- 1 file changed, 28 insertions(+), 41 deletions(-) diff --git a/src/commands/Economy/fishdex.js b/src/commands/Economy/fishdex.js index fca545c071..27a4655904 100644 --- a/src/commands/Economy/fishdex.js +++ b/src/commands/Economy/fishdex.js @@ -1,18 +1,17 @@ import { ActionRowBuilder, - ButtonBuilder, - ButtonStyle, 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 FISH_PER_PAGE = 5; +const RARITIES = [...new Set(FISH_TYPES.map(fish => fish.rarity))]; const RARITY_COLORS = { common: 0x95A5A6, uncommon: 0x2ECC71, @@ -48,9 +47,8 @@ function createImageUrl(fish) { return `https://image.pollinations.ai/prompt/${encodeURIComponent(prompt)}?width=512&height=512&nologo=true&seed=${seed}`; } -function makeFishEmbeds(page, targetUser, fishCollection, totalPages) { - const start = page * FISH_PER_PAGE; - const pageFish = FISH_TYPES.slice(start, start + FISH_PER_PAGE); +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; @@ -73,7 +71,7 @@ function makeFishEmbeds(page, targetUser, fishCollection, totalPages) { if (index === pageFish.length - 1) { embed.setFooter({ - text: `${targetUser.username}: ${caughtSpecies}/${FISH_TYPES.length} spesies | Halaman ${page + 1}/${totalPages}`, + text: `${targetUser.username}: ${caughtSpecies}/${FISH_TYPES.length} spesies | ${pageFish.length} ikan ${rarity}`, iconURL: targetUser.displayAvatarURL(), }); } @@ -81,21 +79,18 @@ function makeFishEmbeds(page, targetUser, fishCollection, totalPages) { }); } -function makeComponents(page, totalPages, customIdPrefix, disabled = false) { - if (totalPages <= 1) return []; - +function makeComponents(selectedRarity, customIdPrefix, disabled = false) { return [ new ActionRowBuilder().addComponents( - new ButtonBuilder() - .setCustomId(`${customIdPrefix}_prev`) - .setLabel('Sebelumnya') - .setStyle(ButtonStyle.Secondary) - .setDisabled(disabled || page === 0), - new ButtonBuilder() - .setCustomId(`${customIdPrefix}_next`) - .setLabel('Berikutnya') - .setStyle(ButtonStyle.Secondary) - .setDisabled(disabled || page === totalPages - 1), + 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, + }))), ), ]; } @@ -123,49 +118,41 @@ export default { const userData = await getEconomyData(client, interaction.guildId, targetUser.id); const fishCollection = userData.fishCollection || {}; - const totalPages = Math.ceil(FISH_TYPES.length / FISH_PER_PAGE); const customIdPrefix = `fishdex_${interaction.id}`; - let page = 0; + let selectedRarity = RARITIES[0]; await interaction.reply({ - embeds: makeFishEmbeds(page, targetUser, fishCollection, totalPages), - components: makeComponents(page, totalPages, customIdPrefix), + embeds: makeFishEmbeds(selectedRarity, targetUser, fishCollection), + components: makeComponents(selectedRarity, customIdPrefix), }); const message = await interaction.fetchReply(); const collector = message.createMessageComponentCollector({ - componentType: ComponentType.Button, + componentType: ComponentType.StringSelect, time: 300000, }); - collector.on('collect', async buttonInteraction => { - if (buttonInteraction.user.id !== interaction.user.id) { - await buttonInteraction.reply({ - content: 'Hanya orang yang membuka fishdex ini yang bisa memakai tombolnya.', + 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 buttonInteraction.deferUpdate(); - if (buttonInteraction.customId === `${customIdPrefix}_prev` && page > 0) { - page--; - } else if ( - buttonInteraction.customId === `${customIdPrefix}_next` - && page < totalPages - 1 - ) { - page++; - } + await selectInteraction.deferUpdate(); + selectedRarity = selectInteraction.values[0]; await message.edit({ - embeds: makeFishEmbeds(page, targetUser, fishCollection, totalPages), - components: makeComponents(page, totalPages, customIdPrefix), + embeds: makeFishEmbeds(selectedRarity, targetUser, fishCollection), + components: makeComponents(selectedRarity, customIdPrefix), }); }); collector.on('end', async () => { try { await message.edit({ - components: makeComponents(page, totalPages, customIdPrefix, true), + components: makeComponents(selectedRarity, customIdPrefix, true), }); } catch (error) { logger.debug('fishdex: could not disable pagination buttons', { From 81c980f27d65e4434bec87e3f6f744a4b4eaeb1b Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 16:03:05 +0000 Subject: [PATCH 47/48] perf(fishdex): shrink fish images to compact thumbnails --- src/commands/Economy/fishdex.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/commands/Economy/fishdex.js b/src/commands/Economy/fishdex.js index 27a4655904..03030a328f 100644 --- a/src/commands/Economy/fishdex.js +++ b/src/commands/Economy/fishdex.js @@ -44,7 +44,7 @@ function createImageUrl(fish) { 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=512&height=512&nologo=true&seed=${seed}`; + return `https://image.pollinations.ai/prompt/${encodeURIComponent(prompt)}?width=256&height=256&nologo=true&seed=${seed}`; } function makeFishEmbeds(rarity, targetUser, fishCollection) { @@ -61,10 +61,8 @@ function makeFishEmbeds(rarity, targetUser, fishCollection) { if (count > 0) { embed - .setDescription( - `**Rarity:** ${fish.rarity.charAt(0).toUpperCase() + fish.rarity.slice(1)}\n**Tertangkap:** ${count} kali` - ) - .setImage(createImageUrl(fish)); + .setDescription(`Tertangkap **${count}x**`) + .setThumbnail(createImageUrl(fish)); } else { embed.setDescription('Belum tertangkap'); } From cdbb5e54200793c45c13e4f2d359c455e048a7e3 Mon Sep 17 00:00:00 2001 From: mannnayla1-code Date: Wed, 11 Nov 2026 17:40:04 +0000 Subject: [PATCH 48/48] feat: update workspace Co-authored-by: traeagent --- riffy_test.mjs | 39 +++++++++++++++++++++++++++++++++++++++ riffy_test2.mjs | 35 +++++++++++++++++++++++++++++++++++ riffy_test3.mjs | 35 +++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 riffy_test.mjs create mode 100644 riffy_test2.mjs create mode 100644 riffy_test3.mjs 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);