Files
1800-skin-checker/bot.backup-codex-20260628-021829.js
T
2026-06-28 10:15:43 -05:00

1901 lines
106 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =============================================
// FORTNITE DISCORD BOT — ALL-IN-ONE
// =============================================
// Commands:
// !setuproles — create all roles
// !addicons — upload skin icons as server emojis
// !setroleicons — apply emojis to roles
// !removeoldemojis — delete old skin emojis
// !verify — verify Fortnite account & auto-assign roles
//
// Run with: node bot.js
// =============================================
require('dotenv').config();
const { Client, GatewayIntentBits, PermissionsBitField, EmbedBuilder, REST, Routes, SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle, AttachmentBuilder } = require('discord.js');
const sharp = require('sharp');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.DirectMessages,
],
partials: ['CHANNEL'],
});
// ─────────────────────────────────────────────────────────────────────────────
// ROLES TO CREATE
// ─────────────────────────────────────────────────────────────────────────────
const roles = [
{ name: 'Fully Golden Peely', color: 0xFFD700 },
{ name: 'OG Aerial Assault Trooper', color: 0x8A8A8A },
{ name: 'OG Ghoul Trooper', color: 0xA89DB8 },
{ name: 'OG Renegade Raider', color: 0x7B2D2D },
{ name: 'OG Skull Trooper', color: 0x9B59B6 },
{ name: 'Barricade', color: 0x9E9E9E },
{ name: 'Double Helix', color: 0xD0443C },
{ name: 'Honor Guard', color: 0x4A90D9 },
{ name: 'Wonder', color: 0x2ECC71 },
{ name: 'Surf Strider', color: 0x1ABC9C },
{ name: 'Stealth Reflex', color: 0xE8927C },
{ name: 'Dark Vertex', color: 0x8E44AD },
{ name: 'Royale Bomber', color: 0xE67E22 },
{ name: 'Wildcat', color: 0xE8832A },
{ name: 'Clyde', color: 0xE8832A },
{ name: 'Eon', color: 0x27AE60 },
{ name: 'Huntmaster Saber', color: 0xD4AC0D },
{ name: 'Dark Skully', color: 0x7D6B8A },
{ name: 'Galaxy', color: 0x5B7DD8 },
{ name: 'Ikonik', color: 0xD0443C },
{ name: 'Black Knight', color: 0xCCCCCC },
{ name: 'Glow', color: 0xBB8FCC },
{ name: 'Rogue Spider Knight', color: 0x2ECC71 },
{ name: 'Thrilldiver', color: 0x4A90D9 },
{ name: 'Freediver', color: 0x9E9E9E },
{ name: 'Neo Versa', color: 0x9B59B6 },
{ name: 'Cobalt Snowfoot', color: 0x1ABC9C },
{ name: 'Florin', color: 0x8E44AD },
{ name: "What's Wrong, Toona Fish?", color: 0xCCCCCC },
{ name: 'Cameo Needs Loot', color: 0x2ECC71 },
{ name: 'Victory!', color: 0x9B59B6 },
{ name: 'Collegiate items', color: 0x2ECC71 },
{ name: 'StreamElements', color: 0xF1C40F },
{ name: 'Llamas', color: 0xFF69B4 },
{ name: 'Repaz', color: 0xD4AC0D },
{ name: 'lollek', color: 0xFFFFFF },
{ name: 'Celebration Heart', color: 0x9B59B6 },
{ name: 'Funflower', color: 0xD4C227 },
{ name: 'Level 100', color: 0xBB44EE },
{ name: 'Alpha Banner', color: 0x777777 },
{ name: 'YouTube Banner', color: 0x777777 },
{ name: 'STW Founders', color: 0xE67E22 },
{ name: 'Ultimate STW Founders', color: 0x888888 },
{ name: 'Rare Usernames', color: 0xF1C40F },
{ name: 'Skin Collector', color: 0x888888 },
{ name: 'PS+ Pack Collector', color: 0x3498DB },
{ name: 'Spray Collector', color: 0xCCCCCC },
{ name: 'Rare Banners', color: 0x888888 },
{ name: 'Emote Collector', color: 0xCCCCCC },
// --- Exclusive Pickaxes ---
{ name: "Raider's Revenge", color: 0x7B2D2D },
{ name: 'Axe of Champions', color: 0xF1C40F },
{ name: 'Blade of Champions', color: 0xF1C40F },
{ name: 'Axecalibur', color: 0xCCCCCC },
{ name: 'EVA', color: 0x4A90D9 },
{ name: 'AC/DC', color: 0xF1C40F },
{ name: 'Resonator', color: 0x27AE60 },
{ name: 'Pinpoint', color: 0xD0443C },
{ name: 'Stellar Axe', color: 0x5B7DD8 },
{ name: 'Stealth Angular Axe', color: 0x9B59B6 },
{ name: 'Dark Razor', color: 0x7D6B8A },
{ name: 'Electri-claw', color: 0xE8832A },
{ name: 'Dark Splitter', color: 0x8E44AD },
{ name: 'Merry Mint Axe', color: 0x2ECC71 },
{ name: 'Indigo Inverter', color: 0x1ABC9C },
{ name: 'Rose Glow', color: 0xFF69B4 },
{ name: 'Controller', color: 0x3498DB },
{ name: 'Pneumatic Twin', color: 0x4A90D9 },
{ name: 'Perfect Point', color: 0x9E9E9E },
{ name: 'Petrol Pump', color: 0xE67E22 },
{ name: 'Future Relic Scythe', color: 0x8E44AD },
// --- Exclusive Emoticons ---
{ name: 'Skull Trooper Emoticon', color: 0x9B59B6 },
{ name: 'Maevys Emoticon', color: 0xD0443C },
{ name: 'ZetFar Emoticon', color: 0x4A90D9 },
// --- PlayStation Exclusives ---
{ name: 'Blue Team Leader', color: 0x3498DB }, // PS Plus Pack 1
{ name: 'Blue Striker', color: 0x3498DB }, // PS Plus Pack 2
{ name: 'Prodigy', color: 0x3498DB }, // PS Plus Pack 4
{ name: 'Carbon Commando', color: 0x3498DB }, // PS Plus Pack 5
{ name: 'Trilogy', color: 0x3498DB }, // PS Plus Pack 8
{ name: 'Point Patroller', color: 0x3498DB }, // PS Plus Pack 9
{ name: 'Cloud Striker', color: 0x3498DB }, // PS Plus Pack 12
{ name: 'Fixer', color: 0x3498DB }, // PS Plus Pack 14
{ name: 'Blizzard Bomber', color: 0x3498DB }, // PS Plus Pack 18
{ name: 'Jun-Hwan', color: 0x3498DB }, // Blue Phoenix Pack
{ name: 'Lina Scorch', color: 0x3498DB }, // Blazin Wheels Pack
{ name: 'Indigo Kuno', color: 0x9B59B6 }, // PS Generations Cup
// --- Twitch / Amazon Prime ---
{ name: 'Sub Commander', color: 0x9146FF }, // Twitch Prime Pack 1
{ name: 'Havoc', color: 0x9146FF }, // Twitch Prime Pack 1
{ name: 'Trailblazer', color: 0x9146FF }, // Twitch Prime Pack 2
// --- Intel / PC Exclusive ---
{ name: 'Reflex', color: 0x0071C5 }, // Intel bundle
// --- OG Battle Pass (C1S2) ---
{ name: 'Sparkle Specialist', color: 0xFF69B4 },
{ name: 'Royale Knight', color: 0xCCCCCC },
{ name: 'Blue Squire', color: 0x4A90D9 },
// --- OG Battle Pass (C1S3) ---
{ name: 'The Reaper', color: 0x888888 },
{ name: 'Elite Agent', color: 0x888888 },
{ name: 'Rogue Agent', color: 0x888888 },
// --- OG Battle Pass (C1S4) ---
{ name: 'Omega', color: 0x8E44AD },
// --- Chapter 2 Season 2 Battle Pass (golden styles) ---
{ name: 'Skye', color: 0xF1C40F },
{ name: 'TNTina', color: 0xF1C40F },
{ name: 'Meowscles', color: 0xF1C40F },
{ name: 'Midas', color: 0xFFD700 },
// --- Gaming Legends / Collab Exclusives ---
{ name: 'Travis Scott', color: 0xF1C40F }, // Icon Series, never returned
{ name: 'Astro Jack', color: 0xF1C40F }, // Icon Series, never returned
{ name: 'Kratos', color: 0xCCCCCC }, // PlayStation collab
{ name: 'Master Chief', color: 0x4A90D9 }, // Xbox collab
// --- World Cup / Tournament ---
{ name: 'World Warrior', color: 0xF1C40F }, // Fortnite World Cup 2019
// --- V-Bucks Card Promo ---
{ name: 'Color Splash Jellie', color: 0xFF69B4 },
// --- Save the World Founders ---
{ name: 'Rose Team Leader', color: 0xE67E22 },
{ name: 'Warpaint', color: 0xE67E22 },
// --- OT Banners ---
{ name: 'OT1 Banner', color: 0xFFD700 },
{ name: 'OT2 Banner', color: 0xFFD700 },
{ name: 'OT3 Banner', color: 0xFFD700 },
{ name: 'OT4 Banner', color: 0xFFD700 },
{ name: 'OT5 Banner', color: 0xFFD700 },
{ name: 'OT6 Banner', color: 0xFFD700 },
{ name: 'OT7 Banner', color: 0xFFD700 },
{ name: 'OT8 Banner', color: 0xFFD700 },
{ name: 'OT9 Banner', color: 0xFFD700 },
{ name: 'OT10 Banner', color: 0xFFD700 },
{ name: 'OT11 Banner', color: 0xFFD700 },
];
// ─────────────────────────────────────────────────────────────────────────────
// EMOJI / ICON MAP
// ─────────────────────────────────────────────────────────────────────────────
const roleIconMap = [
{ name: 'Fully Golden Peely', id: 'CID_701_Athena_Commando_M_BananaAgent', emoji: 'FullyGoldenPeely',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/cid_701_athena_commando_m_bananaagent/variants/progressive/stage4.png' },
{ name: 'OG Aerial Assault Trooper', id: 'CID_017_Athena_Commando_M', emoji: 'AerialAssaultTrooper' },
{ name: 'OG Ghoul Trooper', id: 'CID_029_Athena_Commando_F_Halloween', emoji: 'GhoulTrooper',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/cid_029_athena_commando_f_halloween/variants/material/mat3.png' },
{ name: 'OG Renegade Raider', id: 'CID_028_Athena_Commando_F', emoji: 'RenegadeRaider' },
{ name: 'OG Skull Trooper', id: 'CID_030_Athena_Commando_M_Halloween', emoji: 'SkullTrooper',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/cid_030_athena_commando_m_halloween/variants/clothingcolor/mat1.png' },
{ name: 'Barricade', id: 'Character_RankedPhantom', emoji: 'Barricade' },
{ name: 'Double Helix', id: 'CID_183_Athena_Commando_M_ModernMilitaryRed', emoji: 'DoubleHelix' },
{ name: 'Honor Guard', id: 'CID_342_Athena_Commando_M_StreetRacerMetallic', emoji: 'HonorGuard' },
{ name: 'Wonder', id: 'CID_434_Athena_Commando_F_StealthHonor', emoji: 'Wonder' },
{ name: 'Surf Strider', id: 'CID_783_Athena_Commando_M_AquaJacket', emoji: 'SurfStrider' },
{ name: 'Stealth Reflex', id: 'CID_386_Athena_Commando_M_StreetOpsStealth', emoji: 'StealthReflex' },
{ name: 'Dark Vertex', id: 'CID_371_Athena_Commando_M_SpeedyMidnight', emoji: 'DarkVertex' },
{ name: 'Royale Bomber', id: 'CID_113_Athena_Commando_M_BlueAce', emoji: 'RoyaleBomber' },
{ name: 'Wildcat', id: 'CID_757_Athena_Commando_F_WildCat', emoji: 'Wildcat' },
{ name: 'Clyde', id: 'Character_UnbrokenTrash', emoji: 'Clyde' },
{ name: 'Eon', id: 'CID_174_Athena_Commando_F_CarbideWhite', emoji: 'Eon' },
{ name: 'Huntmaster Saber', id: 'Character_MasterKeyOrder', emoji: 'HuntmasterSaber' },
{ name: 'Dark Skully', id: 'CID_850_Athena_Commando_F_SkullBriteCube', emoji: 'DarkSkully' },
{ name: 'Galaxy', id: 'CID_175_Athena_Commando_M_Celestial', emoji: 'Galaxy' },
{ name: 'Ikonik', id: 'CID_313_Athena_Commando_M_KpopFashion', emoji: 'Ikonik' },
// { name: 'Black Knight', id: 'CID_035_Athena_Commando_M_Medieval', emoji: 'BlackKnight' }, // temporarily disabled — upload stalls
{ name: 'Glow', id: 'CID_479_Athena_Commando_F_Davinci', emoji: 'Glow' },
{ name: 'Rogue Spider Knight', id: 'CID_516_Athena_Commando_M_BlackWidowRogue', emoji: 'RogueSpiderKnight' },
{ name: 'Thrilldiver', id: 'Character_SummerUnsuitable', emoji: 'Thrilldiver' },
{ name: 'Freediver', id: 'Character_CoolSuitable', emoji: 'Freediver' },
{ name: 'Neo Versa', id: 'CID_441_Athena_Commando_F_CyberScavengerBlue', emoji: 'NeoVersa' },
{ name: 'Cobalt Snowfoot', id: 'Character_SnowNinjaDark', emoji: 'CobaltSnowfoot' },
{ name: 'Florin', id: 'Character_AsterOrder', emoji: 'Florin' },
{ name: "What's Wrong, Toona Fish?", id: 'CID_A_201_Athena_Commando_M_TeriyakiFishToon', emoji: 'ToonaFish' },
{ name: 'Cameo Needs Loot', id: 'Emoji_S20_NeedLoot', emoji: 'Cameo',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/emoji_s20_needloot/smallicon.png' },
// --- Exclusive Pickaxes ---
{ name: "Raider's Revenge", id: 'Pickaxe_Lockjaw', emoji: 'RaidersRevenge' },
{ name: 'Axe of Champions', id: 'Pickaxe_ID_376_FNCS', emoji: 'AxeOfChampions' },
{ name: 'Blade of Champions', id: 'Pickaxe_FNCS_S32', emoji: 'BladeOfChampions' },
{ name: 'Axecalibur', id: 'Pickaxe_ID_011_Medieval', emoji: 'Axecalibur' },
{ name: 'EVA', id: 'Pickaxe_ID_028_Space', emoji: 'EVA' },
{ name: 'AC/DC', id: 'Pickaxe_ID_013_Teslacoil', emoji: 'ACDC' },
{ name: 'Resonator', id: 'Pickaxe_ID_077_CarbideWhite', emoji: 'Resonator' },
{ name: 'Pinpoint', id: 'Pickaxe_ID_099_ModernMilitaryRed', emoji: 'Pinpoint' },
{ name: 'Stellar Axe', id: 'Pickaxe_ID_116_Celestial', emoji: 'StellarAxe' },
{ name: 'Stealth Angular Axe', id: 'Pickaxe_ID_189_StreetOpsStealth', emoji: 'StealthAngularAxe' },
{ name: 'Dark Razor', id: 'Pickaxe_ID_178_SpeedyMidnight', emoji: 'DarkRazor' },
{ name: 'Electri-claw', id: 'Pickaxe_ID_398_WildCatFemale', emoji: 'Electriclaw' },
{ name: 'Dark Splitter', id: 'Pickaxe_ID_461_SkullBriteCube', emoji: 'DarkSplitter' },
{ name: 'Merry Mint Axe', id: 'Pickaxe_ID_294_CandyCane', emoji: 'MerryMintAxe' },
{ name: 'Indigo Inverter', id: 'Pickaxe_SnowNinjaDark', emoji: 'IndigoInverter' },
{ name: 'Rose Glow', id: 'Pickaxe_ID_153_RoseLeader', emoji: 'RoseGlow' },
{ name: 'Controller', id: 'Pickaxe_ID_088_PSBurnout', emoji: 'Controller' },
{ name: 'Pneumatic Twin', id: 'Pickaxe_ID_256_TechOpsBlue', emoji: 'PneumaticTwin' },
{ name: 'Perfect Point', id: 'Pickaxe_ID_464_LongShortsMale', emoji: 'PerfectPoint' },
{ name: 'Petrol Pump', id: 'Pickaxe_RetroWheels', emoji: 'PetrolPump' },
{ name: 'Future Relic Scythe', id: 'Pickaxe_SnowSoldierFashion', emoji: 'FutureRelicScythe' },
// --- OT Banners (OT1–OT11) ---
{ name: 'OT1 Banner', id: 'OT1Banner', emoji: 'OT1Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot1banner/icon.png' },
{ name: 'OT2 Banner', id: 'OT2Banner', emoji: 'OT2Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot2banner/icon.png' },
{ name: 'OT3 Banner', id: 'OT3Banner', emoji: 'OT3Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot3banner/icon.png' },
{ name: 'OT4 Banner', id: 'OT4Banner', emoji: 'OT4Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot4banner/icon.png' },
{ name: 'OT5 Banner', id: 'OT5Banner', emoji: 'OT5Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot5banner/icon.png' },
{ name: 'OT6 Banner', id: 'OT6Banner', emoji: 'OT6Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot6banner/icon.png' },
{ name: 'OT7 Banner', id: 'OT7Banner', emoji: 'OT7Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot7banner/icon.png' },
{ name: 'OT8 Banner', id: 'OT8Banner', emoji: 'OT8Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot8banner/icon.png' },
{ name: 'OT9 Banner', id: 'OT9Banner', emoji: 'OT9Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot9banner/icon.png' },
{ name: 'OT10 Banner', id: 'OT10Banner', emoji: 'OT10Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot10banner/icon.png' },
{ name: 'OT11 Banner', id: 'OT11Banner', emoji: 'OT11Banner', bannerUrl: 'https://fortnite-api.com/images/banners/ot11banner/icon.png' },
// --- Exclusive Emoticons (in-game emoji cosmetics) ---
{ name: 'Skull Trooper Emoticon', id: 'Emoji_Skulltrooper', emoji: 'EmoticonSkullTrooper',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/emoji_skulltrooper/smallicon.png' },
{ name: 'Maevys Emoticon', id: 'Emoji_S22_Showdown_Mae', emoji: 'EmoticonMaevys',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/emoji_s22_showdown_mae/smallicon.png' },
{ name: 'ZetFar Emoticon', id: 'Emoji_S22_Showdown_Zet', emoji: 'EmoticonZetFar',
overrideUrl: 'https://fortnite-api.com/images/cosmetics/br/emoji_s22_showdown_zet/smallicon.png' },
// --- PlayStation Exclusives ---
{ name: 'Blue Team Leader', id: 'CID_052_Athena_Commando_F_PSBlue', emoji: 'BlueTeamLeader' },
{ name: 'Blue Striker', id: 'CID_138_Athena_Commando_M_PSBurnout', emoji: 'BlueStriker' },
{ name: 'Prodigy', id: 'CID_296_Athena_Commando_M_Math', emoji: 'Prodigy' },
{ name: 'Carbon Commando', id: 'CID_360_Athena_Commando_M_TechOpsBlue', emoji: 'CarbonCommando' },
{ name: 'Trilogy', id: 'CID_657_Athena_Commando_F_TechOpsBlue', emoji: 'Trilogy' },
{ name: 'Point Patroller', id: 'CID_711_Athena_Commando_M_LongShorts', emoji: 'PointPatroller' },
{ name: 'Cloud Striker', id: 'CID_926_Athena_Commando_F_StreetFashionDiamond', emoji: 'CloudStriker' },
{ name: 'Fixer', id: 'CID_A_101_Athena_Commando_M_TacticalWoodlandBlue', emoji: 'Fixer' },
{ name: 'Blizzard Bomber', id: 'CID_A_421_Athena_Commando_F_BlizzardBomber', emoji: 'BlizzardBomber' },
{ name: 'Jun-Hwan', id: 'Character_BlueJet', emoji: 'JunHwan' },
{ name: 'Lina Scorch', id: 'Character_RetroWheels', emoji: 'LinaScorch' },
{ name: 'Indigo Kuno', id: 'CID_660_Athena_Commando_F_BandageNinjaBlue', emoji: 'IndigoKuno' },
// --- Twitch / Amazon Prime ---
{ name: 'Sub Commander', id: 'CID_085_Athena_Commando_M_Twitch', emoji: 'SubCommander' },
{ name: 'Havoc', id: 'CID_089_Athena_Commando_M_RetroGrey', emoji: 'Havoc' },
{ name: 'Trailblazer', id: 'CID_114_Athena_Commando_F_TacticalWoodland', emoji: 'Trailblazer' },
// --- Intel / PC Exclusive ---
{ name: 'Reflex', id: 'CID_259_Athena_Commando_M_StreetOps', emoji: 'Reflex' },
// --- OG Battle Pass ---
{ name: 'Sparkle Specialist', id: 'CID_039_Athena_Commando_F_Disco', emoji: 'SparkleSpecialist' },
{ name: 'Royale Knight', id: 'CID_033_Athena_Commando_F_Medieval', emoji: 'RoyaleKnight' },
{ name: 'Blue Squire', id: 'CID_032_Athena_Commando_M_Medieval', emoji: 'BlueSquire' },
{ name: 'The Reaper', id: 'CID_084_Athena_Commando_M_Assassin', emoji: 'TheReaper' },
{ name: 'Elite Agent', id: 'CID_083_Athena_Commando_F_Tactical', emoji: 'EliteAgent' },
{ name: 'Rogue Agent', id: 'CID_090_Athena_Commando_M_Tactical', emoji: 'RogueAgent' },
{ name: 'Omega', id: 'CID_116_Athena_Commando_M_CarbideBlack', emoji: 'Omega' },
// --- Ch2S2 Battle Pass gold styles ---
{ name: 'Skye', id: 'CID_690_Athena_Commando_F_Photographer', emoji: 'Skye' },
{ name: 'TNTina', id: 'CID_691_Athena_Commando_F_TNTina', emoji: 'TNTina' },
{ name: 'Meowscles', id: 'CID_693_Athena_Commando_M_BuffCat', emoji: 'Meowscles' },
{ name: 'Midas', id: 'CID_694_Athena_Commando_M_CatBurglar', emoji: 'Midas' },
// --- Gaming Legends / Collab ---
{ name: 'Travis Scott', id: 'CID_703_Athena_Commando_M_Cyclone', emoji: 'TravisScott' },
{ name: 'Astro Jack', id: 'CID_761_Athena_Commando_M_CycloneSpace', emoji: 'AstroJack' },
{ name: 'Kratos', id: 'CID_964_Athena_Commando_M_Historian_869BC', emoji: 'Kratos' },
{ name: 'Master Chief', id: 'CID_971_Athena_Commando_M_Jupiter_S0Z6M', emoji: 'MasterChief' },
// --- World Cup / Tournament ---
{ name: 'World Warrior', id: 'CID_478_Athena_Commando_F_WorldCup', emoji: 'WorldWarrior' },
// --- V-Bucks Card Promo ---
{ name: 'Color Splash Jellie', id: 'Character_TubaGummi', emoji: 'ColorSplashJellie' },
// --- Save the World Founders ---
{ name: 'Rose Team Leader', id: 'CID_096_Athena_Commando_F_Founder', emoji: 'RoseTeamLeader' },
{ name: 'Warpaint', id: 'CID_095_Athena_Commando_M_Founder', emoji: 'Warpaint' },
];
// ─────────────────────────────────────────────────────────────────────────────
// VERIFY ROLE REQUIREMENTS
// ─────────────────────────────────────────────────────────────────────────────
const ROLE_REQUIREMENTS = [
// OG Season 1 skins (style required)
{ role: 'OG Renegade Raider', id: 'cid_028_athena_commando_f', styleTag: 'Mat3' },
{ role: 'OG Aerial Assault Trooper', id: 'cid_017_athena_commando_m', styleTag: 'Stage2' },
{ role: 'OG Ghoul Trooper', id: 'cid_029_athena_commando_f_halloween', styleTag: 'Mat3' },
{ role: 'OG Skull Trooper', id: 'cid_030_athena_commando_m_halloween', styleTag: 'Mat1' },
// OG Battle Pass skins
{ role: 'Black Knight', id: 'cid_035_athena_commando_m_medieval' },
{ role: 'Blue Squire', id: 'cid_032_athena_commando_m_medieval' },
{ role: 'Royale Knight', id: 'cid_033_athena_commando_f_medieval' },
{ role: 'Sparkle Specialist', id: 'cid_039_athena_commando_f_disco' },
{ role: 'The Reaper', id: 'cid_084_athena_commando_m_assassin' },
{ role: 'Elite Agent', id: 'cid_083_athena_commando_f_tactical' },
{ role: 'Omega', id: 'cid_116_athena_commando_m_carbideblack', styleTag: 'Stage5' },
{ role: 'Carbide', id: 'cid_115_athena_commando_m_carbideblue', styleTag: 'Stage5' },
// Samsung bundles
{ role: 'Galaxy', id: 'cid_175_athena_commando_m_celestial' },
{ role: 'Stellar Axe', id: 'pickaxe_id_116_celestial' },
{ role: 'Ikonik', id: 'cid_313_athena_commando_m_kpopfashion' },
{ role: 'Scenario Emote', id: 'eid_kpopdance03' },
{ role: 'Glow', id: 'cid_479_athena_commando_f_davinci' },
{ role: 'Levitate Emote', id: 'eid_davinci' },
// Device / Platform exclusives
{ role: 'Honor Guard', id: 'cid_342_athena_commando_m_streetracermetallic' },
{ role: 'Wonder', id: 'cid_434_athena_commando_f_stealthhonor' },
{ role: 'Eon', id: 'cid_174_athena_commando_f_carbidewhite' },
{ role: 'Resonator', id: 'pickaxe_id_077_carbidewhite' },
{ role: 'Dark Vertex', id: 'cid_371_athena_commando_m_speedymidnight' },
{ role: 'Double Helix', id: 'cid_183_athena_commando_m_modernmilitaryred' },
{ role: 'Pinpoint', id: 'pickaxe_id_099_modernmilitaryred' },
{ role: 'Neo Versa', id: 'cid_441_athena_commando_f_cyberscavengerblue' },
{ role: 'Cobalt Snowfoot', id: 'character_snowninjadark' },
{ role: 'Royale Bomber', id: 'cid_113_athena_commando_m_blueace' },
{ role: 'Wildcat', id: 'cid_757_athena_commando_f_wildcat' },
{ role: 'Stealth Reflex', id: 'cid_386_athena_commando_m_streetopsstealth' },
{ role: 'Reflex', id: 'cid_259_athena_commando_m_streetops' },
{ role: 'Angular Axe', id: 'pickaxe_id_118_streetops' },
{ role: 'Surf Strider', id: 'cid_783_athena_commando_m_aquajacket' },
{ role: 'Rogue Spider Knight', id: 'cid_516_athena_commando_m_blackwidowrogue' },
{ role: 'Huntmaster Saber', id: 'character_masterkeyorder' },
// PlayStation Plus exclusives
{ role: 'Blue Team Leader', id: 'cid_052_athena_commando_f_psblue' },
{ role: 'Blue Striker', id: 'cid_138_athena_commando_m_psburnout' },
{ role: 'Carbon Commando', id: 'cid_360_athena_commando_m_techopsblue' },
{ role: 'Prodigy', id: 'cid_296_athena_commando_m_math' },
{ role: 'Cloud Striker', id: 'cid_926_athena_commando_f_streetfashiondiamond' },
{ role: 'Fixer', id: 'cid_a_101_athena_commando_m_tacticalwoodlandblue' },
{ role: 'Point Patroller', id: 'cid_711_athena_commando_m_longshorts' },
// Twitch Prime exclusives
{ role: 'Sub Commander', id: 'cid_085_athena_commando_m_twitch' },
{ role: 'Havoc', id: 'cid_089_athena_commando_m_retrogrey' },
{ role: 'Trailblazer', id: 'cid_114_athena_commando_f_tacticalwoodland' },
// Exclusive Pickaxes
{ role: "Raider's Revenge", id: 'pickaxe_lockjaw', styleTag: 'Stage2' },
{ role: 'AC/DC', id: 'pickaxe_id_013_teslacoil' },
{ role: 'Merry Mint Axe', id: 'pickaxe_id_294_candycane' },
{ role: 'Tenderizer', id: 'pickaxe_id_044_tacticalurbanhammer' },
{ role: 'Axe of Champions', id: 'pickaxe_id_376_fncs' },
{ role: 'Blade of Champions', id: 'pickaxe_fncs_s32' },
// OG / Exclusive Emotes
{ role: 'Fresh Emote', id: 'eid_fresh' },
{ role: 'Rambunctious Emote', id: 'eid_zippy' },
{ role: 'Tidy Emote', id: 'eid_tidy' },
{ role: 'Floss Emote', id: 'eid_floss' },
{ role: 'Take the L Emote', id: 'eid_takethelathena' },
{ role: 'Orange Justice Emote', id: 'eid_goodvibes' },
{ role: 'Ride the Pony Emote', id: 'eid_ridethepony_athena' },
{ role: 'Pony Up Emote', id: 'eid_ridetheponymultiple' },
{ role: 'Evasive Maneuvers Emote', id: 'eid_temple' },
// Exclusive Emoticons
{ role: 'Cameo Needs Loot', id: 'emoji_s20_needloot' },
// Founder exclusives
{ role: 'Rose Team Leader', id: 'cid_096_athena_commando_f_founder' },
{ role: 'Warpaint', id: 'cid_095_athena_commando_m_founder' },
{ role: 'Fully Golden Peely', id: 'cid_701_athena_commando_m_bananaagent', styleTag: 'Stage4' },
// Samsung Galaxy bundle items
{ role: 'Galactic Disc', id: 'bid_138_celestial' },
{ role: 'Discovery Glider', id: 'glider_id_090_celestial' },
// IKONIK bundle - Scenario already added above
// Glow bundle - Levitate already added above
// EON bundle
{ role: 'Aurora Glider', id: 'glider_id_056_carbidewhite' },
// Dark Vertex bundle
{ role: 'Dark Deflector', id: 'bid_234_speedymidnight' },
{ role: 'Razor Edge', id: 'pickaxe_id_065_speedyred' },
{ role: 'Dark Forerunner Glider', id: 'glider_id_131_speedymidnight' },
// Double Helix bundle
{ role: 'Telemetry', id: 'bid_114_modernmilitaryred' },
{ role: 'Rotor Glider', id: 'glider_id_075_modernmilitaryred' },
// Neo Versa bundle
{ role: 'Neo Phrenzy', id: 'bid_288_cyberscavengerfemaleblue' },
// Royale Bomber bundle
{ role: 'Royale Air Glider', id: 'glider_id_105_snowboard' },
// Reflex bundle
{ role: 'Response Unit', id: 'bid_140_streetopsmale' },
{ role: 'Pivot Glider', id: 'glider_id_092_streetops' },
// Wildcat bundle
{ role: 'Sleek Strike', id: 'bid_521_wildcat' },
// Rogue Spider Knight bundle
{ role: 'Time Keeper', id: 'bid_395_phantom' },
// Stealth Reflex bundle
{ role: 'Stealth Response Unit', id: 'bid_249_streetopsstealth' },
// Aerial Assault One glider (S1 exclusive)
{ role: 'Aerial Assault One', id: 'glider_id_001', styleTag: 'Stage2' },
// Extra exclusive pickaxes from master list
{ role: 'Trusty No. 2', id: 'pickaxe_id_029_assassin' },
{ role: 'Party Animal', id: 'pickaxe_id_020_keg' },
{ role: 'Crowbar', id: 'pickaxe_id_092_bling' },
{ role: 'Instigator', id: 'pickaxe_id_039_tacticalblack' },
// Missing emotes from master list
{ role: "Widow's Pirouette Emote", id: 'eid_ashtonboardwalk' },
{ role: 'Hot Marat Emote', id: 'eid_wir' },
{ role: 'Kiss the Cup Emote', id: 'eid_trophycelebration' },
// Missing skins from master list
{ role: 'Gildedguy', id: 'cid_a_141_athena_commando_m_dojo' },
{ role: 'Tart Tycoon', id: 'cid_851_athena_commando_m_bittenhead' },
// Gliders
{ role: 'Mako Glider', id: 'glider_warthog' },
// Rare / STW / Creator banners (checked via homebase_banners in common_core)
{ role: 'Alpha Tester Banner', bannerId: 'OtherBanner51' },
{ role: 'World Cup Banner', bannerId: 'BRWorldCup2019' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner1' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner2' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner3' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner4' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner5' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner6' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner7' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner8' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner9' },
// STW Founder banners
{ role: 'STW Founder Banner', bannerId: 'FounderTier5Banner1' },
{ role: 'STW Founder Banner', bannerId: 'FounderTier4Banner1' },
{ role: 'STW Founder Banner', bannerId: 'FounderTier3Banner1' },
{ role: 'STW Founder Banner', bannerId: 'FounderTier2Banner1' },
{ role: 'STW Founder Banner', bannerId: 'FounderTier1Banner1' },
// STW Endurance banners
{ role: 'STW Endurance Banner', bannerId: 'EnduranceWave30Theater04' },
{ role: 'STW Endurance Banner', bannerId: 'EnduranceWave30Theater03' },
{ role: 'STW Endurance Banner', bannerId: 'EnduranceWave30Theater02' },
{ role: 'STW Endurance Banner', bannerId: 'EnduranceWave30Theater01' },
// STW Survival / Wargames banners
{ role: 'STW Event Banner', bannerId: 'SurvivalBannerStorm2018Hard' },
{ role: 'STW Event Banner', bannerId: 'SurvivalBannerHolidayHard' },
{ role: 'STW Event Banner', bannerId: 'WargamesTprifts' },
{ role: 'STW Event Banner', bannerId: 'WargamesTiki' },
{ role: 'STW Event Banner', bannerId: 'WargamesPods' },
// STW Achievement banners
{ role: 'STW Achievement Banner', bannerId: 'AchievementUnspeakableHorrors' },
{ role: 'STW Achievement Banner', bannerId: 'AchievementWorldExplorer' },
{ role: 'STW Achievement Banner', bannerId: 'AchievementGuardianAngel' },
{ role: 'STW Achievement Banner', bannerId: 'AchievementPlaysWellWithOthers' },
{ role: 'STW Achievement Banner', bannerId: 'AchievementTalentedBuilder' },
{ role: 'STW Achievement Banner', bannerId: 'AchievementGoGnome' },
{ role: 'STW Achievement Banner', bannerId: 'AchievementLootLegend' },
// FNCS / Skirmish / World Cup banners
{ role: 'FNCS Banner', bannerId: 'BRS23_FNCS_Drops' },
{ role: 'FNCS Banner', bannerId: 'S11FNCS' },
{ role: 'Skirmish Banner', bannerId: 'BRSkirmishBushBandits' },
{ role: 'Skirmish Banner', bannerId: 'BRSkirmishDustyDogs' },
{ role: 'Skirmish Banner', bannerId: 'BRSkirmishFortKnights' },
{ role: 'Skirmish Banner', bannerId: 'BRSkirmishLuckyLlamas' },
{ role: 'Skirmish Banner', bannerId: 'BRSkirmishRiftRaiders' },
// Season Level 100 banners
{ role: 'Season Level 100 Banner', bannerId: 'BRS10Level100' },
{ role: 'Season Level 100 Banner', bannerId: 'BRS9Lvl100' },
{ role: 'Season Level 100 Banner', bannerId: 'BRS8Level100' },
// Creator / Influencer banners 10-58
{ role: 'Creator Banner', bannerId: 'InfluencerBanner10' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner11' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner12' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner13' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner14' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner15' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner16' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner17' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner18' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner19' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner20' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner21' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner22' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner23' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner24' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner25' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner26' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner27' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner28' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner29' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner30' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner31' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner32' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner33' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner34' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner35' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner36' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner37' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner38' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner39' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner40' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner41' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner42' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner43' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner44' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner45' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner46' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner47' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner48' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner49' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner50' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner51' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner52' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner53' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner54' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner55' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner56' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner57' },
{ role: 'Creator Banner', bannerId: 'InfluencerBanner58' },
// OT Banners
{ role: 'OT1 Banner', bannerId: 'OT1Banner' },
{ role: 'OT2 Banner', bannerId: 'OT2Banner' },
{ role: 'OT3 Banner', bannerId: 'OT3Banner' },
{ role: 'OT4 Banner', bannerId: 'OT4Banner' },
{ role: 'OT5 Banner', bannerId: 'OT5Banner' },
{ role: 'OT6 Banner', bannerId: 'OT6Banner' },
{ role: 'OT7 Banner', bannerId: 'OT7Banner' },
{ role: 'OT8 Banner', bannerId: 'OT8Banner' },
{ role: 'OT9 Banner', bannerId: 'OT9Banner' },
{ role: 'OT10 Banner', bannerId: 'OT10Banner' },
{ role: 'OT11 Banner', bannerId: 'OT11Banner' },
// STW Founders
{ role: 'STW Founders', id: 'cid_095_athena_commando_m_founder' },
{ role: 'Ultimate STW Founders', id: 'tiertoken_collectionbook_ultimate', stw: true },
]
// ─────────────────────────────────────────────────────────────────────────────
// EPIC OAUTH CONSTANTS
// ─────────────────────────────────────────────────────────────────────────────
const SWITCH_TOKEN = 'OThmN2U0MmMyZTNhNGY4NmE3NGViNDNmYmI0MWVkMzk6MGEyNDQ5YTItMDAxYS00NTFlLWFmZWMtM2U4MTI5MDFjNGQ3';
const IOS_TOKEN = 'M2Y2OWU1NmM3NjQ5NDkyYzhjYzI5ZjFhZjA4YThhMTI6YjUxZWU5Y2IxMjIzNGY1MGE2OWVmYTY3ZWY1MzgxMmU=';
const EPIC_OAUTH = 'https://account-public-service-prod.ol.epicgames.com/account/api/oauth/token';
const EPIC_OAUTH_PROD03 = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/token';
const EPIC_EXCHANGE = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/exchange';
const EPIC_DEVICE_AUTH = 'https://account-public-service-prod03.ol.epicgames.com/account/api/oauth/deviceAuthorization';
const pending = new Map();
const verifiedUsers = new Map(); // userId -> { guildId, assignedRoles: string[], epicName: string }
const LOG_CHANNEL_ID = '1517695849952706642';
// ─────────────────────────────────────────────────────────────────────────────
// SHARED HELPERS
// ─────────────────────────────────────────────────────────────────────────────
async function fetchWithTimeout(url, ms = 12000) {
const ctrl = new AbortController();
const timer = setTimeout(() => ctrl.abort(), ms);
try { return await fetch(url, { signal: ctrl.signal }); }
finally { clearTimeout(timer); }
}
// ─── get icon URL — build directly from known CDN pattern, no API roundtrip ──
async function fetchIconUrl(cosmeticId) {
// Direct CDN URL — fortnite-api.com images are always at this pattern
const url = `https://fortnite-api.com/images/cosmetics/br/${cosmeticId.toLowerCase()}/icon.png`;
const res = await fetchWithTimeout(url, 8000);
if (res.ok) return url;
// Fallback to API lookup if direct URL 404s
const apiRes = await fetchWithTimeout(`https://fortnite-api.com/v2/cosmetics/br/${cosmeticId}`, 8000);
if (!apiRes.ok) throw new Error(`API ${apiRes.status} for ${cosmeticId}`);
const json = await apiRes.json();
if (!json.data?.images?.icon) throw new Error(`No icon for ${cosmeticId}`);
return json.data.images.icon;
}
async function downloadAndProcess(imageUrl) {
const res = await fetchWithTimeout(imageUrl, 15000);
if (!res.ok) throw new Error(`Download failed ${res.status}`);
const raw = Buffer.from(await res.arrayBuffer());
return sharp(raw)
.resize(128, 128, { fit: 'cover', position: 'centre', kernel: 'lanczos3' })
.sharpen({ sigma: 0.8, m1: 1.0, m2: 2.0 })
.png({ compressionLevel: 6 })
.toBuffer();
}
async function createEmojiSafe(guild, buffer, name, reason) {
for (let attempt = 1; attempt <= 5; attempt++) {
try {
return await guild.emojis.create({ attachment: buffer, name, reason });
} catch (err) {
const isRL = err.status === 429 || err.httpStatus === 429 ||
(err.message || '').toLowerCase().includes('rate limit');
if (isRL) {
const wait = ((err.retryAfter || 5) * 1000) + 1000;
console.warn(` ⏳ Rate limited "${name}", waiting ${Math.ceil(wait/1000)}s...`);
await new Promise(r => setTimeout(r, wait));
} else {
throw err;
}
}
}
throw new Error(`Gave up on emoji "${name}"`);
}
// ─── Epic helpers ─────────────────────────────────────────────────────────────
async function getSwitchAccessToken() {
const res = await fetch(EPIC_OAUTH, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Authorization': `basic ${SWITCH_TOKEN}` },
body: 'grant_type=client_credentials',
});
return (await res.json()).access_token;
}
async function createDeviceCode(token) {
const res = await fetch(EPIC_DEVICE_AUTH, {
method: 'POST',
headers: { 'Authorization': `bearer ${token}`, 'Content-Type': 'application/x-www-form-urlencoded' },
});
return res.json();
}
async function waitForLogin(deviceCode, maxMs = 300000) {
const start = Date.now();
while (Date.now() - start < maxMs) {
await new Promise(r => setTimeout(r, 10000));
const res = await fetch(EPIC_OAUTH_PROD03, {
method: 'POST',
headers: { 'Authorization': `basic ${SWITCH_TOKEN}`, 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=device_code&device_code=${deviceCode}`,
});
const data = await res.json();
if (res.status === 200 && data.access_token) return data;
const err = data.errorCode || '';
if (err.includes('not_found') || err.includes('expired')) return null;
}
return null;
}
async function exchangeForIosToken(accessToken) {
const exRes = await fetch(EPIC_EXCHANGE, { headers: { 'Authorization': `bearer ${accessToken}` } });
const exData = await exRes.json();
const res = await fetch(EPIC_OAUTH_PROD03, {
method: 'POST',
headers: { 'Authorization': `basic ${IOS_TOKEN}`, 'Content-Type': 'application/x-www-form-urlencoded' },
body: `grant_type=exchange_code&exchange_code=${exData.code}`,
});
return res.json();
}
async function getProfile(accountId, accessToken, profileId) {
const url = `https://fortnite-public-service-prod11.ol.epicgames.com/fortnite/api/game/v2/profile/${accountId}/client/QueryProfile?profileId=${profileId}&rvn=-1`;
const res = await fetch(url, {
method: 'POST',
headers: { 'Authorization': `bearer ${accessToken}`, 'Content-Type': 'application/json' },
body: '{}',
});
return res.json();
}
// ─────────────────────────────────────────────────────────────────────────────
// SKIN CHECKER IMAGE RENDERER (RiftCheckerBot style)
// ─────────────────────────────────────────────────────────────────────────────
const { createCanvas, loadImage, GlobalFonts } = require('@napi-rs/canvas');
const path = require('path');
// Register Fortnite font (Burbank Big Condensed Black) + Tahoma
GlobalFonts.registerFromPath(
path.join(__dirname, 'BurbankBigCondensed-Black.ttf'), 'Burbank'
);
GlobalFonts.registerFromPath('C:/Windows/Fonts/tahoma.ttf', 'Tahoma');
GlobalFonts.registerFromPath('C:/Windows/Fonts/tahomabd.ttf', 'TahomaB');
// Custom background URL — set with !setbg <url>, cleared with !clearbg
let customBgUrl = null;
const ICON = 96;
const COLS = 12; // wider rows = shorter images
const LABEL = 26;
const CELL_H = ICON + LABEL;
const PAD = 14;
const HDR_H = 72;
const FTR_H = 36;
const RC = {
common: '#c8c8c8', uncommon: '#60aa3a', rare: '#4990e2',
epic: '#874bd2', legendary: '#d3782e', mythic: '#ffd700',
icon: '#54ceec', marvel: '#e63c3c', dark: '#641464',
frozen: '#94dffc', lava: '#ca4d1d', shadow: '#505050',
gaming: '#1a8e48', slurp: '#00e9b0',
};
const OG_STYLE_ICONS = {
'cid_029_athena_commando_f_halloween': 'https://fortnite-api.com/images/cosmetics/br/cid_029_athena_commando_f_halloween/variants/material/mat3.png',
'cid_030_athena_commando_m_halloween': 'https://fortnite-api.com/images/cosmetics/br/cid_030_athena_commando_m_halloween/variants/clothingcolor/mat1.png',
'cid_028_athena_commando_f': 'https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png',
'cid_701_athena_commando_m_bananaagent': 'https://fortnite-api.com/images/cosmetics/br/cid_701_athena_commando_m_bananaagent/variants/progressive/stage4.png',
'cid_116_athena_commando_m_carbideblack': 'https://fortnite-api.com/images/cosmetics/br/cid_116_athena_commando_m_carbideblack/variants/parts/stage1.png',
'cid_694_athena_commando_m_catburglar': 'https://fortnite-api.com/images/cosmetics/br/cid_694_athena_commando_m_catburglar/variants/progressive/stage4.png',
'cid_693_athena_commando_m_buffcat': 'https://fortnite-api.com/images/cosmetics/br/cid_693_athena_commando_m_buffcat/variants/progressive/stage4.png',
'cid_691_athena_commando_f_tntina': 'https://fortnite-api.com/images/cosmetics/br/cid_691_athena_commando_f_tntina/variants/progressive/stage7.png',
'cid_690_athena_commando_f_photographer': 'https://fortnite-api.com/images/cosmetics/br/cid_690_athena_commando_f_photographer/variants/progressive/stage4.png',
};
// Cache the logo so we only fetch once per session
let _logoBuffer = null;
async function getLogoBuffer() {
if (_logoBuffer) return _logoBuffer;
try {
const res = await fetchWithTimeout(
'https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png',
8000
);
if (res.ok) _logoBuffer = Buffer.from(await res.arrayBuffer());
} catch { _logoBuffer = null; }
return _logoBuffer;
}
const CACHE_DIR = path.join(__dirname, 'RiftCheckerBot-main', 'cache');
const RARITY_DIR = path.join(__dirname, 'RiftCheckerBot-main', 'styles', 'rift', 'rarity');
const ICON_DIR = path.join(__dirname, 'RiftCheckerBot-main', 'cosmetic_icons');
const fs = require('fs');
// Cosmetics that have special OG variant images in local cache
// key = lowercase cosmetic ID, value = local cache filename
const LOCAL_OG_ICONS = {
'cid_029_athena_commando_f_halloween': 'pink_ghoul.png', // Pink Ghoul
'cid_030_athena_commando_m_halloween': 'purple_skull.png', // Purple Skull
'cid_028_athena_commando_f': 'og_rene.png', // Black & Gold Renegade Raider
'cid_017_athena_commando_m': 'og_aat.png', // OG Aerial Assault Trooper
'cid_116_athena_commando_m_carbideblack': 'omega_max.png', // Max Omega
'cid_701_athena_commando_m_bananaagent': 'gold_peely.png', // Gold Agent Peely
};
// Remote OG variant URLs for cosmetics not in local cache
const REMOTE_OG_ICONS = {
'pickaxe_lockjaw': 'https://fortnite-api.com/images/cosmetics/br/pickaxe_lockjaw/variants/particle/particle1.png',
'glider_id_001': 'https://fortnite-api.com/images/cosmetics/br/glider_id_001/variants/particle/particle1.png',
'cid_694_athena_commando_m_catburglar': 'https://fortnite-api.com/images/cosmetics/br/cid_694_athena_commando_m_catburglar/variants/progressive/stage4.png',
'cid_693_athena_commando_m_buffcat': 'https://fortnite-api.com/images/cosmetics/br/cid_693_athena_commando_m_buffcat/variants/progressive/stage4.png',
'cid_691_athena_commando_f_tntina': 'https://fortnite-api.com/images/cosmetics/br/cid_691_athena_commando_f_tntina/variants/progressive/stage7.png',
'cid_690_athena_commando_f_photographer': 'https://fortnite-api.com/images/cosmetics/br/cid_690_athena_commando_f_photographer/variants/progressive/stage4.png',
};
// Load a rarity background image from local cache
const rarityImageCache = {};
async function getRarityImage(rarity) {
const key = (rarity || 'common').toLowerCase();
if (rarityImageCache[key]) return rarityImageCache[key];
const filePath = path.join(RARITY_DIR, `${key}.png`);
if (fs.existsSync(filePath)) {
try {
const img = await loadImage(fs.readFileSync(filePath));
rarityImageCache[key] = img;
return img;
} catch { /* fall through */ }
}
// fallback to common
const fallback = path.join(RARITY_DIR, 'common.png');
if (fs.existsSync(fallback)) {
const img = await loadImage(fs.readFileSync(fallback));
rarityImageCache['common'] = img;
return img;
}
return null;
}
// Get cosmetic icon — local cache first, then remote
async function fetchSkinImageBuffer(cosmeticId, useOgStyle = false) {
const id = cosmeticId.toLowerCase();
// 1. Try local OG variant if requested
if (useOgStyle && LOCAL_OG_ICONS[id]) {
const localPath = path.join(CACHE_DIR, LOCAL_OG_ICONS[id]);
if (fs.existsSync(localPath)) return fs.readFileSync(localPath);
}
// 2. Try remote OG variant if requested
if (useOgStyle && REMOTE_OG_ICONS[id]) {
try {
const res = await fetchWithTimeout(REMOTE_OG_ICONS[id], 8000);
if (res.ok) return Buffer.from(await res.arrayBuffer());
} catch { /* fall through */ }
}
// 3. Check if this is a banner ID — fetch from banner API
const bannerPrefixes = ['ot','influencerbanner','foundertier','brs','survival','endurance','wargames','achievement','otherbanner','skirmish','worldcup'];
if (bannerPrefixes.some(p => id.startsWith(p))) {
// Try local RiftCheckerBot cache first
try {
const files = fs.readdirSync(CACHE_DIR);
const match = files.find(f => f.toLowerCase() === id + '.png' || f.toLowerCase() === cosmeticId + '.png');
if (match) return fs.readFileSync(path.join(CACHE_DIR, match));
} catch { /* fall through */ }
// Fetch from banner API
try {
const bMeta = await getBannerMeta();
const iconUrl = bMeta[id]?.icon;
if (iconUrl) {
const res = await fetchWithTimeout(iconUrl, 8000);
if (res.ok) return Buffer.from(await res.arrayBuffer());
}
} catch { /* fall through */ }
return null;
}
// 4. Try local cache with exact cosmetic ID match
try {
const files = fs.readdirSync(CACHE_DIR);
const baseName = cosmeticId + '.png';
const match = files.find(f => f.toLowerCase() === baseName.toLowerCase());
if (match) return fs.readFileSync(path.join(CACHE_DIR, match));
} catch { /* fall through */ }
// 5. Fortnite API CDN
const iconUrl = `https://fortnite-api.com/images/cosmetics/br/${id}/icon.png`;
try {
const res = await fetchWithTimeout(iconUrl, 8000);
if (res.ok) return Buffer.from(await res.arrayBuffer());
} catch { /* fall through */ }
// 6. Fallback: smallIcon
const smallUrl = `https://fortnite-api.com/images/cosmetics/br/${id}/smallicon.png`;
try {
const res = await fetchWithTimeout(smallUrl, 8000);
if (res.ok) return Buffer.from(await res.arrayBuffer());
} catch { /* give up */ }
return null;
}
async function renderLockerImage(items, countLabel, subtitle, submittedBy = null, ogStyleOwned = null) {
if (!items || items.length === 0) return null;
const LOGO_SIZE = HDR_H - 8;
const rows = Math.ceil(items.length / COLS);
const cols = Math.min(items.length, COLS);
const W = Math.max(cols * ICON + PAD * 2, 400);
const H = HDR_H + rows * CELL_H + PAD + FTR_H;
const canvas = createCanvas(W, H);
const ctx = canvas.getContext('2d');
// ── Background ───────────────────────────────────────────────────────────
if (customBgUrl) {
try {
const res = await fetchWithTimeout(customBgUrl, 10000);
if (res.ok) {
let rawBuf = Buffer.from(await res.arrayBuffer());
const contentType = res.headers.get('content-type') || '';
const isGif = customBgUrl.toLowerCase().endsWith('.gif') || contentType.includes('gif');
// For GIFs extract first frame; for everything else just resize
const processed = await sharp(rawBuf, { animated: isGif })
.resize(W, H, { fit: 'cover', position: 'centre' })
.png()
.toBuffer();
ctx.drawImage(await loadImage(processed), 0, 0, W, H);
ctx.fillStyle = 'rgba(0,0,0,0.55)';
ctx.fillRect(0, 0, W, H);
} else { throw new Error('bg fetch failed'); }
} catch {
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#1c1c28'); bg.addColorStop(1, '#0e0e18');
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
}
} else {
const bg = ctx.createLinearGradient(0, 0, 0, H);
bg.addColorStop(0, '#1c1c28'); bg.addColorStop(1, '#0e0e18');
ctx.fillStyle = bg; ctx.fillRect(0, 0, W, H);
}
// Grid lines
ctx.strokeStyle = 'rgba(255,255,255,0.05)'; ctx.lineWidth = 1;
for (let c = 0; c <= cols; c++) {
ctx.beginPath(); ctx.moveTo(PAD + c * ICON, HDR_H); ctx.lineTo(PAD + c * ICON, HDR_H + rows * CELL_H); ctx.stroke();
}
for (let r = 0; r <= rows; r++) {
ctx.beginPath(); ctx.moveTo(PAD, HDR_H + r * CELL_H); ctx.lineTo(PAD + cols * ICON, HDR_H + r * CELL_H); ctx.stroke();
}
// ── Logo ─────────────────────────────────────────────────────────────────
const logoBuf = await getLogoBuffer();
let logoEndX = PAD;
if (logoBuf) {
try {
const logoResized = await sharp(logoBuf).resize(LOGO_SIZE, LOGO_SIZE, { fit: 'cover' }).png().toBuffer();
const logoImg = await loadImage(logoResized);
const cx = PAD + LOGO_SIZE / 2, cy = 4 + LOGO_SIZE / 2, lr = LOGO_SIZE / 2;
ctx.save();
ctx.beginPath(); ctx.arc(cx, cy, lr, 0, Math.PI * 2); ctx.clip();
ctx.drawImage(logoImg, PAD, 4, LOGO_SIZE, LOGO_SIZE);
ctx.restore();
ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(cx, cy, lr, 0, Math.PI * 2); ctx.stroke();
logoEndX = PAD + LOGO_SIZE + 10;
} catch { logoEndX = PAD; }
}
// ── Header text ───────────────────────────────────────────────────────────
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 26px TahomaB';
ctx.fillText(countLabel, logoEndX, 40);
ctx.fillStyle = '#aaaaaa';
ctx.font = '13px Tahoma';
ctx.fillText(subtitle.toUpperCase(), logoEndX, 60);
// ── Cosmetic grid ─────────────────────────────────────────────────────────
let idx = 0;
for (const item of items) {
const col = idx % COLS;
const row = Math.floor(idx / COLS);
const x = PAD + col * ICON;
const y = HDR_H + row * CELL_H;
const id = item.id.toLowerCase();
// Determine if we should show OG style for this item
const hasOgRequirement = !!OG_STYLE_REQUIREMENTS[id];
const ownsOgStyle = ogStyleOwned ? ogStyleOwned.has(id) : false;
const useOg = hasOgRequirement && ownsOgStyle;
// Effective rarity: mythic if OG style owned, otherwise use metaMap rarity
const effectiveRarity = useOg ? 'mythic' : (item.rarity || 'common');
// ── Draw rarity background from local cache ───────────────────────────
const rarityFilePath = path.join(RARITY_DIR, `${effectiveRarity}.png`);
if (fs.existsSync(rarityFilePath)) {
try {
const rarBuf = await sharp(fs.readFileSync(rarityFilePath))
.resize(ICON, ICON, { fit: 'fill' }).png().toBuffer();
ctx.drawImage(await loadImage(rarBuf), x, y, ICON, ICON);
} catch { /* skip */ }
} else {
ctx.fillStyle = (RC[effectiveRarity] || RC.common) + '28';
ctx.fillRect(x, y, ICON, ICON);
}
// ── Gradient border around the cell ──────────────────────────────────
const borderColor = RC[effectiveRarity] || RC.common;
// Top edge gradient (left to right)
const topGrad = ctx.createLinearGradient(x, y, x + ICON, y);
topGrad.addColorStop(0, borderColor + '00');
topGrad.addColorStop(0.3, borderColor + 'ff');
topGrad.addColorStop(0.7, borderColor + 'ff');
topGrad.addColorStop(1, borderColor + '00');
ctx.strokeStyle = topGrad; ctx.lineWidth = 2;
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x + ICON, y); ctx.stroke();
// Bottom edge gradient
const botGrad = ctx.createLinearGradient(x, y + ICON, x + ICON, y + ICON);
botGrad.addColorStop(0, borderColor + '00');
botGrad.addColorStop(0.3, borderColor + 'ff');
botGrad.addColorStop(0.7, borderColor + 'ff');
botGrad.addColorStop(1, borderColor + '00');
ctx.strokeStyle = botGrad;
ctx.beginPath(); ctx.moveTo(x, y + ICON); ctx.lineTo(x + ICON, y + ICON); ctx.stroke();
// Left edge gradient (top to bottom)
const leftGrad = ctx.createLinearGradient(x, y, x, y + ICON);
leftGrad.addColorStop(0, borderColor + '00');
leftGrad.addColorStop(0.3, borderColor + 'ff');
leftGrad.addColorStop(0.7, borderColor + 'ff');
leftGrad.addColorStop(1, borderColor + '00');
ctx.strokeStyle = leftGrad;
ctx.beginPath(); ctx.moveTo(x, y); ctx.lineTo(x, y + ICON); ctx.stroke();
// Right edge gradient
const rightGrad = ctx.createLinearGradient(x + ICON, y, x + ICON, y + ICON);
rightGrad.addColorStop(0, borderColor + '00');
rightGrad.addColorStop(0.3, borderColor + 'ff');
rightGrad.addColorStop(0.7, borderColor + 'ff');
rightGrad.addColorStop(1, borderColor + '00');
ctx.strokeStyle = rightGrad;
ctx.beginPath(); ctx.moveTo(x + ICON, y); ctx.lineTo(x + ICON, y + ICON); ctx.stroke();
// ── Draw cosmetic icon ────────────────────────────────────────────────
const buf = await fetchSkinImageBuffer(item.id, useOg);
if (buf) {
try {
const resized = await sharp(buf).resize(ICON - 6, ICON - 6, { fit: 'cover' }).png().toBuffer();
ctx.drawImage(await loadImage(resized), x + 3, y + 3, ICON - 6, ICON - 6);
} catch { /* skip */ }
}
// ── Name label ────────────────────────────────────────────────────────
ctx.fillStyle = '#ffffff';
ctx.font = '10px Tahoma';
ctx.textAlign = 'center';
const maxW = ICON - 4;
let name = item.name || '';
while (name.length > 3 && ctx.measureText(name).width > maxW) name = name.slice(0, -1);
if (name !== item.name) name = name.slice(0, -1) + '..';
ctx.fillText(name, x + ICON / 2, y + ICON + 17, maxW);
ctx.textAlign = 'left';
idx++;
}
// ── Footer ────────────────────────────────────────────────────────────────
const footerY = H - FTR_H;
ctx.fillStyle = 'rgba(0,0,0,0.6)';
ctx.fillRect(0, footerY, W, FTR_H);
ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 1;
ctx.beginPath(); ctx.moveTo(0, footerY); ctx.lineTo(W, footerY); ctx.stroke();
ctx.fillStyle = '#ffffff';
ctx.font = 'bold 11px TahomaB';
ctx.fillText(submittedBy ? `Submitted by: ${submittedBy}` : 'Made by jitterbuxg', PAD, footerY + 14);
ctx.fillStyle = '#ffd700';
ctx.font = '10px Tahoma';
ctx.fillText('.gg/collecting', PAD, footerY + 28);
ctx.fillStyle = '#888888';
ctx.font = '10px Tahoma';
ctx.textAlign = 'right';
ctx.fillText(new Date().toLocaleDateString('en-US', { month: 'short', day: 'numeric', year: 'numeric' }), W - PAD, footerY + 21);
ctx.textAlign = 'left';
return canvas.toBuffer('image/png');
}
// Banner metadata cache — fetched once from fortnite-api.com/v1/banners
let bannerMetaCache = null;
async function getBannerMeta() {
if (bannerMetaCache) return bannerMetaCache;
try {
const res = await fetchWithTimeout('https://fortnite-api.com/v1/banners', 10000);
if (res.ok) {
const json = await res.json();
const map = {};
for (const b of (json.data || [])) {
map[b.id.toLowerCase()] = {
name: b.devName || b.id,
icon: b.images?.icon || b.images?.smallIcon,
rarity: 'common',
};
}
bannerMetaCache = map;
return map;
}
} catch { /* ignore */ }
return {};
}
const BASE_EXCLUSIVE_BANNER_IDS = [
// OT Banners
'ot1banner','ot2banner','ot3banner','ot4banner','ot5banner',
'ot6banner','ot7banner','ot8banner','ot9banner','ot10banner','ot11banner',
// Alpha tester / rare
'otherbanner51',
// World Cup / FNCS / Skirmish
'brworldcup2019','brs9worldcup2019','brs23_fncs_drops','s11fncs',
'brskirmishbushbandits','brskirmishdustydogs','brskirmishfortknights',
'brskirmishluckyllamas','brskirmishriftraiders',
// Season level 100
'brs8level100','brs9lvl100','brs10level100',
];
function looksLikeBannerId(rawId) {
const id = String(rawId || '').trim().toLowerCase();
if (!id) return false;
return (
id.includes('banner') ||
id.startsWith('ot') ||
id.startsWith('brs') ||
id.startsWith('brseason') ||
id.startsWith('brworldcup') ||
id.startsWith('brskirmish') ||
id.startsWith('endurance') ||
id.startsWith('wargames') ||
id.startsWith('survival') ||
id.startsWith('achievement') ||
id.startsWith('starlight') ||
id.startsWith('winter2018') ||
id.startsWith('newsletter') ||
id.startsWith('horde')
);
}
function getExclusiveBannerIds() {
const ids = new Set(BASE_EXCLUSIVE_BANNER_IDS);
for (const req of ROLE_REQUIREMENTS) {
if (req.bannerId) ids.add(req.bannerId.toLowerCase());
}
try {
const listPath = path.join(__dirname, 'RiftCheckerBot-main', 'exclusive.txt');
const lines = fs.readFileSync(listPath, 'utf8').split(/\r?\n/);
for (const line of lines) {
const id = line.trim().toLowerCase();
if (looksLikeBannerId(id)) ids.add(id);
}
} catch { /* bundled list is optional */ }
return ids;
}
function commonCoreOwnsId(ccItems, wantedId) {
const needle = wantedId.toLowerCase();
return Object.values(ccItems || {}).some(item => {
const templateId = (item.templateId || '').toLowerCase();
const itemId = templateId.split(':').pop();
return itemId === needle || templateId.includes(needle);
});
}
function collectOwnedExclusiveBannerItems(ccItems, bannerMeta, metaMap = null) {
const items = [];
const seen = new Set();
for (const id of getExclusiveBannerIds()) {
if (seen.has(id) || !commonCoreOwnsId(ccItems, id)) continue;
seen.add(id);
const info = bannerMeta[id] || {};
const name = info.name || id;
items.push({ id, name, rarity: 'mythic' });
if (metaMap) metaMap[id] = { name, rarity: 'mythic' };
}
return items;
}
// Batch fetch cosmetic metadata (name + rarity) for up to 50 IDs at once
async function fetchCosmeticMeta(ids) {
const map = {};
if (!ids || ids.length === 0) return map;
for (let i = 0; i < ids.length; i += 50) {
const chunk = ids.slice(i, i + 50);
try {
const qs = chunk.map(id => `id=${encodeURIComponent(id)}`).join('&');
const res = await fetchWithTimeout(`https://fortnite-api.com/v2/cosmetics/br/search/ids?language=en&${qs}`, 10000);
if (!res.ok) continue;
const json = await res.json();
for (const item of (json.data || [])) {
map[item.id.toLowerCase()] = { name: item.name, rarity: item.rarity?.value || 'common' };
}
} catch { /* skip chunk on error */ }
}
return map;
}
// Cosmetics that have OG style requirements - id -> one or more owned style tags
const OG_STYLE_REQUIREMENTS = {
'cid_029_athena_commando_f_halloween': ['Mat3'], // Pink Ghoul
'cid_030_athena_commando_m_halloween': ['Mat1'], // Purple Skull
'cid_028_athena_commando_f': ['Mat3'], // Black & Gold Renegade Raider
'cid_017_athena_commando_m': ['Stage2'], // OG Aerial Assault Trooper
'cid_116_athena_commando_m_carbideblack': ['Stage5'], // Max Omega
'cid_701_athena_commando_m_bananaagent': ['Stage4'], // Gold Agent Peely
'pickaxe_lockjaw': ['Stage2'], // Black & Gold Raider's Revenge
'glider_id_001': ['Stage2'], // OG Aerial Assault One
};
function requiredStyleTags(reqOrTags) {
if (!reqOrTags) return [];
if (Array.isArray(reqOrTags)) return reqOrTags;
if (Array.isArray(reqOrTags.styleTags)) return reqOrTags.styleTags;
return reqOrTags.styleTag ? [reqOrTags.styleTag] : [];
}
function ownsAnyStyleTag(ownedStyleSet, tags) {
const wanted = requiredStyleTags(tags).map(tag => String(tag).toLowerCase());
if (wanted.length === 0) return true;
if (!ownedStyleSet) return false;
const owned = [...ownedStyleSet].map(tag => String(tag).toLowerCase());
return wanted.some(tag => owned.includes(tag));
}
function qualifiesForRequirement(req, ownedMap) {
const id = req.id.toLowerCase();
if (!ownedMap[id]) return false;
return ownsAnyStyleTag(ownedMap[id], req);
}
function uniqueIds(ids) {
return [...new Set(ids.filter(Boolean))];
}
function buildOwnedMap(athenaData) {
const owned = {}; // id -> Set of owned style tags
const ogStyleOwned = new Set();
const outfits = [];
const pickaxes = [];
const emotes = [];
const emoticons = []; // AthenaEmoji type (in-game emoticons, NOT emotes)
const backblings = [];
const gliders = [];
const sprays = [];
const items = athenaData?.profileChanges?.[0]?.profile?.items || {};
for (const item of Object.values(items)) {
const tpl = (item.templateId || '').toLowerCase();
if (!tpl.startsWith('athena') && !tpl.startsWith('character')) continue;
const id = tpl.split(':')[1] || '';
if (!owned[id]) owned[id] = new Set();
for (const v of (item.attributes?.variants || []))
for (const tag of (v.owned || [])) owned[id].add(tag);
if (tpl.startsWith('athenacharacter:') || tpl.startsWith('character:')) outfits.push(id);
else if (tpl.startsWith('athenapickaxe:')) pickaxes.push(id);
else if (tpl.startsWith('athenadance:')) emotes.push(id);
else if (tpl.startsWith('athenaemoji:')) emoticons.push(id);
else if (tpl.startsWith('athenabackpack:')) backblings.push(id);
else if (tpl.startsWith('athenaglider:')) gliders.push(id);
else if (tpl.startsWith('athenasprays:') || tpl.startsWith('athenaspray:')) sprays.push(id);
}
// Populate ogStyleOwned after all variants are collected
for (const id of Object.keys(owned)) {
if (OG_STYLE_REQUIREMENTS[id] && ownsAnyStyleTag(owned[id], OG_STYLE_REQUIREMENTS[id])) {
ogStyleOwned.add(id);
}
}
return {
owned,
ogStyleOwned,
outfits: uniqueIds(outfits),
pickaxes: uniqueIds(pickaxes),
emotes: uniqueIds(emotes),
emoticons: uniqueIds(emoticons),
backblings: uniqueIds(backblings),
gliders: uniqueIds(gliders),
sprays: uniqueIds(sprays),
};
}
function getRolesForLocker(ownedMap, ccItems) {
const earned = [];
for (const req of ROLE_REQUIREMENTS) {
if (req.bannerId) {
const has = Object.values(ccItems).some(i =>
(i.templateId || '').toLowerCase().includes(req.bannerId.toLowerCase())
);
if (has) earned.push(req.role);
continue;
}
if (req.stw) {
const ccIds = Object.values(ccItems).map(i => (i.templateId || '').toLowerCase());
if (ccIds.some(cc => cc.includes(req.id.toLowerCase()))) earned.push(req.role);
continue;
}
if (qualifiesForRequirement(req, ownedMap)) earned.push(req.role);
}
const ccIds = Object.values(ccItems).map(i => (i.templateId || '').toLowerCase());
const hasFounder = ccIds.some(cc => cc.includes('founder'));
const hasUltimate = ccIds.some(cc => cc.includes('tiertoken_collectionbook_ultimate'));
if (hasUltimate) { earned.push('Ultimate STW Founders'); earned.push('STW Founders'); }
else if (hasFounder) earned.push('STW Founders');
return [...new Set(earned)];
}
// ─────────────────────────────────────────────────────────────────────────────
// BOT READY
// ─────────────────────────────────────────────────────────────────────────────
client.once('ready', async () => {
console.log(`✅ Logged in as ${client.user.tag}`);
console.log('Commands: !setuproles | !addicons | !skipemoji | !stopemojis | !setroleicons | !removeoldemojis | !verify');
// Set bot avatar to Renegade Raider Black & Gold style
try {
const imgRes = await fetch('https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png');
const imgBuf = Buffer.from(await imgRes.arrayBuffer());
await client.user.setAvatar(imgBuf);
console.log('✅ Bot avatar set to Renegade Raider Black & Gold.');
} catch (err) {
console.warn('Could not set avatar (may already be set or rate limited):', err.message);
}
// Register /commands slash command to guild for instant availability
const rest = new REST({ version: '10' }).setToken(process.env.TOKEN);
const slashCommands = [
new SlashCommandBuilder()
.setName('commands')
.setDescription('List all bot commands')
.toJSON(),
];
try {
await rest.put(Routes.applicationGuildCommands(client.user.id, process.env.EMOJI_GUILD_ID), { body: slashCommands });
console.log('✅ Slash commands registered to guild.');
} catch (err) {
console.error('Failed to register slash commands:', err.message);
}
});
// ─────────────────────────────────────────────────────────────────────────────
// /commands slash command
// ─────────────────────────────────────────────────────────────────────────────
client.on('interactionCreate', async (interaction) => {
if (!interaction.isChatInputCommand() || interaction.commandName !== 'commands') return;
const embed = new EmbedBuilder()
.setColor(0x1a1a2e)
.setAuthor({
name: 'jitterbuxg',
iconURL: 'https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png',
})
.setTitle('Bot Commands')
.setThumbnail('https://fortnite-api.com/images/cosmetics/br/pickaxe_lockjaw/variants/particle/particle1.png')
.addFields(
{
name: 'Admin Setup',
value: [
'`!setuproles` — Create all Fortnite roles',
'`!addicons` — Upload skin icons as server emojis',
'`!setroleicons` — Apply emojis as role icons',
'`!removeoldemojis` — Wipe all emojis for a fresh upload',
'`!setbg <url>` — Set custom background (supports GIFs)',
'`!clearbg` — Reset to default background',
].join('\n'),
inline: false,
},
{
name: 'During !addicons',
value: '`!skipemoji` — Skip current · `!stopemojis` — Cancel session',
inline: false,
},
{
name: 'Verification',
value: '`!verify` — Link your Epic account and auto-assign roles based on your locker',
inline: false,
},
{
name: 'What !verify checks',
value: [
'**OG Skins** — Renegade Raider (B&G), Ghoul Trooper (Pink), Skull Trooper (Purple), + more',
'**Exclusives** — Galaxy, Ikonik, Glow, Honor Guard, Double Helix, Wildcat, + more',
'**Pickaxes** — Raider\'s Revenge (B&G), Axe of Champions, AC/DC, + more',
'**Emotes** — Fresh, Floss, Take the L, Orange Justice, + more',
'**Banners** — OT1–OT11, Creator Banners, STW Founders, + more',
].join('\n'),
inline: false,
}
)
.setImage('https://fortnite-api.com/images/cosmetics/br/pickaxe_lockjaw/variants/particle/particle1.png')
.setFooter({ text: 'Made by jitterbuxg • .gg/collecting' })
.setTimestamp();
await interaction.reply({ embeds: [embed], ephemeral: false });
});
// ─────────────────────────────────────────────────────────────────────────────
// !setbg / !clearbg — custom background for skin checker images
// ─────────────────────────────────────────────────────────────────────────────
client.on('messageCreate', async (message) => {
if (message.author.bot) return;
if (message.content.startsWith('!setbg ')) {
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator))
return message.reply('Need Administrator permission.');
const url = message.content.slice(7).trim();
if (!url.startsWith('http')) return message.reply('Please provide a valid image URL.');
customBgUrl = url;
return message.reply(`Background set. All future skin checker images will use this background.`);
}
if (message.content === '!clearbg') {
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator))
return message.reply('Need Administrator permission.');
customBgUrl = null;
return message.reply('Background cleared. Back to default dark gradient.');
}
});
// ─────────────────────────────────────────────────────────────────────────────
// !setuproles
// ─────────────────────────────────────────────────────────────────────────────
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!setuproles') return;
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator))
return message.reply('Need Administrator permission.');
await message.reply(`Creating up to ${roles.length} roles...`);
let created = 0, skipped = 0;
const existing = message.guild.roles.cache.map(r => r.name.toLowerCase());
for (const role of roles) {
if (existing.includes(role.name.toLowerCase())) { skipped++; continue; }
try {
await message.guild.roles.create({ name: role.name, color: role.color, reason: 'Bulk role setup' });
console.log(`Created: ${role.name}`);
created++;
await new Promise(r => setTimeout(r, 300));
} catch (err) {
console.error(`Failed "${role.name}": ${err.message}`);
}
}
message.channel.send(`Done! Created **${created}** roles, skipped **${skipped}** existing.`);
});
// ─────────────────────────────────────────────────────────────────────────────
// !removeoldemojis
// ─────────────────────────────────────────────────────────────────────────────
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!removeoldemojis') return;
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator))
return message.reply('Need Administrator permission.');
const emojiGuild = await client.guilds.fetch(process.env.EMOJI_GUILD_ID).catch(() => null);
if (!emojiGuild) return message.reply('Could not find the emoji server. Check EMOJI_GUILD_ID in .env');
await message.reply(`Removing ALL emojis from **${emojiGuild.name}**...`);
console.log(`🎯 Clearing all emojis from: ${emojiGuild.name} (${emojiGuild.id})`);
await emojiGuild.emojis.fetch();
const allEmojis = [...emojiGuild.emojis.cache.values()];
console.log(`📋 Found ${allEmojis.length} emojis to delete`);
let removed = 0;
for (const emoji of allEmojis) {
try {
await emoji.delete('Clearing all emojis for fresh upload');
console.log(`🗑 Deleted: ${emoji.name}`);
removed++;
await new Promise(r => setTimeout(r, 400));
} catch (err) {
console.error(`Failed to delete ${emoji.name}: ${err.message}`);
}
}
message.channel.send(`Removed **${removed}** emojis from **${emojiGuild.name}**. Run !addicons to re-upload.`);
});
// ─────────────────────────────────────────────────────────────────────────────
// !addicons + !skipemoji + !stopemojis
// ─────────────────────────────────────────────────────────────────────────────
// Shared state for the active addicons session
let emojiSession = null; // { resolve, name, channel }
// !skipemoji — skip the currently stuck emoji
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!skipemoji') return;
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return;
if (!emojiSession) return message.reply('No emoji upload is currently in progress.');
message.reply(`Skipping **${emojiSession.name}**...`);
emojiSession.resolve('skip');
});
// !stopemojis — stop the whole session
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!stopemojis') return;
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator)) return;
if (!emojiSession) return message.reply('No emoji upload is currently in progress.');
message.reply('Stopping emoji upload session.');
emojiSession.resolve('stop');
});
// Lock to prevent !addicons running multiple times simultaneously
let addIconsRunning = false;
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!addicons') return;
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator))
return message.reply('Need Administrator permission.');
if (addIconsRunning)
return message.reply('An emoji upload is already in progress. Wait for it to finish or use `!stopemojis` to cancel.');
const emojiGuild = await client.guilds.fetch(process.env.EMOJI_GUILD_ID).catch(() => null);
if (!emojiGuild) return message.reply('Could not find the emoji server. Check EMOJI_GUILD_ID in .env');
await message.reply(`Uploading ${roleIconMap.length} skin icons to **${emojiGuild.name}**.\nType \`!skipemoji\` to skip a stuck one, or \`!stopemojis\` to cancel.`);
await emojiGuild.emojis.fetch();
const existing = new Set(emojiGuild.emojis.cache.map(e => e.name.toLowerCase()));
let added = 0, skipped = 0, failed = 0;
for (const entry of roleIconMap) {
if (existing.has(entry.emoji.toLowerCase())) {
console.log(`⏭ Exists: ${entry.emoji}`);
skipped++;
continue;
}
// Set up skip/stop listener for this entry
let sessionResolve;
const skipPromise = new Promise(res => { sessionResolve = res; });
emojiSession = { resolve: sessionResolve, name: entry.name, channel: message.channel };
process.stdout.write(`📥 ${entry.name} — fetching... `);
try {
const iconUrl = entry.overrideUrl || entry.bannerUrl || await fetchIconUrl(entry.id);
process.stdout.write(`processing... `);
const buffer = await downloadAndProcess(iconUrl);
process.stdout.write(`uploading... `);
// Race the upload against a skip/stop signal
const result = await Promise.race([
createEmojiSafe(emojiGuild, buffer, entry.emoji, `Skin icon: ${entry.name}`)
.then(() => 'done'),
skipPromise,
]);
if (result === 'stop') {
console.log('🛑 Stopped by user.');
emojiSession = null;
return message.channel.send(`Stopped. Added **${added}**, skipped **${skipped}**, failed **${failed}**.`);
}
if (result === 'skip') {
console.log(`⏭ Skipped: ${entry.name}`);
skipped++;
emojiSession = null;
continue;
}
console.log(`✅`);
added++;
} catch (err) {
console.log(`❌ ${err.message}`);
failed++;
}
emojiSession = null;
await new Promise(r => setTimeout(r, 2000));
}
emojiSession = null;
message.channel.send(`Done. Added **${added}** emojis to **${emojiGuild.name}**. Skipped **${skipped}**. Failed **${failed}**.`);
});
// ─────────────────────────────────────────────────────────────────────────────
// !setroleicons
// ─────────────────────────────────────────────────────────────────────────────
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!setroleicons') return;
if (!message.member.permissions.has(PermissionsBitField.Flags.Administrator))
return message.reply('Need Administrator permission.');
const emojiGuild = await client.guilds.fetch(process.env.EMOJI_GUILD_ID).catch(() => null);
if (!emojiGuild) return message.reply('Could not find the emoji server. Check EMOJI_GUILD_ID in .env');
await message.reply('Applying emojis to roles...');
await emojiGuild.emojis.fetch();
await message.guild.roles.fetch();
let updated = 0, skipped = 0, failed = 0;
for (const entry of roleIconMap) {
const role = message.guild.roles.cache.find(r => r.name.toLowerCase() === entry.name.toLowerCase());
const emoji = emojiGuild.emojis.cache.find(e => e.name.toLowerCase() === entry.emoji.toLowerCase());
if (!role) { console.log(`⏭ Role not found: ${entry.name}`); skipped++; continue; }
if (!emoji) { console.log(`⚠ Emoji missing: ${entry.emoji} — run !addicons first`); skipped++; continue; }
try {
await role.setIcon(emoji);
console.log(`✅ ${entry.name} → :${entry.emoji}:`);
updated++;
} catch (err) {
console.error(`❌ "${entry.name}": ${err.message}`);
failed++;
}
await new Promise(r => setTimeout(r, 500));
}
message.channel.send(`Done. Applied icons to **${updated}** roles. Skipped **${skipped}**. Failed **${failed}**.`);
});
// ─────────────────────────────────────────────────────────────────────────────
// !unverify — remove verification and strip roles
// ─────────────────────────────────────────────────────────────────────────────
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!unverify') return;
const userId = message.author.id;
if (!verifiedUsers.has(userId))
return message.reply('You are not currently verified. Use `!verify` to link your account.');
const prev = verifiedUsers.get(userId);
const guild = client.guilds.cache.get(prev.guildId) || message.guild;
if (guild) {
try {
const member = await guild.members.fetch(userId).catch(() => null);
if (member) {
for (const roleName of prev.assignedRoles) {
const role = guild.roles.cache.find(r => r.name.toLowerCase() === roleName.toLowerCase());
if (role && member.roles.cache.has(role.id)) {
await member.roles.remove(role, 'User unverified').catch(() => null);
}
}
}
} catch (err) {
console.error('Unverify role removal error:', err.message);
}
}
verifiedUsers.delete(userId);
message.reply(`Your verification as **${prev.epicName}** has been removed and your roles have been stripped. You can now use \`!verify\` to link a different account.`);
});
// ─────────────────────────────────────────────────────────────────────────────
// !verify
// ─────────────────────────────────────────────────────────────────────────────
client.on('messageCreate', async (message) => {
if (message.author.bot || message.content !== '!verify') return;
const userId = message.author.id;
// Support DMs — if in DM, we need to find the guild separately
let guild = message.guild;
if (!guild) {
// Try to find the main guild from EMOJI_GUILD_ID
guild = client.guilds.cache.get(process.env.EMOJI_GUILD_ID) || client.guilds.cache.first();
if (!guild) return message.reply('Could not find the server. Please use !verify in the server.');
}
// Block if already verified
if (verifiedUsers.has(userId)) {
const prev = verifiedUsers.get(userId);
return message.reply(`You are already verified as **${prev.epicName}**. Use \`!unverify\` to unlink your account and verify a different one.`);
}
if (pending.has(userId))
return message.reply('You already have a verification in progress. Check your DMs.');
await message.reply('A login link has been sent to your DMs.');
try {
const switchToken = await getSwitchAccessToken();
const deviceData = await createDeviceCode(switchToken);
if (!deviceData.user_code || !deviceData.device_code)
return message.author.send('Failed to generate a login link. Try again later.');
const loginUrl = `https://www.epicgames.com/activate?userCode=${deviceData.user_code}`;
const embed = new EmbedBuilder()
.setColor(0x5865F2)
.setTitle('Fortnite Account Verification')
.setDescription(
`**[Click here to verify →](${loginUrl})**\n\n` +
`Code: \`${deviceData.user_code}\`\n\n` +
`Expires in **5 minutes**. The bot will check your locker and assign your roles automatically.`
)
.setFooter({ text: 'Read-only — the bot only checks your cosmetics.' });
await message.author.send({ embeds: [embed] });
pending.set(userId, { guildId: guild.id, channelId: message.channel.id });
const tokenData = await waitForLogin(deviceData.device_code);
pending.delete(userId);
if (!tokenData)
return message.author.send('Login expired. Use !verify to try again.');
const iosData = await exchangeForIosToken(tokenData.access_token);
if (!iosData.access_token)
return message.author.send('Authentication failed. Please try again.');
const { account_id: accountId, displayName = 'Unknown', access_token: accessToken } = iosData;
await message.author.send(`Logged in as **${displayName}**. Fetching locker...`);
const [athenaData, commonCoreData] = await Promise.all([
getProfile(accountId, accessToken, 'athena'),
getProfile(accountId, accessToken, 'common_core'),
]);
if (!athenaData?.profileChanges)
return message.author.send('Failed to read your locker. Please try again.');
const { owned: ownedMap, ogStyleOwned, outfits, pickaxes, emotes, emoticons, backblings, gliders, sprays } = buildOwnedMap(athenaData);
const ccItems = commonCoreData?.profileChanges?.[0]?.profile?.items || {};
const earnedRoles = getRolesForLocker(ownedMap, ccItems);
const member = await guild.members.fetch(userId).catch(() => null);
if (!member)
return message.author.send('Could not find you in the server. Make sure you are still a member.');
const assigned = [], notFound = [];
for (const roleName of earnedRoles) {
const role = guild.roles.cache.find(r => r.name.toLowerCase() === roleName.toLowerCase());
if (!role) { notFound.push(roleName); continue; }
if (!member.roles.cache.has(role.id))
await member.roles.add(role, 'Fortnite locker verification').catch(() => null);
assigned.push(roleName);
}
// Store verified state so they can't re-verify without !unverify
verifiedUsers.set(userId, { guildId: guild.id, assignedRoles: assigned, epicName: displayName });
const verifyEmbed = new EmbedBuilder()
.setColor(0x2B2D31)
.setTitle(`${displayName}'s Verification`)
.setDescription(
assigned.length === 0
? 'No exclusive roles found.'
: assigned.map(r => `— ${r}`).join('\n')
)
.addFields(
{ name: 'Total Outfits', value: String(outfits.length), inline: true },
{ name: 'Total Pickaxes', value: String(pickaxes.length), inline: true },
)
.setFooter({ text: 'Made by jitterbuxg' });
await message.author.send({ embeds: [verifyEmbed] });
// Send verification log to designated channel
try {
const logChannel = await client.channels.fetch(LOG_CHANNEL_ID).catch(() => null);
if (logChannel) {
// Fetch server emojis to use next to role names
const emojiGuild = await client.guilds.fetch(process.env.EMOJI_GUILD_ID).catch(() => null);
await emojiGuild?.emojis.fetch().catch(() => null);
const getServerEmoji = (roleName) => {
if (!emojiGuild) return '';
// Map role names to emoji names we uploaded
const emojiNameMap = {
'Galaxy': 'Galaxy', 'Ikonik': 'Ikonik', 'Glow': 'Glow',
'OG Renegade Raider': 'RenegadeRaider', 'OG Ghoul Trooper': 'GhoulTrooper',
'OG Skull Trooper': 'SkullTrooper', 'OG Aerial Assault Trooper': 'AerialAssaultTrooper',
'Black Knight': 'BlackKnight', 'Double Helix': 'DoubleHelix',
'Honor Guard': 'HonorGuard', 'Wonder': 'Wonder', 'Eon': 'Eon',
'Dark Vertex': 'DarkVertex', 'Neo Versa': 'NeoVersa',
'Royale Bomber': 'RoyaleBomber', 'Wildcat': 'Wildcat',
'Stealth Reflex': 'StealthReflex', 'Reflex': 'Reflex',
"Raider's Revenge": 'RaidersRevenge', 'Stellar Axe': 'StellarAxe',
'AC/DC': 'ACDC', 'Merry Mint Axe': 'MerryMintAxe',
'Axe of Champions': 'AxeOfChampions', 'Blade of Champions': 'BladeOfChampions',
'Fully Golden Peely': 'FullyGoldenPeely',
};
const emojiName = emojiNameMap[roleName];
if (!emojiName) return '';
const emoji = emojiGuild.emojis.cache.find(e => e.name.toLowerCase() === emojiName.toLowerCase());
return emoji ? `${emoji} ` : '';
};
const logEmbed = new EmbedBuilder()
.setColor(assigned.length > 0 ? 0xFFD700 : 0x444444)
.setAuthor({
name: displayName,
iconURL: 'https://fortnite-api.com/images/cosmetics/br/cid_028_athena_commando_f/variants/material/mat3.png',
})
.setTitle('Account Verified')
.setDescription(
assigned.length === 0
? 'No exclusive cosmetics found on this account.'
: assigned.map(r => `${getServerEmoji(r)}${r}`).join('\n')
)
.addFields(
{ name: 'Discord', value: message.author.username, inline: true },
{ name: 'Epic Account', value: displayName, inline: true },
{ name: 'Roles Given', value: String(assigned.length), inline: true },
)
.setFooter({ text: '.gg/collecting • Made by jitterbuxg' })
.setTimestamp();
await logChannel.send({ embeds: [logEmbed] });
}
} catch (err) {
console.error('Log channel error:', err.message);
}
// ── 1. Fetch metadata for all owned cosmetics ─────────────────────────
const allIds = [...outfits, ...pickaxes];
const metaMap = await fetchCosmeticMeta(allIds);
// Override rarity to mythic for known exclusives
for (const req of ROLE_REQUIREMENTS) {
if (!req.bannerId) {
const id = req.id.toLowerCase();
if (qualifiesForRequirement(req, ownedMap) && metaMap[id]) {
metaMap[id].rarity = 'mythic';
if (requiredStyleTags(req).length > 0) metaMap[id].name = req.role;
}
}
}
// ── 2. Exclusives rendered image ──────────────────────────────────────
try {
const exclusiveItems = [];
const exclusiveSeen = new Set();
const addExclusiveItem = (item) => {
const key = item.id.toLowerCase();
if (exclusiveSeen.has(key)) return;
exclusiveSeen.add(key);
exclusiveItems.push(item);
};
// Exclusive cosmetics (skins, pickaxes, emotes, emoticons)
for (const req of ROLE_REQUIREMENTS) {
if (req.bannerId || req.stw) continue;
const id = req.id.toLowerCase();
if (!qualifiesForRequirement(req, ownedMap)) continue;
const meta = metaMap[id] || {};
addExclusiveItem({ id, name: req.role || meta.name || id, rarity: 'mythic' });
}
// Exclusive & STW banners - pulled from role requirements and bundled master list
const bannerMeta = await getBannerMeta();
for (const item of collectOwnedExclusiveBannerItems(ccItems, bannerMeta, metaMap)) {
addExclusiveItem(item);
}
if (exclusiveItems.length > 0) {
await message.author.send('Building your exclusive cosmetics image...');
const exclusiveImg = await renderLockerImage(
exclusiveItems,
`${exclusiveItems.length} (0)`,
'EXCLUSIVES',
message.author.username,
ogStyleOwned
);
if (exclusiveImg) {
const top5 = exclusiveItems.slice(0, 5).map(i => i.name).join(' + ');
const suffix = exclusiveItems.length > 5 ? ` + ${exclusiveItems.length - 5} more` : '';
await message.author.send({
content: `**${top5}${suffix}** (${outfits.length} skin count)`,
files: [{ attachment: exclusiveImg, name: 'exclusives.png' }],
});
}
}
} catch (err) {
console.error('Exclusive image error:', err.message);
}
// ── 3. Full locker — all items, sorted by rarity, one image per category ──
try {
// Rarity sort order (matches RiftCheckerBot)
const RARITY_ORDER = ['mythic','legendary','dark','lava','slurp','starwars','marvel','frozen','gaminglegends','shadow','icon','dc','epic','rare','uncommon','common'];
const sortByRarity = (ids) => [...ids].sort((a, b) => {
const ra = RARITY_ORDER.indexOf(metaMap[a.toLowerCase()]?.rarity || 'common');
const rb = RARITY_ORDER.indexOf(metaMap[b.toLowerCase()]?.rarity || 'common');
return (ra === -1 ? 99 : ra) - (rb === -1 ? 99 : rb);
});
// Collect owned exclusive banners
const bannerMeta = await getBannerMeta();
const ownedExclusiveBannerIds = [];
for (const req of ROLE_REQUIREMENTS) {
if (!req.bannerId) continue;
const owned = Object.values(ccItems).some(i =>
(i.templateId || '').toLowerCase().includes(req.bannerId.toLowerCase())
);
if (owned) {
const bid = req.bannerId.toLowerCase();
if (!metaMap[bid]) {
metaMap[bid] = { name: bannerMeta[bid]?.name || req.role, rarity: 'mythic' };
} else {
metaMap[bid].rarity = 'mythic';
}
ownedExclusiveBannerIds.push(req.bannerId);
}
}
// Fetch metadata for all cosmetic categories
const allCategoryIds = [...outfits, ...pickaxes, ...emotes, ...emoticons, ...backblings, ...gliders, ...sprays];
const allMeta = await fetchCosmeticMeta(allCategoryIds);
for (const [k, v] of Object.entries(allMeta)) metaMap[k] = metaMap[k] || v;
const categories = [
{ ids: sortByRarity(outfits), label: 'OUTFITS', subtitle: `${outfits.length} outfits` },
{ ids: sortByRarity(pickaxes), label: 'PICKAXES', subtitle: `${pickaxes.length} pickaxes` },
{ ids: sortByRarity(emotes), label: 'EMOTES', subtitle: `${emotes.length} emotes` },
{ ids: sortByRarity(emoticons), label: 'EMOTICONS', subtitle: `${emoticons.length} emoticons` },
{ ids: sortByRarity(backblings), label: 'BACK BLINGS', subtitle: `${backblings.length} back blings` },
{ ids: sortByRarity(gliders), label: 'GLIDERS', subtitle: `${gliders.length} gliders` },
{ ids: sortByRarity(sprays), label: 'SPRAYS', subtitle: `${sprays.length} sprays` },
...(ownedExclusiveBannerIds.length > 0
? [{ ids: ownedExclusiveBannerIds, label: 'BANNERS', subtitle: `${ownedExclusiveBannerIds.length} banners` }]
: []),
].filter(c => c.ids.length > 0);
if (categories.length === 0) {
await message.author.send('No cosmetics found in locker.');
return;
}
await message.author.send(`Building your full locker (${categories.length} categories)...`);
for (const cat of categories) {
const items = cat.ids.map(id => ({
id,
name: metaMap[id.toLowerCase()]?.name || id,
rarity: metaMap[id.toLowerCase()]?.rarity || 'common',
}));
const img = await renderLockerImage(
items,
`${items.length} (0)`,
cat.label,
message.author.username,
ogStyleOwned
);
if (img) {
await message.author.send({
content: `**${displayName} — ${cat.subtitle}**`,
files: [{ attachment: img, name: `${cat.label.toLowerCase().replace(/\s/g, '_')}.png` }],
});
}
await new Promise(r => setTimeout(r, 500));
}
} catch (err) {
console.error('Locker image error:', err.message);
}
} catch (err) {
pending.delete(userId);
console.error('Verify error:', err);
message.author.send('Unexpected error. Try again.').catch(() => null);
}
});
client.login(process.env.TOKEN);