// plugins/zombiedice.js - Zombie Dice multiplayer game plugin const fs = require('fs'); const path = require('path'); module.exports = { init(bot) { console.log('Zombie Dice plugin initialized'); this.bot = bot; // Persistent player statistics (saved to file) this.playerStats = new Map(); // nick -> { totalBrains, bestGame, totalGames, totalTurns, name } // Current game scores (reset each game) this.gameScores = new Map(); // nick -> { brains, eliminated } // Game state with multiplayer support this.gameState = { phase: 'idle', // 'idle', 'joining', 'active' players: [], joinTimer: null, startTime: null, currentPlayer: 0, channel: null, joinTimeLimit: 30000, // 30 seconds warningTime: 25000, // Warning at 25 seconds (5 sec left) // Current turn state turnBrains: 0, turnShotguns: 0, footstepsDice: [], // Actual dice objects that rolled footsteps diceCup: [] }; // Zombie dice definitions this.diceTypes = { green: { count: 6, faces: ['🧠', '🧠', '🧠', '👣', '👣', '💥'], color: 'Green' }, yellow: { count: 4, faces: ['🧠', '🧠', '👣', '👣', '👣', '💥'], color: 'Yellow' }, red: { count: 3, faces: ['🧠', '👣', '👣', '💥', '💥', '💥'], color: 'Red' } }; // Set up score file path this.scoresFile = path.join(__dirname, 'zombiedice_scores.json'); // Load existing scores this.loadScores(); }, cleanup(bot) { console.log('Zombie Dice plugin cleaned up'); // Clear any timers if (this.gameState.joinTimer) { clearTimeout(this.gameState.joinTimer); } // Save scores before cleanup this.saveScores(); // Clear any active games on cleanup this.resetGameState(); // Clear game scores if (this.gameScores) { this.gameScores.clear(); } }, // Reset game state to idle resetGameState() { if (this.gameState.joinTimer) { clearTimeout(this.gameState.joinTimer); } this.gameState = { phase: 'idle', players: [], joinTimer: null, startTime: null, currentPlayer: 0, channel: null, joinTimeLimit: 30000, warningTime: 25000, turnBrains: 0, turnShotguns: 0, footstepsDice: [], diceCup: [] }; }, // 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 this.playerStats = new Map(Object.entries(scoresObject)); console.log(`🧟 Loaded ${this.playerStats.size} zombie dice player stats from ${this.scoresFile}`); // Log top 3 players if any exist if (this.playerStats.size > 0) { const topPlayers = Array.from(this.playerStats.values()) .sort((a, b) => b.totalBrains - a.totalBrains) .slice(0, 3); console.log('🏆 Top zombie players:', topPlayers.map(p => `${p.name}(${p.totalBrains})`).join(', ')); } } else { console.log(`🧟 No existing zombie dice scores file found, starting fresh`); } } catch (error) { console.error(`❌ Error loading zombie dice scores:`, error); this.playerStats = 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.playerStats); const data = JSON.stringify(scoresObject, null, 2); fs.writeFileSync(this.scoresFile, data, 'utf8'); console.log(`💾 Saved ${this.playerStats.size} zombie dice player stats to ${this.scoresFile}`); } catch (error) { console.error(`❌ Error saving zombie dice scores:`, error); } }, // Update a player's persistent stats and save to file updatePlayerStats(nick, updates) { // Ensure playerStats Map exists if (!this.playerStats) { this.playerStats = new Map(); } if (!this.playerStats.has(nick)) { this.playerStats.set(nick, { totalBrains: 0, bestGame: 0, totalGames: 0, totalTurns: 0, name: nick }); } const player = this.playerStats.get(nick); Object.assign(player, updates); // Save to file after each update this.saveScores(); }, // Initialize player for current game initGamePlayer(nick) { // Ensure gameScores Map exists if (!this.gameScores) { this.gameScores = new Map(); } this.gameScores.set(nick, { brains: 0, eliminated: false }); // Initialize persistent stats if new player if (!this.playerStats.has(nick)) { this.updatePlayerStats(nick, { totalBrains: 0, bestGame: 0, totalGames: 0, totalTurns: 0, name: nick }); } }, // Start the join timer startJoinTimer(target) { this.gameState.startTime = Date.now(); // Set warning timer (25 seconds) setTimeout(() => { if (this.gameState.phase === 'joining') { this.bot.say(target, '⏰ 5 seconds left to join the zombie hunt!'); } }, this.gameState.warningTime); // Set game start timer (30 seconds) this.gameState.joinTimer = setTimeout(() => { this.startGame(target); }, this.gameState.joinTimeLimit); }, // Start the actual game startGame(target) { if (this.gameState.players.length < 2) { this.bot.say(target, '😞 Nobody else joined the zombie hunt. Game cancelled.'); this.resetGameState(); this.gameScores.clear(); return; } // Initialize all players this.gameState.players.forEach(nick => { this.initGamePlayer(nick); }); // Initialize dice cup this.initializeDiceCup(); // Start the game this.gameState.phase = 'active'; this.gameState.currentPlayer = 0; const playerList = this.gameState.players.join(' vs '); this.bot.say(target, `🧟 Zombie hunt starting! ${playerList} - ${this.gameState.players[0]} goes first!`); this.bot.say(target, `🎯 Goal: Collect 13 brains to win! But 3 shotguns = bust!`); }, // Initialize the dice cup initializeDiceCup() { this.gameState.diceCup = []; // Add all dice to cup Object.entries(this.diceTypes).forEach(([type, data]) => { for (let i = 0; i < data.count; i++) { this.gameState.diceCup.push({ type, faces: data.faces, color: data.color }); } }); // Shuffle the cup this.shuffleDiceCup(); }, // Shuffle dice cup shuffleDiceCup() { for (let i = this.gameState.diceCup.length - 1; i > 0; i--) { const j = Math.floor(Math.random() * (i + 1)); [this.gameState.diceCup[i], this.gameState.diceCup[j]] = [this.gameState.diceCup[j], this.gameState.diceCup[i]]; } }, // Draw dice from cup drawDice(count) { const drawn = []; for (let i = 0; i < count && this.gameState.diceCup.length > 0; i++) { drawn.push(this.gameState.diceCup.pop()); } return drawn; }, // Roll dice and return results rollDice(dice) { return dice.map(die => { const face = die.faces[Math.floor(Math.random() * die.faces.length)]; return { ...die, result: face }; }); }, // Start a new turn startTurn(target, playerName) { this.gameState.turnBrains = 0; this.gameState.turnShotguns = 0; this.gameState.footstepsDice = []; // If cup is low, refill and shuffle if (this.gameState.diceCup.length < 3) { this.initializeDiceCup(); } // Draw 3 dice and roll for first roll of turn const dice = this.drawDice(3); const results = this.rollDice(dice); this.processRoll(target, playerName, results, true); // true = first roll }, // Process dice roll results processRoll(target, playerName, results, isFirstRoll = false) { let brains = 0; let shotguns = 0; let newFootstepsDice = []; // Count results and track footsteps dice results.forEach(die => { switch (die.result) { case '🧠': brains++; break; case '💥': shotguns++; break; case '👣': newFootstepsDice.push(die); // Keep the actual die object break; } }); // Update turn totals this.gameState.turnBrains += brains; this.gameState.turnShotguns += shotguns; this.gameState.footstepsDice = newFootstepsDice; // Replace with new footsteps dice // Show roll results with colored dice - IRC color codes: Green=3, Yellow=8, Red=4 const rollDisplay = results.map(die => { let colorCode; switch (die.color) { case 'Green': colorCode = '\x0303'; break; // Green text case 'Yellow': colorCode = '\x0308'; break; // Yellow text case 'Red': colorCode = '\x0304'; break; // Red text default: colorCode = ''; } return `${die.result}${colorCode}(${die.color.charAt(0)})\x0F`; // \x0F resets color }).join(' '); // Send detailed results to player let detailMsg = `Roll: ${rollDisplay} | Brains: ${this.gameState.turnBrains} | Shotguns: ${this.gameState.turnShotguns}`; if (this.gameState.footstepsDice.length > 0) { const footstepsDisplay = this.gameState.footstepsDice.map(die => { let colorCode; switch (die.color) { case 'Green': colorCode = '\x0303'; break; case 'Yellow': colorCode = '\x0308'; break; case 'Red': colorCode = '\x0304'; break; default: colorCode = ''; } return `${colorCode}👣(${die.color.charAt(0)})\x0F`; }).join(' '); detailMsg += ` | Holding: ${footstepsDisplay}`; } this.bot.notice(playerName, detailMsg); // Check for bust (3+ shotguns) if (this.gameState.turnShotguns >= 3) { this.bot.say(target, `${playerName}: 💥💥💥 SHOTGUNNED! Lost ${this.gameState.turnBrains} brains!`); this.nextTurn(target); return; } // Check if can continue const totalDiceAvailable = this.gameState.footstepsDice.length + this.gameState.diceCup.length; if (totalDiceAvailable < 3) { // Not enough dice to make 3, must stop this.bot.say(target, `${playerName}: Not enough dice left! Auto-banking ${this.gameState.turnBrains} brains.`); this.bankBrains(target, playerName); return; } if (this.gameState.turnBrains === 0) { this.bot.say(target, `${playerName}: No brains yet - use !zombie to keep rolling!`); } else { this.bot.say(target, `${playerName}: Use !zombie to keep rolling or !brain to bank your ${this.gameState.turnBrains} brains!`); } }, // Bank brains and end turn bankBrains(target, playerName) { const playerGame = this.gameScores.get(playerName); playerGame.brains += this.gameState.turnBrains; this.bot.say(target, `${playerName}: Banked ${this.gameState.turnBrains} brains! Total: ${playerGame.brains}`); // Check for win if (playerGame.brains >= 13) { this.bot.say(target, `🧟‍♂️ ${playerName} WINS with ${playerGame.brains} brains! 🧟‍♂️`); this.endGame(target); return; } this.nextTurn(target); }, // Move to next player's turn nextTurn(target) { // Update turn stats for current player const currentPlayerName = this.gameState.players[this.gameState.currentPlayer]; const playerStats = this.playerStats.get(currentPlayerName); this.updatePlayerStats(currentPlayerName, { totalTurns: playerStats.totalTurns + 1 }); // IMPORTANT: Reset all turn state before moving to next player this.gameState.turnBrains = 0; this.gameState.turnShotguns = 0; this.gameState.footstepsDice = []; // Move to next player this.gameState.currentPlayer = (this.gameState.currentPlayer + 1) % this.gameState.players.length; const nextPlayer = this.gameState.players[this.gameState.currentPlayer]; this.bot.say(target, `🧟 ${nextPlayer}'s turn to hunt zombies!`); this.startTurn(target, nextPlayer); }, // End the game endGame(target) { // Ensure gameScores exists before processing if (!this.gameScores) { this.gameScores = new Map(); } // Save final scores to persistent stats this.gameState.players.forEach(playerName => { const gameScore = this.gameScores.get(playerName); const playerStats = this.playerStats.get(playerName); if (gameScore && playerStats) { const updates = { totalBrains: playerStats.totalBrains + gameScore.brains, totalGames: playerStats.totalGames + 1 }; // Update best game if this was better if (gameScore.brains > playerStats.bestGame) { updates.bestGame = gameScore.brains; this.bot.say(target, `🏆 ${playerName} set a new personal best: ${gameScore.brains} brains!`); } this.updatePlayerStats(playerName, updates); console.log(`🧟 Saved game stats for ${playerName}: brains=${gameScore.brains}`); } }); // Clear current game data this.gameScores.clear(); this.bot.say(target, '🏁 Zombie hunt ended! Use !zombie to start new hunt.'); this.resetGameState(); }, commands: [ { name: 'zombie', description: 'Start/join zombie dice game or roll dice during play', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; const to = context.channel || context.replyTo; // Only allow channel play if (!context.channel) { bot.say(target, 'Zombie dice games can only be played in channels!'); return; } // Handle different game phases switch (plugin.gameState.phase) { case 'idle': // Start new game and join timer plugin.gameState.phase = 'joining'; plugin.gameState.players = [from]; plugin.gameState.channel = to; bot.say(target, `🧟 ${from} started a zombie hunt! Others have 30 seconds to join with !zombie`); plugin.startJoinTimer(target); break; case 'joining': // Player wants to join during countdown if (plugin.gameState.players.includes(from)) { bot.say(target, `${from}: You're already in the hunt!`); return; } if (plugin.gameState.players.length >= 6) { // Max 6 players bot.say(target, `${from}: Hunt is full! (Max 6 players)`); return; } plugin.gameState.players.push(from); bot.say(target, `🧟 ${from} joined the hunt! (${plugin.gameState.players.length} players)`); break; case 'active': // Game is running - handle continue rolling if (!plugin.gameState.players.includes(from)) { bot.say(target, `${from}: You're not in the current hunt!`); return; } const currentPlayerName = plugin.gameState.players[plugin.gameState.currentPlayer]; if (from !== currentPlayerName) { bot.say(target, `${from}: It's ${currentPlayerName}'s turn!`); return; } // Continue rolling - re-roll footsteps dice + draw new ones to make 3 total const footstepsCount = plugin.gameState.footstepsDice.length; const neededDice = 3 - footstepsCount; // Check if we have enough dice if (neededDice > plugin.gameState.diceCup.length) { bot.say(target, `${from}: Not enough dice left to continue! Auto-banking.`); plugin.bankBrains(target, from); return; } // Draw new dice and combine with footsteps const newDice = plugin.drawDice(neededDice); const allDice = [...plugin.gameState.footstepsDice, ...newDice]; // Roll all 3 dice (footsteps + new) const results = plugin.rollDice(allDice); plugin.processRoll(target, from, results, false); // false = not first roll break; } } }, { name: 'brain', description: 'Bank your brains and end your turn in zombie dice', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; if (plugin.gameState.phase !== 'active' || !plugin.gameState.players.includes(from)) { bot.say(target, `${from}: You're not in an active zombie hunt!`); return; } const currentPlayerName = plugin.gameState.players[plugin.gameState.currentPlayer]; if (from !== currentPlayerName) { bot.say(target, `${from}: It's ${currentPlayerName}'s turn!`); return; } if (plugin.gameState.turnBrains === 0) { bot.say(target, `${from}: No brains to bank!`); return; } plugin.bankBrains(target, from); } }, { name: 'quitzombie', description: 'Quit the current zombie dice game', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; if (plugin.gameState.phase === 'idle') { bot.say(target, `${from}: No zombie hunt to quit!`); return; } if (!plugin.gameState.players.includes(from)) { bot.say(target, `${from}: You're not in the current hunt!`); return; } // Remove player from game plugin.gameState.players = plugin.gameState.players.filter(p => p !== from); bot.say(target, `${from} fled the zombie hunt!`); // Check if game should continue if (plugin.gameState.phase === 'joining') { if (plugin.gameState.players.length === 0) { bot.say(target, 'Hunt cancelled - no survivors left.'); plugin.resetGameState(); } } else if (plugin.gameState.phase === 'active') { if (plugin.gameState.players.length <= 1) { if (plugin.gameState.players.length === 1) { bot.say(target, `${plugin.gameState.players[0]} wins by survival!`); } plugin.endGame(target); } else { // Adjust current player if needed if (plugin.gameState.currentPlayer >= plugin.gameState.players.length) { plugin.gameState.currentPlayer = 0; } plugin.nextTurn(target); } } } }, { name: 'topzombie', description: 'Show top zombie dice players', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; if (plugin.playerStats.size === 0) { bot.say(target, 'No zombie dice scores recorded yet! Use !zombie to start hunting.'); return; } // Convert to array and sort by total brains const sortedScores = Array.from(plugin.playerStats.values()) .sort((a, b) => b.totalBrains - a.totalBrains) .slice(0, 5); // Top 5 bot.say(target, '🧟‍♂️ Top Zombie Hunters:'); sortedScores.forEach((player, index) => { const rank = index + 1; const trophy = rank === 1 ? '🥇' : rank === 2 ? '🥈' : rank === 3 ? '🥉' : `${rank}.`; bot.say(target, `${trophy} ${player.name}: ${player.totalBrains} brains (best: ${player.bestGame}, ${player.totalGames} hunts)`); }); } }, { name: 'zombiestatus', description: 'Show current zombie dice game status', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; if (plugin.gameState.phase === 'idle') { bot.say(target, 'No active zombie hunt. Use !zombie to start hunting!'); return; } if (plugin.gameState.phase === 'joining') { const timeLeft = Math.ceil((plugin.gameState.joinTimeLimit - (Date.now() - plugin.gameState.startTime)) / 1000); bot.say(target, `🧟 Joining phase: ${plugin.gameState.players.join(', ')} (${timeLeft}s left)`); return; } if (plugin.gameState.phase === 'active') { const currentPlayer = plugin.gameState.players[plugin.gameState.currentPlayer]; let statusMsg = `🧟‍♂️ Active hunt: `; plugin.gameState.players.forEach((player, i) => { const gameScore = plugin.gameScores.get(player); if (i > 0) statusMsg += ' vs '; statusMsg += `${player}(${gameScore.brains})`; }); bot.say(target, statusMsg); let turnMsg = `🎲 ${currentPlayer}'s turn | Turn brains: ${plugin.gameState.turnBrains} | Shotguns: ${plugin.gameState.turnShotguns}/3`; if (plugin.gameState.footstepsDice.length > 0) { const footstepsDisplay = plugin.gameState.footstepsDice.map(die => { let colorCode; switch (die.color) { case 'Green': colorCode = '\x0303'; break; case 'Yellow': colorCode = '\x0308'; break; case 'Red': colorCode = '\x0304'; break; default: colorCode = ''; } return `${colorCode}👣(${die.color.charAt(0)})\x0F`; }).join(' '); turnMsg += ` | Holding: ${footstepsDisplay}`; } bot.say(target, turnMsg); } } }, { name: 'resetzombie', description: 'Reset all zombie dice scores (admin command)', execute: function(context, bot) { const plugin = module.exports; const target = context.replyTo; const from = context.nick; // Simple admin check const adminNicks = ['admin', 'owner', 'cancerbot', 'megasconed']; if (!adminNicks.includes(from)) { bot.say(target, `${from}: Access denied - admin only command`); return; } const playerCount = plugin.playerStats.size; plugin.playerStats.clear(); plugin.saveScores(); bot.say(target, `🗑️ ${from}: Reset ${playerCount} zombie dice player stats`); } } ] };