← back to browse
goat command

top

Premium EXP & Balance Leaderboard

0
views
0
likes
0
installs
raw source
"use strict";

const { createCanvas, loadImage } = require("canvas");
const fs = require("fs-extra");
const path = require("path");
const axios = require("axios");

module.exports = {
  config: {
    name: "top",
    version: "8.0",
    author: "Ariyan ai",
    role: 0,

    shortDescription: {
      en: "Premium EXP & Balance Leaderboard"
    },

    longDescription: {
      en: "Show top users by EXP or balance with a stylish leaderboard image."
    },

    category: "ECONOMY",

    guide: {
      en:
        "{pn} rank - Top EXP leaderboard\n" +
        "{pn} bal - Top balance leaderboard"
    }
  },

  onStart: async function ({ event, usersData, message }) {

    // ==========================================
    // MODE
    // ==========================================

    const args = (event.body || "").trim().split(/\s+/);
    const mode = args[1]?.toLowerCase();

    if (!mode || !["rank", "bal", "balance"].includes(mode)) {
      return message.reply(
        "❌ Usage:\n\n" +
        "🏆 /top rank\n" +
        "→ Top EXP leaderboard\n\n" +
        "💰 /top bal\n" +
        "→ Top Balance leaderboard"
      );
    }

    const isRank = mode === "rank";

    // ==========================================
    // TITLE
    // ==========================================

    const title = isRank
      ? "EXPERIENCE RANK LEADERBOARD"
      : "TOP BALANCE LEADERBOARD";

    const subtitle = isRank
      ? "THE STRONGEST PLAYERS BY EXPERIENCE"
      : "THE RICHEST PLAYERS IN THE ECONOMY";

    // ==========================================
    // GET USERS
    // ==========================================

    let allUsers;

    try {
      allUsers = await usersData.getAll();
    } catch (error) {
      console.error("TOP getAll Error:", error);

      return message.reply(
        "❌ User data load করতে সমস্যা হয়েছে।"
      );
    }

    if (!Array.isArray(allUsers) || allUsers.length === 0) {
      return message.reply(
        "❌ কোনো user data পাওয়া যায়নি।"
      );
    }

    // ==========================================
    // VALUE
    // ==========================================

    const getValue = user => {

      const value = isRank
        ? Number(user.exp || 0)
        : Number(user.money || 0);

      return Number.isFinite(value)
        ? value
        : 0;
    };

    // ==========================================
    // SORT ALL USERS
    // IMPORTANT:
    // We sort ALL users first so the user's
    // actual global rank can be calculated.
    // ==========================================

    const sortedUsers = allUsers
      .filter(user => user && user.userID)
      .sort((a, b) => {
        return getValue(b) - getValue(a);
      });

    if (!sortedUsers.length) {
      return message.reply(
        "❌ Leaderboard empty."
      );
    }

    // ==========================================
    // TOP 17
    // ==========================================

    const topUsers = sortedUsers.slice(0, 17);

    // ==========================================
    // CURRENT USER
    // ==========================================

    const currentUserID = String(event.senderID);

    let currentUserIndex = sortedUsers.findIndex(
      user => String(user.userID) === currentUserID
    );

    let currentUser;

    if (currentUserIndex !== -1) {

      currentUser = sortedUsers[currentUserIndex];

    } else {

      // Sometimes the sender isn't returned by getAll()
      try {

        currentUser = await usersData.get(
          currentUserID
        );

      } catch (_) {}

      if (!currentUser) {

        currentUser = {
          userID: currentUserID,
          name: "Unknown",
          exp: 0,
          money: 0
        };

      }

      // Calculate approximate rank
      currentUserIndex = sortedUsers.filter(
        user => getValue(user) > getValue(currentUser)
      ).length;
    }

    const currentRank = currentUserIndex + 1;

    // ==========================================
    // FORMAT NUMBER
    // ==========================================

    function formatNumber(num) {

      num = Number(num) || 0;

      const units = [
        { value: 1e18, suffix: "Qi" },
        { value: 1e15, suffix: "Qa" },
        { value: 1e12, suffix: "T" },
        { value: 1e9, suffix: "B" },
        { value: 1e6, suffix: "M" },
        { value: 1e3, suffix: "K" }
      ];

      for (const unit of units) {

        if (num >= unit.value) {

          return (
            (num / unit.value)
              .toFixed(2)
              .replace(/\.00$/, "")
              .replace(/(\.\d)0$/, "$1") +
            unit.suffix
          );

        }
      }

      return Math.floor(num).toLocaleString();
    }

    // ==========================================
    // SAFE NAME
    // ==========================================

    function safeName(name) {

      return String(name || "Unknown")
        .replace(/[<>]/g, "")
        .trim() || "Unknown";
    }

    // ==========================================
    // TEXT TRUNCATE
    // ==========================================

    function truncate(ctx, text, maxWidth) {

      text = String(text);

      if (
        ctx.measureText(text).width <= maxWidth
      ) {
        return text;
      }

      let result = text;

      while (
        result.length > 1 &&
        ctx.measureText(result + "…").width > maxWidth
      ) {
        result = result.slice(0, -1);
      }

      return result + "…";
    }

    // ==========================================
    // CANVAS
    // ==========================================

    const WIDTH = 1000;
    const HEIGHT = 1780;

    const canvas = createCanvas(
      WIDTH,
      HEIGHT
    );

    const ctx = canvas.getContext("2d");

    // ==========================================
    // BACKGROUND
    // ==========================================

    const bg = ctx.createLinearGradient(
      0,
      0,
      WIDTH,
      HEIGHT
    );

    bg.addColorStop(0, "#07152f");
    bg.addColorStop(0.45, "#0c2450");
    bg.addColorStop(1, "#071329");

    ctx.fillStyle = bg;

    ctx.fillRect(
      0,
      0,
      WIDTH,
      HEIGHT
    );

    // ==========================================
    // BACKGROUND GLOW
    // ==========================================

    const glow = ctx.createRadialGradient(
      WIDTH / 2,
      180,
      20,
      WIDTH / 2,
      180,
      550
    );

    glow.addColorStop(
      0,
      isRank
        ? "rgba(168,85,247,0.35)"
        : "rgba(59,130,246,0.35)"
    );

    glow.addColorStop(
      1,
      "rgba(0,0,0,0)"
    );

    ctx.fillStyle = glow;

    ctx.fillRect(
      0,
      0,
      WIDTH,
      700
    );

    // ==========================================
    // STARS
    // ==========================================

    for (let i = 0; i < 180; i++) {

      const x =
        Math.random() * WIDTH;

      const y =
        Math.random() * HEIGHT;

      const radius =
        Math.random() * 1.8 + 0.3;

      ctx.beginPath();

      ctx.arc(
        x,
        y,
        radius,
        0,
        Math.PI * 2
      );

      ctx.fillStyle =
        `rgba(255,255,255,${Math.random() * 0.45 + 0.1})`;

      ctx.fill();
    }

    // ==========================================
    // HEADER
    // ==========================================

    ctx.textAlign = "center";

    ctx.font =
      "bold 38px Arial";

    ctx.fillStyle =
      isRank
        ? "#d8b4fe"
        : "#93c5fd";

    ctx.shadowColor =
      isRank
        ? "rgba(168,85,247,0.9)"
        : "rgba(59,130,246,0.9)";

    ctx.shadowBlur = 22;

    ctx.fillText(
      title,
      WIDTH / 2,
      65
    );

    ctx.shadowBlur = 0;

    ctx.font =
      "16px Arial";

    ctx.fillStyle =
      "#b8c7e6";

    ctx.fillText(
      subtitle,
      WIDTH / 2,
      95
    );

    // ==========================================
    // HEADER LINE
    // ==========================================

    const lineGradient =
      ctx.createLinearGradient(
        100,
        0,
        900,
        0
      );

    lineGradient.addColorStop(
      0,
      "rgba(255,255,255,0)"
    );

    lineGradient.addColorStop(
      0.5,
      isRank
        ? "#a855f7"
        : "#38bdf8"
    );

    lineGradient.addColorStop(
      1,
      "rgba(255,255,255,0)"
    );

    ctx.fillStyle =
      lineGradient;

    ctx.fillRect(
      100,
      125,
      800,
      2
    );

    // ==========================================
    // AVATAR CACHE
    // ==========================================

    const avatarCache = {};

    const avatarUsers = [
      ...topUsers
    ];

    if (
      currentUser &&
      !avatarUsers.some(
        u =>
          String(u.userID) ===
          String(currentUser.userID)
      )
    ) {
      avatarUsers.push(currentUser);
    }

    for (const user of avatarUsers) {

      try {

        let avatarURL = null;

        try {

          avatarURL =
            await usersData.getAvatarUrl(
              user.userID
            );

        } catch (_) {}

        if (!avatarURL) continue;

        const response =
          await axios.get(
            avatarURL,
            {
              responseType:
                "arraybuffer",
              timeout: 8000
            }
          );

        avatarCache[user.userID] =
          await loadImage(
            Buffer.from(
              response.data
            )
          );

      } catch (_) {}
    }

    // ==========================================
    // DRAW AVATAR
    // ==========================================

    function drawAvatar(
      user,
      x,
      y,
      radius,
      borderColor
    ) {

      if (!user) return;

      // Glow ring
      ctx.save();

      ctx.beginPath();

      ctx.arc(
        x,
        y,
        radius + 8,
        0,
        Math.PI * 2
      );

      ctx.strokeStyle =
        borderColor;

      ctx.lineWidth = 4;

      ctx.shadowColor =
        borderColor;

      ctx.shadowBlur = 22;

      ctx.stroke();

      ctx.restore();

      // Avatar
      ctx.save();

      ctx.beginPath();

      ctx.arc(
        x,
        y,
        radius,
        0,
        Math.PI * 2
      );

      ctx.clip();

      if (
        avatarCache[user.userID]
      ) {

        ctx.drawImage(
          avatarCache[user.userID],
          x - radius,
          y - radius,
          radius * 2,
          radius * 2
        );

      } else {

        ctx.fillStyle =
          "#263858";

        ctx.fillRect(
          x - radius,
          y - radius,
          radius * 2,
          radius * 2
        );

        ctx.fillStyle =
          "#94a3b8";

        ctx.font =
          `bold ${Math.floor(radius * 0.65)}px Arial`;

        ctx.textAlign =
          "center";

        ctx.textBaseline =
          "middle";

        ctx.fillText(
          safeName(user.name)
            .charAt(0)
            .toUpperCase(),
          x,
          y
        );

        ctx.textBaseline =
          "alphabetic";
      }

      ctx.restore();

      // Ring
      ctx.beginPath();

      ctx.arc(
        x,
        y,
        radius,
        0,
        Math.PI * 2
      );

      ctx.strokeStyle =
        borderColor;

      ctx.lineWidth = 3;

      ctx.stroke();
    }

    // ==========================================
    // TOP 3
    // ==========================================

    const top3 = [

      {
        user: topUsers[1],
        x: 245,
        y: 270,
        r: 75,
        rank: 2,
        color: "#cbd5e1"
      },

      {
        user: topUsers[0],
        x: 500,
        y: 230,
        r: 96,
        rank: 1,
        color: "#facc15"
      },

      {
        user: topUsers[2],
        x: 755,
        y: 270,
        r: 75,
        rank: 3,
        color: "#fb923c"
      }

    ];

    for (const item of top3) {

      if (!item.user) continue;

      drawAvatar(
        item.user,
        item.x,
        item.y,
        item.r,
        item.color
      );

      // Rank badge
      ctx.beginPath();

      ctx.arc(
        item.x +
          item.r * 0.72,
        item.y -
          item.r * 0.72,
        22,
        0,
        Math.PI * 2
      );

      ctx.fillStyle =
        item.color;

      ctx.fill();

      ctx.font =
        "bold 16px Arial";

      ctx.fillStyle =
        "#071329";

      ctx.textAlign =
        "center";

      ctx.textBaseline =
        "middle";

      ctx.fillText(
        `#${item.rank}`,
        item.x +
          item.r * 0.72,
        item.y -
          item.r * 0.72
      );

      ctx.textBaseline =
        "alphabetic";

      // Name
      ctx.font =
        "bold 20px Arial";

      ctx.fillStyle =
        "#ffffff";

      ctx.fillText(
        truncate(
          ctx,
          safeName(
            item.user.name
          ),
          210
        ),
        item.x,
        item.y +
          item.r +
          34
      );

      // Value
      ctx.font =
        "bold 18px Arial";

      ctx.fillStyle =
        item.color;

      ctx.fillText(
        isRank
          ? `Level ${formatNumber(getValue(item.user))}`
          : `$${formatNumber(getValue(item.user))}`,
        item.x,
        item.y +
          item.r +
          62
      );
    }

    // ==========================================
    // LEADERBOARD LIST
    // ==========================================

    const LIST_START = 435;

    const ROW_H = 62;

    const GAP = 9;

    const maxValue =
      Math.max(
        getValue(topUsers[0]),
        1
      );

    for (
      let i = 3;
      i < topUsers.length;
      i++
    ) {

      const user =
        topUsers[i];

      const y =
        LIST_START +
        (i - 3) *
          (ROW_H + GAP);

      const x = 45;

      const w =
        WIDTH - 90;

      const h =
        ROW_H;

      // ========================================
      // CARD
      // ========================================

      ctx.beginPath();

      const radius = 14;

      ctx.moveTo(
        x + radius,
        y
      );

      ctx.lineTo(
        x + w - radius,
        y
      );

      ctx.arcTo(
        x + w,
        y,
        x + w,
        y + radius,
        radius
      );

      ctx.lineTo(
        x + w,
        y + h - radius
      );

      ctx.arcTo(
        x + w,
        y + h,
        x + w - radius,
        y + h,
        radius
      );

      ctx.lineTo(
        x + radius,
        y + h
      );

      ctx.arcTo(
        x,
        y + h,
        x,
        y + h - radius,
        radius
      );

      ctx.lineTo(
        x,
        y + radius
      );

      ctx.arcTo(
        x,
        y,
        x + radius,
        y,
        radius
      );

      ctx.closePath();

      ctx.fillStyle =
        "rgba(30,41,78,0.86)";

      ctx.fill();

      ctx.strokeStyle =
        "rgba(148,163,184,0.12)";

      ctx.lineWidth = 1;

      ctx.stroke();

      // ========================================
      // RANK
      // ========================================

      ctx.textAlign =
        "left";

      ctx.font =
        "bold 17px Arial";

      ctx.fillStyle =
        "#dbeafe";

      ctx.fillText(
        `#${i + 1}`,
        65,
        y + 37
      );

      // ========================================
      // AVATAR
      // ========================================

      const avX = 130;

      const avY =
        y + ROW_H / 2;

      const avR = 22;

      ctx.save();

      ctx.beginPath();

      ctx.arc(
        avX,
        avY,
        avR,
        0,
        Math.PI * 2
      );

      ctx.clip();

      if (
        avatarCache[user.userID]
      ) {

        ctx.drawImage(
          avatarCache[user.userID],
          avX - avR,
          avY - avR,
          avR * 2,
          avR * 2
        );

      } else {

        ctx.fillStyle =
          "#334155";

        ctx.fillRect(
          avX - avR,
          avY - avR,
          avR * 2,
          avR * 2
        );

      }

      ctx.restore();

      // Avatar border
      ctx.beginPath();

      ctx.arc(
        avX,
        avY,
        avR,
        0,
        Math.PI * 2
      );

      ctx.strokeStyle =
        "rgba(147,197,253,0.35)";

      ctx.lineWidth = 2;

      ctx.stroke();

      // ========================================
      // NAME
      // ========================================

      ctx.font =
        "bold 18px Arial";

      ctx.fillStyle =
        "#f1f5f9";

      ctx.fillText(
        truncate(
          ctx,
          safeName(user.name),
          190
        ),
        175,
        y + 37
      );

      // ========================================
      // PROGRESS BAR
      // ========================================

      const barX = 395;

      const barY =
        y + 27;

      const barW = 250;

      const barH = 9;

      ctx.beginPath();

      ctx.roundRect(
        barX,
        barY,
        barW,
        barH,
        5
      );

      ctx.fillStyle =
        "rgba(2,6,23,0.85)";

      ctx.fill();

      const ratio =
        Math.min(
          getValue(user) /
            maxValue,
          1
        );

      const activeW =
        Math.max(
          ratio * barW,
          7
        );

      const progressGradient =
        ctx.createLinearGradient(
          barX,
          0,
          barX + activeW,
          0
        );

      if (isRank) {

        progressGradient.addColorStop(
          0,
          "#8b5cf6"
        );

        progressGradient.addColorStop(
          1,
          "#c084fc"
        );

      } else {

        progressGradient.addColorStop(
          0,
          "#06b6d4"
        );

        progressGradient.addColorStop(
          1,
          "#38bdf8"
        );
      }

      ctx.beginPath();

      ctx.roundRect(
        barX,
        barY,
        activeW,
        barH,
        5
      );

      ctx.fillStyle =
        progressGradient;

      ctx.shadowColor =
        isRank
          ? "#a855f7"
          : "#22d3ee";

      ctx.shadowBlur = 8;

      ctx.fill();

      ctx.shadowBlur = 0;

      // ========================================
      // VALUE
      // ========================================

      ctx.textAlign =
        "right";

      ctx.font =
        "bold 17px Arial";

      ctx.fillStyle =
        isRank
          ? "#d8b4fe"
          : "#7dd3fc";

      ctx.fillText(
        isRank
          ? `${formatNumber(getValue(user))} EXP`
          : `$${formatNumber(getValue(user))}`,
        WIDTH - 65,
        y + 37
      );
    }

    // ==========================================
    // CURRENT USER PANEL
    // ==========================================

    const selfY = 1570;

    const selfX = 45;

    const selfW =
      WIDTH - 90;

    const selfH = 86;

    // Panel
    ctx.beginPath();

    ctx.roundRect(
      selfX,
      selfY,
      selfW,
      selfH,
      18
    );

    ctx.fillStyle =
      "rgba(5,20,50,0.96)";

    ctx.fill();

    ctx.strokeStyle =
      isRank
        ? "#a855f7"
        : "#38bdf8";

    ctx.lineWidth = 2;

    ctx.shadowColor =
      isRank
        ? "rgba(168,85,247,0.55)"
        : "rgba(56,189,248,0.55)";

    ctx.shadowBlur = 15;

    ctx.stroke();

    ctx.shadowBlur = 0;

    // ==========================================
    // SELF RANK
    // ==========================================

    ctx.textAlign =
      "left";

    ctx.font =
      "bold 18px Arial";

    ctx.fillStyle =
      "#e2e8f0";

    ctx.fillText(
      `#${currentRank}`,
      68,
      selfY + 51
    );

    // ==========================================
    // SELF AVATAR
    // ==========================================

    const selfAvatarX = 160;

    const selfAvatarY =
      selfY + selfH / 2;

    const selfAvatarR = 31;

    drawAvatar(
      currentUser,
      selfAvatarX,
      selfAvatarY,
      selfAvatarR,
      isRank
        ? "#a855f7"
        : "#38bdf8"
    );

    // ==========================================
    // SELF NAME
    // ==========================================

    ctx.font =
      "bold 20px Arial";

    ctx.fillStyle =
      "#ffffff";

    ctx.fillText(
      truncate(
        ctx,
        safeName(
          currentUser.name
        ),
        300
      ),
      215,
      selfY + 40
    );

    ctx.font =
      "14px Arial";

    ctx.fillStyle =
      "#94a3b8";

    ctx.fillText(
      "YOUR POSITION",
      215,
      selfY + 63
    );

    // ==========================================
    // SELF VALUE
    // ==========================================

    ctx.textAlign =
      "right";

    ctx.font =
      "bold 22px Arial";

    ctx.fillStyle =
      isRank
        ? "#d8b4fe"
        : "#7dd3fc";

    ctx.fillText(
      isRank
        ? `Lv.${formatNumber(
            getValue(currentUser)
          )} • ${formatNumber(
            getValue(currentUser)
          )} EXP`
        : `$${formatNumber(
            getValue(currentUser)
          )}`,
      WIDTH - 70,
      selfY + 48
    );

    // ==========================================
    // FOOTER
    // ==========================================

    ctx.textAlign =
      "center";

    ctx.font =
      "13px Arial";

    ctx.fillStyle =
      "rgba(191,219,254,0.55)";

    ctx.fillText(
      `✦ Ariyan ai • ${
        isRank
          ? "Experience Ranking"
          : "Balance Ranking"
      } • Total Users: ${sortedUsers.length}`,
      WIDTH / 2,
      HEIGHT - 25
    );

    // ==========================================
    // SAVE IMAGE
    // ==========================================

    const cacheDir =
      path.join(
        __dirname,
        "cache"
      );

    await fs.ensureDir(
      cacheDir
    );

    const imagePath =
      path.join(
        cacheDir,
        `top_${isRank ? "rank" : "bal"}_${Date.now()}.png`
      );

    await fs.writeFile(
      imagePath,
      canvas.toBuffer("image/png")
    );

    // ==========================================
    // SEND
    // ==========================================

    return message.reply(
      {
        body: isRank
          ? `🏆 𝐄𝐗𝐏 𝐑𝐀𝐍𝐊𝐈𝐍𝐆\n\n📍 Your Position: #${currentRank}`
          : `💰 𝐁𝐀𝐋𝐀𝐍𝐂𝐄 𝐑𝐀𝐍𝐊𝐈𝐍𝐆\n\n📍 Your Position: #${currentRank}`,

        attachment:
          fs.createReadStream(
            imagePath
          )
      },

      () => {

        fs.remove(
          imagePath
        ).catch(() => {});

      }
    );
  }
};

View raw file