- Added hunt/feed duck mechanics (80% hunt, 20% feed) - Implemented persistent scoring system - Added channel control commands (\!stopducks/\!startducks) - Enhanced duck hunt with wrong action penalties - Organized bot structure with botmain.js as main file - Added comprehensive documentation (README.md) - Included 17 plugins with various games and utilities 🦆 Duck Hunt Features: - Hunt ducks with \!shoot/\!bang (80% of spawns) - Feed ducks with \!feed (20% of spawns) - Persistent scores saved to JSON - Channel-specific controls for #bakedbeans - Reaction time tracking and special achievements 🎮 Other Games: - Casino games (slots, coinflip, hi-lo, scratch cards) - Multiplayer games (pigs, zombie dice, quiplash) - Text generation (babble, conspiracy, drunk historian) - Interactive features (story writing, emojify, combos) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
339 lines
No EOL
13 KiB
JavaScript
339 lines
No EOL
13 KiB
JavaScript
// 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'}`);
|
|
}
|
|
}
|
|
]
|
|
}; |