const mongoose = require("mongoose"); const { createCanvas, loadImage } = require("canvas"); const fs = require("fs-extra"); const path = require("path"); const axios = require("axios"); // Schema Definition with full fields const bankUserSchema = new mongoose.Schema({ userID: { type: String, required: true, unique: true }, balance: { type: Number, default: 0 }, wheelCount: { type: Number, default: 0 }, wheelResetTime: { type: Number, default: 0 } }); let BankUser; if (mongoose.models.DiabloBankUser) { BankUser = mongoose.models.DiabloBankUser; // Dynamic field injection to avoid old schema cache bug BankUser.schema.add({ wheelCount: { type: Number, default: 0 }, wheelResetTime: { type: Number, default: 0 } }); } else { BankUser = mongoose.model("DiabloBankUser", bankUserSchema); } const MAX_BET = 69000000000; const MAX_PLAY_LIMIT = 15; const COOLDOWN_TIME_MS = 3 * 60 * 60 * 1000; // 3 Hours const WHEEL_VIDEO_URL = "https://files.catbox.moe/wvlnvl.mp4"; const WHEEL_SLICES = [ { label: "LOSE", mult: 0, weight: 40, hex: "#ef4444" }, { label: "50 PTS", mult: 0.5, weight: 20, hex: "#f97316" }, { label: "100 PTS", mult: 1.0, weight: 15, hex: "#eab308" }, { label: "250 PTS", mult: 1.5, weight: 10, hex: "#3b82f6" }, { label: "500 PTS", mult: 2.0, weight: 8, hex: "#a855f7" }, { label: "1000 PTS", mult: 5.0, weight: 5, hex: "#06b6d4" }, { label: "JACKPOT", mult: 10.0, weight: 2, hex: "#ec4899" } ]; module.exports = { config: { name: "wheel", aliases: ["wof", "wheeloffortune", "spinwheel"], version: "1.0.1", author: "Pratik Shah", countDown: 5, role: 0, shortDescription: "Play Animated Wheel of Fortune & Win Bank Money", category: "games", guide: { en: "{p}wheel " } }, formatMoney: function (num) { if (num >= 1000000000) return (num / 1000000000).toFixed(1).replace(/\.0$/, "") + "B"; if (num >= 1000000) return (num / 1000000).toFixed(1).replace(/\.0$/, "") + "M"; if (num >= 1000) return (num / 1000).toFixed(1).replace(/\.0$/, "") + "K"; return num.toLocaleString(); }, parseBetAmount: function (input, userBalance) { if (!input) return null; const str = input.toString().trim().toLowerCase(); if (str === "all" || str === "max") return userBalance; const match = str.match(/^(\d+(?:\.\d+)?)\s*([kmb])?$/); if (!match) return null; let value = parseFloat(match[1]); const unit = match[2]; if (unit === "k") value *= 1000; else if (unit === "m") value *= 1000000; else if (unit === "b") value *= 1000000000; return Math.floor(value); }, getRandomOutcome: function () { const totalWeight = WHEEL_SLICES.reduce((sum, item) => sum + item.weight, 0); let rand = Math.random() * totalWeight; for (const slice of WHEEL_SLICES) { if (rand < slice.weight) return slice; rand -= slice.weight; } return WHEEL_SLICES[0]; }, onStart: async function ({ api, event, args, message, usersData }) { const sendMsg = (txt) => message && typeof message.reply === "function" ? message.reply(txt) : api.sendMessage(txt, event.threadID, event.messageID); const { senderID } = event; const now = Date.now(); const rawBet = args[0]; if (!rawBet) { return sendMsg("āš ļø š—£š—Ÿš—˜š—”š—¦š—˜ š—˜š—”š—§š—˜š—„ š—” š—•š—˜š—§ š—”š— š—¢š—Øš—”š—§!\nšŸ‘‰ š—Øš˜€š—®š—“š—²: !š˜„š—µš—²š—²š—¹ <š—®š—ŗš—¼š˜‚š—»š˜>\nšŸ‘‰ š—˜š˜…š—®š—ŗš—½š—¹š—²: !š˜„š—µš—²š—²š—¹ šŸ­š—ŗ"); } try { let user = await BankUser.findOne({ userID: senderID }); if (!user) { user = await BankUser.create({ userID: senderID, balance: 0, wheelCount: 0, wheelResetTime: 0 }); } let currentWheelCount = user.wheelCount || 0; let currentResetTime = user.wheelResetTime || 0; // Reset Cooldown logic if (!currentResetTime || now > currentResetTime) { currentWheelCount = 0; currentResetTime = now + COOLDOWN_TIME_MS; await BankUser.updateOne({ userID: senderID }, { $set: { wheelCount: 0, wheelResetTime: currentResetTime } }); } if (currentWheelCount >= MAX_PLAY_LIMIT) { const remainingMs = currentResetTime - now; const remainingMins = Math.ceil(remainingMs / (1000 * 60)); const hours = Math.floor(remainingMins / 60); const mins = remainingMins % 60; const timeStr = hours > 0 ? `${hours}h ${mins}m` : `${mins}m`; return sendMsg(`šŸ›‘ š—šš—”š— š—˜ š—Ÿš—œš— š—œš—§ š—„š—˜š—”š—–š—›š—˜š——!\nšŸ‘‰ You have played ${MAX_PLAY_LIMIT}/${MAX_PLAY_LIMIT} times in 3 hours.\nā³ Please wait ${timeStr} to play again!`); } const currentBalance = user.balance || 0; const betAmount = this.parseBetAmount(rawBet, currentBalance); if (betAmount === null || isNaN(betAmount) || betAmount <= 0) { return sendMsg("āš ļø š—œš—”š—©š—”š—Ÿš—œš—— š—•š—˜š—§ š—™š—¢š—„š— š—”š—§!\nšŸ‘‰ š—Øš˜€š—² š—»š˜‚š—ŗš—Æš—²š—æš˜€ š—¼š—æ š˜€š˜‚š—³š—³š—¶š˜…š—²š˜€ š—¹š—¶š—øš—²: šŸ­š—ø, šŸ®.šŸ±š—ŗ, šŸ­š—Æ, š—ŗš—®š˜…"); } if (betAmount > MAX_BET) { return sendMsg(`āš ļø š— š—”š—«š—œš— š—Øš—  š—•š—˜š—§ š—Ÿš—œš— š—œš—§ š—œš—¦ $${this.formatMoney(MAX_BET)} USD!`); } if (currentBalance < betAmount) { return sendMsg(`āŒ š—œš—”š—¦š—Øš—™š—™š—œš—–š—œš—˜š—”š—§ š—•š—”š—Ÿš—”š—”š—–š—˜!\nšŸ’³ š—¬š—¼š˜‚š—æ š—–š˜‚š—æš—æš—²š—»š˜ š—•š—®š—¹š—®š—»š—°š—²: $${this.formatMoney(currentBalance)} USD`); } let userName = senderID; if (usersData && typeof usersData.getName === "function") { try { userName = await usersData.getName(senderID); } catch (e) {} } // Increment Count in Database directly currentWheelCount += 1; await BankUser.updateOne({ userID: senderID }, { $set: { wheelCount: currentWheelCount, wheelResetTime: currentResetTime } }); // Step 1: Send Spinning Wheel Video const cacheDir = path.join(__dirname, "cache"); await fs.ensureDir(cacheDir); const videoPath = path.join(cacheDir, `wheel_${senderID}_${Date.now()}.mp4`); let videoMsgInfo = null; try { const videoRes = await axios({ url: WHEEL_VIDEO_URL, responseType: "stream" }); const writer = fs.createWriteStream(videoPath); videoRes.data.pipe(writer); await new Promise((resolve, reject) => { writer.on("finish", resolve); writer.on("error", reject); }); const remainingAttempts = MAX_PLAY_LIMIT - currentWheelCount; const videoPayload = { body: `šŸŽ” š—¦š—£š—œš—”š—”š—œš—”š—š š—§š—›š—˜ š—Ŗš—›š—˜š—˜š—Ÿ š—¢š—™ š—™š—¢š—„š—§š—Øš—”š—˜...\nšŸ‘¤ Player: ${userName.toUpperCase()}\nšŸ’µ Bet: $${this.formatMoney(betAmount)} USD\nšŸŽÆ Limit Remaining: ${remainingAttempts}/${MAX_PLAY_LIMIT}`, attachment: fs.createReadStream(videoPath) }; videoMsgInfo = await new Promise((resolve) => { if (message && typeof message.reply === "function") { message.reply(videoPayload, (err, info) => resolve(info || err)); } else { api.sendMessage(videoPayload, event.threadID, (err, info) => resolve(info || err), event.messageID); } }); } catch (e) { console.log("Wheel Video download failed, skipping to canvas result...", e); } finally { if (fs.existsSync(videoPath)) fs.unlinkSync(videoPath); } // ā³ Wait 17 Seconds await new Promise(res => setTimeout(res, 17000)); if (videoMsgInfo && videoMsgInfo.messageID) { try { if (typeof api.unsendMessage === "function") { api.unsendMessage(videoMsgInfo.messageID); } } catch (e) {} } const outcome = this.getRandomOutcome(); const winAmount = Math.floor(betAmount * outcome.mult); const netChange = winAmount - betAmount; // Atomic Balance Update const updatedUser = await BankUser.findOneAndUpdate( { userID: senderID }, { $inc: { balance: netChange } }, { new: true } ); // Step 2: Render Canvas Card const canvas = createCanvas(900, 500); const ctx = canvas.getContext("2d"); ctx.fillStyle = "#090d16"; ctx.fillRect(0, 0, 900, 500); const laserGlows = [ { color: outcome.hex, blur: 30, width: 12 }, { color: "#ffffff", blur: 4, width: 2 } ]; laserGlows.forEach(g => { ctx.save(); ctx.shadowColor = g.color; ctx.shadowBlur = g.blur; ctx.strokeStyle = g.color; ctx.lineWidth = g.width; ctx.strokeRect(20, 20, 860, 460); ctx.restore(); }); ctx.fillStyle = "#ffffff"; ctx.font = "900 30px 'Segoe UI', Sans-serif"; ctx.fillText("šŸŽ” WHEEL OF FORTUNE RESULT šŸŽ”", 50, 75); ctx.fillStyle = netChange >= 0 ? "#4ade80" : "#ef4444"; ctx.font = "bold 16px 'Segoe UI', Sans-serif"; ctx.fillText( netChange >= 0 ? `ā–ŗ CONGRATS! LANDED ON ${outcome.label} (${outcome.mult}X MULTIPLIER)` : `ā–ŗ HARD LUCK! LANDED ON ${outcome.label}`, 52, 105 ); try { const avatarUrl = `https://graph.facebook.com/${senderID}/picture?height=300&width=300&access_token=6628568379%7Cc1e620fa708a1d5696fb991c1bde5662`; const avatarImg = await loadImage(avatarUrl); ctx.save(); ctx.shadowColor = outcome.hex; ctx.shadowBlur = 20; ctx.beginPath(); ctx.arc(740, 115, 60, 0, Math.PI * 2); ctx.clip(); ctx.drawImage(avatarImg, 680, 55, 120, 120); ctx.restore(); ctx.strokeStyle = outcome.hex; ctx.lineWidth = 4; ctx.beginPath(); ctx.arc(740, 115, 60, 0, Math.PI * 2); ctx.stroke(); } catch (e) {} ctx.save(); ctx.shadowColor = outcome.hex; ctx.shadowBlur = 25; ctx.fillStyle = outcome.hex; ctx.beginPath(); ctx.arc(450, 240, 85, 0, Math.PI * 2); ctx.fill(); ctx.restore(); ctx.fillStyle = "#ffffff"; ctx.font = "900 22px 'Segoe UI', Sans-serif"; ctx.textAlign = "center"; ctx.fillText(outcome.label, 450, 240); ctx.font = "bold 14px 'Segoe UI', Sans-serif"; ctx.fillText(`${outcome.mult}X MULTIPLIER`, 450, 265); ctx.textAlign = "left"; ctx.fillStyle = "#64748b"; ctx.font = "bold 14px 'Segoe UI', Sans-serif"; ctx.fillText("PLAYER", 50, 385); ctx.fillText("BET AMOUNT", 250, 385); ctx.fillText(netChange >= 0 ? "PROFIT" : "LOSS", 430, 385); ctx.fillText("NEW BALANCE", 600, 385); ctx.fillText("3H LIMIT", 760, 385); ctx.fillStyle = "#ffffff"; ctx.font = "bold 18px 'Segoe UI', Sans-serif"; const shortName = userName.length > 12 ? userName.substring(0, 12) + "..." : userName; ctx.fillText(shortName, 50, 415); ctx.fillStyle = "#38bdf8"; ctx.fillText(`$${this.formatMoney(betAmount)}`, 250, 415); ctx.fillStyle = netChange >= 0 ? "#4ade80" : "#ef4444"; ctx.fillText(`${netChange >= 0 ? "+" : "-"}$${this.formatMoney(Math.abs(netChange))}`, 430, 415); ctx.fillStyle = "#f1c40f"; ctx.fillText(`$${this.formatMoney(updatedUser.balance)}`, 600, 415); ctx.fillStyle = "#a855f7"; ctx.fillText(`${currentWheelCount}/${MAX_PLAY_LIMIT}`, 760, 415); const cardPath = path.join(cacheDir, `wheel_res_${senderID}_${Date.now()}.png`); await fs.writeFile(cardPath, canvas.toBuffer("image/png")); const finalPayload = { body: netChange >= 0 ? `šŸŽ‰ š—–š—¢š—”š—šš—„š—”š—§š—Øš—Ÿš—”š—§š—œš—¢š—”š—¦ ${userName.toUpperCase()}!\nšŸŽÆ š—Ÿš—®š—»š—±š—²š—± š—¢š—»: ${outcome.label}\nšŸ’Ž š—¬š—¢š—Ø š—Ŗš—¢š—” $${this.formatMoney(winAmount)} USD!` : `šŸ’” š—•š—˜š—§š—§š—˜š—„ š—Ÿš—Øš—–š—ž š—”š—˜š—«š—§ š—§š—œš— š—˜ ${userName.toUpperCase()}!\nšŸŽÆ š—Ÿš—®š—»š—±š—²š—± š—¢š—»: ${outcome.label}\nšŸ“‰ š—¬š—¢š—Ø š—Ÿš—¢š—¦š—§ $${this.formatMoney(betAmount)} USD!`, attachment: fs.createReadStream(cardPath) }; const sendCallback = () => { if (fs.existsSync(cardPath)) fs.unlinkSync(cardPath); }; return message && typeof message.reply === "function" ? message.reply(finalPayload, sendCallback) : api.sendMessage(finalPayload, event.threadID, sendCallback, event.messageID); } catch (err) { console.error(err); return sendMsg("āŒ š—Ŗš—›š—˜š—˜š—Ÿ š—šš—”š— š—˜ š—˜š—„š—„š—¢š—„!"); } } };