- Add Rock Paper Scissors game with PvP and bot modes - Fixed syntax errors and improved game mechanics - PvP moves now require PM for secrecy - Add Word Scramble game with difficulty levels - Multiple word categories and persistent scoring - Enhance duck hunt with better statistics tracking - Separate points vs duck count tracking - Fixed migration logic issues - Add core rate limiting system (5 commands/30s) - Admin whitelist for megasconed - Automatic cleanup and unblocking - Improve reload functionality for hot-reloading plugins - Add channel-specific commands (\!stopducks/\!startducks) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
731 lines
No EOL
30 KiB
JavaScript
731 lines
No EOL
30 KiB
JavaScript
// plugins/duck-hunt.js - Duck Hunt Game Plugin
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
module.exports = {
|
|
gameState: {
|
|
activeDucks: new Map(), // channel -> duck data
|
|
lastActivity: new Map(), // channel -> timestamp
|
|
duckTimers: new Map(), // channel -> timer
|
|
scores: new Map(), // nick -> score
|
|
channelActivity: new Map(), // channel -> activity count
|
|
enabledChannels: new Map() // channel -> enabled status (default true)
|
|
},
|
|
|
|
duckTypes: [
|
|
{ emoji: '🦆', name: 'Mallard Duck', points: 1, rarity: 'common', sound: 'Quack!' },
|
|
{ emoji: '🦢', name: 'Swan', points: 2, rarity: 'uncommon', sound: 'Honk!' },
|
|
{ emoji: '🐧', name: 'Penguin', points: 3, rarity: 'rare', sound: 'Squawk!' },
|
|
{ emoji: '🦅', name: 'Eagle', points: 5, rarity: 'epic', sound: 'SCREECH!' },
|
|
{ emoji: '🦉', name: 'Owl', points: 4, rarity: 'rare', sound: 'Hoot!' },
|
|
{ emoji: '🪿', name: 'Goose', points: 2, rarity: 'uncommon', sound: 'HONK HONK!' },
|
|
{ emoji: '🦜', name: 'Parrot', points: 3, rarity: 'rare', sound: 'Polly wants a cracker!' }
|
|
],
|
|
|
|
rareDucks: [
|
|
{ emoji: '🏆', name: 'Golden Duck', points: 10, rarity: 'legendary', sound: '*shimmers*' },
|
|
{ emoji: '💎', name: 'Diamond Duck', points: 15, rarity: 'mythical', sound: '*sparkles*' },
|
|
{ emoji: '🌟', name: 'Cosmic Duck', points: 20, rarity: 'cosmic', sound: '*otherworldly quack*' }
|
|
],
|
|
|
|
init(bot) {
|
|
console.log('Duck Hunt plugin initialized - Get ready to hunt! 🦆🔫');
|
|
|
|
// Set up score file path
|
|
this.scoresFile = path.join(__dirname, 'duck_hunt_scores.json');
|
|
|
|
// Load existing scores
|
|
this.loadScores();
|
|
|
|
this.startDuckSpawning(bot);
|
|
},
|
|
|
|
cleanup(bot) {
|
|
console.log('Duck Hunt plugin cleaned up - Ducks are safe... for now');
|
|
// Save scores before cleanup
|
|
this.saveScores();
|
|
// Clear all timers
|
|
this.gameState.duckTimers.forEach(timer => clearTimeout(timer));
|
|
this.gameState.duckTimers.clear();
|
|
},
|
|
|
|
commands: [
|
|
{
|
|
name: 'shoot',
|
|
description: 'Shoot a duck if one is present',
|
|
execute(context, bot) {
|
|
module.exports.shootDuck(context, bot);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'bang',
|
|
description: 'Alternative shoot command',
|
|
execute(context, bot) {
|
|
module.exports.shootDuck(context, bot);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'duckscore',
|
|
description: 'Check your duck hunting score',
|
|
execute(context, bot) {
|
|
const playerData = module.exports.gameState.scores.get(context.nick);
|
|
if (!playerData) {
|
|
bot.say(context.replyTo, `🎯 ${context.nick} hasn't shot any ducks yet!`);
|
|
return;
|
|
}
|
|
|
|
const points = module.exports.getTotalPoints(playerData);
|
|
const ducks = module.exports.getTotalDucks(playerData);
|
|
const isMigrated = playerData.migrated;
|
|
|
|
if (isMigrated && ducks === 0) {
|
|
bot.say(context.replyTo, `🎯 ${context.nick}: ${points} points (duck count tracked from next shot)`);
|
|
} else {
|
|
bot.say(context.replyTo, `🎯 ${context.nick}: ${points} points from ${ducks} ducks!`);
|
|
}
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'topducks',
|
|
description: 'Show top duck hunters by points',
|
|
execute(context, bot) {
|
|
module.exports.showLeaderboard(context, bot);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'duckcount',
|
|
description: 'Show top duck hunters by quantity',
|
|
execute(context, bot) {
|
|
module.exports.showDuckCount(context, bot);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'duckstats',
|
|
description: 'Show duck hunting statistics and most shot birds',
|
|
execute(context, bot) {
|
|
module.exports.showStats(context, bot);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'stopducks',
|
|
description: 'Stop duck spawning in this channel (bakedbeans only)',
|
|
execute(context, bot) {
|
|
module.exports.stopDucks(context, bot);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'startducks',
|
|
description: 'Resume duck spawning in this channel (bakedbeans only)',
|
|
execute(context, bot) {
|
|
module.exports.startDucks(context, bot);
|
|
}
|
|
},
|
|
|
|
{
|
|
name: 'feed',
|
|
description: 'Feed a duck if one is present',
|
|
execute(context, bot) {
|
|
module.exports.feedDuck(context, bot);
|
|
}
|
|
}
|
|
],
|
|
|
|
// Handle all channel messages to track activity
|
|
onMessage(data, bot) {
|
|
if (data.isChannel) {
|
|
this.trackActivity(data.target);
|
|
this.scheduleNextDuck(data.target, bot);
|
|
}
|
|
},
|
|
|
|
trackActivity(channel) {
|
|
const now = Date.now();
|
|
this.gameState.lastActivity.set(channel, now);
|
|
|
|
// Increment activity counter
|
|
const currentActivity = this.gameState.channelActivity.get(channel) || 0;
|
|
this.gameState.channelActivity.set(channel, currentActivity + 1);
|
|
},
|
|
|
|
scheduleNextDuck(channel, bot) {
|
|
// Don't schedule if there's already a timer or active duck
|
|
if (this.gameState.duckTimers.has(channel) || this.gameState.activeDucks.has(channel)) {
|
|
return;
|
|
}
|
|
|
|
// Check if ducks are enabled for this channel
|
|
if (!this.isDuckHuntEnabled(channel)) {
|
|
return;
|
|
}
|
|
|
|
// Random delay between 6-20 minutes, but weighted towards longer times
|
|
const minDelay = 6 * 60 * 1000; // 6 minutes
|
|
const maxDelay = 20 * 60 * 1000; // 20 minutes
|
|
|
|
// Use exponential distribution to favor longer waits
|
|
const randomFactor = Math.random() * Math.random(); // 0-1, skewed towards 0
|
|
const delay = minDelay + (randomFactor * (maxDelay - minDelay));
|
|
|
|
console.log(`🦆 Scheduling duck for ${channel} in ${Math.round(delay/1000)} seconds`);
|
|
|
|
const timer = setTimeout(() => {
|
|
this.spawnDuck(channel, bot);
|
|
}, delay);
|
|
|
|
this.gameState.duckTimers.set(channel, timer);
|
|
},
|
|
|
|
spawnDuck(channel, bot) {
|
|
// Clear the timer
|
|
this.gameState.duckTimers.delete(channel);
|
|
|
|
// Check if channel has been active recently (within last 30 minutes)
|
|
const lastActivity = this.gameState.lastActivity.get(channel) || 0;
|
|
const thirtyMinutesAgo = Date.now() - (30 * 60 * 1000);
|
|
|
|
if (lastActivity < thirtyMinutesAgo) {
|
|
console.log(`🦆 Channel ${channel} inactive, duck going back to sleep`);
|
|
return; // Channel is quiet, don't spawn duck
|
|
}
|
|
|
|
// Don't spawn if there's already an active duck
|
|
if (this.gameState.activeDucks.has(channel)) {
|
|
return;
|
|
}
|
|
|
|
const duck = this.getRandomDuck();
|
|
const spawnTime = Date.now();
|
|
|
|
// Randomly determine if this is a hunt duck (80%) or feed duck (20%)
|
|
const isHuntDuck = Math.random() < 0.8;
|
|
|
|
// Duck flies away after 45-90 seconds if not shot/fed
|
|
const flyAwayTime = 45000 + (Math.random() * 45000);
|
|
|
|
this.gameState.activeDucks.set(channel, {
|
|
duck: duck,
|
|
spawnTime: spawnTime,
|
|
isHuntDuck: isHuntDuck,
|
|
timer: setTimeout(() => {
|
|
this.duckFliesAway(channel, bot, duck);
|
|
}, flyAwayTime)
|
|
});
|
|
|
|
// Duck spawn messages based on type
|
|
let spawnMessages;
|
|
if (isHuntDuck) {
|
|
spawnMessages = [
|
|
`${duck.emoji} *${duck.sound}* A ${duck.name} flies into the channel! Quick, !shoot it!`,
|
|
`${duck.emoji} *WHOOSH* A ${duck.name} appears! Someone !shoot it before it escapes!`,
|
|
`${duck.emoji} *${duck.sound}* Look! A wild ${duck.name}! Type !shoot to hunt it!`,
|
|
`${duck.emoji} *flutters* A ${duck.name} lands nearby! !shoot to bag it!`,
|
|
`${duck.emoji} *${duck.sound}* A ${duck.name} swoops in! !bang or !shoot to take it down!`
|
|
];
|
|
} else {
|
|
spawnMessages = [
|
|
`${duck.emoji} *${duck.sound}* A hungry ${duck.name} waddles into the channel! !feed it some bread!`,
|
|
`${duck.emoji} *chirp chirp* A ${duck.name} looks around for food! Quick, !feed it!`,
|
|
`${duck.emoji} *${duck.sound}* A friendly ${duck.name} approaches peacefully! !feed it something tasty!`,
|
|
`${duck.emoji} *peep peep* A ${duck.name} seems hungry! !feed it before it leaves!`,
|
|
`${duck.emoji} *${duck.sound}* A gentle ${duck.name} lands nearby looking for food! !feed it!`
|
|
];
|
|
}
|
|
|
|
const message = this.randomChoice(spawnMessages);
|
|
bot.say(channel, message);
|
|
|
|
console.log(`🦆 Spawned ${duck.name} in ${channel}`);
|
|
},
|
|
|
|
duckFliesAway(channel, bot, duck) {
|
|
this.gameState.activeDucks.delete(channel);
|
|
|
|
const escapeMessages = [
|
|
`${duck.emoji} *${duck.sound}* The ${duck.name} got away! Better luck next time!`,
|
|
`${duck.emoji} *whoosh* The ${duck.name} flies away safely! Too slow, hunters!`,
|
|
`${duck.emoji} *${duck.sound}* Nobody shot the ${duck.name}! It escapes into the sunset!`,
|
|
`${duck.emoji} The ${duck.name} laughs at your slow reflexes and flies away!`
|
|
];
|
|
|
|
bot.say(channel, this.randomChoice(escapeMessages));
|
|
console.log(`🦆 Duck escaped from ${channel}`);
|
|
|
|
// Schedule next duck
|
|
this.scheduleNextDuck(channel, bot);
|
|
},
|
|
|
|
shootDuck(context, bot) {
|
|
const channel = context.channel;
|
|
if (!channel) {
|
|
bot.say(context.replyTo, '🦆 You can only hunt ducks in channels!');
|
|
return;
|
|
}
|
|
|
|
const activeDuck = this.gameState.activeDucks.get(channel);
|
|
if (!activeDuck) {
|
|
const failMessages = [
|
|
'🎯 *BANG!* You shoot at empty air! No ducks here!',
|
|
'🔫 *click* Nothing to shoot! Wait for a duck to appear!',
|
|
'🦆 The only thing you hit was your own ego! No ducks present!',
|
|
'💨 *WHOOSH* Your bullet flies into the void! No targets!',
|
|
'🎪 *BANG!* You\'ve shot a circus balloon! Still not a duck!'
|
|
];
|
|
bot.say(context.replyTo, this.randomChoice(failMessages));
|
|
return;
|
|
}
|
|
|
|
// Check if this is a feed duck (wrong action)
|
|
if (!activeDuck.isHuntDuck) {
|
|
const duck = activeDuck.duck;
|
|
const wrongActionMessages = [
|
|
`😱 *BANG!* You shot at the friendly ${duck.name}! It flies away terrified! You monster!`,
|
|
`🚫 *BOOM!* The peaceful ${duck.name} was just hungry! Now it's gone because of you!`,
|
|
`💔 *BLAM!* You scared away the gentle ${duck.name}! It just wanted food!`,
|
|
`😰 *BANG!* The ${duck.name} flees in terror! Wrong action, hunter!`,
|
|
`🦆💨 *BOOM!* You frightened the hungry ${duck.name}! It escapes without eating!`
|
|
];
|
|
|
|
// Clear the duck
|
|
clearTimeout(activeDuck.timer);
|
|
this.gameState.activeDucks.delete(channel);
|
|
|
|
bot.say(context.replyTo, this.randomChoice(wrongActionMessages));
|
|
console.log(`🦆 ${context.nick} tried to shoot a feed duck in ${channel}`);
|
|
|
|
// Schedule next duck
|
|
this.scheduleNextDuck(channel, bot);
|
|
return;
|
|
}
|
|
|
|
// Clear the fly-away timer
|
|
clearTimeout(activeDuck.timer);
|
|
|
|
// Calculate reaction time
|
|
const reactionTime = Date.now() - activeDuck.spawnTime;
|
|
const reactionSeconds = (reactionTime / 1000).toFixed(2);
|
|
|
|
// Remove duck from active ducks
|
|
this.gameState.activeDucks.delete(channel);
|
|
|
|
// Award points and track duck type
|
|
const duck = activeDuck.duck;
|
|
this.updatePlayerScore(context.nick, duck.name, duck.points);
|
|
|
|
// Save scores to file after each duck shot
|
|
this.saveScores();
|
|
|
|
// Success messages with variety
|
|
const successMessages = [
|
|
`🎯 *BANG!* ${context.nick} shot the ${duck.name}! +${duck.points} points! (${reactionSeconds}s reaction time)`,
|
|
`🔫 *BOOM!* ${context.nick} bagged a ${duck.name}! +${duck.points} points in ${reactionSeconds} seconds!`,
|
|
`🏆 Nice shot, ${context.nick}! You got the ${duck.name} worth ${duck.points} points! (${reactionSeconds}s)`,
|
|
`💥 *BLAM!* ${context.nick} is a crack shot! ${duck.name} down for ${duck.points} points! Reaction: ${reactionSeconds}s`,
|
|
`🎪 ${context.nick} with the quick draw! ${duck.name} eliminated! +${duck.points} points (${reactionSeconds}s)`
|
|
];
|
|
|
|
// Add special messages for rare ducks
|
|
if (duck.rarity === 'legendary' || duck.rarity === 'mythical' || duck.rarity === 'cosmic') {
|
|
bot.say(channel, `🌟 ✨ RARE DUCK ALERT! ✨ 🌟`);
|
|
}
|
|
|
|
bot.say(channel, this.randomChoice(successMessages));
|
|
|
|
// Extra flair for special achievements
|
|
if (reactionTime < 1000) {
|
|
bot.say(channel, '⚡ LIGHTNING FAST! That was under 1 second!');
|
|
} else if (duck.points >= 10) {
|
|
bot.say(channel, `💎 LEGENDARY SHOT! You got a ${duck.rarity} duck!`);
|
|
}
|
|
|
|
console.log(`🎯 ${context.nick} shot ${duck.name} in ${channel} (${reactionSeconds}s)`);
|
|
|
|
// Schedule next duck
|
|
this.scheduleNextDuck(channel, bot);
|
|
},
|
|
|
|
feedDuck(context, bot) {
|
|
const channel = context.channel;
|
|
if (!channel) {
|
|
bot.say(context.replyTo, '🦆 You can only feed ducks in channels!');
|
|
return;
|
|
}
|
|
|
|
const activeDuck = this.gameState.activeDucks.get(channel);
|
|
if (!activeDuck) {
|
|
const failMessages = [
|
|
'🍞 You scatter breadcrumbs in the empty air! No ducks here!',
|
|
'🌾 You hold out seeds but there\'s nothing to feed!',
|
|
'🦆 You make feeding noises but no ducks are around!',
|
|
'🥖 Your bread goes stale waiting for a duck to appear!',
|
|
'🍀 You offer lettuce to the void! Where are the ducks?'
|
|
];
|
|
bot.say(context.replyTo, this.randomChoice(failMessages));
|
|
return;
|
|
}
|
|
|
|
// Check if this is a hunt duck (wrong action)
|
|
if (activeDuck.isHuntDuck) {
|
|
const duck = activeDuck.duck;
|
|
const wrongActionMessages = [
|
|
`🔫 *WHOOSH* The wild ${duck.name} ignores your food and flies away! You should have shot it!`,
|
|
`🏹 *FLAP FLAP* The ${duck.name} doesn't want food, it's a wild hunter! Wrong approach!`,
|
|
`🎯 *SWOOSH* The aggressive ${duck.name} flies off! It needed to be hunted, not fed!`,
|
|
`💨 *FLUTTER* The ${duck.name} wasn't hungry for food! You missed your shot!`,
|
|
`🦆 *ESCAPE* The wild ${duck.name} laughs at your bread and flies away! Should have used !shoot!`
|
|
];
|
|
|
|
// Clear the duck
|
|
clearTimeout(activeDuck.timer);
|
|
this.gameState.activeDucks.delete(channel);
|
|
|
|
bot.say(context.replyTo, this.randomChoice(wrongActionMessages));
|
|
console.log(`🦆 ${context.nick} tried to feed a hunt duck in ${channel}`);
|
|
|
|
// Schedule next duck
|
|
this.scheduleNextDuck(channel, bot);
|
|
return;
|
|
}
|
|
|
|
// Clear the fly-away timer
|
|
clearTimeout(activeDuck.timer);
|
|
|
|
// Calculate reaction time
|
|
const reactionTime = Date.now() - activeDuck.spawnTime;
|
|
const reactionSeconds = (reactionTime / 1000).toFixed(2);
|
|
|
|
// Remove duck from active ducks
|
|
this.gameState.activeDucks.delete(channel);
|
|
|
|
// Award points and track duck type (same as hunting)
|
|
const duck = activeDuck.duck;
|
|
this.updatePlayerScore(context.nick, duck.name, duck.points);
|
|
|
|
// Save scores to file after each duck fed
|
|
this.saveScores();
|
|
|
|
// Success messages for feeding
|
|
const successMessages = [
|
|
`🍞 *nom nom* ${context.nick} fed the ${duck.name}! +${duck.points} points! It waddles away happily! (${reactionSeconds}s reaction time)`,
|
|
`🌾 *chirp* ${context.nick} gave seeds to the ${duck.name}! +${duck.points} points in ${reactionSeconds} seconds! So wholesome!`,
|
|
`🥖 *happy quack* ${context.nick} shared bread with the ${duck.name}! +${duck.points} points! (${reactionSeconds}s) What a kind soul!`,
|
|
`🍀 *munch munch* ${context.nick} fed the ${duck.name} some greens! +${duck.points} points! Reaction: ${reactionSeconds}s`,
|
|
`🎪 ${context.nick} the duck whisperer! Fed the ${duck.name} perfectly! +${duck.points} points (${reactionSeconds}s)`
|
|
];
|
|
|
|
// Add special messages for rare ducks
|
|
if (duck.rarity === 'legendary' || duck.rarity === 'mythical' || duck.rarity === 'cosmic') {
|
|
bot.say(channel, `🌟 ✨ RARE DUCK FEEDING! ✨ 🌟`);
|
|
}
|
|
|
|
bot.say(channel, this.randomChoice(successMessages));
|
|
|
|
// Extra flair for special achievements
|
|
if (reactionTime < 1000) {
|
|
bot.say(channel, '⚡ LIGHTNING FAST FEEDING! That was under 1 second!');
|
|
} else if (duck.points >= 10) {
|
|
bot.say(channel, `💎 LEGENDARY FEEDING! You fed a ${duck.rarity} duck!`);
|
|
}
|
|
|
|
console.log(`🦆 ${context.nick} fed ${duck.name} in ${channel} (${reactionSeconds}s)`);
|
|
|
|
// Schedule next duck
|
|
this.scheduleNextDuck(channel, bot);
|
|
},
|
|
|
|
getRandomDuck() {
|
|
const rand = Math.random();
|
|
|
|
// Rare duck chances (very low)
|
|
if (rand < 0.005) { // 0.5% chance for cosmic
|
|
return this.rareDucks[2]; // Cosmic Duck
|
|
} else if (rand < 0.015) { // 1% more for mythical
|
|
return this.rareDucks[1]; // Diamond Duck
|
|
} else if (rand < 0.04) { // 2.5% more for legendary
|
|
return this.rareDucks[0]; // Golden Duck
|
|
}
|
|
|
|
// Regular ducks with weighted probabilities
|
|
const weights = [40, 25, 15, 8, 10, 25, 15]; // Common to rare
|
|
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
const randomWeight = Math.random() * totalWeight;
|
|
|
|
let currentWeight = 0;
|
|
for (let i = 0; i < this.duckTypes.length; i++) {
|
|
currentWeight += weights[i];
|
|
if (randomWeight <= currentWeight) {
|
|
return this.duckTypes[i];
|
|
}
|
|
}
|
|
|
|
return this.duckTypes[0]; // Fallback to mallard
|
|
},
|
|
|
|
showLeaderboard(context, bot) {
|
|
const scores = Array.from(this.gameState.scores.entries())
|
|
.sort((a, b) => this.getTotalPoints(b[1]) - this.getTotalPoints(a[1]))
|
|
.slice(0, 10);
|
|
|
|
if (scores.length === 0) {
|
|
bot.say(context.replyTo, '🦆 No one has shot any ducks yet! Get hunting!');
|
|
return;
|
|
}
|
|
|
|
bot.say(context.replyTo, '🏆 TOP DUCK HUNTERS (by points) 🏆');
|
|
scores.forEach((score, index) => {
|
|
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎯';
|
|
const points = this.getTotalPoints(score[1]);
|
|
bot.say(context.replyTo, `${medal} ${index + 1}. ${score[0]}: ${points} points`);
|
|
});
|
|
},
|
|
|
|
showDuckCount(context, bot) {
|
|
// Filter out migrated users with 0 duck count for this leaderboard
|
|
const scores = Array.from(this.gameState.scores.entries())
|
|
.filter(([nick, data]) => {
|
|
const ducks = this.getTotalDucks(data);
|
|
return ducks > 0 || !data.migrated; // Include non-migrated users even with 0 ducks
|
|
})
|
|
.sort((a, b) => this.getTotalDucks(b[1]) - this.getTotalDucks(a[1]))
|
|
.slice(0, 10);
|
|
|
|
if (scores.length === 0) {
|
|
bot.say(context.replyTo, '🦆 No one has shot any tracked ducks yet! Get hunting!');
|
|
bot.say(context.replyTo, '📝 Note: Duck counts are tracked from new shots only (not migrated data)');
|
|
return;
|
|
}
|
|
|
|
bot.say(context.replyTo, '🦆 TOP DUCK COUNTERS (by quantity) 🦆');
|
|
scores.forEach((score, index) => {
|
|
const medal = index === 0 ? '🥇' : index === 1 ? '🥈' : index === 2 ? '🥉' : '🎯';
|
|
const ducks = this.getTotalDucks(score[1]);
|
|
bot.say(context.replyTo, `${medal} ${index + 1}. ${score[0]}: ${ducks} ducks`);
|
|
});
|
|
},
|
|
|
|
showStats(context, bot) {
|
|
const totalDucks = Array.from(this.gameState.scores.values()).reduce((a, b) => a + this.getTotalDucks(b), 0);
|
|
const totalPoints = Array.from(this.gameState.scores.values()).reduce((a, b) => a + this.getTotalPoints(b), 0);
|
|
const totalHunters = this.gameState.scores.size;
|
|
const activeDucks = this.gameState.activeDucks.size;
|
|
|
|
// Calculate most shot duck types
|
|
const duckTypeStats = {};
|
|
this.gameState.scores.forEach(playerData => {
|
|
if (typeof playerData === 'object' && playerData.duckTypes) {
|
|
Object.entries(playerData.duckTypes).forEach(([duckType, count]) => {
|
|
duckTypeStats[duckType] = (duckTypeStats[duckType] || 0) + count;
|
|
});
|
|
}
|
|
});
|
|
|
|
const sortedDuckTypes = Object.entries(duckTypeStats)
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 5);
|
|
|
|
bot.say(context.replyTo, `🦆 DUCK HUNT STATISTICS 🦆`);
|
|
bot.say(context.replyTo, `🎯 Total ducks shot: ${totalDucks}`);
|
|
bot.say(context.replyTo, `🏆 Total points earned: ${totalPoints}`);
|
|
bot.say(context.replyTo, `🏹 Active hunters: ${totalHunters}`);
|
|
bot.say(context.replyTo, `🦆 Ducks currently in channels: ${activeDucks}`);
|
|
|
|
if (sortedDuckTypes.length > 0) {
|
|
bot.say(context.replyTo, `📊 Most hunted: ${sortedDuckTypes[0][0]} (${sortedDuckTypes[0][1]} times)`);
|
|
if (sortedDuckTypes.length > 1) {
|
|
bot.say(context.replyTo, `🥈 Second most: ${sortedDuckTypes[1][0]} (${sortedDuckTypes[1][1]} times)`);
|
|
}
|
|
}
|
|
},
|
|
|
|
startDuckSpawning(bot) {
|
|
// Check for inactive channels every 5 minutes and clean up
|
|
setInterval(() => {
|
|
const now = Date.now();
|
|
const thirtyMinutesAgo = now - (30 * 60 * 1000);
|
|
|
|
this.gameState.lastActivity.forEach((lastActivity, channel) => {
|
|
if (lastActivity < thirtyMinutesAgo) {
|
|
// Channel is inactive, clear any timers
|
|
const timer = this.gameState.duckTimers.get(channel);
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
this.gameState.duckTimers.delete(channel);
|
|
console.log(`🦆 Cleared inactive duck timer for ${channel}`);
|
|
}
|
|
}
|
|
});
|
|
}, 5 * 60 * 1000); // Every 5 minutes
|
|
},
|
|
|
|
randomChoice(array) {
|
|
return array[Math.floor(Math.random() * array.length)];
|
|
},
|
|
|
|
isDuckHuntEnabled(channel) {
|
|
// Default to enabled if not explicitly set
|
|
return this.gameState.enabledChannels.get(channel) !== false;
|
|
},
|
|
|
|
stopDucks(context, bot) {
|
|
// Only allow this command from #bakedbeans channel
|
|
if (context.channel !== '#bakedbeans') {
|
|
bot.say(context.replyTo, '🦆 This command can only be used in #bakedbeans!');
|
|
return;
|
|
}
|
|
|
|
const channel = context.channel;
|
|
|
|
// Set channel as disabled
|
|
this.gameState.enabledChannels.set(channel, false);
|
|
|
|
// Clear any existing timer
|
|
const timer = this.gameState.duckTimers.get(channel);
|
|
if (timer) {
|
|
clearTimeout(timer);
|
|
this.gameState.duckTimers.delete(channel);
|
|
}
|
|
|
|
// Remove any active duck
|
|
const activeDuck = this.gameState.activeDucks.get(channel);
|
|
if (activeDuck) {
|
|
clearTimeout(activeDuck.timer);
|
|
this.gameState.activeDucks.delete(channel);
|
|
}
|
|
|
|
bot.say(context.replyTo, '🦆 Duck hunt disabled in this channel! Use !startducks to resume.');
|
|
console.log(`🦆 Duck hunt disabled in ${channel} by ${context.nick}`);
|
|
},
|
|
|
|
startDucks(context, bot) {
|
|
// Only allow this command from #bakedbeans channel
|
|
if (context.channel !== '#bakedbeans') {
|
|
bot.say(context.replyTo, '🦆 This command can only be used in #bakedbeans!');
|
|
return;
|
|
}
|
|
|
|
const channel = context.channel;
|
|
|
|
// Set channel as enabled
|
|
this.gameState.enabledChannels.set(channel, true);
|
|
|
|
bot.say(context.replyTo, '🦆 Duck hunt resumed in this channel! Ducks will start spawning again.');
|
|
console.log(`🦆 Duck hunt enabled in ${channel} by ${context.nick}`);
|
|
|
|
// Schedule the next duck
|
|
this.scheduleNextDuck(channel, bot);
|
|
},
|
|
|
|
// Load scores from file
|
|
loadScores() {
|
|
try {
|
|
if (fs.existsSync(this.scoresFile)) {
|
|
const data = fs.readFileSync(this.scoresFile, 'utf8');
|
|
const scoresObject = JSON.parse(data);
|
|
|
|
// Convert back to Map and migrate old format if needed
|
|
this.gameState.scores = new Map();
|
|
let migrationNeeded = false;
|
|
|
|
for (const [nick, scoreData] of Object.entries(scoresObject)) {
|
|
// Check if this is old format (just a number) or new format (object)
|
|
if (typeof scoreData === 'number') {
|
|
// Old format: migrate to new structure
|
|
this.gameState.scores.set(nick, {
|
|
totalPoints: scoreData,
|
|
totalDucks: 0, // We don't know the real duck count, start fresh
|
|
duckTypes: {},
|
|
migrated: true // Flag to indicate this is migrated data
|
|
});
|
|
migrationNeeded = true;
|
|
} else {
|
|
// New format: use as-is
|
|
this.gameState.scores.set(nick, scoreData);
|
|
}
|
|
}
|
|
|
|
console.log(`🦆 Loaded ${this.gameState.scores.size} duck hunter scores from ${this.scoresFile}`);
|
|
|
|
if (migrationNeeded) {
|
|
console.log('📈 Migrated old score format to new detailed tracking');
|
|
this.saveScores(); // Save migrated data
|
|
}
|
|
|
|
// Log top 3 hunters if any exist
|
|
if (this.gameState.scores.size > 0) {
|
|
const topHunters = Array.from(this.gameState.scores.entries())
|
|
.sort((a, b) => this.getTotalPoints(b[1]) - this.getTotalPoints(a[1]))
|
|
.slice(0, 3);
|
|
console.log('🏆 Top duck hunters:', topHunters.map(([name, data]) => `${name}(${this.getTotalPoints(data)}pts)`).join(', '));
|
|
}
|
|
} else {
|
|
console.log(`🦆 No existing duck hunt scores file found, starting fresh`);
|
|
}
|
|
} catch (error) {
|
|
console.error(`❌ Error loading duck hunt scores:`, error);
|
|
this.gameState.scores = new Map(); // Reset to empty if load fails
|
|
}
|
|
},
|
|
|
|
// Save scores to file
|
|
saveScores() {
|
|
try {
|
|
// Convert Map to plain object for JSON serialization
|
|
const scoresObject = Object.fromEntries(this.gameState.scores);
|
|
const data = JSON.stringify(scoresObject, null, 2);
|
|
|
|
fs.writeFileSync(this.scoresFile, data, 'utf8');
|
|
console.log(`💾 Saved ${this.gameState.scores.size} duck hunter scores to ${this.scoresFile}`);
|
|
} catch (error) {
|
|
console.error(`❌ Error saving duck hunt scores:`, error);
|
|
}
|
|
},
|
|
|
|
// Helper functions for new scoring system
|
|
getTotalPoints(playerData) {
|
|
if (typeof playerData === 'number') {
|
|
return playerData; // Old format compatibility
|
|
}
|
|
return playerData.totalPoints || 0;
|
|
},
|
|
|
|
getTotalDucks(playerData) {
|
|
if (typeof playerData === 'number') {
|
|
return playerData; // Old format estimate
|
|
}
|
|
return playerData.totalDucks || 0;
|
|
},
|
|
|
|
initializePlayerData(nick) {
|
|
if (!this.gameState.scores.has(nick)) {
|
|
this.gameState.scores.set(nick, {
|
|
totalPoints: 0,
|
|
totalDucks: 0,
|
|
duckTypes: {},
|
|
migrated: false // New players have accurate data from the start
|
|
});
|
|
}
|
|
},
|
|
|
|
updatePlayerScore(nick, duckName, points) {
|
|
this.initializePlayerData(nick);
|
|
const playerData = this.gameState.scores.get(nick);
|
|
|
|
// Update totals
|
|
playerData.totalPoints += points;
|
|
playerData.totalDucks += 1;
|
|
|
|
// Update duck type count
|
|
if (!playerData.duckTypes[duckName]) {
|
|
playerData.duckTypes[duckName] = 0;
|
|
}
|
|
playerData.duckTypes[duckName] += 1;
|
|
|
|
this.gameState.scores.set(nick, playerData);
|
|
}
|
|
}; |