// 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); } };