// plugins/combo.js - Track consecutive message combos module.exports = { init(bot) { console.log('🔥 Combo plugin initialized'); this.bot = bot; // ================================ // EASY CONFIG - EDIT THIS SECTION // ================================ this.config = { minComboLength: 3, // Minimum messages for a combo announcement maxComboLength: 50, // Maximum combo before forcing break comboBreakTime: 300000, // 5 minutes - auto break combo if idle this long ignoreShortMessages: true, // Ignore messages under 3 characters ignoreBots: ['services', 'chanserv', 'nickserv'], // Bot nicks to ignore comboEmojis: ['🔥', '💥', '⚡', '🎯', '🚀', '💫', '⭐', '✨'], // Random emojis for announcements enableChannelStats: true // Track per-channel combo records }; // Current combo tracking this.currentCombos = new Map(); // channel -> { nick, count, lastMessageTime, broken } // Combo records and stats this.comboRecords = new Map(); // channel -> { topCombo: {nick, count, date}, allTimeStats: Map(nick -> {best, total, current}) } // Last message tracking for combo breaking this.lastMessages = new Map(); // channel -> { nick, time } }, cleanup(bot) { console.log('🔥 Combo plugin cleaned up'); }, // Check if we should ignore this message shouldIgnoreMessage(nick, message) { // Ignore bots if (this.config.ignoreBots.includes(nick.toLowerCase())) { return true; } // Ignore very short messages if configured if (this.config.ignoreShortMessages && message.trim().length < 3) { return true; } // Ignore commands (starting with !) if (message.trim().startsWith('!')) { return true; } return false; }, // Get or create channel stats getChannelStats(channel) { if (!this.comboRecords.has(channel)) { this.comboRecords.set(channel, { topCombo: { nick: null, count: 0, date: null }, allTimeStats: new Map() }); } return this.comboRecords.get(channel); }, // Get or create user stats for channel getUserStats(channel, nick) { const channelStats = this.getChannelStats(channel); if (!channelStats.allTimeStats.has(nick)) { channelStats.allTimeStats.set(nick, { best: 0, total: 0, current: 0 }); } return channelStats.allTimeStats.get(nick); }, // Check if combo should be broken due to time checkComboTimeout(channel) { const combo = this.currentCombos.get(channel); if (!combo || combo.broken) return; const timeSinceLastMessage = Date.now() - combo.lastMessageTime; if (timeSinceLastMessage > this.config.comboBreakTime) { this.breakCombo(channel, 'timeout'); } }, // Break the current combo breakCombo(channel, reason = 'interrupted') { const combo = this.currentCombos.get(channel); if (!combo || combo.broken) return; combo.broken = true; // Update user stats if (this.config.enableChannelStats) { const userStats = this.getUserStats(channel, combo.nick); userStats.current = 0; // Reset current combo // Check if this was a personal best if (combo.count > userStats.best) { userStats.best = combo.count; } // Check if this was a channel record (but don't announce) const channelStats = this.getChannelStats(channel); if (combo.count > channelStats.topCombo.count) { channelStats.topCombo = { nick: combo.nick, count: combo.count, date: new Date().toISOString() }; } } // No automatic announcements - only show when !combo is used }, // Get random emoji from config getRandomEmoji() { return this.config.comboEmojis[Math.floor(Math.random() * this.config.comboEmojis.length)]; }, // Process a message for combo tracking processMessage(channel, nick, message) { // Skip if we should ignore this message if (this.shouldIgnoreMessage(nick, message)) { return; } // Check for combo timeouts first this.checkComboTimeout(channel); const lastMessage = this.lastMessages.get(channel); const currentCombo = this.currentCombos.get(channel); const now = Date.now(); // Update last message tracker this.lastMessages.set(channel, { nick, time: now }); // Check if this continues an existing combo if (currentCombo && !currentCombo.broken && currentCombo.nick === nick) { // Continue the combo currentCombo.count++; currentCombo.lastMessageTime = now; // Update user stats if (this.config.enableChannelStats) { const userStats = this.getUserStats(channel, nick); userStats.current = currentCombo.count; userStats.total++; } // No automatic announcements - combos are silent until !combo is used // Check for forced break at max length if (currentCombo.count >= this.config.maxComboLength) { this.breakCombo(channel, 'forced'); } } else { // Break any existing combo (different user spoke) if (currentCombo && !currentCombo.broken && currentCombo.nick !== nick) { this.breakCombo(channel, 'interrupted'); } // Start new combo this.currentCombos.set(channel, { nick: nick, count: 1, lastMessageTime: now, broken: false }); // Update user stats if (this.config.enableChannelStats) { const userStats = this.getUserStats(channel, nick); userStats.current = 1; userStats.total++; } } }, // Message event handler onMessage(data, bot) { if (data.isChannel) { this.processMessage(data.target, data.nick, data.message); } }, commands: [ { name: 'combo', description: 'Show current combo status', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const channel = context.channel || target; // Check for timeouts first plugin.checkComboTimeout(channel); const currentCombo = plugin.currentCombos.get(channel); if (!currentCombo || currentCombo.broken || currentCombo.count < 2) { bot.say(target, '🔥 No active combo right now.'); } else { const emoji = plugin.getRandomEmoji(); const timeAgo = Math.floor((Date.now() - currentCombo.lastMessageTime) / 1000); bot.say(target, `${emoji} ${currentCombo.nick} is on a ${currentCombo.count}-message combo! (${timeAgo}s ago)`); } } }, { name: 'combostats', description: 'Show combo statistics for this channel', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const channel = context.channel || target; if (!plugin.config.enableChannelStats) { bot.say(target, 'Channel stats are disabled.'); return; } const channelStats = plugin.getChannelStats(channel); // Show channel record if (channelStats.topCombo.nick) { const date = new Date(channelStats.topCombo.date).toLocaleDateString(); bot.say(target, `🏆 Channel Record: ${channelStats.topCombo.nick} with ${channelStats.topCombo.count} messages (${date})`); } else { bot.say(target, '🏆 No channel record set yet.'); } // Show top 5 personal bests const topUsers = Array.from(channelStats.allTimeStats.entries()) .sort((a, b) => b[1].best - a[1].best) .slice(0, 5) .filter(([nick, stats]) => stats.best > 0); if (topUsers.length > 0) { bot.say(target, '📊 Top Personal Bests:'); topUsers.forEach(([nick, stats], index) => { const rank = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : `${index + 1}.`; bot.say(target, `${rank} ${nick}: ${stats.best} messages (${stats.total} total messages)`); }); } else { bot.say(target, '📊 No stats recorded yet.'); } } }, { name: 'combome', description: 'Show your personal combo stats', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const channel = context.channel || target; if (!plugin.config.enableChannelStats) { bot.say(target, 'Personal stats are disabled.'); return; } const userStats = plugin.getUserStats(channel, from); const currentCombo = plugin.currentCombos.get(channel); let message = `📈 ${from}'s stats: Best ${userStats.best}, Total messages ${userStats.total}`; if (currentCombo && !currentCombo.broken && currentCombo.nick === from && currentCombo.count > 1) { message += `, Current combo: ${currentCombo.count} 🔥`; } bot.say(target, message); } }, { name: 'combobreak', description: 'Break your own combo (admin can break any combo)', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const channel = context.channel || target; const args = context.args; const currentCombo = plugin.currentCombos.get(channel); if (!currentCombo || currentCombo.broken) { bot.say(target, `${from}: No active combo to break.`); return; } // Admin check - can break anyone's combo const adminNicks = ['admin', 'owner', 'cancerbot', 'megasconed']; const isAdmin = adminNicks.includes(from); if (isAdmin && args.length > 0) { // Admin breaking specific user's combo - no announcement const targetNick = args[0]; if (currentCombo.nick === targetNick) { plugin.breakCombo(channel, 'forced'); } else { bot.say(target, `${from}: ${targetNick} doesn't have an active combo.`); } } else if (currentCombo.nick === from) { // User breaking their own combo - no announcement plugin.breakCombo(channel, 'voluntary'); } else { // Not their combo and not admin bot.say(target, `${from}: You can only break your own combo! (${currentCombo.nick} has the current combo)`); } } }, { name: 'combosettings', description: 'Show current combo plugin settings', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; bot.say(target, `⚙️ Combo Settings:`); bot.say(target, `• Min combo for tracking: ${plugin.config.minComboLength} messages`); bot.say(target, `• Max combo length: ${plugin.config.maxComboLength} messages`); bot.say(target, `• Combo timeout: ${plugin.config.comboBreakTime / 60000} minutes`); bot.say(target, `• Ignore short messages: ${plugin.config.ignoreShortMessages ? 'Yes' : 'No'}`); bot.say(target, `• Channel stats: ${plugin.config.enableChannelStats ? 'Enabled' : 'Disabled'}`); } } ] };