commit a3ed25f8ddae3edf7c05004f01ac4e657762326a Author: megaproxy Date: Thu Jul 17 19:03:45 2025 +0000 Initial commit: Enhanced IRC bot with duck hunt game - 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 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e8736c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ + +# Environment variables +.env +.env.local +.env.development.local +.env.test.local +.env.production.local + +# Logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Database files +*.db +*.db-journal + +# OS generated files +.DS_Store +.DS_Store? +._* +.Spotlight-V100 +.Trashes +ehthumbs.db +Thumbs.db + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# Temporary files +.tmp/ +temp/ \ No newline at end of file diff --git a/CONVERSATION_SUMMARY.md b/CONVERSATION_SUMMARY.md new file mode 100644 index 0000000..663548a --- /dev/null +++ b/CONVERSATION_SUMMARY.md @@ -0,0 +1,129 @@ +# Conversation Summary - Duck Hunt Enhancements + +## Session Overview +Date: July 17, 2025 +Task: Enhanced the duck hunt plugin with hunt/feed mechanics and improved bot structure + +## Key Accomplishments + +### 1. Duck Hunt Plugin Enhancements +- **Added Hunt/Feed System**: 80% hunt ducks (require `!shoot`/`!bang`), 20% feed ducks (require `!feed`) +- **New Commands**: Added `!feed` command alongside existing `!shoot`/`!bang` +- **Enhanced Spawn Messages**: Different messages for hunt vs feed ducks +- **Wrong Action Penalties**: Shooting feed ducks or feeding hunt ducks causes them to flee +- **Persistent Scores**: Implemented file-based score storage (`duck_hunt_scores.json`) +- **Channel Control**: Added `!stopducks`/`!startducks` commands (bakedbeans only) + +### 2. Bot Structure Improvements +- **Identified Main Bot**: Determined `debug_bot.js` was the working production bot +- **File Reorganization**: Renamed `debug_bot.js` โ†’ `botmain.js` for clarity +- **Cleanup**: Removed Windows Zone.Identifier files and outdated versions +- **Documentation**: Created comprehensive README.md + +## Technical Implementation Details + +### Duck Hunt Mechanics +```javascript +// Duck type assignment (80/20 split) +const isHuntDuck = Math.random() < 0.8; + +// Stored in activeDucks with type flag +this.gameState.activeDucks.set(channel, { + duck: duck, + spawnTime: spawnTime, + isHuntDuck: isHuntDuck, + timer: setTimeout(...) +}); +``` + +### Spawn Messages +- **Hunt Ducks**: "Quick, !shoot it!", "Someone !shoot it before it escapes!" +- **Feed Ducks**: "!feed it some bread!", "It looks hungry, !feed it!" + +### Wrong Action Handling +- Shooting feed ducks: "You scared away the gentle duck!" +- Feeding hunt ducks: "The wild duck ignores your food and flies away!" + +### Persistent Storage +- Follows same pattern as other plugins (coinflip, slots, etc.) +- Saves/loads from `plugins/duck_hunt_scores.json` +- Auto-saves after each successful hunt/feed + +## File Structure After Changes + +``` +cancerbot/ +โ”œโ”€โ”€ botmain.js # Main bot (was debug_bot.js) +โ”œโ”€โ”€ complete_irc_bot.js # Alternative bot +โ”œโ”€โ”€ bot_template.js # Template (was botmain.js) +โ”œโ”€โ”€ plugin_template.js # Plugin template +โ”œโ”€โ”€ plugins/ +โ”‚ โ”œโ”€โ”€ duck_hunt_plugin.js # Enhanced with hunt/feed +โ”‚ โ”œโ”€โ”€ duck_hunt_scores.json # New persistent scores +โ”‚ โ””โ”€โ”€ [17 other plugins...] +โ””โ”€โ”€ README.md # Comprehensive documentation +``` + +## Bot Configuration +- **Server**: irc.libera.chat:6667 +- **Nickname**: cancerbot +- **Channel**: #bakedbeans +- **Main File**: `botmain.js` +- **Features**: Verbose logging, plugin hot-reload, admin commands + +## Commands Added/Modified + +### New Commands +- `!feed` - Feed hungry ducks for points +- `!stopducks` - Stop duck spawning (bakedbeans only) +- `!startducks` - Resume duck spawning (bakedbeans only) + +### Enhanced Commands +- `!shoot`/`!bang` - Now handle wrong action penalties +- `!topducks` - Now shows persistent scores +- `!duckscore` - Now shows persistent scores + +## Key Code Changes + +### 1. Duck Type Assignment (spawnDuck function) +- Added `isHuntDuck` boolean flag +- Conditional spawn messages based on type +- Stored duck type in gameState + +### 2. Shoot Function Enhancement +- Added wrong action detection for feed ducks +- Penalty messages and duck removal +- Maintained existing success logic + +### 3. New Feed Function +- Mirror of shoot function for feed ducks +- Wrong action detection for hunt ducks +- Same scoring system as hunting + +### 4. Persistent Storage +- Added `fs` requirement +- `loadScores()` and `saveScores()` methods +- Auto-save after each successful action + +## Testing Status +- โœ… Plugin loads successfully (17 plugins total) +- โœ… All commands registered properly +- โœ… Bot connects to IRC and joins #bakedbeans +- โœ… Persistent scores working (creates JSON file) +- โœ… Verbose logging functional + +## Next Steps for Future Development +1. Test hunt/feed mechanics in live environment +2. Consider adding statistics tracking (hunts vs feeds) +3. Potential seasonal duck types or events +4. Balance tweaking if needed (80/20 ratio) + +## Important Notes +- **Main Bot File**: Always use `botmain.js` to run the bot +- **Admin Access**: `!reloadplugins` works for user 'megasconed' +- **Channel Restriction**: `!stopducks`/`!startducks` only work in #bakedbeans +- **Scoring**: Hunt and feed actions award identical points (keeps it simple) + +--- + +*Conversation saved before holiday break. Bot ready for production use.* \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..ad87a03 --- /dev/null +++ b/README.md @@ -0,0 +1,135 @@ +# CancerBot - IRC Bot with Plugin System + +An IRC bot for #bakedbeans on irc.libera.chat with a comprehensive plugin system and various games. + +## Quick Start + +```bash +# Install dependencies +npm install + +# Run the main bot +node botmain.js +``` + +## Bot Configuration + +- **Server**: irc.libera.chat:6667 +- **Nickname**: cancerbot +- **Channel**: #bakedbeans +- **Command Prefix**: ! + +## Main Files + +- `botmain.js` - **Main bot file** (run this one) +- `complete_irc_bot.js` - Alternative bot without debug features +- `bot_template.js` - Template for reference +- `plugin_template.js` - Plugin development template + +## Duck Hunt Plugin (Enhanced) + +The duck hunt game has been enhanced with a hunt/feed system for more engaging gameplay. + +### Game Mechanics +- **80% Hunt Ducks**: Require `!shoot` or `!bang` to hunt +- **20% Feed Ducks**: Require `!feed` to feed +- **Persistent Scores**: Scores saved to `plugins/duck_hunt_scores.json` +- **Reaction Time**: Tracked for both hunting and feeding +- **Wrong Action Penalties**: Using the wrong command makes the duck flee + +### Commands +- `!shoot` / `!bang` - Hunt aggressive ducks +- `!feed` - Feed hungry ducks +- `!duckscore` - Check your score +- `!topducks` - View leaderboard +- `!duckstats` - Show game statistics +- `!stopducks` - Stop duck spawning (bakedbeans only) +- `!startducks` - Resume duck spawning (bakedbeans only) + +### Duck Types +- **Common**: ๐Ÿฆ† Mallard (1pt), ๐Ÿฆข Swan (2pts), ๐Ÿชฟ Goose (2pts) +- **Rare**: ๐Ÿง Penguin (3pts), ๐Ÿฆœ Parrot (3pts), ๐Ÿฆ‰ Owl (4pts), ๐Ÿฆ… Eagle (5pts) +- **Legendary**: ๐Ÿ† Golden Duck (10pts), ๐Ÿ’Ž Diamond Duck (15pts), ๐ŸŒŸ Cosmic Duck (20pts) + +### Spawn Messages +- **Hunt Ducks**: "Quick, !shoot it!", "Someone !shoot it before it escapes!" +- **Feed Ducks**: "!feed it some bread!", "It looks hungry, !feed it!" + +## Other Games/Plugins + +### Casino Games (All with persistent scores) +- **Coin Flip**: `!flip` - Bet on heads/tails +- **Hi-Lo**: `!hilo` - Guess if next card is higher/lower +- **Slots**: `!slots` - Spin the slot machine +- **Scratch Cards**: `!scratch` - Virtual scratch cards + +### Multiplayer Games +- **Pass the Pigs**: `!pigs` - Multiplayer dice game +- **Zombie Dice**: `!zombie` - Multiplayer zombie brain collecting +- **Quiplash**: `!quiplash` - Answer prompts and vote + +### Text/Chat Plugins +- **Babble**: `!babble` - Generate text from learned patterns +- **Story**: `!story` - Collaborative story writing +- **Emojify**: `!emojify` - Convert text to emojis +- **Conspiracy**: `!conspiracy` - Generate conspiracy theories +- **Drunk Historian**: `!history` - Historical "facts" with varying sobriety + +### Utility +- **Basic**: `!ping`, `!help`, `!time` +- **Combo**: Track message combos in channels +- **Bot Reply**: Responds to other bots + +## Admin Commands + +- `!reloadplugins` - Reload all plugins (megasconed only) + +## Plugin Development + +See `plugin_template.js` for creating new plugins. Plugins support: +- Command registration +- Event handling (onMessage, etc.) +- Persistent storage +- Initialization and cleanup + +## Recent Changes + +### Duck Hunt Enhancements (Latest) +- Added hunt/feed mechanic (80% hunt, 20% feed) +- New `!feed` command for feeding ducks +- Wrong action penalties for immersion +- Persistent score storage +- Channel control commands (`!stopducks`/`!startducks`) + +### Bot Structure Cleanup +- Renamed `debug_bot.js` to `botmain.js` as main bot +- Removed Windows Zone.Identifier files +- Organized file structure for clarity + +## Technical Notes + +- Uses Node.js with net module for IRC connection +- SQLite3 for data persistence in some plugins +- Plugin system with hot-reloading capability +- Comprehensive error handling and logging +- Automatic reconnection on disconnect + +## File Structure + +``` +cancerbot/ +โ”œโ”€โ”€ botmain.js # Main bot file +โ”œโ”€โ”€ complete_irc_bot.js # Alternative bot +โ”œโ”€โ”€ bot_template.js # Bot template +โ”œโ”€โ”€ plugin_template.js # Plugin template +โ”œโ”€โ”€ plugins/ # Plugin directory +โ”‚ โ”œโ”€โ”€ duck_hunt_plugin.js +โ”‚ โ”œโ”€โ”€ coinflip_plugin.js +โ”‚ โ”œโ”€โ”€ [other plugins...] +โ”‚ โ”œโ”€โ”€ duck_hunt_scores.json +โ”‚ โ””โ”€โ”€ [other score files...] +โ”œโ”€โ”€ package.json +โ””โ”€โ”€ README.md +``` + +Enjoy your holiday! ๐Ÿฆ†๐Ÿ–๏ธ \ No newline at end of file diff --git a/bot_template.js b/bot_template.js new file mode 100644 index 0000000..37ed755 --- /dev/null +++ b/bot_template.js @@ -0,0 +1,296 @@ +// bot.js - Main IRC Bot with Plugin System +const net = require('net'); +const fs = require('fs'); +const path = require('path'); + +class IRCBot { + constructor(config) { + this.config = { + server: config.server || 'irc.libera.chat', + port: config.port || 6667, + nick: config.nick || 'MyBot', + channels: config.channels || ['#test'], + commandPrefix: config.commandPrefix || '!', + pluginsDir: config.pluginsDir || './plugins' + }; + + this.socket = null; + this.plugins = new Map(); + this.commands = new Map(); + this.connected = false; + + this.loadPlugins(); + } + + connect() { + console.log(`Connecting to ${this.config.server}:${this.config.port}`); + + this.socket = net.createConnection(this.config.port, this.config.server); + + this.socket.on('connect', () => { + console.log('Connected to IRC server'); + this.send(`NICK ${this.config.nick}`); + this.send(`USER ${this.config.nick} 0 * :${this.config.nick}`); + }); + + this.socket.on('data', (data) => { + const lines = data.toString().split('\r\n'); + lines.forEach(line => { + if (line.trim()) { + this.handleMessage(line); + } + }); + }); + + this.socket.on('error', (err) => { + console.error('Socket error:', err); + }); + + this.socket.on('close', () => { + console.log('Connection closed'); + this.connected = false; + // Reconnect after 5 seconds + setTimeout(() => this.connect(), 5000); + }); + } + + send(message) { + if (this.socket && this.socket.writable) { + console.log('โ†’', message); + this.socket.write(message + '\r\n'); + } + } + + handleMessage(line) { + console.log('โ†', line); + + // Handle PING + if (line.startsWith('PING')) { + this.send('PONG ' + line.substring(5)); + return; + } + + // Handle successful connection + if (line.includes('001')) { + this.connected = true; + console.log('Successfully connected and registered'); + // Join channels + this.config.channels.forEach(channel => { + this.send(`JOIN ${channel}`); + }); + return; + } + + // Parse IRC message + const parsed = this.parseMessage(line); + if (parsed) { + this.handleParsedMessage(parsed); + } + } + + parseMessage(line) { + // Basic IRC message parsing + const match = line.match(/^:([^!]+)!([^@]+)@([^\s]+)\s+(\w+)\s+([^:]+):(.*)$/); + if (match) { + return { + nick: match[1], + user: match[2], + host: match[3], + command: match[4], + target: match[5].trim(), + message: match[6] + }; + } + return null; + } + + handleParsedMessage(msg) { + // Handle PRIVMSG (channel messages and private messages) + if (msg.command === 'PRIVMSG') { + const isChannel = msg.target.startsWith('#'); + const replyTo = isChannel ? msg.target : msg.nick; + + // Check if it's a command + if (msg.message.startsWith(this.config.commandPrefix)) { + const commandLine = msg.message.substring(this.config.commandPrefix.length); + const [commandName, ...args] = commandLine.split(' '); + + this.executeCommand(commandName, { + nick: msg.nick, + user: msg.user, + host: msg.host, + channel: isChannel ? msg.target : null, + replyTo: replyTo, + args: args, + fullMessage: msg.message + }); + } + + // Emit message event for plugins + this.emit('message', { + nick: msg.nick, + user: msg.user, + host: msg.host, + target: msg.target, + message: msg.message, + isChannel: isChannel, + replyTo: replyTo + }); + } + } + + executeCommand(commandName, context) { + const command = this.commands.get(commandName); + if (command) { + try { + command.execute(context, this); + } catch (error) { + console.error(`Error executing command ${commandName}:`, error); + this.say(context.replyTo, `Error executing command: ${error.message}`); + } + } + } + + loadPlugins() { + if (!fs.existsSync(this.config.pluginsDir)) { + fs.mkdirSync(this.config.pluginsDir, { recursive: true }); + console.log(`Created plugins directory: ${this.config.pluginsDir}`); + return; + } + + const pluginFiles = fs.readdirSync(this.config.pluginsDir) + .filter(file => file.endsWith('.js')); + + pluginFiles.forEach(file => { + this.loadPlugin(file); + }); + + console.log(`Loaded ${pluginFiles.length} plugins`); + } + + loadPlugin(filename) { + const filePath = path.join(this.config.pluginsDir, filename); + + try { + // Clear require cache to allow reloading + delete require.cache[require.resolve(filePath)]; + + const plugin = require(filePath); + + if (typeof plugin.init === 'function') { + plugin.init(this); + } + + // Register commands + if (plugin.commands) { + plugin.commands.forEach(command => { + this.commands.set(command.name, command); + console.log(`Registered command: ${command.name}`); + }); + } + + this.plugins.set(filename, plugin); + console.log(`Loaded plugin: ${filename}`); + + } catch (error) { + console.error(`Error loading plugin ${filename}:`, error); + } + } + + reloadPlugin(filename) { + const plugin = this.plugins.get(filename); + if (plugin) { + // Unregister old commands + if (plugin.commands) { + plugin.commands.forEach(command => { + this.commands.delete(command.name); + }); + } + + // Call cleanup if available + if (typeof plugin.cleanup === 'function') { + plugin.cleanup(this); + } + + this.plugins.delete(filename); + } + + // Load the plugin again + this.loadPlugin(filename); + } + + reloadAllPlugins() { + // Clear all plugins and commands + this.plugins.forEach((plugin, filename) => { + if (typeof plugin.cleanup === 'function') { + plugin.cleanup(this); + } + }); + + this.plugins.clear(); + this.commands.clear(); + + // Reload all plugins + this.loadPlugins(); + } + + // Helper methods for plugins + say(target, message) { + this.send(`PRIVMSG ${target} :${message}`); + } + + action(target, message) { + this.send(`PRIVMSG ${target} :\x01ACTION ${message}\x01`); + } + + notice(target, message) { + this.send(`NOTICE ${target} :${message}`); + } + + join(channel) { + this.send(`JOIN ${channel}`); + } + + part(channel, reason = '') { + this.send(`PART ${channel} :${reason}`); + } + + // Simple event system for plugins + emit(event, data) { + this.plugins.forEach(plugin => { + if (typeof plugin[`on${event.charAt(0).toUpperCase() + event.slice(1)}`] === 'function') { + try { + plugin[`on${event.charAt(0).toUpperCase() + event.slice(1)}`](data, this); + } catch (error) { + console.error(`Error in plugin event handler:`, error); + } + } + }); + } +} + +// Configuration +const config = { + server: 'irc.libera.chat', + port: 6667, + nick: 'MyBot', + channels: ['#test'], + commandPrefix: '!', + pluginsDir: './plugins' +}; + +// Create and start bot +const bot = new IRCBot(config); +bot.connect(); + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\nShutting down bot...'); + if (bot.socket) { + bot.send('QUIT :Bot shutting down'); + bot.socket.end(); + } + process.exit(0); +}); + +module.exports = IRCBot; \ No newline at end of file diff --git a/botmain.js b/botmain.js new file mode 100644 index 0000000..8b26f70 --- /dev/null +++ b/botmain.js @@ -0,0 +1,423 @@ +// bot.js - Main IRC Bot with Plugin System (Debug Version) + +// ================================ +// BOT CONFIGURATION - EDIT HERE +// ================================ +const config = { + server: 'irc.libera.chat', // IRC server address + port: 6667, // IRC server port (6667 for plain, 6697 for SSL) + nick: 'cancerbot', // Bot's nickname + channels: ['#bakedbeans'], // Channels to join (add more like: ['#channel1', '#channel2']) + commandPrefix: '!', // Command prefix (e.g., !help, !ping) + pluginsDir: './plugins' // Directory containing plugin files +}; + +const net = require('net'); +const fs = require('fs'); +const path = require('path'); + +class IRCBot { + constructor(config) { + this.config = { + server: config.server || 'irc.libera.chat', + port: config.port || 6667, + nick: config.nick || 'MyBot', + channels: config.channels || ['#test'], + commandPrefix: config.commandPrefix || '!', + pluginsDir: config.pluginsDir || './plugins' + }; + + this.socket = null; + this.plugins = new Map(); + this.commands = new Map(); + this.connected = false; + + console.log('๐Ÿ”ง Bot configuration:', this.config); + this.loadPlugins(); + } + + connect() { + console.log(`Connecting to ${this.config.server}:${this.config.port}`); + + this.socket = net.createConnection(this.config.port, this.config.server); + + this.socket.on('connect', () => { + console.log('Connected to IRC server'); + this.send(`NICK ${this.config.nick}`); + this.send(`USER ${this.config.nick} 0 * :${this.config.nick}`); + }); + + this.socket.on('data', (data) => { + const lines = data.toString().split('\r\n'); + lines.forEach(line => { + if (line.trim()) { + this.handleMessage(line); + } + }); + }); + + this.socket.on('error', (err) => { + console.error('Socket error:', err); + }); + + this.socket.on('close', () => { + console.log('Connection closed'); + this.connected = false; + // Reconnect after 5 seconds + setTimeout(() => this.connect(), 5000); + }); + } + + send(message) { + if (this.socket && this.socket.writable) { + console.log('โ†’', message); + this.socket.write(message + '\r\n'); + } + } + + handleMessage(line) { + console.log('โ†', line); + + // Handle PING + if (line.startsWith('PING')) { + this.send('PONG ' + line.substring(5)); + return; + } + + // Handle successful connection + if (line.includes('001')) { + this.connected = true; + console.log('Successfully connected and registered'); + // Join channels + this.config.channels.forEach(channel => { + this.send(`JOIN ${channel}`); + }); + return; + } + + // Parse IRC message + const parsed = this.parseMessage(line); + if (parsed) { + this.handleParsedMessage(parsed); + } + } + + parseMessage(line) { + // Basic IRC message parsing + const match = line.match(/^:([^!]+)!([^@]+)@([^\s]+)\s+(\w+)\s+([^:]+):(.*)$/); + if (match) { + return { + nick: match[1], + user: match[2], + host: match[3], + command: match[4], + target: match[5].trim(), + message: match[6] + }; + } + return null; + } + + handleParsedMessage(msg) { + // Handle PRIVMSG (channel messages and private messages) + if (msg.command === 'PRIVMSG') { + const isChannel = msg.target.startsWith('#'); + const replyTo = isChannel ? msg.target : msg.nick; + + // Check if it's a command + if (msg.message.startsWith(this.config.commandPrefix)) { + const commandLine = msg.message.substring(this.config.commandPrefix.length); + const [commandName, ...args] = commandLine.split(' '); + + // Handle built-in admin commands first + if (commandName === 'reloadplugins' && msg.nick === 'megasconed') { + this.say(replyTo, '๐Ÿ”„ Reloading all plugins...'); + const pluginCount = this.plugins.size; + this.reloadAllPlugins(); + this.say(replyTo, `โœ… Reloaded ${pluginCount} plugins. New commands: ${Array.from(this.commands.keys()).join(', ')}`); + return; + } + + this.executeCommand(commandName, { + nick: msg.nick, + user: msg.user, + host: msg.host, + channel: isChannel ? msg.target : null, + replyTo: replyTo, + args: args, + fullMessage: msg.message + }); + } + + // Emit message event for plugins + this.emit('message', { + nick: msg.nick, + user: msg.user, + host: msg.host, + target: msg.target, + message: msg.message, + isChannel: isChannel, + replyTo: replyTo + }); + } + } + + executeCommand(commandName, context) { + const command = this.commands.get(commandName); + if (command) { + try { + command.execute(context, this); + } catch (error) { + console.error(`Error executing command ${commandName}:`, error); + this.say(context.replyTo, `Error executing command: ${error.message}`); + } + } else { + console.log(`Command not found: ${commandName}`); + } + } + + loadPlugins() { + console.log(`๐Ÿ” Looking for plugins in: ${this.config.pluginsDir}`); + console.log(`๐Ÿ” Full path: ${path.resolve(this.config.pluginsDir)}`); + + if (!fs.existsSync(this.config.pluginsDir)) { + console.log(`๐Ÿ“ Creating plugins directory: ${this.config.pluginsDir}`); + fs.mkdirSync(this.config.pluginsDir, { recursive: true }); + + // Create a basic example plugin + this.createExamplePlugin(); + return; + } + + console.log(`๐Ÿ“ Plugins directory exists, scanning for .js files...`); + + let pluginFiles; + try { + pluginFiles = fs.readdirSync(this.config.pluginsDir) + .filter(file => file.endsWith('.js')); + console.log(`๐Ÿ“‹ Found files:`, pluginFiles); + } catch (error) { + console.error(`โŒ Error reading plugins directory:`, error); + return; + } + + if (pluginFiles.length === 0) { + console.log(`โš ๏ธ No .js files found in plugins directory`); + console.log(`๐Ÿ’ก Creating example plugin...`); + this.createExamplePlugin(); + return; + } + + pluginFiles.forEach(file => { + console.log(`๐Ÿ”„ Loading plugin: ${file}`); + this.loadPlugin(file); + }); + + console.log(`โœ… Loaded ${pluginFiles.length} plugins`); + console.log(`๐ŸŽฎ Available commands:`, Array.from(this.commands.keys())); + } + + createExamplePlugin() { + const examplePlugin = `// plugins/basic.js - Basic commands plugin +module.exports = { + init(bot) { + console.log('Basic plugin initialized'); + }, + + cleanup(bot) { + console.log('Basic plugin cleaned up'); + }, + + commands: [ + { + name: 'ping', + description: 'Responds with pong', + execute(context, bot) { + bot.say(context.replyTo, \`\${context.nick}: pong!\`); + } + }, + + { + name: 'help', + description: 'Shows available commands', + execute(context, bot) { + const commands = Array.from(bot.commands.keys()); + bot.say(context.replyTo, \`Available commands: \${commands.join(', ')}\`); + } + }, + + { + name: 'time', + description: 'Shows current time', + execute(context, bot) { + const now = new Date().toLocaleString(); + bot.say(context.replyTo, \`Current time: \${now}\`); + } + } + ] +};`; + + const pluginPath = path.join(this.config.pluginsDir, 'basic.js'); + try { + fs.writeFileSync(pluginPath, examplePlugin); + console.log(`โœ… Created example plugin: ${pluginPath}`); + + // Give filesystem a moment to settle + setTimeout(() => { + console.log(`๐Ÿ”„ Loading newly created plugin...`); + this.loadPlugin('basic.js'); + }, 100); + + } catch (writeError) { + console.error(`โŒ Error creating example plugin:`, writeError); + } + } + + loadPlugin(filename) { + const filePath = path.join(this.config.pluginsDir, filename); + console.log(`๐Ÿ”„ Attempting to load: ${filePath}`); + + try { + // Check if file exists + if (!fs.existsSync(filePath)) { + console.error(`โŒ Plugin file not found: ${filePath}`); + return; + } + + // Get absolute path for require.resolve + const absolutePath = path.resolve(filePath); + console.log(`๐Ÿ“ Absolute path: ${absolutePath}`); + + // Clear require cache to allow reloading (only if already cached) + try { + const fullPath = require.resolve(absolutePath); + console.log(`๐Ÿ—‘๏ธ Clearing cache for: ${fullPath}`); + delete require.cache[fullPath]; + } catch (resolveError) { + console.log(`โ„น๏ธ File not in cache yet: ${absolutePath}`); + } + + // Require the plugin + console.log(`๐Ÿ“ฅ Requiring plugin: ${absolutePath}`); + const plugin = require(absolutePath); + console.log(`๐Ÿ“‹ Plugin object:`, Object.keys(plugin)); + + // Initialize plugin + if (typeof plugin.init === 'function') { + console.log(`๐Ÿš€ Initializing plugin: ${filename}`); + plugin.init(this); + } else { + console.log(`โš ๏ธ Plugin ${filename} has no init function`); + } + + // Register commands + if (plugin.commands && Array.isArray(plugin.commands)) { + console.log(`๐Ÿ“ Registering ${plugin.commands.length} commands from ${filename}`); + plugin.commands.forEach(command => { + if (command.name && typeof command.execute === 'function') { + this.commands.set(command.name, command); + console.log(`โœ… Registered command: ${command.name}`); + } else { + console.error(`โŒ Invalid command in ${filename}:`, command); + } + }); + } else { + console.log(`โš ๏ธ Plugin ${filename} has no commands array`); + } + + this.plugins.set(filename, plugin); + console.log(`โœ… Successfully loaded plugin: ${filename}`); + + } catch (error) { + console.error(`โŒ Error loading plugin ${filename}:`, error.message); + console.error(`๐Ÿ“ Stack trace:`, error.stack); + } + } + + reloadPlugin(filename) { + const plugin = this.plugins.get(filename); + if (plugin) { + // Unregister old commands + if (plugin.commands) { + plugin.commands.forEach(command => { + this.commands.delete(command.name); + }); + } + + // Call cleanup if available + if (typeof plugin.cleanup === 'function') { + plugin.cleanup(this); + } + + this.plugins.delete(filename); + } + + // Load the plugin again + this.loadPlugin(filename); + } + + reloadAllPlugins() { + // Clear all plugins and commands + this.plugins.forEach((plugin, filename) => { + if (typeof plugin.cleanup === 'function') { + plugin.cleanup(this); + } + }); + + this.plugins.clear(); + this.commands.clear(); + + // Reload all plugins + this.loadPlugins(); + } + + // Helper methods for plugins + say(target, message) { + this.send(`PRIVMSG ${target} :${message}`); + } + + action(target, message) { + this.send(`PRIVMSG ${target} :\x01ACTION ${message}\x01`); + } + + notice(target, message) { + this.send(`NOTICE ${target} :${message}`); + } + + join(channel) { + this.send(`JOIN ${channel}`); + } + + part(channel, reason = '') { + this.send(`PART ${channel} :${reason}`); + } + + // Simple event system for plugins + emit(event, data) { + this.plugins.forEach(plugin => { + if (typeof plugin[`on${event.charAt(0).toUpperCase() + event.slice(1)}`] === 'function') { + try { + plugin[`on${event.charAt(0).toUpperCase() + event.slice(1)}`](data, this); + } catch (error) { + console.error(`Error in plugin event handler:`, error); + } + } + }); + } +} + +// Create and start bot +const bot = new IRCBot(config); +bot.connect(); + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\nShutting down bot...'); + if (bot.socket) { + bot.send('QUIT :Bot shutting down'); + bot.socket.end(); + } + process.exit(0); +}); + +module.exports = IRCBot; \ No newline at end of file diff --git a/complete_irc_bot.js b/complete_irc_bot.js new file mode 100644 index 0000000..693e3a7 --- /dev/null +++ b/complete_irc_bot.js @@ -0,0 +1,299 @@ +// bot.js - Main IRC Bot with Plugin System + +// ================================ +// BOT CONFIGURATION - EDIT HERE +// ================================ +const config = { + server: 'irc.libera.chat', // IRC server address + port: 6667, // IRC server port (6667 for plain, 6697 for SSL) + nick: 'cancerbot', // Bot's nickname + channels: ['#bakedbeans'], // Channels to join (add more like: ['#channel1', '#channel2']) + commandPrefix: '!', // Command prefix (e.g., !help, !ping) + pluginsDir: './plugins' // Directory containing plugin files +}; + +const net = require('net'); +const fs = require('fs'); +const path = require('path'); + +class IRCBot { + constructor(config) { + this.config = { + server: config.server || 'irc.libera.chat', + port: config.port || 6667, + nick: config.nick || 'MyBot', + channels: config.channels || ['#test'], + commandPrefix: config.commandPrefix || '!', + pluginsDir: config.pluginsDir || './plugins' + }; + + this.socket = null; + this.plugins = new Map(); + this.commands = new Map(); + this.connected = false; + + this.loadPlugins(); + } + + connect() { + console.log(`Connecting to ${this.config.server}:${this.config.port}`); + + this.socket = net.createConnection(this.config.port, this.config.server); + + this.socket.on('connect', () => { + console.log('Connected to IRC server'); + this.send(`NICK ${this.config.nick}`); + this.send(`USER ${this.config.nick} 0 * :${this.config.nick}`); + }); + + this.socket.on('data', (data) => { + const lines = data.toString().split('\r\n'); + lines.forEach(line => { + if (line.trim()) { + this.handleMessage(line); + } + }); + }); + + this.socket.on('error', (err) => { + console.error('Socket error:', err); + }); + + this.socket.on('close', () => { + console.log('Connection closed'); + this.connected = false; + // Reconnect after 5 seconds + setTimeout(() => this.connect(), 5000); + }); + } + + send(message) { + if (this.socket && this.socket.writable) { + console.log('โ†’', message); + this.socket.write(message + '\r\n'); + } + } + + handleMessage(line) { + console.log('โ†', line); + + // Handle PING + if (line.startsWith('PING')) { + this.send('PONG ' + line.substring(5)); + return; + } + + // Handle successful connection + if (line.includes('001')) { + this.connected = true; + console.log('Successfully connected and registered'); + // Join channels + this.config.channels.forEach(channel => { + this.send(`JOIN ${channel}`); + }); + return; + } + + // Parse IRC message + const parsed = this.parseMessage(line); + if (parsed) { + this.handleParsedMessage(parsed); + } + } + + parseMessage(line) { + // Basic IRC message parsing + const match = line.match(/^:([^!]+)!([^@]+)@([^\s]+)\s+(\w+)\s+([^:]+):(.*)$/); + if (match) { + return { + nick: match[1], + user: match[2], + host: match[3], + command: match[4], + target: match[5].trim(), + message: match[6] + }; + } + return null; + } + + handleParsedMessage(msg) { + // Handle PRIVMSG (channel messages and private messages) + if (msg.command === 'PRIVMSG') { + const isChannel = msg.target.startsWith('#'); + const replyTo = isChannel ? msg.target : msg.nick; + + // Check if it's a command + if (msg.message.startsWith(this.config.commandPrefix)) { + const commandLine = msg.message.substring(this.config.commandPrefix.length); + const [commandName, ...args] = commandLine.split(' '); + + this.executeCommand(commandName, { + nick: msg.nick, + user: msg.user, + host: msg.host, + channel: isChannel ? msg.target : null, + replyTo: replyTo, + args: args, + fullMessage: msg.message + }); + } + + // Emit message event for plugins + this.emit('message', { + nick: msg.nick, + user: msg.user, + host: msg.host, + target: msg.target, + message: msg.message, + isChannel: isChannel, + replyTo: replyTo + }); + } + } + + executeCommand(commandName, context) { + const command = this.commands.get(commandName); + if (command) { + try { + command.execute(context, this); + } catch (error) { + console.error(`Error executing command ${commandName}:`, error); + this.say(context.replyTo, `Error executing command: ${error.message}`); + } + } + } + + loadPlugins() { + if (!fs.existsSync(this.config.pluginsDir)) { + fs.mkdirSync(this.config.pluginsDir, { recursive: true }); + console.log(`Created plugins directory: ${this.config.pluginsDir}`); + return; + } + + const pluginFiles = fs.readdirSync(this.config.pluginsDir) + .filter(file => file.endsWith('.js')); + + pluginFiles.forEach(file => { + this.loadPlugin(file); + }); + + console.log(`Loaded ${pluginFiles.length} plugins`); + } + + loadPlugin(filename) { + const filePath = path.join(this.config.pluginsDir, filename); + + try { + // Clear require cache to allow reloading + delete require.cache[require.resolve(filePath)]; + + const plugin = require(filePath); + + if (typeof plugin.init === 'function') { + plugin.init(this); + } + + // Register commands + if (plugin.commands) { + plugin.commands.forEach(command => { + this.commands.set(command.name, command); + console.log(`Registered command: ${command.name}`); + }); + } + + this.plugins.set(filename, plugin); + console.log(`Loaded plugin: ${filename}`); + + } catch (error) { + console.error(`Error loading plugin ${filename}:`, error); + } + } + + reloadPlugin(filename) { + const plugin = this.plugins.get(filename); + if (plugin) { + // Unregister old commands + if (plugin.commands) { + plugin.commands.forEach(command => { + this.commands.delete(command.name); + }); + } + + // Call cleanup if available + if (typeof plugin.cleanup === 'function') { + plugin.cleanup(this); + } + + this.plugins.delete(filename); + } + + // Load the plugin again + this.loadPlugin(filename); + } + + reloadAllPlugins() { + // Clear all plugins and commands + this.plugins.forEach((plugin, filename) => { + if (typeof plugin.cleanup === 'function') { + plugin.cleanup(this); + } + }); + + this.plugins.clear(); + this.commands.clear(); + + // Reload all plugins + this.loadPlugins(); + } + + // Helper methods for plugins + say(target, message) { + this.send(`PRIVMSG ${target} :${message}`); + } + + action(target, message) { + this.send(`PRIVMSG ${target} :\x01ACTION ${message}\x01`); + } + + notice(target, message) { + this.send(`NOTICE ${target} :${message}`); + } + + join(channel) { + this.send(`JOIN ${channel}`); + } + + part(channel, reason = '') { + this.send(`PART ${channel} :${reason}`); + } + + // Simple event system for plugins + emit(event, data) { + this.plugins.forEach(plugin => { + if (typeof plugin[`on${event.charAt(0).toUpperCase() + event.slice(1)}`] === 'function') { + try { + plugin[`on${event.charAt(0).toUpperCase() + event.slice(1)}`](data, this); + } catch (error) { + console.error(`Error in plugin event handler:`, error); + } + } + }); + } +} + +// Create and start bot +const bot = new IRCBot(config); +bot.connect(); + +// Handle graceful shutdown +process.on('SIGINT', () => { + console.log('\nShutting down bot...'); + if (bot.socket) { + bot.send('QUIT :Bot shutting down'); + bot.socket.end(); + } + process.exit(0); +}); + +module.exports = IRCBot; \ No newline at end of file diff --git a/index.html b/index.html new file mode 100644 index 0000000..8b45cce --- /dev/null +++ b/index.html @@ -0,0 +1,509 @@ + + + + + + Server Music Player + + + +
+

๐ŸŽต Server Music Player

+

Random FLAC player from server directory

+ +
+

โš™๏ธ Server Configuration

+
+ + +
+
+ + +
+ +
+ +
+ Click "Connect" to start browsing your music library +
+ + + + +
+ + + + \ No newline at end of file diff --git a/notplugins/example_plugin.js b/notplugins/example_plugin.js new file mode 100644 index 0000000..0f0a328 --- /dev/null +++ b/notplugins/example_plugin.js @@ -0,0 +1,73 @@ +// plugins/basic.js - Example plugin with basic commands +module.exports = { + // Plugin initialization + init(bot) { + console.log('Basic plugin initialized'); + this.bot = bot; + }, + + // Plugin cleanup (called when reloading/unloading) + cleanup(bot) { + console.log('Basic plugin cleaned up'); + }, + + // Event handlers + onMessage(data, bot) { + // This gets called for every message + // You can add custom logic here + if (data.message.includes('hello') && data.message.includes(bot.config.nick)) { + bot.say(data.replyTo, `Hello ${data.nick}!`); + } + }, + + // Commands that this plugin provides + commands: [ + { + name: 'ping', + description: 'Responds with pong', + execute(context, bot) { + bot.say(context.replyTo, `${context.nick}: pong!`); + } + }, + + { + name: 'echo', + description: 'Echoes back the message', + execute(context, bot) { + if (context.args.length === 0) { + bot.say(context.replyTo, `${context.nick}: Please provide a message to echo`); + return; + } + + const message = context.args.join(' '); + bot.say(context.replyTo, `${context.nick}: ${message}`); + } + }, + + { + name: 'help', + description: 'Shows available commands', + execute(context, bot) { + const commands = Array.from(bot.commands.keys()); + bot.say(context.replyTo, `Available commands: ${commands.join(', ')}`); + } + }, + + { + name: 'reload', + description: 'Reloads all plugins (admin only)', + execute(context, bot) { + // Simple admin check (you can make this more sophisticated) + const adminNicks = ['admin', 'owner']; // Add your admin nicks here + + if (!adminNicks.includes(context.nick)) { + bot.say(context.replyTo, `${context.nick}: Access denied`); + return; + } + + bot.reloadAllPlugins(); + bot.say(context.replyTo, `${context.nick}: Plugins reloaded`); + } + } + ] +}; \ No newline at end of file diff --git a/notplugins/pigs_plugin.js b/notplugins/pigs_plugin.js new file mode 100644 index 0000000..624f409 --- /dev/null +++ b/notplugins/pigs_plugin.js @@ -0,0 +1,409 @@ +// plugins/pigs.js - Pass the Pigs game plugin +module.exports = { + init(bot) { + console.log('Pass the Pigs plugin initialized'); + this.bot = bot; + + // Game state and scores storage + this.pigScores = new Map(); // nick -> { totalScore, bestRoll, rollCount, name, eliminated } + this.gameState = { + active: false, + players: [], + currentPlayer: 0, + turnScore: 0, + channel: null + }; + }, + + cleanup(bot) { + console.log('Pass the Pigs plugin cleaned up'); + // Clear any active games on cleanup + this.gameState = { + active: false, + players: [], + currentPlayer: 0, + turnScore: 0, + channel: null + }; + }, + + // Pass the Pigs game simulation with increased difficulty + rollPigs() { + // Actual probabilities from 11,954 sample study + const positions = [ + { name: 'Side (no dot)', points: 0, weight: 34.9 }, + { name: 'Side (dot)', points: 0, weight: 30.2 }, + { name: 'Razorback', points: 5, weight: 22.4 }, + { name: 'Trotter', points: 5, weight: 8.8 }, + { name: 'Snouter', points: 10, weight: 3.0 }, + { name: 'Leaning Jowler', points: 15, weight: 0.61 } + ]; + + const rollPig = () => { + const totalWeight = positions.reduce((sum, pos) => sum + pos.weight, 0); + let random = Math.random() * totalWeight; + + for (const position of positions) { + random -= position.weight; + if (random <= 0) { + return position; + } + } + return positions[0]; // fallback + }; + + const pig1 = rollPig(); + const pig2 = rollPig(); + + // Check for touching conditions first (increased probability for more difficulty) + if (Math.random() < 0.035) { // 3.5% chance of touching (was 1.5%) + if (Math.random() < 0.25) { // 25% of touches are Piggyback + return { result: 'Piggyback', points: -9999, description: '๐Ÿท๐Ÿ“š Piggyback! ELIMINATED!' }; + } else { // 75% are Makin' Bacon/Oinker + return { result: 'Makin\' Bacon', points: -999, description: '๐Ÿฅ“ Makin\' Bacon! Lose ALL points!' }; + } + } + + // Handle side positions (Sider vs Pig Out) + const pig1IsSide = pig1.name.startsWith('Side'); + const pig2IsSide = pig2.name.startsWith('Side'); + + if (pig1IsSide && pig2IsSide) { + // Both on sides - increased chance of opposite sides (Pig Out) + const pig1Dot = pig1.name.includes('dot'); + const pig2Dot = pig2.name.includes('dot'); + + // Bias toward opposite sides for more busts + const oppositeRoll = Math.random(); + if (oppositeRoll < 0.6) { // 60% chance of opposite sides when both are siders + return { result: 'Pig Out', points: 0, description: '๐Ÿ’ฅ Pig Out! Turn ends!' }; + } else { + return { result: 'Sider', points: 1, description: '๐Ÿท Sider' }; + } + } + + // If only one pig is on its side, that pig scores nothing + if (pig1IsSide && !pig2IsSide) { + return { + result: pig2.name, + points: pig2.points, + description: `๐Ÿท ${pig2.name} (${pig2.points})` + }; + } + + if (pig2IsSide && !pig1IsSide) { + return { + result: pig1.name, + points: pig1.points, + description: `๐Ÿท ${pig1.name} (${pig1.points})` + }; + } + + // Neither pig is on its side - normal scoring + if (pig1.name === pig2.name) { + // Double combinations - sum doubled (quadruple individual) + const doublePoints = (pig1.points + pig2.points) * 2; + switch (pig1.name) { + case 'Razorback': + return { result: 'Double Razorback', points: doublePoints, description: '๐Ÿท๐Ÿท Double Razorback (20)' }; + case 'Trotter': + return { result: 'Double Trotter', points: doublePoints, description: '๐Ÿท๐Ÿท Double Trotter (20)' }; + case 'Snouter': + return { result: 'Double Snouter', points: doublePoints, description: '๐Ÿท๐Ÿท Double Snouter (40)' }; + case 'Leaning Jowler': + return { result: 'Double Leaning Jowler', points: doublePoints, description: '๐Ÿท๐Ÿท Double Leaning Jowler (60)' }; + } + } + + // Mixed combination - sum of individual scores + const totalPoints = pig1.points + pig2.points; + return { + result: 'Mixed Combo', + points: totalPoints, + description: `๐Ÿท ${pig1.name} + ${pig2.name} (${totalPoints})` + }; + }, + + // Turn management for pig game + nextTurn(target) { + this.gameState.currentPlayer = (this.gameState.currentPlayer + 1) % 2; + const nextPlayer = this.gameState.players[this.gameState.currentPlayer]; + + // Check if next player is eliminated + const nextPlayerStats = this.pigScores.get(nextPlayer); + if (nextPlayerStats.eliminated) { + const winner = this.gameState.players[(this.gameState.currentPlayer + 1) % 2]; + this.bot.say(target, `${nextPlayer} is eliminated! ${winner} wins!`); + this.endGame(target); + return; + } + + this.bot.say(target, `๐ŸŽฒ ${nextPlayer}'s turn!`); + }, + + endGame(target) { + this.bot.say(target, '๐Ÿ Game ended! Use !pigs to start new game.'); + this.gameState = { + active: false, + players: [], + currentPlayer: 0, + turnScore: 0, + channel: null + }; + }, + + commands: [ + { + name: 'pigs', + description: 'Start or join a Pass the Pigs game', + execute(context, bot) { + const target = context.replyTo; + const from = context.nick; + const to = context.channel || context.replyTo; + + // Only allow channel play for turn-based games + if (!context.channel) { + bot.say(target, 'Turn-based pig games can only be played in channels!'); + return; + } + + // Game setup phase + if (!this.gameState.active) { + // First player joins + if (this.gameState.players.length === 0) { + this.gameState.players.push(from); + this.gameState.channel = to; + + // Initialize player + if (!this.pigScores.has(from)) { + this.pigScores.set(from, { + totalScore: 0, + bestRoll: 0, + rollCount: 0, + name: from, + eliminated: false + }); + } + + bot.say(target, `๐Ÿท ${from} wants to start a pig game! Second player use !pigs to join`); + return; + } + + // Second player joins - start game + if (this.gameState.players.length === 1 && !this.gameState.players.includes(from)) { + this.gameState.players.push(from); + this.gameState.active = true; + this.gameState.currentPlayer = 0; + this.gameState.turnScore = 0; + + // Initialize second player + if (!this.pigScores.has(from)) { + this.pigScores.set(from, { + totalScore: 0, + bestRoll: 0, + rollCount: 0, + name: from, + eliminated: false + }); + } + + bot.say(target, `๐ŸŽฎ ${this.gameState.players[0]} vs ${this.gameState.players[1]} - ${this.gameState.players[0]} goes first!`); + return; + } + + // Handle various edge cases + if (this.gameState.players.includes(from)) { + bot.say(target, `${from}: You're already in the game!`); + return; + } else { + bot.say(target, `${from}: Game is full! Wait for current game to finish.`); + return; + } + } + + // Game is active - check if it's player's turn + if (!this.gameState.players.includes(from)) { + bot.say(target, `${from}: You're not in the current game!`); + return; + } + + const currentPlayerName = this.gameState.players[this.gameState.currentPlayer]; + if (from !== currentPlayerName) { + bot.say(target, `${from}: It's ${currentPlayerName}'s turn!`); + return; + } + + // Check if current player is eliminated + const playerStats = this.pigScores.get(from); + if (playerStats.eliminated) { + bot.say(target, `${from}: You are eliminated!`); + this.endGame(target); + return; + } + + // Roll the pigs! + const roll = this.rollPigs(); + playerStats.rollCount++; + + let message = ''; + let endTurn = false; + let endGame = false; + + if (roll.points === -9999) { + // Piggyback - eliminate player and end game + playerStats.eliminated = true; + message = `${from}: ${roll.description}`; + endGame = true; + } else if (roll.points === -999) { + // Makin' Bacon - lose all points and end turn + const lostPoints = playerStats.totalScore; + playerStats.totalScore = 0; + this.gameState.turnScore = 0; + message = `${from}: ${roll.description} Lost ${lostPoints} total!`; + endTurn = true; + } else if (roll.points === 0) { + // Pig Out - lose turn score and end turn + message = `${from}: ${roll.description} Lost ${this.gameState.turnScore} turn points!`; + this.gameState.turnScore = 0; + endTurn = true; + } else { + // Normal scoring - add to turn score + this.gameState.turnScore += roll.points; + + // Update best roll if this is better + if (roll.points > playerStats.bestRoll) { + playerStats.bestRoll = roll.points; + } + + const potentialTotal = playerStats.totalScore + this.gameState.turnScore; + message = `${from}: ${roll.description} | Turn: ${this.gameState.turnScore} | Total: ${playerStats.totalScore}`; + + // Check potential win + if (potentialTotal >= 100) { + message += ` | Can win!`; + } + } + + bot.say(target, message); + + if (endGame) { + this.endGame(target); + } else if (endTurn) { + this.nextTurn(target); + } + }.bind(this) + }, + + { + name: 'bank', + description: 'Bank your turn points in Pass the Pigs', + execute(context, bot) { + const target = context.replyTo; + const from = context.nick; + + if (!this.gameState.active || !this.gameState.players.includes(from)) { + bot.say(target, `${from}: You're not in an active pig game!`); + return; + } + + const currentPlayerName = this.gameState.players[this.gameState.currentPlayer]; + if (from !== currentPlayerName) { + bot.say(target, `${from}: It's ${currentPlayerName}'s turn!`); + return; + } + + if (this.gameState.turnScore === 0) { + bot.say(target, `${from}: No points to bank!`); + return; + } + + // Bank the points + const playerStats = this.pigScores.get(from); + playerStats.totalScore += this.gameState.turnScore; + + bot.say(target, `${from}: Banked ${this.gameState.turnScore} points! Total: ${playerStats.totalScore}`); + + // Check for win + if (playerStats.totalScore >= 100) { + bot.say(target, `๐ŸŽ‰ ${from} WINS with ${playerStats.totalScore} points! ๐ŸŽ‰`); + this.endGame(target); + return; + } + + this.gameState.turnScore = 0; + this.nextTurn(target); + }.bind(this) + }, + + { + name: 'quitpigs', + description: 'Quit the current pig game', + execute(context, bot) { + const target = context.replyTo; + const from = context.nick; + + if (!this.gameState.active && this.gameState.players.length === 0) { + bot.say(target, `${from}: No pig game to quit!`); + return; + } + + if (this.gameState.players.includes(from)) { + bot.say(target, `${from} quit the pig game!`); + this.endGame(target); + } else { + bot.say(target, `${from}: You're not in the current game!`); + } + }.bind(this) + }, + + { + name: 'toppigs', + description: 'Show top pig players', + execute(context, bot) { + const target = context.replyTo; + + if (this.pigScores.size === 0) { + bot.say(target, 'No pig scores recorded yet! Use !pigs to start playing.'); + return; + } + + // Convert to array and sort by total score + const sortedScores = Array.from(this.pigScores.values()) + .sort((a, b) => b.totalScore - a.totalScore) + .slice(0, 5); // Top 5 + + bot.say(target, '๐Ÿ† Top Pig Players:'); + + sortedScores.forEach((player, index) => { + const rank = index + 1; + const trophy = rank === 1 ? '๐Ÿฅ‡' : rank === 2 ? '๐Ÿฅˆ' : rank === 3 ? '๐Ÿฅ‰' : `${rank}.`; + bot.say(target, `${trophy} ${player.name}: ${player.totalScore} points (best roll: ${player.bestRoll}, ${player.rollCount} rolls)`); + }); + }.bind(this) + }, + + { + name: 'pigstatus', + description: 'Show current pig game status', + execute(context, bot) { + const target = context.replyTo; + + if (!this.gameState.active) { + if (this.gameState.players.length === 1) { + bot.say(target, `๐Ÿท ${this.gameState.players[0]} is waiting for a second player! Use !pigs to join.`); + } else { + bot.say(target, 'No active pig game. Use !pigs to start one!'); + } + return; + } + + const currentPlayer = this.gameState.players[this.gameState.currentPlayer]; + const player1Stats = this.pigScores.get(this.gameState.players[0]); + const player2Stats = this.pigScores.get(this.gameState.players[1]); + + bot.say(target, `๐ŸŽฎ Current Game: ${this.gameState.players[0]} (${player1Stats.totalScore}) vs ${this.gameState.players[1]} (${player2Stats.totalScore})`); + bot.say(target, `๐ŸŽฒ ${currentPlayer}'s turn | Turn score: ${this.gameState.turnScore}`); + }.bind(this) + } + ] +}; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4594d43 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2031 @@ +{ + "name": "IRCBOT", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "express": "^5.1.0", + "sqlite3": "^5.1.7", + "ws": "^8.18.3" + } + }, + "node_modules/@gar/promisify": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.3.tgz", + "integrity": "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw==", + "optional": true + }, + "node_modules/@npmcli/fs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.1.1.tgz", + "integrity": "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ==", + "optional": true, + "dependencies": { + "@gar/promisify": "^1.0.1", + "semver": "^7.3.5" + } + }, + "node_modules/@npmcli/move-file": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", + "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "deprecated": "This functionality has been moved to @npmcli/fs", + "optional": true, + "dependencies": { + "mkdirp": "^1.0.4", + "rimraf": "^3.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@tootallnate/once": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", + "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "optional": true + }, + "node_modules/accepts": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz", + "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==", + "dependencies": { + "mime-types": "^3.0.0", + "negotiator": "^1.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "optional": true, + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "optional": true, + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "optional": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", + "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "dependencies": { + "bytes": "^3.1.2", + "content-type": "^1.0.5", + "debug": "^4.4.0", + "http-errors": "^2.0.0", + "iconv-lite": "^0.6.3", + "on-finished": "^2.4.1", + "qs": "^6.14.0", + "raw-body": "^3.0.0", + "type-is": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "optional": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "15.3.0", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", + "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", + "optional": true, + "dependencies": { + "@npmcli/fs": "^1.0.0", + "@npmcli/move-file": "^1.0.1", + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "glob": "^7.1.4", + "infer-owner": "^1.0.4", + "lru-cache": "^6.0.0", + "minipass": "^3.1.1", + "minipass-collect": "^1.0.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.2", + "mkdirp": "^1.0.3", + "p-map": "^4.0.0", + "promise-inflight": "^1.0.1", + "rimraf": "^3.0.2", + "ssri": "^8.0.1", + "tar": "^6.0.2", + "unique-filename": "^1.1.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "engines": { + "node": ">=10" + } + }, + "node_modules/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "optional": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "optional": true + }, + "node_modules/content-disposition": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.0.tgz", + "integrity": "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz", + "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==", + "engines": { + "node": ">=6.6.0" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "optional": true + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/detect-libc": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", + "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "optional": true + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "optional": true + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/expand-template": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", + "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/express": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/express/-/express-5.1.0.tgz", + "integrity": "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA==", + "dependencies": { + "accepts": "^2.0.0", + "body-parser": "^2.2.0", + "content-disposition": "^1.0.0", + "content-type": "^1.0.5", + "cookie": "^0.7.1", + "cookie-signature": "^1.2.1", + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "finalhandler": "^2.1.0", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "merge-descriptors": "^2.0.0", + "mime-types": "^3.0.0", + "on-finished": "^2.4.1", + "once": "^1.4.0", + "parseurl": "^1.3.3", + "proxy-addr": "^2.0.7", + "qs": "^6.14.0", + "range-parser": "^1.2.1", + "router": "^2.2.0", + "send": "^1.1.0", + "serve-static": "^2.2.0", + "statuses": "^2.0.1", + "type-is": "^2.0.1", + "vary": "^1.1.2" + }, + "engines": { + "node": ">= 18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==" + }, + "node_modules/finalhandler": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.0.tgz", + "integrity": "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q==", + "dependencies": { + "debug": "^4.4.0", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "on-finished": "^2.4.1", + "parseurl": "^1.3.3", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz", + "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==" + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "optional": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/github-from-package": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", + "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==" + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "optional": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "optional": true + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "optional": true + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", + "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "optional": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "optional": true, + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "optional": true, + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "optional": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/infer-owner": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", + "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "optional": true + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "optional": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==" + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "optional": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "optional": true + }, + "node_modules/is-promise": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", + "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "optional": true + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "optional": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "optional": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-fetch-happen": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-9.1.0.tgz", + "integrity": "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg==", + "optional": true, + "dependencies": { + "agentkeepalive": "^4.1.3", + "cacache": "^15.2.0", + "http-cache-semantics": "^4.1.0", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-lambda": "^1.0.1", + "lru-cache": "^6.0.0", + "minipass": "^3.1.3", + "minipass-collect": "^1.0.2", + "minipass-fetch": "^1.3.2", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.2", + "promise-retry": "^2.0.1", + "socks-proxy-agent": "^6.0.0", + "ssri": "^8.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz", + "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/merge-descriptors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz", + "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "optional": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-collect": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", + "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-fetch": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.4.1.tgz", + "integrity": "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw==", + "optional": true, + "dependencies": { + "minipass": "^3.1.0", + "minipass-sized": "^1.0.3", + "minizlib": "^2.0.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "encoding": "^0.1.12" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "optional": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==" + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/napi-build-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", + "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==" + }, + "node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "optional": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-abi": { + "version": "3.75.0", + "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", + "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==" + }, + "node_modules/node-gyp": { + "version": "8.4.1", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-8.4.1.tgz", + "integrity": "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w==", + "optional": true, + "dependencies": { + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^9.1.0", + "nopt": "^5.0.0", + "npmlog": "^6.0.0", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.2", + "which": "^2.0.2" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": ">= 10.12.0" + } + }, + "node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "optional": true, + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "optional": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-to-regexp": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.2.0.tgz", + "integrity": "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ==", + "engines": { + "node": ">=16" + } + }, + "node_modules/prebuild-install": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", + "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dependencies": { + "detect-libc": "^2.0.0", + "expand-template": "^2.0.3", + "github-from-package": "0.0.0", + "minimist": "^1.2.3", + "mkdirp-classic": "^0.5.3", + "napi-build-utils": "^2.0.0", + "node-abi": "^3.3.0", + "pump": "^3.0.0", + "rc": "^1.2.7", + "simple-get": "^4.0.0", + "tar-fs": "^2.0.0", + "tunnel-agent": "^0.6.0" + }, + "bin": { + "prebuild-install": "bin.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "optional": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "optional": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/pump": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", + "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.0.tgz", + "integrity": "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.6.3", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "optional": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/router": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", + "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==", + "dependencies": { + "debug": "^4.4.0", + "depd": "^2.0.0", + "is-promise": "^4.0.0", + "parseurl": "^1.3.3", + "path-to-regexp": "^8.0.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/send/-/send-1.2.0.tgz", + "integrity": "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw==", + "dependencies": { + "debug": "^4.3.5", + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "etag": "^1.8.1", + "fresh": "^2.0.0", + "http-errors": "^2.0.0", + "mime-types": "^3.0.1", + "ms": "^2.1.3", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "statuses": "^2.0.1" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ==", + "dependencies": { + "encodeurl": "^2.0.0", + "escape-html": "^1.0.3", + "parseurl": "^1.3.3", + "send": "^1.2.0" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "optional": true + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "optional": true + }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/simple-get": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", + "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "decompress-response": "^6.0.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "optional": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.6", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.6.tgz", + "integrity": "sha512-pe4Y2yzru68lXCb38aAqRf5gvN8YdjP1lok5o0J7BOHljkyCGKVz7H3vpVIXKD27rj2giOJ7DwVyk/GWrPHDWA==", + "optional": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-6.2.1.tgz", + "integrity": "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ==", + "optional": true, + "dependencies": { + "agent-base": "^6.0.2", + "debug": "^4.3.3", + "socks": "^2.6.2" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "optional": true + }, + "node_modules/sqlite3": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/sqlite3/-/sqlite3-5.1.7.tgz", + "integrity": "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog==", + "hasInstallScript": true, + "dependencies": { + "bindings": "^1.5.0", + "node-addon-api": "^7.0.0", + "prebuild-install": "^7.1.1", + "tar": "^6.1.11" + }, + "optionalDependencies": { + "node-gyp": "8.x" + }, + "peerDependencies": { + "node-gyp": "8.x" + }, + "peerDependenciesMeta": { + "node-gyp": { + "optional": true + } + } + }, + "node_modules/ssri": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", + "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", + "optional": true, + "dependencies": { + "minipass": "^3.1.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "optional": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar-fs": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-fs/node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==" + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/type-is": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz", + "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==", + "dependencies": { + "content-type": "^1.0.5", + "media-typer": "^1.1.0", + "mime-types": "^3.0.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/unique-filename": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", + "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", + "optional": true, + "dependencies": { + "unique-slug": "^2.0.0" + } + }, + "node_modules/unique-slug": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", + "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", + "optional": true, + "dependencies": { + "imurmurhash": "^0.1.4" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..f1f179c --- /dev/null +++ b/package.json @@ -0,0 +1,7 @@ +{ + "dependencies": { + "express": "^5.1.0", + "sqlite3": "^5.1.7", + "ws": "^8.18.3" + } +} diff --git a/plugin_template.js b/plugin_template.js new file mode 100644 index 0000000..e9da55d --- /dev/null +++ b/plugin_template.js @@ -0,0 +1,37 @@ +// plugins/template.js - Template for creating new plugins +module.exports = { + // Called when plugin is loaded + init(bot) { + console.log('Template plugin initialized'); + // You can store references, setup timers, etc. + this.bot = bot; + }, + + // Called when plugin is unloaded/reloaded + cleanup(bot) { + console.log('Template plugin cleaned up'); + // Clean up timers, connections, etc. + }, + + // Event handlers (optional) + onMessage(data, bot) { + // Called for every message + // data contains: nick, user, host, target, message, isChannel, replyTo + // Uncomment and use parameters as needed: + // console.log(`Message from ${data.nick}: ${data.message}`); + }, + + // Commands array + commands: [ + { + name: 'example', + description: 'An example command', + execute(context, bot) { + // context contains: nick, user, host, channel, replyTo, args, fullMessage + // bot is the IRCBot instance with methods like say(), action(), notice() + + bot.say(context.replyTo, `Hello ${context.nick}!`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/babble_plugin_creative(3).js b/plugins/babble_plugin_creative(3).js new file mode 100644 index 0000000..9c44d76 --- /dev/null +++ b/plugins/babble_plugin_creative(3).js @@ -0,0 +1,773 @@ +// plugins/babble.js - Fresh Markov Chain Text Generator plugin with user-specific generation +const path = require('path'); +const fs = require('fs'); + +module.exports = { + init(bot) { + console.log('๐ŸŽฏ Babble plugin starting...'); + this.bot = bot; + + // Try to load sqlite3 + try { + this.sqlite3 = require('sqlite3').verbose(); + console.log('โœ… SQLite3 loaded successfully'); + } catch (error) { + console.error('โŒ SQLite3 failed to load:', error.message); + this.sqlite3 = null; + return; + } + + // Database setup + this.dbPath = path.join(__dirname, 'babble.db'); + console.log(`๐Ÿ“ Database will be created at: ${this.dbPath}`); + + // Initialize database + this.initDB(); + + // Simple settings + this.settings = { + minLength: 5, + excludeBots: ['cancerbot', 'services'], + excludeCommands: true + }; + + console.log('โœ… Babble plugin initialized'); + }, + + cleanup(bot) { + console.log('๐ŸŽฏ Babble plugin cleaning up'); + if (this.db) { + this.db.close(); + } + }, + + initDB() { + try { + this.db = new this.sqlite3.Database(this.dbPath, (err) => { + if (err) { + console.error('โŒ Database creation failed:', err.message); + this.db = null; + return; + } + + console.log('โœ… Database created/opened successfully'); + this.createTables(); + }); + } catch (error) { + console.error('โŒ Database init error:', error.message); + this.db = null; + } + }, + + createTables() { + if (!this.db) return; + + // Simple messages table + this.db.run(` + CREATE TABLE IF NOT EXISTS messages ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nick TEXT, + channel TEXT, + message TEXT, + created DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `, (err) => { + if (err) { + console.error('โŒ Messages table creation failed:', err.message); + } else { + console.log('โœ… Messages table ready'); + } + }); + + // Check if word_pairs table exists and if it has nick column + this.db.get("PRAGMA table_info(word_pairs)", (err, result) => { + if (err) { + console.log('๐Ÿ”ง Creating new word_pairs table...'); + this.createWordPairsTable(); + } else { + // Table exists, check if it has nick column + this.db.all("PRAGMA table_info(word_pairs)", (err, columns) => { + if (err) { + console.error('โŒ Error checking table structure:', err.message); + return; + } + + const hasNickColumn = columns.some(col => col.name === 'nick'); + + if (!hasNickColumn) { + console.log('๐Ÿ”ง Migrating word_pairs table to add nick column...'); + this.migrateWordPairsTable(); + } else { + console.log('โœ… Word pairs table ready with nick column'); + console.log('๐ŸŽฏ Database fully initialized'); + } + }); + } + }); + }, + + createWordPairsTable() { + this.db.run(` + CREATE TABLE word_pairs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + word1 TEXT, + word2 TEXT, + count INTEGER DEFAULT 1, + nick TEXT, + UNIQUE(word1, word2, nick) + ) + `, (err) => { + if (err) { + console.error('โŒ Word pairs table creation failed:', err.message); + } else { + console.log('โœ… Word pairs table created with nick column'); + console.log('๐ŸŽฏ Database fully initialized'); + } + }); + }, + + migrateWordPairsTable() { + // Rename old table + this.db.run("ALTER TABLE word_pairs RENAME TO word_pairs_old", (err) => { + if (err) { + console.error('โŒ Failed to rename old table:', err.message); + return; + } + + // Create new table with nick column + this.db.run(` + CREATE TABLE word_pairs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + word1 TEXT, + word2 TEXT, + count INTEGER DEFAULT 1, + nick TEXT, + UNIQUE(word1, word2, nick) + ) + `, (err) => { + if (err) { + console.error('โŒ Failed to create new table:', err.message); + return; + } + + // Copy old data (nick will be NULL for old entries) + this.db.run(` + INSERT INTO word_pairs (word1, word2, count, nick) + SELECT word1, word2, count, NULL FROM word_pairs_old + `, (err) => { + if (err) { + console.error('โŒ Failed to migrate data:', err.message); + return; + } + + // Drop old table + this.db.run("DROP TABLE word_pairs_old", (err) => { + if (err) { + console.error('โŒ Failed to drop old table:', err.message); + } else { + console.log('โœ… Database migration completed successfully'); + console.log('๐ŸŽฏ Database fully initialized'); + } + }); + }); + }); + }); + }, + + shouldLog(nick, message) { + // Skip bots + if (this.settings.excludeBots.includes(nick.toLowerCase())) { + return false; + } + + // Skip commands + if (this.settings.excludeCommands && message.startsWith('!')) { + return false; + } + + // Skip short messages + if (message.length < this.settings.minLength) { + return false; + } + + return true; + }, + + cleanMessage(message) { + // Remove IRC colors and formatting + message = message.replace(/[\x02\x1D\x1F\x16\x0F]|\x03\d{0,2}(,\d{0,2})?/g, ''); + + // Basic cleanup + message = message.toLowerCase().trim(); + message = message.replace(/[^\w\s]/g, ' '); // Remove punctuation + message = message.replace(/\s+/g, ' '); // Multiple spaces to single + + return message; + }, + + storeMessage(nick, channel, message) { + if (!this.db) { + console.log('โŒ No database available for storing message'); + return; + } + + if (!this.shouldLog(nick, message)) { + console.log(`โญ๏ธ Skipping message from ${nick}: ${message.substring(0, 20)}...`); + return; + } + + const cleaned = this.cleanMessage(message); + if (cleaned.length < this.settings.minLength) { + console.log('โญ๏ธ Message too short after cleaning'); + return; + } + + console.log(`๐Ÿ“ Storing message from ${nick}: "${cleaned}"`); + + // Store the message + this.db.run( + 'INSERT INTO messages (nick, channel, message) VALUES (?, ?, ?)', + [nick, channel, cleaned], + (err) => { + if (err) { + console.error('โŒ Failed to store message:', err.message); + } else { + console.log('โœ… Message stored successfully'); + this.buildPairs(cleaned, nick); + } + } + ); + }, + + buildPairs(message, nick) { + if (!this.db) return; + + const words = message.split(' ').filter(w => w.length > 0); + if (words.length < 2) return; + + // Add START and END markers + const allWords = ['', ...words, '']; + + // Create word pairs + for (let i = 0; i < allWords.length - 1; i++) { + const word1 = allWords[i]; + const word2 = allWords[i + 1]; + + this.db.run(` + INSERT INTO word_pairs (word1, word2, count, nick) VALUES (?, ?, 1, ?) + ON CONFLICT(word1, word2, nick) DO UPDATE SET count = count + 1 + `, [word1, word2, nick], (err) => { + if (err) { + console.error('โŒ Failed to store word pair:', err.message); + } else { + console.log(`๐Ÿ”— Stored pair: "${word1}" โ†’ "${word2}" (${nick})`); + } + }); + } + }, + + generateText(callback, targetNick = null, creative = false) { + if (!this.db) { + callback('No database available'); + return; + } + + const mode = creative ? 'creative' : 'normal'; + console.log(`๐Ÿค– Starting ${mode} text generation${targetNick ? ` for ${targetNick}` : ''}...`); + + // First check if target user exists and has enough data + if (targetNick) { + this.db.get( + 'SELECT COUNT(*) as count FROM word_pairs WHERE nick = ?', + [targetNick], + (err, row) => { + if (err || !row || row.count < 10) { + callback(`Not enough data for user "${targetNick}" (need at least 10 word pairs)`); + return; + } + this.performGeneration(callback, targetNick, creative); + } + ); + } else { + this.performGeneration(callback, null, creative); + } + }, + + performGeneration(callback, targetNick, creative = false) { + const self = this; // Capture 'this' reference + // Strategy: Start from a random word + this.getRandomStartWord((err, startWord) => { + if (err) { + callback(err); + return; + } + + let currentWord = startWord; + let result = []; + let attempts = 0; + const maxAttempts = creative ? 80 : 50; // More attempts for creative mode + const maxLength = creative ? + Math.floor(Math.random() * 15) + 8 : // Creative: 8-22 words + Math.floor(Math.random() * 10) + 5; // Normal: 5-14 words + + console.log(`๐ŸŽฒ Starting with word: "${currentWord}" (${creative ? 'CREATIVE' : 'normal'} mode)`); + + // Add the starting word if it's not a marker + if (currentWord !== '' && currentWord !== '') { + result.push(currentWord); + } + + const getNext = () => { + if (attempts++ > maxAttempts || result.length >= maxLength) { + if (result.length >= 3) { + const prefix = targetNick ? `[${targetNick}] ` : ''; + const emoji = creative ? '๐ŸŽจ ' : '๐Ÿค– '; + callback(null, prefix + emoji + result.join(' ')); + } else { + callback('Generation failed - too short'); + } + return; + } + + // Build query based on whether we're targeting a specific user + let query = 'SELECT word2, count FROM word_pairs WHERE word1 = ?'; + let params = [currentWord]; + + if (targetNick) { + query += ' AND nick = ?'; + params.push(targetNick); + } + + query += ' ORDER BY RANDOM() LIMIT 15'; // More options for creative mode + + this.db.all(query, params, (err, rows) => { + if (err) { + console.error('โŒ Database error during generation:', err.message); + callback('Database error: ' + err.message); + return; + } + + if (rows.length === 0) { + // If stuck, try jumping to a random word + console.log(`๐Ÿ”„ No next words for "${currentWord}", trying random jump`); + + // In creative mode, try mixing users more often + const mixUsers = creative && Math.random() < 0.4; + const jumpTargetNick = mixUsers ? null : targetNick; + + this.getRandomWord((err, randomWord) => { + if (err || !randomWord || randomWord === '' || randomWord === '') { + if (result.length >= 3) { + const prefix = targetNick ? `[${targetNick}] ` : ''; + const emoji = creative ? '๐ŸŽจ ' : '๐Ÿค– '; + callback(null, prefix + emoji + result.join(' ')); + } else { + callback('Generation incomplete'); + } + return; + } + + result.push(randomWord); + currentWord = randomWord; + console.log(`๐ŸŽฒ Random jump to: "${randomWord}"${mixUsers ? ' (mixed user)' : ''}`); + getNext(); + }, jumpTargetNick); + return; + } + + // Different selection strategies for creative vs normal mode + let nextWord; + + if (creative) { + // Creative mode: Much more chaos and randomness + const randomChoice = Math.random(); + + if (randomChoice < 0.6) { + // 60% chance: Pick rare/uncommon words for weirdness + const uncommon = rows.filter(r => r.count <= 2); + if (uncommon.length > 0) { + nextWord = uncommon[Math.floor(Math.random() * uncommon.length)].word2; + console.log(`๐ŸŽจ Weird choice: "${nextWord}"`); + } else { + // If no uncommon words, pick any random one + nextWord = rows[Math.floor(Math.random() * rows.length)].word2; + console.log(`๐ŸŽจ Random fallback: "${nextWord}"`); + } + } else if (randomChoice < 0.8) { + // 20% chance: Pick completely random word (ignore context) + self.getRandomWord((err, randomWord) => { + if (!err && randomWord && randomWord !== '' && randomWord !== '') { + result.push(randomWord); + currentWord = randomWord; + console.log(`๐ŸŽจ Context break: "${randomWord}"`); + getNext(); + return; + } + // Fallback to normal selection + nextWord = rows[Math.floor(Math.random() * rows.length)].word2; + console.log(`๐ŸŽจ Fallback random: "${nextWord}"`); + processNextWord(); + }, null); // null = any user for maximum chaos + return; + } else { + // 20% chance: Normal weighted selection for some coherence + const totalWeight = rows.reduce((sum, row) => sum + row.count, 0); + let random = Math.random() * totalWeight; + + nextWord = rows[0].word2; + for (const row of rows) { + random -= row.count; + if (random <= 0) { + nextWord = row.word2; + break; + } + } + console.log(`๐ŸŽจ Weighted choice: "${nextWord}"`); + } + } else { + // Normal mode: Original logic + if (Math.random() < 0.3) { + // 30% chance: Pick a less common word for creativity + const lessCommon = rows.filter(r => r.count <= 2); + if (lessCommon.length > 0) { + nextWord = lessCommon[Math.floor(Math.random() * lessCommon.length)].word2; + console.log(`๐ŸŽจ Creative choice: "${nextWord}"`); + } else { + nextWord = rows[Math.floor(Math.random() * Math.min(5, rows.length))].word2; + } + } else { + // 70% chance: Normal weighted selection + const totalWeight = rows.reduce((sum, row) => sum + row.count, 0); + let random = Math.random() * totalWeight; + + nextWord = rows[0].word2; + for (const row of rows) { + random -= row.count; + if (random <= 0) { + nextWord = row.word2; + break; + } + } + } + } + + processNextWord(); + + function processNextWord() { + console.log(`๐Ÿ”— Selected next word: "${nextWord}"`); + + // Check for end + if (nextWord === '') { + if (result.length >= 3) { + const prefix = targetNick ? `[${targetNick}] ` : ''; + const emoji = creative ? '๐ŸŽจ ' : '๐Ÿค– '; + callback(null, prefix + emoji + result.join(' ')); + } else { + // Too short, try to continue with random word + self.getRandomWord((err, randomWord) => { + if (!err && randomWord && randomWord !== '' && randomWord !== '') { + result.push(randomWord); + currentWord = randomWord; + getNext(); + } else { + const prefix = targetNick ? `[${targetNick}] ` : ''; + const emoji = creative ? '๐ŸŽจ ' : '๐Ÿค– '; + callback(null, prefix + emoji + result.join(' ')); + } + }, targetNick); + } + return; + } + + // Add word and continue + if (nextWord !== '') { + result.push(nextWord); + } + + currentWord = nextWord; + getNext(); + } + }); + }; + + getNext(); + }, targetNick); + }, + + getRandomStartWord(callback, targetNick = null) { + if (!this.db) { + callback('No database available'); + return; + } + + // 50% chance to start with , 50% chance to start with random word + if (Math.random() < 0.5) { + callback(null, ''); + return; + } + + // Build query for random word that appears as word1 + let query = 'SELECT word1 FROM word_pairs WHERE word1 != "" AND word1 != ""'; + let params = []; + + if (targetNick) { + query += ' AND nick = ?'; + params.push(targetNick); + } + + query += ' ORDER BY RANDOM() LIMIT 1'; + + this.db.get(query, params, (err, row) => { + if (err || !row) { + callback(null, ''); // Fallback + } else { + callback(null, row.word1); + } + }); + }, + + getRandomWord(callback, targetNick = null) { + if (!this.db) { + callback('No database available'); + return; + } + + let query = 'SELECT word2 FROM word_pairs WHERE word2 != "" AND word2 != ""'; + let params = []; + + if (targetNick) { + query += ' AND nick = ?'; + params.push(targetNick); + } + + query += ' ORDER BY RANDOM() LIMIT 1'; + + this.db.get(query, params, (err, row) => { + if (err || !row) { + callback('No random word found'); + } else { + callback(null, row.word2); + } + }); + }, + + getStats(callback) { + if (!this.db) { + callback('No database available'); + return; + } + + this.db.get('SELECT COUNT(*) as messages FROM messages', (err, msgRow) => { + if (err) { + callback('Database error: ' + err.message); + return; + } + + this.db.get('SELECT COUNT(*) as pairs FROM word_pairs', (err, pairRow) => { + if (err) { + callback('Database error: ' + err.message); + return; + } + + this.db.get('SELECT COUNT(DISTINCT nick) as users FROM messages', (err, userRow) => { + if (err) { + callback('Database error: ' + err.message); + return; + } + + callback(null, { + messages: msgRow.messages, + pairs: pairRow.pairs, + users: userRow.users + }); + }); + }); + }); + }, + + // This is the key - message event handler + onMessage(data, bot) { + console.log(`๐Ÿ“จ Babble received message from ${data.nick}: "${data.message}"`); + + if (data.isChannel) { + this.storeMessage(data.nick, data.target, data.message); + } + }, + + commands: [ + { + name: 'babble', + description: 'Generate random text from learned messages, optionally like a specific user', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + // Check if user specified a target nick + const targetNick = args.length > 0 ? args[0] : null; + + plugin.generateText((err, text) => { + if (err) { + bot.say(target, `${from}: ${err}`); + } else { + bot.say(target, text); + } + }, targetNick, false); + } + }, + + { + name: 'babblecreative', + description: 'Generate weird, funny, and strange text with maximum chaos', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + // Check if user specified a target nick + const targetNick = args.length > 0 ? args[0] : null; + + plugin.generateText((err, text) => { + if (err) { + bot.say(target, `${from}: ${err}`); + } else { + bot.say(target, text); + } + }, targetNick, true); // true = creative mode + } + }, + + { + name: 'babblestats', + description: 'Show babble learning statistics', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + plugin.getStats((err, stats) => { + if (err) { + bot.say(target, `Error: ${err}`); + } else { + bot.say(target, `๐Ÿง  Learned: ${stats.messages} messages, ${stats.pairs} word pairs, ${stats.users} users`); + } + }); + } + }, + + { + name: 'babbleusers', + description: 'Show users available for targeted babbling', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + if (!plugin.db) { + bot.say(target, 'No database available'); + return; + } + + plugin.db.all( + 'SELECT nick, COUNT(*) as word_count FROM word_pairs WHERE nick IS NOT NULL GROUP BY nick ORDER BY word_count DESC LIMIT 10', + (err, rows) => { + if (err) { + bot.say(target, 'Database error: ' + err.message); + return; + } + + if (rows.length === 0) { + bot.say(target, '๐Ÿค– No users found in babble database yet.'); + return; + } + + const userList = rows.map(row => `${row.nick}(${row.word_count})`).join(', '); + bot.say(target, `๐ŸŽญ Available users: ${userList}`); + bot.say(target, `๐Ÿ’ก Usage: !babble or !babblecreative `); + } + ); + } + }, + + { + name: 'babbleadd', + description: 'Manually add a message to learning database', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + if (args.length === 0) { + bot.say(target, `Usage: !babbleadd `); + return; + } + + const message = args.join(' '); + plugin.storeMessage(from, target, message); + bot.say(target, `${from}: Added message to learning database`); + } + }, + + { + name: 'babbletest', + description: 'Add test data to verify babble is working', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + const testMessages = [ + 'hello world how are you doing today', + 'i love programming with javascript and node', + 'the weather is really nice outside today', + 'coding is fun but sometimes challenging', + 'artificial intelligence is fascinating technology' + ]; + + testMessages.forEach(msg => { + plugin.storeMessage('testuser', target, msg); + }); + + bot.say(target, `${from}: Added ${testMessages.length} test messages. Try !babble in a few seconds.`); + } + }, + + { + name: 'babblereset', + description: 'Reset all babble data (admin only)', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + const adminNicks = ['admin', 'owner', 'cancerbot', 'megasconed']; + + if (!adminNicks.includes(from)) { + bot.say(target, `${from}: Access denied - admin only command`); + return; + } + + if (!plugin.db) { + bot.say(target, `${from}: No database available`); + return; + } + + plugin.db.run('DELETE FROM messages', (err) => { + if (err) { + bot.say(target, `${from}: Error clearing messages: ${err.message}`); + return; + } + + plugin.db.run('DELETE FROM word_pairs', (err) => { + if (err) { + bot.say(target, `${from}: Error clearing word pairs: ${err.message}`); + } else { + bot.say(target, `${from}: All babble data cleared`); + } + }); + }); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/basic.js b/plugins/basic.js new file mode 100644 index 0000000..bbcda09 --- /dev/null +++ b/plugins/basic.js @@ -0,0 +1,38 @@ +// plugins/basic.js - Basic commands plugin +module.exports = { + init(bot) { + console.log('Basic plugin initialized'); + }, + + cleanup(bot) { + console.log('Basic plugin cleaned up'); + }, + + commands: [ + { + name: 'ping', + description: 'Responds with pong', + execute(context, bot) { + bot.say(context.replyTo, `${context.nick}: pong!`); + } + }, + + { + name: 'help', + description: 'Shows available commands', + execute(context, bot) { + const commands = Array.from(bot.commands.keys()); + bot.say(context.replyTo, `Available commands: ${commands.join(', ')}`); + } + }, + + { + name: 'time', + description: 'Shows current time', + execute(context, bot) { + const now = new Date().toLocaleString(); + bot.say(context.replyTo, `Current time: ${now}`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/bots_reply_plugin.js b/plugins/bots_reply_plugin.js new file mode 100644 index 0000000..a9c3247 --- /dev/null +++ b/plugins/bots_reply_plugin.js @@ -0,0 +1,19 @@ +// plugins/bots-reply.js - Plugin to respond to .bots +module.exports = { + init(bot) { + console.log('Bots reply plugin initialized'); + }, + + cleanup(bot) { + console.log('Bots reply plugin cleaned up'); + }, + + // Handle incoming messages + onMessage(data, bot) { + // Check if message is exactly ".bots" + if (data.message.trim() === '.bots') { + // Reply to the channel or private message + bot.say(data.replyTo, '๐Ÿค– Beep boop! I\'m a shitty Node.js bot powered by vibes'); + } + } +}; \ No newline at end of file diff --git a/plugins/coinflip_plugin.js b/plugins/coinflip_plugin.js new file mode 100644 index 0000000..866acfa --- /dev/null +++ b/plugins/coinflip_plugin.js @@ -0,0 +1,337 @@ +// plugins/coinflip.js - Coin Flip Casino game plugin +const fs = require('fs'); +const path = require('path'); + +module.exports = { + init(bot) { + console.log('Coin Flip Casino plugin initialized'); + this.bot = bot; + + // Persistent player statistics (saved to file) + this.playerStats = new Map(); // nick -> { totalWins, totalLosses, totalFlips, biggestWin, winStreak, bestStreak, name } + + // Set up score file path + this.scoresFile = path.join(__dirname, 'coinflip_scores.json'); + + // Load existing scores + this.loadScores(); + }, + + cleanup(bot) { + console.log('Coin Flip Casino plugin cleaned up'); + // Save scores before cleanup + this.saveScores(); + }, + + // 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} coin flip 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.totalWins - a.totalWins) + .slice(0, 3); + console.log('๐Ÿ† Top coin flippers:', topPlayers.map(p => `${p.name}(${p.totalWins}W)`).join(', ')); + } + } else { + console.log(`๐Ÿช™ No existing coin flip scores file found, starting fresh`); + } + } catch (error) { + console.error(`โŒ Error loading coin flip 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} coin flip player stats to ${this.scoresFile}`); + } catch (error) { + console.error(`โŒ Error saving coin flip 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, { + totalWins: 0, + totalLosses: 0, + totalFlips: 0, + biggestWin: 0, + winStreak: 0, + bestStreak: 0, + name: nick + }); + } + + const player = this.playerStats.get(nick); + Object.assign(player, updates); + + // Save to file after each update + this.saveScores(); + }, + + // Flip the coin and return result + flipCoin() { + return Math.random() < 0.5 ? 'heads' : 'tails'; + }, + + // Calculate win amount based on bet + calculateWinnings(bet, isWin) { + if (!isWin) return 0; + + // Simple 1:1 payout for coin flip + return bet; + }, + + commands: [ + { + name: 'flip', + description: 'Flip a coin and bet on the outcome', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + // Parse command: !flip [bet] + if (args.length === 0) { + bot.say(target, `Usage: !flip [bet] - Example: !flip heads 5`); + return; + } + + const guess = args[0].toLowerCase(); + if (guess !== 'heads' && guess !== 'tails') { + bot.say(target, `${from}: Choose 'heads' or 'tails'`); + return; + } + + // Parse bet amount (default to 1) + let bet = 1; + if (args.length > 1) { + bet = parseInt(args[1]); + if (isNaN(bet) || bet < 1 || bet > 100) { + bot.say(target, `${from}: Bet must be between 1 and 100`); + return; + } + } + + // Initialize player if new + if (!plugin.playerStats.has(from)) { + plugin.updatePlayerStats(from, { + totalWins: 0, + totalLosses: 0, + totalFlips: 0, + biggestWin: 0, + winStreak: 0, + bestStreak: 0, + name: from + }); + } + + // Flip the coin + const result = plugin.flipCoin(); + const isWin = guess === result; + const winnings = plugin.calculateWinnings(bet, isWin); + + // Update player stats + const player = plugin.playerStats.get(from); + const updates = { + totalFlips: player.totalFlips + 1 + }; + + if (isWin) { + updates.totalWins = player.totalWins + 1; + updates.winStreak = player.winStreak + 1; + + // Update best streak + if (updates.winStreak > player.bestStreak) { + updates.bestStreak = updates.winStreak; + } + + // Update biggest win + if (winnings > player.biggestWin) { + updates.biggestWin = winnings; + } + } else { + updates.totalLosses = player.totalLosses + 1; + updates.winStreak = 0; // Reset streak on loss + } + + // Choose coin flip animation + const coinEmojis = ['๐Ÿช™', '๐Ÿฅ‡', '๐Ÿ’ฐ']; + const coinEmoji = coinEmojis[Math.floor(Math.random() * coinEmojis.length)]; + + // Result message with color coding + let resultEmoji, resultText; + if (result === 'heads') { + resultEmoji = '๐Ÿ‘‘'; + resultText = 'HEADS'; + } else { + resultEmoji = '๐Ÿฆ…'; + resultText = 'TAILS'; + } + + // Create result message + let output = `${coinEmoji} ${from}: ${resultEmoji} ${resultText}! `; + + if (isWin) { + output += `โœ… WIN! +${winnings} pts`; + if (updates.winStreak > 1) { + output += ` | ๐Ÿ”ฅ ${updates.winStreak} streak!`; + } + } else { + output += `โŒ LOSE! -${bet} pts`; + } + + // Add totals + output += ` | W:${updates.totalWins} L:${updates.totalLosses}`; + + bot.say(target, output); + plugin.updatePlayerStats(from, updates); + } + }, + + { + name: 'flipstats', + description: 'Show your coin flip statistics', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + if (!plugin.playerStats.has(from)) { + bot.say(target, `${from}: You haven't flipped any coins yet! Use !flip to start.`); + return; + } + + const player = plugin.playerStats.get(from); + const winRate = player.totalFlips > 0 ? ((player.totalWins / player.totalFlips) * 100).toFixed(1) : 0; + + bot.say(target, `๐Ÿช™ ${from}: ${player.totalWins}W/${player.totalLosses}L (${winRate}%) | Best: ${player.biggestWin} | Streak: ${player.winStreak} (best: ${player.bestStreak}) | Flips: ${player.totalFlips}`); + } + }, + + { + name: 'topflip', + description: 'Show top coin flip players', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + if (plugin.playerStats.size === 0) { + bot.say(target, 'No coin flip scores recorded yet! Use !flip to start playing.'); + return; + } + + // Get top players by different metrics + const byWins = Array.from(plugin.playerStats.values()) + .sort((a, b) => b.totalWins - a.totalWins) + .slice(0, 3); + + const byStreak = Array.from(plugin.playerStats.values()) + .sort((a, b) => b.bestStreak - a.bestStreak) + .slice(0, 1); + + let output = '๐Ÿ† Top Flippers: '; + byWins.forEach((player, i) => { + const trophy = i === 0 ? '๐Ÿฅ‡' : i === 1 ? '๐Ÿฅˆ' : '๐Ÿฅ‰'; + const winRate = player.totalFlips > 0 ? ((player.totalWins / player.totalFlips) * 100).toFixed(0) : 0; + output += `${trophy}${player.name}(${player.totalWins}W/${winRate}%) `; + }); + + if (byStreak[0] && byStreak[0].bestStreak > 0) { + output += `| ๐Ÿ”ฅ Best Streak: ${byStreak[0].name}(${byStreak[0].bestStreak})`; + } + + bot.say(target, output); + } + }, + + { + name: 'fliptest', + description: 'Test coin fairness with multiple flips', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + // Parse number of flips (default 10, max 100) + let numFlips = 10; + if (args.length > 0) { + numFlips = parseInt(args[0]); + if (isNaN(numFlips) || numFlips < 1 || numFlips > 100) { + bot.say(target, `${from}: Number of flips must be between 1 and 100`); + return; + } + } + + // Perform multiple flips + let heads = 0; + let tails = 0; + let sequence = ''; + + for (let i = 0; i < numFlips; i++) { + const result = plugin.flipCoin(); + if (result === 'heads') { + heads++; + sequence += '๐Ÿ‘‘'; + } else { + tails++; + sequence += '๐Ÿฆ…'; + } + } + + const headsPercent = ((heads / numFlips) * 100).toFixed(1); + const tailsPercent = ((tails / numFlips) * 100).toFixed(1); + + bot.say(target, `๐Ÿช™ ${from}: ${numFlips} flips | ๐Ÿ‘‘ ${heads} (${headsPercent}%) | ๐Ÿฆ… ${tails} (${tailsPercent}%)`); + bot.say(target, `Sequence: ${sequence}`); + } + }, + + { + name: 'resetflip', + description: 'Reset all coin flip 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} coin flip player stats`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/coinflip_scores.json b/plugins/coinflip_scores.json new file mode 100644 index 0000000..a94f6fa --- /dev/null +++ b/plugins/coinflip_scores.json @@ -0,0 +1,11 @@ +{ + "megasconed": { + "totalWins": 0, + "totalLosses": 3, + "totalFlips": 3, + "biggestWin": 0, + "winStreak": 0, + "bestStreak": 0, + "name": "megasconed" + } +} \ No newline at end of file diff --git a/plugins/combo_plugin.js b/plugins/combo_plugin.js new file mode 100644 index 0000000..0057509 --- /dev/null +++ b/plugins/combo_plugin.js @@ -0,0 +1,339 @@ +// 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'}`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/conspiracy_generator_plugin(1).js b/plugins/conspiracy_generator_plugin(1).js new file mode 100644 index 0000000..7b6a054 --- /dev/null +++ b/plugins/conspiracy_generator_plugin(1).js @@ -0,0 +1,166 @@ +// plugins/conspiracy-generator.js - Conspiracy Theory Generator Plugin +module.exports = { + init(bot) { + console.log('Conspiracy Generator plugin initialized'); + }, + + cleanup(bot) { + console.log('Conspiracy Generator plugin cleaned up'); + }, + + commands: [ + { + name: 'conspiracy', + description: 'Generates a wild conspiracy theory', + execute(context, bot) { + const theory = module.exports.generateConspiracy(); + bot.say(context.replyTo, `๐Ÿ•ต๏ธ CONSPIRACY ALERT: ${theory}`); + } + }, + + { + name: 'deepstate', + description: 'Generates a deep state conspiracy', + execute(context, bot) { + const theory = module.exports.generateDeepStateConspiracy(); + bot.say(context.replyTo, `๐Ÿ›๏ธ DEEP STATE EXPOSED: ${theory}`); + } + }, + + { + name: 'aliens', + description: 'Generates an alien conspiracy', + execute(context, bot) { + const theory = module.exports.generateAlienConspiracy(); + bot.say(context.replyTo, `๐Ÿ‘ฝ ALIEN DISCLOSURE: ${theory}`); + } + } + ], + + generateConspiracy() { + const entities = [ + 'The government', 'Big Tech', 'The Illuminati', 'Lizard people', 'The media', + 'Scientists', 'Your ISP', 'Netflix', 'The postal service', 'Spotify', + 'Discord mods', 'Wikipedia editors', 'McDonald\'s', 'Amazon', 'Your phone', + 'Smart TVs', 'The weather app', 'Dating apps', 'Social media influencers', + 'Coffee shops', 'Your smart fridge', 'GPS satellites', 'Streaming services' + ]; + + const methods = [ + 'is using', 'has been secretly controlling', 'is manipulating', 'has been hiding', + 'is programming', 'has been infiltrating', 'is weaponizing', 'has been monitoring', + 'is experimenting with', 'has been harvesting', 'is controlling', 'has been replacing', + 'is encoding messages in', 'has been mind-controlling people through', 'is tracking you via', + 'has been brainwashing people with', 'is stealing your data through', 'has been plotting using' + ]; + + const objects = [ + 'banana peels', 'Wi-Fi signals', 'pizza toppings', 'your sleep schedule', 'memes', + 'your Spotify playlists', 'traffic lights', 'automatic toilets', 'elevator music', + 'your weird dreams', 'typos in your texts', 'loading screens', 'captcha puzzles', + 'your phone battery', 'microwave radiation', 'bluetooth connections', 'your autocorrect', + 'the way USB cables work', 'your Netflix recommendations', 'ads for things you just talked about', + 'the fact that you always lose one sock', 'why your earbuds get tangled', 'construction zones' + ]; + + const purposes = [ + 'to control your mind', 'to steal your personal data', 'to make you buy more stuff', + 'to influence elections', 'to create the perfect consumer', 'to track your movements', + 'to predict your behavior', 'to control the weather', 'to make you addicted to social media', + 'to harvest your emotional energy', 'to test new forms of mind control', 'to create chaos', + 'to distract you from the real truth', 'to program you to like pineapple on pizza', + 'to make you forget important things', 'to synchronize your blinks with 5G towers', + 'to collect your personal conversations', 'to influence your dating choices', + 'to make you perpetually tired', 'to control your coffee consumption', 'to manipulate your mood', + 'to create artificial scarcity', 'to make you question reality' + ]; + + const entity = module.exports.randomChoice(entities); + const method = module.exports.randomChoice(methods); + const object = module.exports.randomChoice(objects); + const purpose = module.exports.randomChoice(purposes); + + return `${entity} ${method} ${object} ${purpose}! Wake up, sheeple! ๐Ÿ‘`; + }, + + generateDeepStateConspiracy() { + const organizations = [ + 'The Shadow Council', 'The Breakfast Club Illuminati', 'The Committee of Suspicious Cats', + 'The Order of the Sacred Meme', 'The Brotherhood of Lost Socks', 'The Cabal of Midnight Snackers', + 'The Society of People Who Press Elevator Buttons Multiple Times', 'The Guild of Left Shoe Conspirators' + ]; + + const plans = [ + 'has been secretly replacing all birds with government drones', + 'is controlling global weather patterns through strategic cloud seeding with glitter', + 'has been programming trees to spy on park conversations', + 'is using grocery store self-checkout machines to scan your soul', + 'has been hiding interdimensional portals in public restrooms', + 'is controlling the stock market through interpretive dance', + 'has been encoding secret messages in elevator music', + 'is using traffic cones to create a massive summoning circle' + ]; + + const evidence = [ + 'Ever notice how pigeons always know where you are?', + 'Why do you think they call it "cloud" storage?', + 'Have you ever seen a baby pigeon? Exactly.', + 'Think about it - when was the last time you saw a bird mechanic?', + 'Why else would they put cameras in every corner?', + 'The evidence is literally falling from the sky (bird poop = tracking powder)', + 'Wake up! Your microwave has been judging your food choices!', + 'The dots are connecting themselves - that\'s not normal!' + ]; + + const org = module.exports.randomChoice(organizations); + const plan = module.exports.randomChoice(plans); + const proof = module.exports.randomChoice(evidence); + + return `${org} ${plan}. ${proof} Connect the dots! ๐Ÿ”—`; + }, + + generateAlienConspiracy() { + const alienTypes = [ + 'Interdimensional beings', 'Time-traveling dolphins', 'Shapeshifting houseplants', + 'Quantum squirrels', 'The real owners of Area 51', 'Alien IT support staff', + 'Extraterrestrial food critics', 'Space cats', 'Cosmic customer service representatives', + 'Intergalactic middle management' + ]; + + const activities = [ + 'have been secretly running all tech support call centers', + 'are the ones who designed traffic patterns in major cities', + 'have been controlling human fashion trends for decades', + 'are responsible for why printers never work when you need them', + 'have been programming humans to form orderly queues', + 'are the reason why you can never find matching socks', + 'have been studying human behavior through reality TV shows', + 'are the ones making sure your phone battery dies at the worst possible moment', + 'have been controlling global meme distribution networks', + 'are secretly running all social media algorithms' + ]; + + const reasons = [ + 'to prepare Earth for the great intergalactic customer satisfaction survey', + 'because they find human confusion highly entertaining', + 'to collect data on human inefficiency for their research', + 'as part of a cosmic social experiment about patience', + 'because they\'re writing the ultimate guide to human behavior', + 'to test if humans can evolve beyond needing instruction manuals', + 'as payment for their cosmic Netflix subscription', + 'because they lost a bet with another alien species', + 'to determine if humans are ready for advanced technology (spoiler: we\'re not)', + 'because they\'re cosmic anthropologists studying "primitive" civilizations' + ]; + + const aliens = module.exports.randomChoice(alienTypes); + const activity = module.exports.randomChoice(activities); + const reason = module.exports.randomChoice(reasons); + + return `${aliens} ${activity} ${reason}. The truth is out there! ๐Ÿ›ธ`; + }, + + randomChoice(array) { + return array[Math.floor(Math.random() * array.length)]; + } +}; \ No newline at end of file diff --git a/plugins/drunk_historian_plugin.js b/plugins/drunk_historian_plugin.js new file mode 100644 index 0000000..f0b56a1 --- /dev/null +++ b/plugins/drunk_historian_plugin.js @@ -0,0 +1,156 @@ +// plugins/drunk-historian.js - Drunk Historian Bot Plugin +module.exports = { + drunkLevel: 0, // Gets progressively more drunk with each story + + init(bot) { + console.log('Drunk Historian plugin initialized - *hic*'); + this.drunkLevel = 0; + }, + + cleanup(bot) { + console.log('Drunk Historian plugin cleaned up - time to sober up!'); + }, + + commands: [ + { + name: 'history', + description: 'Tells a historical "fact" (gets progressively more drunk)', + execute(context, bot) { + const story = module.exports.tellHistory(); + bot.say(context.replyTo, story); + module.exports.drunkLevel++; + } + }, + + { + name: 'ancienthistory', + description: 'Tells ancient history while very drunk', + execute(context, bot) { + module.exports.drunkLevel = Math.max(5, module.exports.drunkLevel); + const story = module.exports.tellAncientHistory(); + bot.say(context.replyTo, story); + module.exports.drunkLevel++; + } + }, + + { + name: 'soberup', + description: 'Historian tries to sober up', + execute(context, bot) { + const oldLevel = module.exports.drunkLevel; + module.exports.drunkLevel = Math.max(0, module.exports.drunkLevel - 3); + if (oldLevel > 0) { + bot.say(context.replyTo, '๐Ÿบโžก๏ธโ˜• *drinks coffee* Okay, okay... let me try to get my facts straight here... *squints at history books*'); + } else { + bot.say(context.replyTo, '๐Ÿ“š I\'m completely sober! Ready to tell you some REAL history! ...for now.'); + } + } + }, + + { + name: 'drunkness', + description: 'Check how drunk the historian is', + execute(context, bot) { + const level = module.exports.drunkLevel; + let status = ''; + if (level === 0) status = '๐Ÿ“š Completely sober and scholarly'; + else if (level <= 2) status = '๐Ÿป Slightly tipsy, facts mostly accurate'; + else if (level <= 4) status = '๐Ÿบ Getting wobbly, mixing up dates'; + else if (level <= 6) status = '๐Ÿฅด Pretty drunk, inventing new historical figures'; + else if (level <= 8) status = '๐Ÿท Very drunk, history is more like fantasy now'; + else status = '๐Ÿคช Completely wasted, pure nonsense incoming'; + + bot.say(context.replyTo, `๐ŸŽญ Historian\'s current state: ${status} (Level ${level})`); + } + } + ], + + tellHistory() { + const level = this.drunkLevel; + + if (level === 0) { + return this.getSoberHistory(); + } else if (level <= 2) { + return this.getSlightlyDrunkHistory(); + } else if (level <= 4) { + return this.getModeratelyDrunkHistory(); + } else if (level <= 6) { + return this.getVeryDrunkHistory(); + } else { + return this.getCompletelyWastedHistory(); + } + }, + + getSoberHistory() { + const facts = [ + '๐Ÿ“š In 1969, Neil Armstrong became the first human to walk on the moon, famously saying "That\'s one small step for man, one giant leap for mankind."', + '๐Ÿ“š The Great Wall of China was built over many centuries, primarily during the Ming Dynasty (1368-1644), to protect against northern invasions.', + '๐Ÿ“š The Renaissance began in Italy during the 14th century and marked a cultural rebirth in art, science, and literature.', + '๐Ÿ“š World War II ended in 1945 after the atomic bombings of Hiroshima and Nagasaki led to Japan\'s surrender.', + '๐Ÿ“š The Roman Empire fell in 476 AD when the Germanic chieftain Odoacer deposed the last Western Roman Emperor.' + ]; + return this.randomChoice(facts); + }, + + getSlightlyDrunkHistory() { + const facts = [ + '๐Ÿป So like, Napoleon was this short French guy... wait, actually he wasn\'t that short, that\'s a myth! *hic* Anyway, he conquered most of Europe and then... uh... Russia happened. Bad idea, Nappy!', + '๐Ÿป The Titanic sank in 1912 because... well, icebergs are basically the ocean\'s way of saying "surprise!" *hic* They said it was unsinkable. Spoiler alert: it sank.', + '๐Ÿป Vikings! *hic* They had horned helmets... no wait, that\'s totally made up. They were actually pretty clean people who traded a lot. But the raiding thing was real. Very real.', + '๐Ÿป Ancient Egypt... pyramids... aliens? No, wait, that\'s not history, that\'s conspiracy theories. *hic* They just had really good engineers and lots of slaves. Sad but true.', + '๐Ÿป Christopher Columbus "discovered" America in 1492, except... *hic* ...there were already people there! Awkward! Also he thought he was in India. GPS would have helped, Chris.' + ]; + return this.randomChoice(facts); + }, + + getModeratelyDrunkHistory() { + const facts = [ + '๐Ÿบ *swaying* OK so... Joan of Arc... she was this French teenager who heard voices... and somehow convinced the king to let her lead an army?? *hic* Like, imagine explaining that to your parents! "Mom, God told me to go fight the English!" Wild times, man.', + '๐Ÿบ The Black Death killed like... *counts on fingers* ...a LOT of people in medieval Europe. Like 30-60% of the population! *hic* But hey, afterwards wages went up because there weren\'t enough workers! Economic supply and demand, baby!', + '๐Ÿบ Benjamin Franklin... *hic* ...this guy was like the ultimate life hacker! Scientist, inventor, diplomat, AND he convinced French people to help America fight Britain! Plus he flew a kite in a storm like a absolute madman! โšก', + '๐Ÿบ *hiccup* The Great Fire of London in 1666... started in a BAKERY! *laughing* Imagine being that baker! "Honey, I may have accidentally burned down the entire city..." But hey, they rebuilt it better!', + '๐Ÿบ Cleopatra... last pharaoh of Egypt... *hic* ...she was actually Greek, not Egyptian! Plot twist! Also she spoke like 9 languages and was probably smarter than most people today. Respect! ๐Ÿ‘‘' + ]; + return this.randomChoice(facts); + }, + + getVeryDrunkHistory() { + const facts = [ + '๐Ÿฅด *stumbling* Lemme tell you about... about Julius Caesar! This guy... *hic* ...he crossed some river with his army and said something about dice! Then he conquered Gaul, which I think is France but with more... more... what\'s the word... GAULS! Then his best friend stabbed him! Et tu, Brute! BETRAYAL! *dramatically gestures*', + '๐Ÿฅด The... the Boston Tea Party! *hic* So these Americans were mad about taxes... so they dressed up as NATIVE AMERICANS and threw perfectly good tea into the harbor! Do you know how much tea costs?! The British were SO MAD! *giggles* It\'s like the world\'s most expensive prank!', + '๐Ÿฅด *nearly falling over* Alexander the Great! This kid... he was like... 20-something and conquered THE ENTIRE KNOWN WORLD! *hic* But then he died at 32, probably from partying too hard! Or poison. Or both! Kids these days... no, wait, that was 2000 years ago...', + '๐Ÿฅด The Dancing Plague of 1518! *twirling around* People in France just started dancing... and couldn\'t stop! For DAYS! Some even danced themselves to death! *hic* It\'s like the world\'s deadliest dance party! What was in the medieval water supply?!', + '๐Ÿฅด *slurring* Henry the Eighth... this guy had SIX WIVES! Six! And he kept... *makes chopping motion* ...off with their heads! Divorce wasn\'t enough for this maniac! Anne Boleyn probably should\'ve swiped left... if Tinder existed in 1536...' + ]; + return this.randomChoice(facts); + }, + + getCompletelyWastedHistory() { + const facts = [ + '๐Ÿคช *completely incoherent* Listen... LISTEN... George Washington... he had WOODEN TEETH! But get this... *hic* ...they weren\'t wood! They were HIPPO IVORY! And human teeth! FROM OTHER PEOPLE! *screaming* THE FIRST PRESIDENT WAS A TOOTH VAMPIRE! Also he grew marijuana! No wait, that was hemp... same plant... different vibes... *collapses*', + '๐Ÿคช *babbling* The Library of Alexandria burned down and we lost ALL THE KNOWLEDGE! *sobbing* Do you know what was in there?! The cure for hangovers! The secret to immortality! Cat memes from ancient Egypt! EVERYTHING! *hic* It was like if the internet got deleted but WITH FIRE!', + '๐Ÿคช *flailing arms* SPARTACUS! This gladiator slave said "NAH FAM" to the Roman Empire and started a rebellion with KITCHEN UTENSILS! *hic* Imagine explaining to Caesar that you got defeated by a guy with a SPOON! "Sir, the slaves are revolting!" "I know, they smell terrible!" "No sir, I mean they\'re ACTUALLY revolting!"', + '๐Ÿคช *falling over* The Trojan War happened because... because... PARIS STOLE HELEN! But not the city Paris, a PERSON named Paris! *hic* So they built a GIANT WOODEN HORSE and hid inside it like some ancient game of hide and seek! "Guys, should we trust this random horse?" "Yeah, what could go wrong?" SPOILER: EVERYTHING WENT WRONG!', + '๐Ÿคช *completely gone* Viking berserkers ate magic mushrooms and fought bears... NAKED! *hic* NAKED MUSHROOM WARRIORS! They probably invented extreme sports! "Hey Olaf, want to sail across the ocean in a wooden boat and raid monasteries?" "SURE BJร–RN, BUT FIRST LET\'S EAT THESE WEIRD MUSHROOMS I FOUND!" *passes out*' + ]; + return this.randomChoice(facts); + }, + + tellAncientHistory() { + const level = Math.max(5, this.drunkLevel); // Force drunk level for ancient history + + const ancientFacts = [ + '๐Ÿคช *extremely drunk* Ancient Mesopotamia invented BEER! *hic* They literally wrote the first recipes! Forget the wheel, forget writing... BEER was the real breakthrough! Civilization started because humans wanted to get drunk together! WE ARE ALL DESCENDANTS OF ANCIENT PARTY ANIMALS!', + '๐Ÿคช *slurring badly* The ancient Egyptians... they mummified EVERYTHING! Cats, dogs, birds, crocodiles... *hic* There\'s probably a mummified ancient Egyptian somewhere who got mummified while drunk and is STILL HUNG OVER in the afterlife! "Osiris, my head hurts..." "You\'ve been dead for 4000 years, Kevin!"', + '๐Ÿคช *barely standing* Ancient Greeks invented DEMOCRACY! But only like... 10% of people could vote! *hic* It\'s like having a pizza party but only the pepperoni slices get to decide what toppings to order! Also they did EVERYTHING naked! Olympics, philosophy, probably grocery shopping! ANCIENT NUDIST DEMOCRACY!', + '๐Ÿคช *completely wasted* Stonehenge! *waves arms wildly* Nobody knows what it is! Ancient calendar? Alien landing pad? The world\'s first escape room? *hic* Maybe ancient druids just got really drunk and said "You know what this field needs? GIANT MYSTERIOUS ROCKS!" *falls over*', + '๐Ÿคช *incoherent rambling* Ancient Romans had GLADIATOR FIGHTS for entertainment! *hic* It\'s like WWE but with ACTUAL DEATH! "And his name is JOHN SPARTACUS!" *makes crowd noises* They also had public bathrooms with NO PRIVACY! Just a row of holes! Ancient Romans had no concept of personal space OR personal time!' + ]; + + return this.randomChoice(ancientFacts); + }, + + randomChoice(array) { + return array[Math.floor(Math.random() * array.length)]; + } +}; \ No newline at end of file diff --git a/plugins/duck_hunt_plugin.js b/plugins/duck_hunt_plugin.js new file mode 100644 index 0000000..4af637f --- /dev/null +++ b/plugins/duck_hunt_plugin.js @@ -0,0 +1,597 @@ +// 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 score = module.exports.gameState.scores.get(context.nick) || 0; + bot.say(context.replyTo, `๐ŸŽฏ ${context.nick} has shot ${score} ducks!`); + } + }, + + { + name: 'topducks', + description: 'Show top duck hunters', + execute(context, bot) { + module.exports.showLeaderboard(context, bot); + } + }, + + { + name: 'duckstats', + description: 'Show duck hunting statistics', + 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 + const duck = activeDuck.duck; + const currentScore = this.gameState.scores.get(context.nick) || 0; + this.gameState.scores.set(context.nick, currentScore + 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 (same as hunting) + const duck = activeDuck.duck; + const currentScore = this.gameState.scores.get(context.nick) || 0; + this.gameState.scores.set(context.nick, currentScore + 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) => b[1] - 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 ๐Ÿ†'); + scores.forEach((score, index) => { + const medal = index === 0 ? '๐Ÿฅ‡' : index === 1 ? '๐Ÿฅˆ' : index === 2 ? '๐Ÿฅ‰' : '๐ŸŽฏ'; + bot.say(context.replyTo, `${medal} ${index + 1}. ${score[0]}: ${score[1]} ducks`); + }); + }, + + showStats(context, bot) { + const totalDucks = Array.from(this.gameState.scores.values()).reduce((a, b) => a + b, 0); + const totalHunters = this.gameState.scores.size; + const activeDucks = this.gameState.activeDucks.size; + + bot.say(context.replyTo, `๐Ÿฆ† DUCK HUNT STATS ๐Ÿฆ†`); + bot.say(context.replyTo, `๐ŸŽฏ Total ducks shot: ${totalDucks}`); + bot.say(context.replyTo, `๐Ÿน Active hunters: ${totalHunters}`); + bot.say(context.replyTo, `๐Ÿฆ† Ducks currently in channels: ${activeDucks}`); + }, + + 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 + this.gameState.scores = new Map(Object.entries(scoresObject)); + console.log(`๐Ÿฆ† Loaded ${this.gameState.scores.size} duck hunter scores from ${this.scoresFile}`); + + // Log top 3 hunters if any exist + if (this.gameState.scores.size > 0) { + const topHunters = Array.from(this.gameState.scores.entries()) + .sort((a, b) => b[1] - a[1]) + .slice(0, 3); + console.log('๐Ÿ† Top duck hunters:', topHunters.map(([name, score]) => `${name}(${score})`).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); + } + } +}; \ No newline at end of file diff --git a/plugins/duck_hunt_scores.json b/plugins/duck_hunt_scores.json new file mode 100644 index 0000000..f76a2d3 --- /dev/null +++ b/plugins/duck_hunt_scores.json @@ -0,0 +1,3 @@ +{ + "Monqui": 5 +} \ No newline at end of file diff --git a/plugins/emojify_plugin(2).js b/plugins/emojify_plugin(2).js new file mode 100644 index 0000000..a9d39b5 --- /dev/null +++ b/plugins/emojify_plugin(2).js @@ -0,0 +1,262 @@ +// plugins/emojify.js - Convert words to emojis +module.exports = { + init(bot) { + console.log('๐Ÿ˜€ Emojify plugin initialized'); + + // Word to emoji mappings + this.emojiMap = { + // Basic emotions & feelings + 'happy': '๐Ÿ˜Š', 'sad': '๐Ÿ˜ข', 'angry': '๐Ÿ˜ ', 'love': 'โค๏ธ', 'heart': 'โค๏ธ', + 'laugh': '๐Ÿ˜‚', 'cry': '๐Ÿ˜ญ', 'smile': '๐Ÿ˜Š', 'mad': '๐Ÿ˜ก', 'joy': '๐Ÿ˜Š', + 'excited': '๐Ÿค—', 'nervous': '๐Ÿ˜ฐ', 'worried': '๐Ÿ˜Ÿ', 'scared': '๐Ÿ˜จ', + 'surprised': '๐Ÿ˜ฒ', 'confused': '๐Ÿ˜•', 'tired': '๐Ÿ˜ด', 'sleepy': '๐Ÿ˜ด', + 'kiss': '๐Ÿ˜˜', 'wink': '๐Ÿ˜‰', 'cool': '๐Ÿ˜Ž', 'shocked': '๐Ÿ˜ฑ', + 'amazing': '๐Ÿคฉ', 'awesome': '๐Ÿคฉ', 'great': '๐Ÿ˜Š', 'wonderful': '๐Ÿ˜', + 'terrible': '๐Ÿ˜ญ', 'awful': '๐Ÿ˜–', 'horrible': '๐Ÿ˜ฑ', 'fantastic': '๐Ÿคฉ', + 'peaceful': '๐Ÿ˜Œ', 'calm': '๐Ÿ˜Œ', 'relaxed': '๐Ÿ˜Œ', 'stressed': '๐Ÿ˜ฐ', + 'disappointed': '๐Ÿ˜ž', 'devastated': '๐Ÿ’”', 'heartbroken': '๐Ÿ’”', 'lonely': '๐Ÿ˜”', + 'embarrassed': '๐Ÿ˜ณ', 'proud': '๐Ÿ˜ค', 'confident': '๐Ÿ˜Ž', 'shy': '๐Ÿ˜ณ', + 'grateful': '๐Ÿ™', 'thankful': '๐Ÿ™', 'blessed': '๐Ÿ™', 'lucky': '๐Ÿ€', + 'funny': '๐Ÿ˜‚', 'hilarious': '๐Ÿ˜‚', 'boring': '๐Ÿ˜ด', 'interesting': '๐Ÿค”', + + // Animals & creatures + 'cat': '๐Ÿฑ', 'dog': '๐Ÿถ', 'bird': '๐Ÿฆ', 'fish': '๐ŸŸ', 'snake': '๐Ÿ', + 'horse': '๐Ÿด', 'cow': '๐Ÿ„', 'pig': '๐Ÿท', 'sheep': '๐Ÿ‘', 'chicken': '๐Ÿ”', + 'duck': '๐Ÿฆ†', 'frog': '๐Ÿธ', 'monkey': '๐Ÿต', 'elephant': '๐Ÿ˜', 'lion': '๐Ÿฆ', + 'tiger': '๐Ÿฏ', 'bear': '๐Ÿป', 'panda': '๐Ÿผ', 'koala': '๐Ÿจ', 'mouse': '๐Ÿญ', + 'rabbit': '๐Ÿฐ', 'fox': '๐ŸฆŠ', 'wolf': '๐Ÿบ', 'bee': '๐Ÿ', 'butterfly': '๐Ÿฆ‹', + 'spider': '๐Ÿ•ท๏ธ', 'ant': '๐Ÿœ', 'bug': '๐Ÿ›', 'worm': '๐Ÿชฑ', 'fly': '๐Ÿชฐ', + 'unicorn': '๐Ÿฆ„', 'dragon': '๐Ÿ‰', 'dinosaur': '๐Ÿฆ•', 'monster': '๐Ÿ‘น', + 'pet': '๐Ÿพ', 'pets': '๐Ÿพ', 'animal': '๐Ÿพ', 'animals': '๐Ÿพ', + 'puppy': '๐Ÿถ', 'kitten': '๐Ÿฑ', 'baby': '๐Ÿ‘ถ', 'babies': '๐Ÿ‘ถ', + + // Food and drinks + 'pizza': '๐Ÿ•', 'burger': '๐Ÿ”', 'fries': '๐ŸŸ', 'hotdog': '๐ŸŒญ', 'taco': '๐ŸŒฎ', + 'sandwich': '๐Ÿฅช', 'bread': '๐Ÿž', 'cheese': '๐Ÿง€', 'meat': '๐Ÿฅฉ', 'chicken': '๐Ÿ—', + 'apple': '๐ŸŽ', 'banana': '๐ŸŒ', 'orange': '๐ŸŠ', 'grapes': '๐Ÿ‡', 'strawberry': '๐Ÿ“', + 'cherry': '๐Ÿ’', 'peach': '๐Ÿ‘', 'pineapple': '๐Ÿ', 'watermelon': '๐Ÿ‰', 'lemon': '๐Ÿ‹', + 'coffee': 'โ˜•', 'tea': '๐Ÿต', 'beer': '๐Ÿบ', 'wine': '๐Ÿท', 'water': '๐Ÿ’ง', + 'milk': '๐Ÿฅ›', 'juice': '๐Ÿงƒ', 'cake': '๐ŸŽ‚', 'cookie': '๐Ÿช', 'chocolate': '๐Ÿซ', + 'icecream': '๐Ÿฆ', 'donut': '๐Ÿฉ', 'candy': '๐Ÿฌ', 'food': '๐Ÿฝ๏ธ', + 'eat': '๐Ÿฝ๏ธ', 'eating': '๐Ÿฝ๏ธ', 'meal': '๐Ÿฝ๏ธ', 'dinner': '๐Ÿฝ๏ธ', + 'lunch': '๐Ÿฝ๏ธ', 'breakfast': '๐Ÿฝ๏ธ', 'snack': '๐Ÿฟ', 'hungry': '๐Ÿฝ๏ธ', + 'drink': '๐Ÿฅค', 'drinking': '๐Ÿฅค', 'thirsty': '๐Ÿฅค', 'soda': '๐Ÿฅค', + 'restaurant': '๐Ÿฝ๏ธ', 'kitchen': '๐Ÿณ', 'cooking': '๐Ÿณ', 'cook': '๐Ÿณ', + 'delicious': '๐Ÿ˜‹', 'yummy': '๐Ÿ˜‹', 'tasty': '๐Ÿ˜‹', 'spicy': '๐ŸŒถ๏ธ', + 'sweet': '๐Ÿฏ', 'sour': '๐Ÿ‹', 'healthy': '๐Ÿฅ—', + + // Weather & nature + 'sun': 'โ˜€๏ธ', 'sunny': 'โ˜€๏ธ', 'moon': '๐ŸŒ™', 'star': 'โญ', 'cloud': 'โ˜๏ธ', + 'cloudy': 'โ˜๏ธ', 'rain': '๐ŸŒง๏ธ', 'rainy': '๐ŸŒง๏ธ', 'snow': 'โ„๏ธ', 'snowy': 'โ„๏ธ', + 'storm': 'โ›ˆ๏ธ', 'thunder': 'โ›ˆ๏ธ', 'lightning': 'โšก', 'wind': '๐Ÿ’จ', 'hot': '๐Ÿ”ฅ', + 'cold': '๐ŸงŠ', 'fire': '๐Ÿ”ฅ', 'ice': '๐ŸงŠ', 'rainbow': '๐ŸŒˆ', + 'weather': '๐ŸŒค๏ธ', 'temperature': '๐ŸŒก๏ธ', 'warm': 'โ˜€๏ธ', 'freezing': '๐ŸงŠ', + 'foggy': '๐ŸŒซ๏ธ', 'fog': '๐ŸŒซ๏ธ', 'tornado': '๐ŸŒช๏ธ', 'hurricane': '๐ŸŒ€', + 'nature': '๐ŸŒฟ', 'forest': '๐ŸŒฒ', 'tree': '๐ŸŒณ', 'flower': '๐ŸŒธ', 'grass': '๐ŸŒฑ', + 'plant': '๐ŸŒฑ', 'garden': '๐ŸŒป', 'leaf': '๐Ÿƒ', 'rose': '๐ŸŒน', + 'desert': '๐Ÿœ๏ธ', 'mountain': 'โ›ฐ๏ธ', 'ocean': '๐ŸŒŠ', 'sea': '๐ŸŒŠ', 'beach': '๐Ÿ–๏ธ', + 'sky': 'โ˜๏ธ', 'earth': '๐ŸŒ', 'world': '๐ŸŒ', 'planet': '๐ŸŒ', + + // Transportation + 'car': '๐Ÿš—', 'bus': '๐ŸšŒ', 'train': '๐Ÿš‚', 'plane': 'โœˆ๏ธ', 'ship': '๐Ÿšข', + 'bike': '๐Ÿšด', 'bicycle': '๐Ÿšด', 'motorcycle': '๐Ÿ๏ธ', 'truck': '๐Ÿšš', 'taxi': '๐Ÿš•', + 'rocket': '๐Ÿš€', 'helicopter': '๐Ÿš', 'boat': 'โ›ต', + 'drive': '๐Ÿš—', 'driving': '๐Ÿš—', 'fly': 'โœˆ๏ธ', 'flying': 'โœˆ๏ธ', + 'ride': '๐Ÿš—', 'travel': '๐Ÿงณ', 'trip': '๐Ÿงณ', 'vacation': '๐Ÿ–๏ธ', + 'road': '๐Ÿ›ฃ๏ธ', 'traffic': '๐Ÿšฆ', 'airport': 'โœˆ๏ธ', + + // Technology + 'computer': '๐Ÿ’ป', 'phone': '๐Ÿ“ฑ', 'tv': '๐Ÿ“บ', 'camera': '๐Ÿ“ท', 'game': '๐ŸŽฎ', + 'internet': '๐ŸŒ', 'email': '๐Ÿ“ง', 'message': '๐Ÿ’ฌ', 'robot': '๐Ÿค–', 'battery': '๐Ÿ”‹', + 'wifi': '๐Ÿ“ถ', 'code': '๐Ÿ’ป', 'coding': '๐Ÿ’ป', 'programming': '๐Ÿ’ป', + 'technology': '๐Ÿ’ป', 'tech': '๐Ÿ’ป', 'digital': '๐Ÿ’ป', 'online': '๐ŸŒ', + 'website': '๐ŸŒ', 'software': '๐Ÿ’ป', 'app': '๐Ÿ“ฑ', 'video': '๐Ÿ“น', + 'photo': '๐Ÿ“ท', 'picture': '๐Ÿ“ท', 'screen': '๐Ÿ“บ', 'laptop': '๐Ÿ’ป', + 'headphones': '๐ŸŽง', 'data': '๐Ÿ’พ', 'password': '๐Ÿ”’', + + // Activities + 'work': '๐Ÿ’ผ', 'school': '๐Ÿซ', 'study': '๐Ÿ“š', 'read': '๐Ÿ“–', 'write': 'โœ๏ธ', + 'music': '๐ŸŽต', 'dance': '๐Ÿ’ƒ', 'sing': '๐ŸŽค', 'play': '๐ŸŽฎ', 'sport': 'โšฝ', + 'football': '๐Ÿˆ', 'soccer': 'โšฝ', 'basketball': '๐Ÿ€', 'tennis': '๐ŸŽพ', + 'swim': '๐ŸŠ', 'run': '๐Ÿƒ', 'walk': '๐Ÿšถ', 'exercise': '๐Ÿ’ช', 'gym': '๐Ÿ‹๏ธ', + 'sleep': '๐Ÿ˜ด', 'dream': '๐Ÿ’ญ', 'party': '๐ŸŽ‰', 'celebrate': '๐ŸŽ‰', + 'job': '๐Ÿ’ผ', 'office': '๐Ÿข', 'meeting': '๐Ÿ‘ฅ', 'learn': '๐Ÿ“š', + 'homework': '๐Ÿ“', 'test': '๐Ÿ“', 'hobby': '๐ŸŽจ', 'fun': '๐ŸŽ‰', + 'movie': '๐ŸŽฌ', 'film': '๐ŸŽฌ', 'show': '๐Ÿ“บ', 'art': '๐ŸŽจ', + 'paint': '๐ŸŽจ', 'draw': 'โœ๏ธ', 'shopping': '๐Ÿ›’', 'shop': '๐Ÿช', + 'buy': '๐Ÿ’ฐ', 'cook': '๐Ÿณ', 'clean': '๐Ÿงน', 'relax': '๐Ÿ˜Œ', + + // Objects + 'house': '๐Ÿ ', 'home': '๐Ÿ ', 'building': '๐Ÿข', 'book': '๐Ÿ“š', + 'pen': 'โœ๏ธ', 'pencil': 'โœ๏ธ', 'paper': '๐Ÿ“„', 'money': '๐Ÿ’ฐ', + 'gift': '๐ŸŽ', 'present': '๐ŸŽ', 'key': '๐Ÿ”‘', 'door': '๐Ÿšช', + 'clock': '๐Ÿ•', 'time': 'โฐ', 'watch': 'โŒš', 'glasses': '๐Ÿ‘“', + 'hat': '๐Ÿ‘’', 'shirt': '๐Ÿ‘•', 'shoes': '๐Ÿ‘Ÿ', 'bag': '๐Ÿ‘œ', + 'chair': '๐Ÿช‘', 'table': '๐Ÿช‘', 'bed': '๐Ÿ›๏ธ', 'lamp': '๐Ÿ’ก', + 'light': '๐Ÿ’ก', 'mirror': '๐Ÿชž', 'tool': '๐Ÿ”ง', 'hammer': '๐Ÿ”จ', + 'box': '๐Ÿ“ฆ', 'bottle': '๐Ÿผ', 'cup': 'โ˜•', 'plate': '๐Ÿฝ๏ธ', + + // Colors + 'red': '๐Ÿ”ด', 'blue': '๐Ÿ”ต', 'green': '๐ŸŸข', 'yellow': '๐ŸŸก', 'orange': '๐ŸŸ ', + 'purple': '๐ŸŸฃ', 'black': 'โšซ', 'white': 'โšช', 'pink': '๐Ÿฉท', 'brown': '๐ŸคŽ', + 'color': '๐ŸŒˆ', 'bright': 'โœจ', 'dark': 'โšซ', + + // Numbers + 'one': '1๏ธโƒฃ', 'two': '2๏ธโƒฃ', 'three': '3๏ธโƒฃ', 'four': '4๏ธโƒฃ', 'five': '5๏ธโƒฃ', + 'six': '6๏ธโƒฃ', 'seven': '7๏ธโƒฃ', 'eight': '8๏ธโƒฃ', 'nine': '9๏ธโƒฃ', 'ten': '๐Ÿ”Ÿ', + '1': '1๏ธโƒฃ', '2': '2๏ธโƒฃ', '3': '3๏ธโƒฃ', '4': '4๏ธโƒฃ', '5': '5๏ธโƒฃ', + '6': '6๏ธโƒฃ', '7': '7๏ธโƒฃ', '8': '8๏ธโƒฃ', '9': '9๏ธโƒฃ', '10': '๐Ÿ”Ÿ', + 'first': '1๏ธโƒฃ', 'second': '2๏ธโƒฃ', 'third': '3๏ธโƒฃ', + + // Directions + 'up': 'โฌ†๏ธ', 'down': 'โฌ‡๏ธ', 'left': 'โฌ…๏ธ', 'right': 'โžก๏ธ', + 'north': 'โฌ†๏ธ', 'south': 'โฌ‡๏ธ', 'east': 'โžก๏ธ', 'west': 'โฌ…๏ธ', + 'here': '๐Ÿ“', 'there': '๐Ÿ‘‰', + + // Actions + 'stop': '๐Ÿ›‘', 'go': 'โœ…', 'yes': 'โœ…', 'no': 'โŒ', 'ok': '๐Ÿ‘Œ', + 'good': '๐Ÿ‘', 'bad': '๐Ÿ‘Ž', 'like': '๐Ÿ‘', 'help': '๐Ÿ†˜', + 'start': 'โ–ถ๏ธ', 'end': 'โน๏ธ', 'open': '๐Ÿ”“', 'close': '๐Ÿ”’', + 'give': '๐Ÿคฒ', 'take': 'โœ‹', 'throw': '๐Ÿคพ', 'jump': '๐Ÿฆ˜', + 'win': '๐Ÿ†', 'lose': '๐Ÿ˜ž', 'try': '๐Ÿ’ช', 'talk': '๐Ÿ’ฌ', + 'listen': '๐Ÿ‘‚', 'see': '๐Ÿ‘๏ธ', 'look': '๐Ÿ‘๏ธ', 'watch': '๐Ÿ‘๏ธ', + + // Symbols + 'question': 'โ“', 'idea': '๐Ÿ’ก', 'magic': 'โœจ', 'star': 'โญ', + 'diamond': '๐Ÿ’Ž', 'crown': '๐Ÿ‘‘', 'trophy': '๐Ÿ†', 'flag': '๐Ÿณ๏ธ', + 'peace': 'โ˜ฎ๏ธ', 'power': '๐Ÿ’ช', 'energy': 'โšก', 'speed': '๐Ÿ’จ', + 'big': '๐Ÿ”ต', 'small': '๐Ÿ”ธ', 'fast': '๐Ÿ’จ', 'slow': '๐ŸŒ', + + // Places + 'city': '๐Ÿ™๏ธ', 'park': '๐Ÿž๏ธ', 'hospital': '๐Ÿฅ', 'bank': '๐Ÿฆ', + 'church': 'โ›ช', 'library': '๐Ÿ“š', 'cafe': 'โ˜•', 'hotel': '๐Ÿจ', + + // Body parts + 'face': '๐Ÿ˜Š', 'eye': '๐Ÿ‘๏ธ', 'nose': '๐Ÿ‘ƒ', 'hand': 'โœ‹', + 'finger': '๐Ÿ‘†', 'hair': '๐Ÿ’‡', 'teeth': '๐Ÿฆท', 'brain': '๐Ÿง ', + + // Time + 'morning': '๐ŸŒ…', 'afternoon': 'โ˜€๏ธ', 'evening': '๐ŸŒ‡', 'night': '๐ŸŒ™', + 'today': '๐Ÿ“…', 'tomorrow': '๐Ÿ“…', 'birthday': '๐ŸŽ‚' + }; + + // Common words that should sometimes be replaced with contextual emojis + this.contextualMap = { + 'and': 'โž•', 'with': '๐Ÿค', 'very': 'โญ', 'really': 'โญ', + 'maybe': '๐Ÿค”', 'definitely': 'โœ…', 'finally': '๐ŸŽ‰', + 'quickly': '๐Ÿ’จ', 'slowly': '๐ŸŒ', 'never': 'โŒ', 'always': 'โ™พ๏ธ', + 'everywhere': '๐ŸŒ', 'everyone': '๐Ÿ‘ฅ', 'everything': '๐ŸŒ' + }; + }, + + cleanup(bot) { + console.log('๐Ÿ˜€ Emojify plugin cleaned up'); + }, + + emojifyText(text) { + if (!text || text.trim().length === 0) { + return 'Please provide a message to emojify! ๐Ÿ˜Š'; + } + + // Split into words and process each one + const words = text.toLowerCase().split(/\s+/); + const result = []; + + for (let word of words) { + // Remove punctuation for matching but keep it for display + const cleanWord = word.replace(/[^\w]/g, ''); + const punctuation = word.replace(/[\w]/g, ''); + + // Check for direct emoji match + if (this.emojiMap[cleanWord]) { + result.push(this.emojiMap[cleanWord] + punctuation); + } + // Check for contextual matches (use sparingly) + else if (this.contextualMap[cleanWord] && Math.random() < 0.3) { + result.push(this.contextualMap[cleanWord] + punctuation); + } + // Check for partial matches (plurals, etc.) + else { + let found = false; + + // Try removing common suffixes + const suffixes = ['s', 'es', 'ed', 'ing', 'ly', 'er', 'est']; + for (const suffix of suffixes) { + if (cleanWord.endsWith(suffix)) { + const root = cleanWord.slice(0, -suffix.length); + if (this.emojiMap[root]) { + result.push(this.emojiMap[root] + punctuation); + found = true; + break; + } + } + } + + // If no match found, keep original word + if (!found) { + result.push(word); + } + } + } + + return result.join(' '); + }, + + commands: [ + { + name: 'emojify', + description: 'Convert words in a message to emojis', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + if (args.length === 0) { + bot.say(target, `${from}: Usage: !emojify `); + return; + } + + const message = args.join(' '); + const emojified = plugin.emojifyText(message); + + bot.say(target, `๐Ÿ˜€ ${emojified}`); + } + }, + + { + name: 'emojihelp', + description: 'Show some example words that can be emojified', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + const examples = [ + 'happy sad angry love', 'cat dog bird fish', + 'pizza coffee beer cake', 'sun rain snow fire', + 'car plane train bike', 'work sleep party dance' + ]; + + const randomExample = examples[Math.floor(Math.random() * examples.length)]; + const emojified = plugin.emojifyText(randomExample); + + bot.say(target, `๐Ÿ“ Example: "${randomExample}" becomes "${emojified}"`); + bot.say(target, `๐Ÿ’ก Try words like: animals, food, weather, emotions, activities, colors, numbers!`); + } + }, + + { + name: 'emojicount', + description: 'Show how many words can be emojified', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + const totalWords = Object.keys(plugin.emojiMap).length; + const contextualWords = Object.keys(plugin.contextualMap).length; + + bot.say(target, `๐Ÿ“Š I know ${totalWords} emoji replacements + ${contextualWords} contextual ones = ${totalWords + contextualWords} total!`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/hilo_plugin.js b/plugins/hilo_plugin.js new file mode 100644 index 0000000..5727921 --- /dev/null +++ b/plugins/hilo_plugin.js @@ -0,0 +1,397 @@ +// plugins/hilo.js - Hi-Lo Casino game plugin +const fs = require('fs'); +const path = require('path'); + +module.exports = { + init(bot) { + console.log('Hi-Lo Casino plugin initialized'); + this.bot = bot; + + // Persistent player statistics (saved to file) + this.playerStats = new Map(); // nick -> { totalWins, totalLosses, totalGames, biggestWin, winStreak, bestStreak, name } + + // Active games storage - nick -> { firstCard, bet, challenger } + this.activeGames = new Map(); + + // Set up score file path + this.scoresFile = path.join(__dirname, 'hilo_scores.json'); + + // Load existing scores + this.loadScores(); + }, + + cleanup(bot) { + console.log('Hi-Lo Casino plugin cleaned up'); + // Save scores before cleanup + this.saveScores(); + // Clear active games + this.activeGames.clear(); + }, + + // 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} Hi-Lo 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.totalWins - a.totalWins) + .slice(0, 3); + console.log('๐Ÿ† Top Hi-Lo players:', topPlayers.map(p => `${p.name}(${p.totalWins})`).join(', ')); + } + } else { + console.log(`๐Ÿƒ No existing Hi-Lo scores file found, starting fresh`); + } + } catch (error) { + console.error(`โŒ Error loading Hi-Lo 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} Hi-Lo player stats to ${this.scoresFile}`); + } catch (error) { + console.error(`โŒ Error saving Hi-Lo 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, { + totalWins: 0, + totalLosses: 0, + totalGames: 0, + biggestWin: 0, + winStreak: 0, + bestStreak: 0, + name: nick + }); + } + + const player = this.playerStats.get(nick); + Object.assign(player, updates); + + // Save to file after each update + this.saveScores(); + }, + + // Generate random card (1-100) + generateCard() { + return Math.floor(Math.random() * 100) + 1; + }, + + // Get card display with emoji + getCardDisplay(card) { + let emoji = '๐Ÿƒ'; + if (card <= 25) emoji = '๐ŸŸข'; // Low (1-25) + else if (card <= 50) emoji = '๐ŸŸก'; // Medium-Low (26-50) + else if (card <= 75) emoji = '๐ŸŸ '; // Medium-High (51-75) + else emoji = '๐Ÿ”ด'; // High (76-100) + + return `${emoji}${card}`; + }, + + commands: [ + { + name: 'hilo', + description: 'Start a Hi-Lo game or guess higher/lower', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + // Check if replying to an existing game + if (plugin.activeGames.has(from)) { + bot.say(target, `${from}: You already have an active Hi-Lo game! Use !higuess or !loguess`); + return; + } + + // Check if someone challenged this player + let challengerGame = null; + for (const [challenger, game] of plugin.activeGames.entries()) { + if (game.challenger === from) { + challengerGame = { challenger, game }; + break; + } + } + + if (challengerGame) { + bot.say(target, `${from}: ${challengerGame.challenger} is waiting for your !higuess or !loguess response!`); + return; + } + + // Parse command: !hilo + if (args.length < 2) { + bot.say(target, `Usage: !hilo - Example: !hilo alice 10`); + return; + } + + const opponent = args[0]; + const bet = parseInt(args[1]); + + if (isNaN(bet) || bet < 1 || bet > 100) { + bot.say(target, `${from}: Bet must be between 1 and 100`); + return; + } + + if (opponent === from) { + bot.say(target, `${from}: You can't challenge yourself!`); + return; + } + + // Initialize players if new + if (!plugin.playerStats.has(from)) { + plugin.updatePlayerStats(from, { + totalWins: 0, + totalLosses: 0, + totalGames: 0, + biggestWin: 0, + winStreak: 0, + bestStreak: 0, + name: from + }); + } + + // Generate first card + const firstCard = plugin.generateCard(); + + // Store the game + plugin.activeGames.set(from, { + firstCard: firstCard, + bet: bet, + challenger: opponent + }); + + const cardDisplay = plugin.getCardDisplay(firstCard); + bot.say(target, `๐Ÿƒ ${from} challenges ${opponent} to Hi-Lo! First card: ${cardDisplay} | Bet: ${bet} pts | ${opponent}: !higuess or !loguess?`); + } + }, + + { + name: 'higuess', + description: 'Guess the next card will be higher', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + plugin.processGuess(target, from, 'higher', bot); + } + }, + + { + name: 'loguess', + description: 'Guess the next card will be lower', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + plugin.processGuess(target, from, 'lower', bot); + } + }, + + { + name: 'hilostats', + description: 'Show your Hi-Lo statistics', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + if (!plugin.playerStats.has(from)) { + bot.say(target, `${from}: You haven't played Hi-Lo yet! Use !hilo to start.`); + return; + } + + const player = plugin.playerStats.get(from); + const winRate = player.totalGames > 0 ? ((player.totalWins / player.totalGames) * 100).toFixed(1) : 0; + + bot.say(target, `๐Ÿƒ ${from}: ${player.totalWins}W/${player.totalLosses}L (${winRate}%) | Best: ${player.biggestWin} | Streak: ${player.winStreak} (best: ${player.bestStreak}) | Games: ${player.totalGames}`); + } + }, + + { + name: 'tophilo', + description: 'Show top Hi-Lo players', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + if (plugin.playerStats.size === 0) { + bot.say(target, 'No Hi-Lo scores recorded yet! Use !hilo to start playing.'); + return; + } + + // Get top players by different metrics + const byWins = Array.from(plugin.playerStats.values()) + .sort((a, b) => b.totalWins - a.totalWins) + .slice(0, 3); + + const byStreak = Array.from(plugin.playerStats.values()) + .sort((a, b) => b.bestStreak - a.bestStreak) + .slice(0, 1); + + let output = '๐Ÿ† Top Hi-Lo: '; + byWins.forEach((player, i) => { + const trophy = i === 0 ? '๐Ÿฅ‡' : i === 1 ? '๐Ÿฅˆ' : '๐Ÿฅ‰'; + const winRate = player.totalGames > 0 ? ((player.totalWins / player.totalGames) * 100).toFixed(0) : 0; + output += `${trophy}${player.name}(${player.totalWins}W/${winRate}%) `; + }); + + if (byStreak[0] && byStreak[0].bestStreak > 0) { + output += `| ๐Ÿ”ฅ Best Streak: ${byStreak[0].name}(${byStreak[0].bestStreak})`; + } + + bot.say(target, output); + } + }, + + { + name: 'resethilo', + description: 'Reset all Hi-Lo 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.activeGames.clear(); + plugin.saveScores(); + + bot.say(target, `๐Ÿ—‘๏ธ ${from}: Reset ${playerCount} Hi-Lo player stats`); + } + } + ], + + // Process the guess (shared logic) + processGuess(target, from, guess, bot) { + // Find the game where this player is the challenger + let gameData = null; + let challenger = null; + + for (const [chalName, game] of this.activeGames.entries()) { + if (game.challenger === from) { + gameData = game; + challenger = chalName; + break; + } + } + + if (!gameData) { + bot.say(target, `${from}: No Hi-Lo challenge found for you!`); + return; + } + + // Initialize challenger if new + if (!this.playerStats.has(from)) { + this.updatePlayerStats(from, { + totalWins: 0, + totalLosses: 0, + totalGames: 0, + biggestWin: 0, + winStreak: 0, + bestStreak: 0, + name: from + }); + } + + // Generate second card + const secondCard = this.generateCard(); + const firstCard = gameData.firstCard; + const bet = gameData.bet; + + // Determine if guess was correct + let isCorrect = false; + if (guess === 'higher' && secondCard > firstCard) isCorrect = true; + if (guess === 'lower' && secondCard < firstCard) isCorrect = true; + if (secondCard === firstCard) { + // Tie - push/draw + bot.say(target, `๐Ÿƒ ${from}: ${this.getCardDisplay(firstCard)}โ†’${this.getCardDisplay(secondCard)} | ๐Ÿค TIE! No winner`); + this.activeGames.delete(challenger); + return; + } + + // Update stats for both players + const challengerPlayer = this.playerStats.get(challenger); + const guesserPlayer = this.playerStats.get(from); + + let challengerUpdates = { totalGames: challengerPlayer.totalGames + 1 }; + let guesserUpdates = { totalGames: guesserPlayer.totalGames + 1 }; + + const firstDisplay = this.getCardDisplay(firstCard); + const secondDisplay = this.getCardDisplay(secondCard); + const direction = guess === 'higher' ? 'HI' : 'LO'; + + if (isCorrect) { + // Guesser wins + guesserUpdates.totalWins = guesserPlayer.totalWins + 1; + guesserUpdates.winStreak = guesserPlayer.winStreak + 1; + challengerUpdates.totalLosses = challengerPlayer.totalLosses + 1; + challengerUpdates.winStreak = 0; + + if (guesserUpdates.winStreak > guesserPlayer.bestStreak) { + guesserUpdates.bestStreak = guesserUpdates.winStreak; + } + + if (bet > guesserPlayer.biggestWin) { + guesserUpdates.biggestWin = bet; + } + + bot.say(target, `๐Ÿƒ ${from}: ${firstDisplay}โ†’${secondDisplay} ${direction} | โœ… CORRECT! +${bet} pts | Streak: ${guesserUpdates.winStreak}`); + } else { + // Challenger wins + challengerUpdates.totalWins = challengerPlayer.totalWins + 1; + challengerUpdates.winStreak = challengerPlayer.winStreak + 1; + guesserUpdates.totalLosses = guesserPlayer.totalLosses + 1; + guesserUpdates.winStreak = 0; + + if (challengerUpdates.winStreak > challengerPlayer.bestStreak) { + challengerUpdates.bestStreak = challengerUpdates.winStreak; + } + + if (bet > challengerPlayer.biggestWin) { + challengerUpdates.biggestWin = bet; + } + + bot.say(target, `๐Ÿƒ ${from}: ${firstDisplay}โ†’${secondDisplay} ${direction} | โŒ WRONG! ${challenger} wins ${bet} pts`); + } + + // Update both players + this.updatePlayerStats(challenger, challengerUpdates); + this.updatePlayerStats(from, guesserUpdates); + + // Remove the game + this.activeGames.delete(challenger); + } +}; \ No newline at end of file diff --git a/plugins/hilo_scores.json b/plugins/hilo_scores.json new file mode 100644 index 0000000..b834030 --- /dev/null +++ b/plugins/hilo_scores.json @@ -0,0 +1,47 @@ +{ + "megasconed": { + "totalWins": 7, + "totalLosses": 10, + "totalGames": 17, + "biggestWin": 100, + "winStreak": 1, + "bestStreak": 3, + "name": "megasconed" + }, + "Monqui": { + "totalWins": 6, + "totalLosses": 1, + "totalGames": 7, + "biggestWin": 100, + "winStreak": 3, + "bestStreak": 3, + "name": "Monqui" + }, + "cr0sis": { + "totalWins": 3, + "totalLosses": 4, + "totalGames": 7, + "biggestWin": 50, + "winStreak": 0, + "bestStreak": 3, + "name": "cr0sis" + }, + "aclonedsheeps": { + "totalWins": 1, + "totalLosses": 1, + "totalGames": 2, + "biggestWin": 100, + "winStreak": 1, + "bestStreak": 1, + "name": "aclonedsheeps" + }, + "death916": { + "totalWins": 0, + "totalLosses": 1, + "totalGames": 1, + "biggestWin": 0, + "winStreak": 0, + "bestStreak": 0, + "name": "death916" + } +} \ No newline at end of file diff --git a/plugins/markov_debug.js b/plugins/markov_debug.js new file mode 100644 index 0000000..4f6991f --- /dev/null +++ b/plugins/markov_debug.js @@ -0,0 +1,215 @@ +// plugins/markov_debug.js - Debug version to diagnose SQLite issues +const fs = require('fs'); +const path = require('path'); + +module.exports = { + init(bot) { + console.log('๐Ÿ” Markov Debug Plugin Starting...'); + this.bot = bot; + + // Test 1: Check if plugins directory exists and is writable + this.testPluginsDirectory(); + + // Test 2: Try to load SQLite3 + this.testSQLite3(); + + // Test 3: Try to create a simple database + this.testDatabaseCreation(); + }, + + cleanup(bot) { + console.log('๐Ÿ” Markov Debug Plugin cleaned up'); + if (this.db) { + this.db.close(); + } + }, + + testPluginsDirectory() { + const pluginsDir = __dirname; + console.log(`๐Ÿ“ Plugins directory: ${pluginsDir}`); + + try { + // Check if directory exists + if (fs.existsSync(pluginsDir)) { + console.log('โœ… Plugins directory exists'); + + // Test write permissions + const testFile = path.join(pluginsDir, 'test_write.tmp'); + fs.writeFileSync(testFile, 'test'); + fs.unlinkSync(testFile); + console.log('โœ… Plugins directory is writable'); + } else { + console.log('โŒ Plugins directory does not exist'); + } + } catch (error) { + console.log('โŒ Plugins directory permission error:', error.message); + } + }, + + testSQLite3() { + console.log('๐Ÿ” Testing SQLite3 installation...'); + + try { + const sqlite3 = require('sqlite3'); + console.log('โœ… SQLite3 module loaded successfully'); + console.log('๐Ÿ“ฆ SQLite3 version:', sqlite3.VERSION); + this.sqlite3 = sqlite3.verbose(); + } catch (error) { + console.log('โŒ SQLite3 loading failed:', error.message); + console.log('๐Ÿ’ก Try: npm install sqlite3 --build-from-source'); + this.sqlite3 = null; + } + }, + + testDatabaseCreation() { + if (!this.sqlite3) { + console.log('โญ๏ธ Skipping database test - SQLite3 not available'); + return; + } + + console.log('๐Ÿ” Testing database creation...'); + + const dbPath = path.join(__dirname, 'test_markov.db'); + console.log(`๐Ÿ—„๏ธ Test database path: ${dbPath}`); + + try { + this.db = new this.sqlite3.Database(dbPath, (err) => { + if (err) { + console.log('โŒ Database creation failed:', err.message); + } else { + console.log('โœ… Test database created successfully'); + + // Try to create a simple table + this.db.run('CREATE TABLE IF NOT EXISTS test (id INTEGER PRIMARY KEY, text TEXT)', (err) => { + if (err) { + console.log('โŒ Table creation failed:', err.message); + } else { + console.log('โœ… Test table created successfully'); + + // Try to insert data + this.db.run('INSERT INTO test (text) VALUES (?)', ['test message'], (err) => { + if (err) { + console.log('โŒ Insert failed:', err.message); + } else { + console.log('โœ… Test insert successful'); + + // Try to read data + this.db.get('SELECT * FROM test', (err, row) => { + if (err) { + console.log('โŒ Select failed:', err.message); + } else { + console.log('โœ… Test select successful:', row); + console.log('๐ŸŽ‰ SQLite3 is working correctly!'); + } + }); + } + }); + } + }); + } + }); + } catch (error) { + console.log('โŒ Database constructor failed:', error.message); + } + }, + + commands: [ + { + name: 'debugmarkov', + description: 'Run diagnostic tests for Markov plugin', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + bot.say(target, `${from}: Running diagnostics...`); + + // Re-run all tests + plugin.testPluginsDirectory(); + plugin.testSQLite3(); + plugin.testDatabaseCreation(); + + bot.say(target, `${from}: Check console for diagnostic results`); + } + }, + + { + name: 'checknpm', + description: 'Check Node.js and NPM environment', + execute: function(context, bot) { + const target = context.replyTo; + const from = context.nick; + + const nodeVersion = process.version; + const platform = process.platform; + const arch = process.arch; + + bot.say(target, `${from}: Node.js ${nodeVersion} on ${platform}-${arch}`); + + // Check if we can see package.json + try { + const packagePath = path.join(process.cwd(), 'package.json'); + if (fs.existsSync(packagePath)) { + const pkg = JSON.parse(fs.readFileSync(packagePath, 'utf8')); + const hasSQLite = pkg.dependencies && pkg.dependencies.sqlite3; + bot.say(target, `${from}: sqlite3 in package.json: ${hasSQLite ? 'โœ…' : 'โŒ'}`); + } else { + bot.say(target, `${from}: No package.json found`); + } + } catch (error) { + bot.say(target, `${from}: Error checking package.json: ${error.message}`); + } + } + }, + + { + name: 'testwrite', + description: 'Test file writing in plugins directory', + execute: function(context, bot) { + const target = context.replyTo; + const from = context.nick; + + try { + const testPath = path.join(__dirname, 'write_test.txt'); + const testData = `Test file created at ${new Date().toISOString()}`; + + fs.writeFileSync(testPath, testData); + + // Verify it was written + const readData = fs.readFileSync(testPath, 'utf8'); + + // Clean up + fs.unlinkSync(testPath); + + bot.say(target, `${from}: โœ… File write test successful`); + } catch (error) { + bot.say(target, `${from}: โŒ File write test failed: ${error.message}`); + } + } + }, + + { + name: 'listsqlite', + description: 'List any existing SQLite database files', + execute: function(context, bot) { + const target = context.replyTo; + const from = context.nick; + + try { + const files = fs.readdirSync(__dirname); + const dbFiles = files.filter(f => f.endsWith('.db') || f.endsWith('.sqlite') || f.endsWith('.sqlite3')); + + if (dbFiles.length > 0) { + bot.say(target, `${from}: Found databases: ${dbFiles.join(', ')}`); + } else { + bot.say(target, `${from}: No database files found in plugins directory`); + } + + bot.say(target, `${from}: All files: ${files.join(', ')}`); + } catch (error) { + bot.say(target, `${from}: Error listing files: ${error.message}`); + } + } + } + ] +}; \ No newline at end of file diff --git a/plugins/pigs_scores.json b/plugins/pigs_scores.json new file mode 100644 index 0000000..939f7be --- /dev/null +++ b/plugins/pigs_scores.json @@ -0,0 +1,20 @@ +{ + "megasconed": { + "totalScore": 51, + "bestRoll": 20, + "rollCount": 18, + "name": "megasconed", + "eliminated": false, + "totalGames": 3, + "totalRolls": 37 + }, + "Monqui": { + "totalScore": 118, + "bestRoll": 20, + "rollCount": 21, + "name": "Monqui", + "eliminated": false, + "totalGames": 3, + "totalRolls": 17 + } +} \ No newline at end of file diff --git a/plugins/pigsmp_plugin_fixed.js b/plugins/pigsmp_plugin_fixed.js new file mode 100644 index 0000000..2328781 --- /dev/null +++ b/plugins/pigsmp_plugin_fixed.js @@ -0,0 +1,668 @@ +// plugins/pigsmp.js - Pass the Pigs Multiplayer game plugin +const fs = require('fs'); +const path = require('path'); + +module.exports = { + init(bot) { + console.log('Pass the Pigs Multiplayer plugin initialized'); + this.bot = bot; + + // Persistent player statistics (saved to file) + this.playerStats = new Map(); // nick -> { bestScore, totalGames, totalRolls, jackpots, name } + + // Current game scores (reset each game) + this.gameScores = new Map(); // nick -> { currentScore, rollCount, eliminated } + + // Game state with multiplayer support + this.gameState = { + phase: 'idle', // 'idle', 'joining', 'active' + players: [], + joinTimer: null, + startTime: null, + currentPlayer: 0, + turnScore: 0, + channel: null, + joinTimeLimit: 60000, // 60 seconds + warningTime: 50000 // Warning at 50 seconds (10 sec left sluts) + }; + + // Set up score file path + this.scoresFile = path.join(__dirname, 'pigsmp_scores.json'); + + // Load existing scores + this.loadScores(); + }, + + cleanup(bot) { + console.log('Pass the Pigs Multiplayer 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, + turnScore: 0, + channel: null, + joinTimeLimit: 30000, + warningTime: 25000 + }; + }, + + // 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} pig multiplayer 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.bestScore - a.bestScore) + .slice(0, 3); + console.log('๐Ÿ† Top players:', topPlayers.map(p => `${p.name}(${p.bestScore})`).join(', ')); + } + } else { + console.log(`๐Ÿท No existing multiplayer scores file found, starting fresh`); + } + } catch (error) { + console.error(`โŒ Error loading pig multiplayer 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} pig multiplayer player stats to ${this.scoresFile}`); + } catch (error) { + console.error(`โŒ Error saving pig multiplayer 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, { + bestScore: 0, + totalGames: 0, + totalRolls: 0, + jackpots: 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(); + } + + if (!this.gameScores.has(nick)) { + this.gameScores.set(nick, { + currentScore: 0, + rollCount: 0, + eliminated: false + }); + } else { + // Reset for new game + this.gameScores.set(nick, { + currentScore: 0, + rollCount: 0, + eliminated: false + }); + } + + // Initialize persistent stats if new player + if (!this.playerStats.has(nick)) { + this.updatePlayerStats(nick, { + bestScore: 0, + totalGames: 0, + totalRolls: 0, + jackpots: 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, 'โฐ 10 seconds left to join the pig game! Join you sluts!'); + } + }, 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 pig game. Game cancelled.'); + this.resetGameState(); + this.gameScores.clear(); + return; + } + + // Initialize all players + this.gameState.players.forEach(nick => { + this.initGamePlayer(nick); + }); + + // Start the game + this.gameState.phase = 'active'; + this.gameState.currentPlayer = 0; + this.gameState.turnScore = 0; + + const playerList = this.gameState.players.join(' vs '); + this.bot.say(target, `๐ŸŽฎ Game starting! ${playerList} - ${this.gameState.players[0]} goes first!`); + }, + + // Pass the Pigs game simulation with reduced difficulty + rollPigs() { + // Actual probabilities from 11,954 sample study + const positions = [ + { name: 'Side (no dot)', points: 0, weight: 34.9 }, + { name: 'Side (dot)', points: 0, weight: 30.2 }, + { name: 'Razorback', points: 5, weight: 22.4 }, + { name: 'Trotter', points: 5, weight: 8.8 }, + { name: 'Snouter', points: 10, weight: 3.0 }, + { name: 'Leaning Jowler', points: 15, weight: 0.61 } + ]; + + const rollPig = () => { + const totalWeight = positions.reduce((sum, pos) => sum + pos.weight, 0); + let random = Math.random() * totalWeight; + + for (const position of positions) { + random -= position.weight; + if (random <= 0) { + return position; + } + } + return positions[0]; // fallback + }; + + const pig1 = rollPig(); + const pig2 = rollPig(); + + // Check for touching conditions first (increased probability for more difficulty) + if (Math.random() < 0.035) { // 3.5% chance of touching (was 1.5%) + if (Math.random() < 0.25) { // 25% of touches are Piggyback + return { result: 'Piggyback', points: -9999, description: '๐Ÿท๐Ÿ“š Piggyback! ELIMINATED!' }; + } else { // 75% are Makin' Bacon/Oinker + return { result: 'Makin\' Bacon', points: -999, description: '๐Ÿฅ“ Makin\' Bacon! Lose ALL points!' }; + } + } + + // Handle side positions (Sider vs Pig Out) + const pig1IsSide = pig1.name.startsWith('Side'); + const pig2IsSide = pig2.name.startsWith('Side'); + + if (pig1IsSide && pig2IsSide) { + // Both on sides - reduced chance of opposite sides (Pig Out) + const pig1Dot = pig1.name.includes('dot'); + const pig2Dot = pig2.name.includes('dot'); + + // Reduced bias toward opposite sides for better gameplay + const oppositeRoll = Math.random(); + if (oppositeRoll < 0.35) { // 35% chance of opposite sides when both are siders + return { result: 'Pig Out', points: 0, description: '๐Ÿ’ฅ Pig Out! Turn ends!' }; + } else { + return { result: 'Sider', points: 1, description: '๐Ÿท Sider' }; + } + } + + // If only one pig is on its side, that pig scores nothing + if (pig1IsSide && !pig2IsSide) { + return { + result: pig2.name, + points: pig2.points, + description: `๐Ÿท ${pig2.name} (${pig2.points})` + }; + } + + if (pig2IsSide && !pig1IsSide) { + return { + result: pig1.name, + points: pig1.points, + description: `๐Ÿท ${pig1.name} (${pig1.points})` + }; + } + + // Neither pig is on its side - normal scoring + if (pig1.name === pig2.name) { + // Double combinations - sum doubled (quadruple individual) + const doublePoints = (pig1.points + pig2.points) * 2; + switch (pig1.name) { + case 'Razorback': + return { result: 'Double Razorback', points: doublePoints, description: '๐Ÿท๐Ÿท Double Razorback (20)' }; + case 'Trotter': + return { result: 'Double Trotter', points: doublePoints, description: '๐Ÿท๐Ÿท Double Trotter (20)' }; + case 'Snouter': + return { result: 'Double Snouter', points: doublePoints, description: '๐Ÿท๐Ÿท Double Snouter (40)' }; + case 'Leaning Jowler': + return { result: 'Double Leaning Jowler', points: doublePoints, description: '๐Ÿท๐Ÿท Double Leaning Jowler (60)' }; + } + } + + // Mixed combination - sum of individual scores + const totalPoints = pig1.points + pig2.points; + return { + result: 'Mixed Combo', + points: totalPoints, + description: `๐Ÿท ${pig1.name} + ${pig2.name} (${totalPoints})` + }; + }, + + // Turn management for multiplayer pig game + nextTurn(target) { + // Find next non-eliminated player + let attempts = 0; + const maxAttempts = this.gameState.players.length; + + do { + this.gameState.currentPlayer = (this.gameState.currentPlayer + 1) % this.gameState.players.length; + attempts++; + + if (attempts >= maxAttempts) { + // All players eliminated somehow - shouldn't happen but safety check + this.bot.say(target, '๐Ÿšจ All players eliminated! Game ending.'); + this.endGame(target); + return; + } + } while (this.gameScores.get(this.gameState.players[this.gameState.currentPlayer])?.eliminated); + + const nextPlayer = this.gameState.players[this.gameState.currentPlayer]; + this.bot.say(target, `๐ŸŽฒ ${nextPlayer}'s turn!`); + }, + + endGame(target) { + // Ensure gameScores exists before processing + if (!this.gameScores) { + this.gameScores = new Map(); + } + + // Save final scores to persistent stats and update records + this.gameState.players.forEach(playerName => { + const gameScore = this.gameScores.get(playerName); + const playerStats = this.playerStats.get(playerName); + + if (gameScore && playerStats) { + const updates = { + totalGames: playerStats.totalGames + 1, + totalRolls: playerStats.totalRolls + gameScore.rollCount + }; + + // Update best score if this game was better + if (gameScore.currentScore > playerStats.bestScore) { + updates.bestScore = gameScore.currentScore; + this.bot.say(target, `๐Ÿ† ${playerName} set a new personal best: ${gameScore.currentScore} points!`); + } + + this.updatePlayerStats(playerName, updates); + console.log(`๐Ÿท Saved game stats for ${playerName}: score=${gameScore.currentScore}, rolls=${gameScore.rollCount}`); + } + }); + + // Clear current game data + this.gameScores.clear(); + + this.bot.say(target, '๐Ÿ Game ended! Use !pigs to start new game.'); + this.resetGameState(); + }, + + commands: [ + { + name: 'pigs', + description: 'Start or join a multiplayer Pass the Pigs game', + 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 for multiplayer games + if (!context.channel) { + bot.say(target, 'Multiplayer pig 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 pig game! Others have 60 seconds to join with !pigs`); + 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 game!`); + return; + } + + if (plugin.gameState.players.length >= 6) { // Max 6 players + bot.say(target, `${from}: Game is full! (Max 6 players)`); + return; + } + + plugin.gameState.players.push(from); + bot.say(target, `๐Ÿท ${from} joined! (${plugin.gameState.players.length} players)`); + break; + + case 'active': + // Game is running - handle roll + if (!plugin.gameState.players.includes(from)) { + bot.say(target, `${from}: You're not in the current game!`); + return; + } + + const currentPlayerName = plugin.gameState.players[plugin.gameState.currentPlayer]; + if (from !== currentPlayerName) { + bot.say(target, `${from}: It's ${currentPlayerName}'s turn!`); + return; + } + + // Check if current player is eliminated + const playerGame = plugin.gameScores.get(from); + if (playerGame.eliminated) { + bot.say(target, `${from}: You are eliminated!`); + plugin.endGame(target); + return; + } + + // Roll the pigs! + const roll = plugin.rollPigs(); + + // Update roll count + playerGame.rollCount++; + + let endTurn = false; + let endGame = false; + + if (roll.points === -9999) { + // Piggyback - eliminate player + playerGame.eliminated = true; + bot.say(target, `${from}: ${roll.description}`); + + // Check if only one player left + const activePlayers = plugin.gameState.players.filter(p => + !plugin.gameScores.get(p)?.eliminated + ); + + if (activePlayers.length <= 1) { + if (activePlayers.length === 1) { + bot.say(target, `๐ŸŽ‰ ${activePlayers[0]} wins by elimination! ๐ŸŽ‰`); + } + endGame = true; + } else { + endTurn = true; + } + } else if (roll.points === -999) { + // Makin' Bacon - lose all points and end turn + const lostPoints = playerGame.currentScore; + playerGame.currentScore = 0; + plugin.gameState.turnScore = 0; + bot.say(target, `${from}: ${roll.description} Lost ${lostPoints} game points!`); + endTurn = true; + } else if (roll.points === 0) { + // Pig Out - lose turn score and end turn + const lostTurn = plugin.gameState.turnScore; + plugin.gameState.turnScore = 0; + bot.say(target, `${from}: ${roll.description} Lost ${lostTurn} turn points!`); + endTurn = true; + } else { + // Normal scoring - add to turn score + plugin.gameState.turnScore += roll.points; + + const potentialTotal = playerGame.currentScore + plugin.gameState.turnScore; + + // Send detailed roll info as notice to the player (NO public message for normal rolls) + let detailMsg = `Your roll: ${roll.description} | Turn: ${plugin.gameState.turnScore} | Game Total: ${playerGame.currentScore}`; + if (potentialTotal >= 100) { + detailMsg += ` | Can win!`; + } + bot.notice(from, detailMsg); + console.log(`Sending notice to ${from}: ${detailMsg}`); + } + + if (endGame) { + plugin.endGame(target); + } else if (endTurn) { + plugin.nextTurn(target); + } + break; + } + } + }, + + { + name: 'bank', + description: 'Bank your turn points in multiplayer Pass the Pigs', + 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 pig game!`); + 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.turnScore === 0) { + bot.say(target, `${from}: No points to bank!`); + return; + } + + // Bank the points + const playerGame = plugin.gameScores.get(from); + const newTotal = playerGame.currentScore + plugin.gameState.turnScore; + + playerGame.currentScore = newTotal; + + bot.say(target, `${from}: Banked ${plugin.gameState.turnScore} points! Game Total: ${newTotal}`); + + // Check for win + if (newTotal >= 100) { + bot.say(target, `๐ŸŽ‰ ${from} WINS with ${newTotal} points! ๐ŸŽ‰`); + plugin.endGame(target); + return; + } + + plugin.gameState.turnScore = 0; + plugin.nextTurn(target); + } + }, + + { + name: 'quitpigs', + description: 'Quit the current multiplayer pig 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 pig game to quit!`); + return; + } + + if (!plugin.gameState.players.includes(from)) { + bot.say(target, `${from}: You're not in the current game!`); + return; + } + + // Remove player from game + plugin.gameState.players = plugin.gameState.players.filter(p => p !== from); + bot.say(target, `${from} quit the pig game!`); + + // Check if game should continue + if (plugin.gameState.phase === 'joining') { + if (plugin.gameState.players.length === 0) { + bot.say(target, 'Game cancelled - no players 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 forfeit!`); + } + 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: 'toppigs', + description: 'Show top multiplayer pig players', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + if (plugin.playerStats.size === 0) { + bot.say(target, 'No multiplayer pig scores recorded yet! Use !pigs to start playing.'); + return; + } + + // Convert to array and sort by best score + const sortedScores = Array.from(plugin.playerStats.values()) + .sort((a, b) => b.bestScore - a.bestScore) + .slice(0, 5); // Top 5 + + bot.say(target, '๐Ÿ† Top Multiplayer Pig Players:'); + + sortedScores.forEach((player, index) => { + const rank = index + 1; + const trophy = rank === 1 ? '๐Ÿฅ‡' : rank === 2 ? '๐Ÿฅˆ' : rank === 3 ? '๐Ÿฅ‰' : `${rank}.`; + bot.say(target, `${trophy} ${player.name}: Best ${player.bestScore} pts (${player.totalGames} games, ${player.totalRolls} rolls)`); + }); + } + }, + + { + name: 'pigstatus', + description: 'Show current multiplayer pig game status', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + if (plugin.gameState.phase === 'idle') { + bot.say(target, 'No active multiplayer pig game. Use !pigs to start one!'); + 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 game: `; + + plugin.gameState.players.forEach((player, i) => { + const gameScore = plugin.gameScores.get(player); + if (i > 0) statusMsg += ' vs '; + statusMsg += `${player}(${gameScore.currentScore})`; + if (gameScore.eliminated) statusMsg += '[OUT]'; + }); + + bot.say(target, statusMsg); + bot.say(target, `๐ŸŽฒ ${currentPlayer}'s turn | Turn score: ${plugin.gameState.turnScore}`); + } + } + }, + + { + name: 'resetpigs', + description: 'Reset all multiplayer pig scores (admin command)', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + // Simple admin check - you can customize this list + const adminNicks = ['admin', 'owner', 'cancerbot', 'megasconed']; // Add your admin nicks here + + 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} multiplayer pig player stats`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/pigsmp_scores.json b/plugins/pigsmp_scores.json new file mode 100644 index 0000000..f67e3bb --- /dev/null +++ b/plugins/pigsmp_scores.json @@ -0,0 +1,37 @@ +{ + "megasconed": { + "bestScore": 109, + "totalGames": 9, + "totalRolls": 133, + "jackpots": 0, + "name": "megasconed" + }, + "Monqui": { + "bestScore": 129, + "totalGames": 9, + "totalRolls": 141, + "jackpots": 0, + "name": "Monqui" + }, + "Budrick": { + "bestScore": 102, + "totalGames": 5, + "totalRolls": 66, + "jackpots": 0, + "name": "Budrick" + }, + "Loungequi": { + "bestScore": 21, + "totalGames": 1, + "totalRolls": 18, + "jackpots": 0, + "name": "Loungequi" + }, + "cr0sis": { + "bestScore": 103, + "totalGames": 2, + "totalRolls": 42, + "jackpots": 0, + "name": "cr0sis" + } +} \ No newline at end of file diff --git a/plugins/quiplash_basic.js b/plugins/quiplash_basic.js new file mode 100644 index 0000000..c4d02c3 --- /dev/null +++ b/plugins/quiplash_basic.js @@ -0,0 +1,517 @@ +// plugins/quiplash.js - Quiplash game for IRC +module.exports = { + init(bot) { + console.log('๐ŸŽฎ Quiplash plugin initialized'); + this.bot = bot; + + // ================================ + // EASY CONFIG - EDIT THIS SECTION + // ================================ + this.config = { + gameChannel: '#quiplash', // ONLY channel where game works + minPlayers: 3, // Minimum players to start + maxPlayers: 8, // Maximum players allowed + promptTimeout: 120000, // 2 minutes to submit answers (in ms) + votingTimeout: 60000, // 1 minute to vote (in ms) + joinTimeout: 30000 // 30 seconds to join game (in ms) + }; + + // Game state + this.gameState = { + phase: 'idle', // 'idle', 'joining', 'prompts', 'voting', 'results' + players: [], // Array of player nicknames + currentPrompt: null, // Current prompt being voted on + answers: new Map(), // nick -> answer text + votes: new Map(), // nick -> voted_for_nick + scores: new Map(), // nick -> total_score + promptIndex: 0, // Which prompt we're on + timers: { + join: null, + prompt: null, + voting: null + } + }; + + // Sample prompts for basic version + this.prompts = [ + "The worst superhero power: _____", + "A rejected Netflix series: _____", + "What aliens probably think about humans: _____", + "The most useless app idea: _____", + "A terrible name for a restaurant: _____", + "The worst thing to find in your pocket: _____", + "A bad slogan for a dating site: _____", + "The weirdest thing to collect: _____", + "A terrible excuse for being late: _____", + "The worst fortune cookie message: _____", + "A rejected crayon color: _____", + "The most awkward place to run into your ex: _____", + "A bad name for a pet: _____", + "The worst thing to hear from a pilot: _____", + "A terrible superhero catchphrase: _____" + ]; + + // Track who we've sent prompts to + this.promptsSent = new Set(); + this.answersReceived = new Set(); + }, + + cleanup(bot) { + console.log('๐ŸŽฎ Quiplash plugin cleaned up'); + this.clearAllTimers(); + this.resetGame(); + }, + + // Clear all active timers + clearAllTimers() { + Object.values(this.gameState.timers).forEach(timer => { + if (timer) clearTimeout(timer); + }); + this.gameState.timers = { join: null, prompt: null, voting: null }; + }, + + // Reset game to idle state + resetGame() { + this.clearAllTimers(); + this.gameState = { + phase: 'idle', + players: [], + currentPrompt: null, + answers: new Map(), + votes: new Map(), + scores: new Map(), + promptIndex: 0, + timers: { join: null, prompt: null, voting: null } + }; + this.promptsSent.clear(); + this.answersReceived.clear(); + }, + + // Check if command is in the correct channel + isValidChannel(context) { + return context.channel === this.config.gameChannel; + }, + + // Start the join phase + startJoinPhase(context) { + this.resetGame(); + this.gameState.phase = 'joining'; + this.gameState.players = [context.nick]; + + this.bot.say(this.config.gameChannel, `๐ŸŽฎ ${context.nick} started a Quiplash game!`); + this.bot.say(this.config.gameChannel, `๐Ÿ’ฌ Type !join to play! Need ${this.config.minPlayers}-${this.config.maxPlayers} players.`); + this.bot.say(this.config.gameChannel, `โฐ 30 seconds to join...`); + + // Set join timer + this.gameState.timers.join = setTimeout(() => { + this.startGameIfReady(); + }, this.config.joinTimeout); + }, + + // Add player to game + addPlayer(nick) { + if (this.gameState.players.includes(nick)) { + return { success: false, message: `${nick}: You're already in the game!` }; + } + + if (this.gameState.players.length >= this.config.maxPlayers) { + return { success: false, message: `${nick}: Game is full! (${this.config.maxPlayers} players max)` }; + } + + this.gameState.players.push(nick); + this.gameState.scores.set(nick, 0); + + return { + success: true, + message: `๐ŸŽฎ ${nick} joined! (${this.gameState.players.length}/${this.config.maxPlayers} players)` + }; + }, + + // Remove player from game + removePlayer(nick) { + const index = this.gameState.players.indexOf(nick); + if (index === -1) { + return { success: false, message: `${nick}: You're not in the game!` }; + } + + this.gameState.players.splice(index, 1); + this.gameState.scores.delete(nick); + this.gameState.answers.delete(nick); + this.gameState.votes.delete(nick); + + return { + success: true, + message: `๐Ÿ‘‹ ${nick} left the game. (${this.gameState.players.length} players remaining)` + }; + }, + + // Check if we can start the game + startGameIfReady() { + if (this.gameState.players.length < this.config.minPlayers) { + this.bot.say(this.config.gameChannel, `๐Ÿ˜ž Not enough players (${this.gameState.players.length}/${this.config.minPlayers}). Game cancelled.`); + this.resetGame(); + return; + } + + this.startPromptPhase(); + }, + + // Start the prompt phase + startPromptPhase() { + this.gameState.phase = 'prompts'; + this.gameState.answers.clear(); + this.promptsSent.clear(); + this.answersReceived.clear(); + + // Pick a random prompt + const promptText = this.prompts[Math.floor(Math.random() * this.prompts.length)]; + this.gameState.currentPrompt = promptText; + + this.bot.say(this.config.gameChannel, `๐ŸŽฏ Round starting! Check your PMs for the prompt.`); + this.bot.say(this.config.gameChannel, `โฐ You have 2 minutes to submit your answer!`); + + // Send prompt to all players via PM + this.gameState.players.forEach(player => { + this.bot.say(player, `๐ŸŽฏ Quiplash Prompt: "${promptText}"`); + this.bot.say(player, `๐Ÿ’ฌ Reply with: !answer `); + this.promptsSent.add(player); + }); + + // Set prompt timer + this.gameState.timers.prompt = setTimeout(() => { + this.startVotingPhase(); + }, this.config.promptTimeout); + }, + + // Handle answer submission + submitAnswer(nick, answerText) { + if (this.gameState.phase !== 'prompts') { + return { success: false, message: 'Not accepting answers right now!' }; + } + + if (!this.gameState.players.includes(nick)) { + return { success: false, message: 'You\'re not in the current game!' }; + } + + if (this.gameState.answers.has(nick)) { + return { success: false, message: 'You already submitted an answer!' }; + } + + if (!answerText || answerText.trim().length === 0) { + return { success: false, message: 'Answer cannot be empty!' }; + } + + if (answerText.length > 100) { + return { success: false, message: 'Answer too long! Keep it under 100 characters.' }; + } + + this.gameState.answers.set(nick, answerText.trim()); + this.answersReceived.add(nick); + + // Check if all players have answered + if (this.answersReceived.size === this.gameState.players.length) { + clearTimeout(this.gameState.timers.prompt); + this.startVotingPhase(); + } + + return { + success: true, + message: `โœ… Answer submitted! (${this.answersReceived.size}/${this.gameState.players.length} received)` + }; + }, + + // Start voting phase + startVotingPhase() { + if (this.gameState.answers.size === 0) { + this.bot.say(this.config.gameChannel, `๐Ÿ˜ž No one submitted answers! Game ended.`); + this.resetGame(); + return; + } + + this.gameState.phase = 'voting'; + this.gameState.votes.clear(); + + this.bot.say(this.config.gameChannel, `๐Ÿ—ณ๏ธ VOTING TIME!`); + this.bot.say(this.config.gameChannel, `๐Ÿ“ Prompt: "${this.gameState.currentPrompt}"`); + this.bot.say(this.config.gameChannel, `๐Ÿ“‹ Answers:`); + + // Display all answers with numbers + const answers = Array.from(this.gameState.answers.entries()); + answers.forEach(([author, answer], index) => { + this.bot.say(this.config.gameChannel, `${index + 1}) ${answer}`); + }); + + this.bot.say(this.config.gameChannel, `๐Ÿ—ณ๏ธ Vote with: !vote (You have 1 minute!)`); + this.bot.say(this.config.gameChannel, `โš ๏ธ You cannot vote for your own answer!`); + + // Set voting timer + this.gameState.timers.voting = setTimeout(() => { + this.showResults(); + }, this.config.votingTimeout); + }, + + // Handle vote submission + submitVote(nick, voteNumber) { + if (this.gameState.phase !== 'voting') { + return { success: false, message: 'Not accepting votes right now!' }; + } + + if (!this.gameState.players.includes(nick)) { + return { success: false, message: 'You\'re not in the current game!' }; + } + + if (this.gameState.votes.has(nick)) { + return { success: false, message: 'You already voted!' }; + } + + const answers = Array.from(this.gameState.answers.entries()); + + if (voteNumber < 1 || voteNumber > answers.length) { + return { success: false, message: `Invalid vote! Choose 1-${answers.length}` }; + } + + const [votedForNick, votedForAnswer] = answers[voteNumber - 1]; + + // Check if voting for themselves + if (votedForNick === nick) { + return { success: false, message: 'You cannot vote for your own answer!' }; + } + + this.gameState.votes.set(nick, votedForNick); + + // Check if all players have voted + const eligibleVoters = this.gameState.players.filter(p => this.gameState.answers.has(p)); + if (this.gameState.votes.size === eligibleVoters.length) { + clearTimeout(this.gameState.timers.voting); + this.showResults(); + } + + return { + success: true, + message: `โœ… Vote recorded! (${this.gameState.votes.size}/${eligibleVoters.length} votes)` + }; + }, + + // Show results and end game + showResults() { + this.gameState.phase = 'results'; + + // Count votes + const voteCount = new Map(); + for (const votedFor of this.gameState.votes.values()) { + voteCount.set(votedFor, (voteCount.get(votedFor) || 0) + 1); + } + + // Update scores + for (const [nick, votes] of voteCount.entries()) { + const currentScore = this.gameState.scores.get(nick) || 0; + this.gameState.scores.set(nick, currentScore + votes); + } + + this.bot.say(this.config.gameChannel, `๐Ÿ† RESULTS!`); + this.bot.say(this.config.gameChannel, `๐Ÿ“ Prompt: "${this.gameState.currentPrompt}"`); + + // Show answers with vote counts and authors + const answers = Array.from(this.gameState.answers.entries()); + answers.forEach(([author, answer]) => { + const votes = voteCount.get(author) || 0; + const voteText = votes === 1 ? '1 vote' : `${votes} votes`; + this.bot.say(this.config.gameChannel, `โ€ข "${answer}" - ${author} (${voteText})`); + }); + + // Show current scores + this.bot.say(this.config.gameChannel, `๐Ÿ“Š Scores:`); + const sortedScores = Array.from(this.gameState.scores.entries()) + .sort((a, b) => b[1] - a[1]); + + sortedScores.forEach(([nick, score], index) => { + const position = index === 0 ? '๐Ÿฅ‡' : index === 1 ? '๐Ÿฅˆ' : index === 2 ? '๐Ÿฅ‰' : `${index + 1}.`; + this.bot.say(this.config.gameChannel, `${position} ${nick}: ${score} points`); + }); + + this.bot.say(this.config.gameChannel, `๐ŸŽฎ Game complete! Use !quiplash to play again.`); + + // Reset game after showing results + setTimeout(() => { + this.resetGame(); + }, 5000); + }, + + commands: [ + { + name: 'quiplash', + description: 'Start a new Quiplash game or show current status', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + // Check if in correct channel + if (!plugin.isValidChannel(context)) { + bot.say(target, `${from}: Quiplash only works in ${plugin.config.gameChannel}!`); + return; + } + + switch (plugin.gameState.phase) { + case 'idle': + plugin.startJoinPhase(context); + break; + + case 'joining': + const timeLeft = Math.ceil((plugin.config.joinTimeout - (Date.now() - Date.now())) / 1000); + bot.say(target, `๐ŸŽฎ Game starting soon! Players: ${plugin.gameState.players.join(', ')}`); + bot.say(target, `๐Ÿ’ฌ Type !join to play!`); + break; + + case 'prompts': + bot.say(target, `๐ŸŽฏ Game in progress! Waiting for answers to: "${plugin.gameState.currentPrompt}"`); + bot.say(target, `๐Ÿ“Š Received: ${plugin.answersReceived.size}/${plugin.gameState.players.length}`); + break; + + case 'voting': + bot.say(target, `๐Ÿ—ณ๏ธ Voting in progress! Use !vote `); + const eligibleVoters = plugin.gameState.players.filter(p => plugin.gameState.answers.has(p)); + bot.say(target, `๐Ÿ“Š Votes: ${plugin.gameState.votes.size}/${eligibleVoters.length}`); + break; + + case 'results': + bot.say(target, `๐Ÿ† Game finishing up! New game starting soon...`); + break; + } + } + }, + + { + name: 'join', + description: 'Join the current Quiplash game', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + if (!plugin.isValidChannel(context)) { + bot.say(target, `${from}: Quiplash only works in ${plugin.config.gameChannel}!`); + return; + } + + if (plugin.gameState.phase !== 'joining') { + bot.say(target, `${from}: No game to join! Use !quiplash to start one.`); + return; + } + + const result = plugin.addPlayer(from); + bot.say(target, result.message); + } + }, + + { + name: 'leave', + description: 'Leave the current Quiplash game', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + if (!plugin.isValidChannel(context)) { + bot.say(target, `${from}: Quiplash only works in ${plugin.config.gameChannel}!`); + return; + } + + if (plugin.gameState.phase === 'idle') { + bot.say(target, `${from}: No active game to leave!`); + return; + } + + const result = plugin.removePlayer(from); + bot.say(target, result.message); + + // Check if too few players remain + if (plugin.gameState.players.length < plugin.config.minPlayers && plugin.gameState.phase !== 'idle') { + bot.say(target, `๐Ÿ˜ž Too few players remaining. Game cancelled.`); + plugin.resetGame(); + } + } + }, + + { + name: 'vote', + description: 'Vote for an answer during voting phase', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + if (!plugin.isValidChannel(context)) { + return; // Silently ignore votes in wrong channel + } + + if (args.length === 0) { + bot.say(target, `${from}: Usage: !vote `); + return; + } + + const voteNumber = parseInt(args[0]); + if (isNaN(voteNumber)) { + bot.say(target, `${from}: Vote must be a number!`); + return; + } + + const result = plugin.submitVote(from, voteNumber); + if (!result.success) { + bot.say(target, `${from}: ${result.message}`); + } else { + // Send confirmation via PM to avoid spam + bot.say(from, result.message); + } + } + }, + + { + name: 'answer', + description: 'Submit your answer (use in PM)', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + // This command should only work in PM + if (context.channel) { + bot.say(target, `${from}: Send your answer via PM! Type /msg ${bot.config.nick} !answer `); + return; + } + + if (args.length === 0) { + bot.say(target, 'Usage: !answer '); + return; + } + + const answer = args.join(' '); + const result = plugin.submitAnswer(from, answer); + bot.say(target, result.message); + } + }, + + { + name: 'players', + description: 'Show current players in the game', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + if (!plugin.isValidChannel(context)) { + bot.say(target, `${from}: Quiplash only works in ${plugin.config.gameChannel}!`); + return; + } + + if (plugin.gameState.players.length === 0) { + bot.say(target, 'No active game. Use !quiplash to start one!'); + } else { + bot.say(target, `๐ŸŽฎ Players (${plugin.gameState.players.length}): ${plugin.gameState.players.join(', ')}`); + } + } + } + ] +}; \ No newline at end of file diff --git a/plugins/scratchcard_plugin(1).js b/plugins/scratchcard_plugin(1).js new file mode 100644 index 0000000..d4d86f4 --- /dev/null +++ b/plugins/scratchcard_plugin(1).js @@ -0,0 +1,455 @@ +// plugins/scratchcard.js - Scratch Cards Casino game plugin +const fs = require('fs'); +const path = require('path'); + +module.exports = { + init(bot) { + console.log('Scratch Cards Casino plugin initialized'); + this.bot = bot; + + // Persistent player statistics (saved to file) + this.playerStats = new Map(); // nick -> { totalWins, totalSpent, totalCards, biggestWin, jackpots, name } + + // Scratch card types with different themes and payouts + this.cardTypes = { + classic: { + name: 'Classic', + cost: 5, + symbols: ['๐Ÿ’', '๐Ÿ‹', '๐ŸŠ', '๐Ÿ‡', '๐Ÿ””', 'โญ', '๐Ÿ’Ž'], + payouts: { + '๐Ÿ’': 2, // 2x + '๐Ÿ‹': 3, // 3x + '๐ŸŠ': 4, // 4x + '๐Ÿ‡': 6, // 6x + '๐Ÿ””': 10, // 10x + 'โญ': 20, // 20x + '๐Ÿ’Ž': 50 // 50x (jackpot) + }, + weights: { + '๐Ÿ’': 25, + '๐Ÿ‹': 20, + '๐ŸŠ': 20, + '๐Ÿ‡': 15, + '๐Ÿ””': 10, + 'โญ': 8, + '๐Ÿ’Ž': 2 + } + }, + lucky: { + name: 'Lucky 7s', + cost: 10, + symbols: ['๐ŸŽฐ', '๐Ÿ€', '๐ŸŽฒ', '๐ŸŽฏ', '๐Ÿ’ฐ', '๐Ÿ†', '๐Ÿ’ฏ'], + payouts: { + '๐ŸŽฐ': 3, + '๐Ÿ€': 5, + '๐ŸŽฒ': 8, + '๐ŸŽฏ': 12, + '๐Ÿ’ฐ': 25, + '๐Ÿ†': 50, + '๐Ÿ’ฏ': 100 // mega jackpot + }, + weights: { + '๐ŸŽฐ': 30, + '๐Ÿ€': 25, + '๐ŸŽฒ': 20, + '๐ŸŽฏ': 15, + '๐Ÿ’ฐ': 6, + '๐Ÿ†': 3, + '๐Ÿ’ฏ': 1 + } + }, + treasure: { + name: 'Treasure Hunt', + cost: 20, + symbols: ['๐Ÿ—๏ธ', 'โš“', '๐Ÿดโ€โ˜ ๏ธ', '๐Ÿ’ฐ', '๐Ÿ‘‘', '๐Ÿ†', '๐Ÿ’Ž'], + payouts: { + '๐Ÿ—๏ธ': 4, + 'โš“': 6, + '๐Ÿดโ€โ˜ ๏ธ': 10, + '๐Ÿ’ฐ': 20, + '๐Ÿ‘‘': 40, + '๐Ÿ†': 80, + '๐Ÿ’Ž': 200 // treasure jackpot + }, + weights: { + '๐Ÿ—๏ธ': 35, + 'โš“': 25, + '๐Ÿดโ€โ˜ ๏ธ': 20, + '๐Ÿ’ฐ': 12, + '๐Ÿ‘‘': 5, + '๐Ÿ†': 2, + '๐Ÿ’Ž': 1 + } + } + }; + + // Set up score file path + this.scoresFile = path.join(__dirname, 'scratchcard_scores.json'); + + // Load existing scores + this.loadScores(); + }, + + cleanup(bot) { + console.log('Scratch Cards Casino plugin cleaned up'); + // Save scores before cleanup + this.saveScores(); + }, + + // 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} scratch card 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.totalWins - a.totalWins) + .slice(0, 3); + console.log('๐Ÿ† Top scratchers:', topPlayers.map(p => `${p.name}(${p.totalWins})`).join(', ')); + } + } else { + console.log(`๐ŸŽซ No existing scratch card scores file found, starting fresh`); + } + } catch (error) { + console.error(`โŒ Error loading scratch card 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} scratch card player stats to ${this.scoresFile}`); + } catch (error) { + console.error(`โŒ Error saving scratch card 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, { + totalWins: 0, + totalSpent: 0, + totalCards: 0, + biggestWin: 0, + jackpots: 0, + name: nick + }); + } + + const player = this.playerStats.get(nick); + Object.assign(player, updates); + + // Save to file after each update + this.saveScores(); + }, + + // Generate random symbol based on weights + getRandomSymbol(cardType) { + const weights = cardType.weights; + const totalWeight = Object.values(weights).reduce((sum, weight) => sum + weight, 0); + let random = Math.random() * totalWeight; + + for (const [symbol, weight] of Object.entries(weights)) { + random -= weight; + if (random <= 0) { + return symbol; + } + } + return Object.keys(weights)[0]; // fallback + }, + + // Create a scratch card (3x3 grid) + createCard(cardType) { + const card = []; + for (let i = 0; i < 9; i++) { + card.push(this.getRandomSymbol(cardType)); + } + return card; + }, + + // Check for winning combinations + checkWinnings(card, cardType) { + const winnings = []; + + // Check rows (3 in a row) + for (let row = 0; row < 3; row++) { + const start = row * 3; + const symbols = [card[start], card[start + 1], card[start + 2]]; + if (symbols[0] === symbols[1] && symbols[1] === symbols[2]) { + const payout = cardType.payouts[symbols[0]] || 0; + winnings.push({ + type: 'row', + symbol: symbols[0], + payout: payout, + positions: [start, start + 1, start + 2] + }); + } + } + + // Check columns (3 in a column) + for (let col = 0; col < 3; col++) { + const symbols = [card[col], card[col + 3], card[col + 6]]; + if (symbols[0] === symbols[1] && symbols[1] === symbols[2]) { + const payout = cardType.payouts[symbols[0]] || 0; + winnings.push({ + type: 'column', + symbol: symbols[0], + payout: payout, + positions: [col, col + 3, col + 6] + }); + } + } + + // Check diagonals + const diag1 = [card[0], card[4], card[8]]; + if (diag1[0] === diag1[1] && diag1[1] === diag1[2]) { + const payout = cardType.payouts[diag1[0]] || 0; + winnings.push({ + type: 'diagonal', + symbol: diag1[0], + payout: payout, + positions: [0, 4, 8] + }); + } + + const diag2 = [card[2], card[4], card[6]]; + if (diag2[0] === diag2[1] && diag2[1] === diag2[2]) { + const payout = cardType.payouts[diag2[0]] || 0; + winnings.push({ + type: 'diagonal', + symbol: diag2[0], + payout: payout, + positions: [2, 4, 6] + }); + } + + return winnings; + }, + + // Format card display + formatCard(card) { + return `โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”\nโ”‚ ${card[0]} โ”‚ ${card[1]} โ”‚ ${card[2]} โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ ${card[3]} โ”‚ ${card[4]} โ”‚ ${card[5]} โ”‚\nโ”œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ค\nโ”‚ ${card[6]} โ”‚ ${card[7]} โ”‚ ${card[8]} โ”‚\nโ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜`; + }, + + commands: [ + { + name: 'scratch', + description: 'Buy and scratch a lottery card', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + + // Parse card type + let cardTypeName = 'classic'; + if (args.length > 0) { + cardTypeName = args[0].toLowerCase(); + } + + const cardType = plugin.cardTypes[cardTypeName]; + if (!cardType) { + const validTypes = Object.keys(plugin.cardTypes).join(', '); + bot.say(target, `${from}: Invalid card type. Choose: ${validTypes}`); + return; + } + + // Initialize player if new + if (!plugin.playerStats.has(from)) { + plugin.updatePlayerStats(from, { + totalWins: 0, + totalSpent: 0, + totalCards: 0, + biggestWin: 0, + jackpots: 0, + name: from + }); + } + + // Create and scratch the card + const card = plugin.createCard(cardType); + const winnings = plugin.checkWinnings(card, cardType); + + // Calculate total winnings + const totalPayout = winnings.reduce((sum, win) => sum + win.payout, 0); + const netWin = totalPayout - cardType.cost; + + // Update player stats + const player = plugin.playerStats.get(from); + const updates = { + totalCards: player.totalCards + 1, + totalSpent: player.totalSpent + cardType.cost + }; + + if (totalPayout > 0) { + updates.totalWins = player.totalWins + totalPayout; + + if (totalPayout > player.biggestWin) { + updates.biggestWin = totalPayout; + } + + // Check for jackpot (high value wins) + const isJackpot = winnings.some(win => win.payout >= 50); + if (isJackpot) { + updates.jackpots = player.jackpots + 1; + } + } + + // Show card with simple format (no ASCII art for IRC) + const cardDisplay = `[${card[0]}][${card[1]}][${card[2]}] [${card[3]}][${card[4]}][${card[5]}] [${card[6]}][${card[7]}][${card[8]}]`; + + let output = `๐ŸŽซ ${from}: ${cardType.name} Card | ${cardDisplay}`; + + if (winnings.length > 0) { + if (totalPayout >= 50) { + output += ` | ๐ŸŽ‰ JACKPOT! ๐ŸŽ‰`; + } + + const winDesc = winnings.map(win => `${win.symbol}x3=${win.payout}`).join(' '); + output += ` | โœ… WIN: ${winDesc} = ${totalPayout} pts`; + + if (netWin > 0) { + output += ` (profit: +${netWin})`; + } + } else { + output += ` | โŒ No matches (cost: -${cardType.cost})`; + } + + // Add running totals + const newTotal = updates.totalWins || player.totalWins; + const newSpent = updates.totalSpent; + const profit = newTotal - newSpent; + output += ` | Total: ${newTotal} spent: ${newSpent} profit: ${profit}`; + + bot.say(target, output); + plugin.updatePlayerStats(from, updates); + } + }, + + { + name: 'scratchinfo', + description: 'Show scratch card types and payouts', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + let output = '๐ŸŽซ SCRATCH CARDS: '; + + const cardDescs = Object.entries(plugin.cardTypes).map(([key, cardType]) => { + const topSymbols = Object.entries(cardType.payouts) + .sort((a, b) => b[1] - a[1]) + .slice(0, 3) + .map(([symbol, payout]) => `${symbol}=${payout}x`) + .join(' '); + + return `${cardType.name}(${cardType.cost}pts): ${topSymbols}`; + }); + + output += cardDescs.join(' | '); + output += ' | ๐ŸŽฏ Get 3-in-a-row to win!'; + + bot.say(target, output); + } + }, + + { + name: 'scratchstats', + description: 'Show your scratch card statistics', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + if (!plugin.playerStats.has(from)) { + bot.say(target, `${from}: You haven't scratched any cards yet! Use !scratch to start.`); + return; + } + + const player = plugin.playerStats.get(from); + const profit = player.totalWins - player.totalSpent; + const winRate = player.totalCards > 0 ? ((player.totalWins / player.totalSpent) * 100).toFixed(1) : 0; + + bot.say(target, `๐ŸŽซ ${from}: ${player.totalCards} cards | Won: ${player.totalWins} | Spent: ${player.totalSpent} | Profit: ${profit} | ROI: ${winRate}% | Best: ${player.biggestWin} | Jackpots: ${player.jackpots}`); + } + }, + + { + name: 'topscratch', + description: 'Show top scratch card players', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + if (plugin.playerStats.size === 0) { + bot.say(target, 'No scratch card scores recorded yet! Use !scratch to start playing.'); + return; + } + + // Get top players by profit + const byProfit = Array.from(plugin.playerStats.values()) + .map(p => ({ ...p, profit: p.totalWins - p.totalSpent })) + .sort((a, b) => b.profit - a.profit) + .slice(0, 3); + + const byJackpots = Array.from(plugin.playerStats.values()) + .sort((a, b) => b.jackpots - a.jackpots) + .slice(0, 1); + + let output = '๐Ÿ† Top Scratchers: '; + byProfit.forEach((player, i) => { + const trophy = i === 0 ? '๐Ÿฅ‡' : i === 1 ? '๐Ÿฅˆ' : '๐Ÿฅ‰'; + output += `${trophy}${player.name}(${player.profit > 0 ? '+' : ''}${player.profit}) `; + }); + + if (byJackpots[0] && byJackpots[0].jackpots > 0) { + output += `| ๐ŸŽฐ Most Jackpots: ${byJackpots[0].name}(${byJackpots[0].jackpots})`; + } + + bot.say(target, output); + } + }, + + { + name: 'resetscratch', + description: 'Reset all scratch card 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} scratch card player stats`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/scratchcard_scores.json b/plugins/scratchcard_scores.json new file mode 100644 index 0000000..489a067 --- /dev/null +++ b/plugins/scratchcard_scores.json @@ -0,0 +1,34 @@ +{ + "megasconed": { + "totalWins": 26, + "totalSpent": 280, + "totalCards": 34, + "biggestWin": 10, + "jackpots": 0, + "name": "megasconed" + }, + "Loungequi": { + "totalWins": 9, + "totalSpent": 65, + "totalCards": 13, + "biggestWin": 4, + "jackpots": 0, + "name": "Loungequi" + }, + "aclonedsheeps": { + "totalWins": 0, + "totalSpent": 10, + "totalCards": 1, + "biggestWin": 0, + "jackpots": 0, + "name": "aclonedsheeps" + }, + "cr0sis": { + "totalWins": 18, + "totalSpent": 35, + "totalCards": 6, + "biggestWin": 8, + "jackpots": 0, + "name": "cr0sis" + } +} \ No newline at end of file diff --git a/plugins/slots_plugin.js b/plugins/slots_plugin.js new file mode 100644 index 0000000..2e2d238 --- /dev/null +++ b/plugins/slots_plugin.js @@ -0,0 +1,328 @@ +// plugins/slots.js - Slot Machine game plugin +const fs = require('fs'); +const path = require('path'); + +module.exports = { + init(bot) { + console.log('Slot Machine plugin initialized'); + this.bot = bot; + + // Player scores storage + this.playerScores = new Map(); // nick -> { totalWins, biggestWin, totalSpins, jackpots, name } + + // Slot machine symbols with weights and payouts + this.symbols = [ + { emoji: '๐Ÿ’', name: 'Cherry', weight: 25, payout: 2 }, + { emoji: '๐ŸŠ', name: 'Orange', weight: 20, payout: 3 }, + { emoji: '๐Ÿ‹', name: 'Lemon', weight: 20, payout: 3 }, + { emoji: '๐Ÿ‡', name: 'Grapes', weight: 15, payout: 5 }, + { emoji: '๐Ÿ””', name: 'Bell', weight: 10, payout: 10 }, + { emoji: 'โญ', name: 'Star', weight: 6, payout: 15 }, + { emoji: '๐Ÿ’Ž', name: 'Diamond', weight: 3, payout: 25 }, + { emoji: '๐ŸŽฐ', name: 'Jackpot', weight: 1, payout: 100 } + ]; + + // Set up score file path + this.scoresFile = path.join(__dirname, 'slots_scores.json'); + + // Load existing scores + this.loadScores(); + }, + + cleanup(bot) { + console.log('Slot Machine plugin cleaned up'); + // Save scores before cleanup + this.saveScores(); + }, + + // 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.playerScores = new Map(Object.entries(scoresObject)); + console.log(`๐ŸŽฐ Loaded ${this.playerScores.size} slot player scores from ${this.scoresFile}`); + + // Log top 3 players if any exist + if (this.playerScores.size > 0) { + const topPlayers = Array.from(this.playerScores.values()) + .sort((a, b) => b.totalWins - a.totalWins) + .slice(0, 3); + console.log('๐Ÿ† Top slot players:', topPlayers.map(p => `${p.name}(${p.totalWins})`).join(', ')); + } + } else { + console.log(`๐ŸŽฐ No existing slot scores file found, starting fresh`); + } + } catch (error) { + console.error(`โŒ Error loading slot scores:`, error); + this.playerScores = 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.playerScores); + const data = JSON.stringify(scoresObject, null, 2); + + fs.writeFileSync(this.scoresFile, data, 'utf8'); + console.log(`๐Ÿ’พ Saved ${this.playerScores.size} slot player scores to ${this.scoresFile}`); + } catch (error) { + console.error(`โŒ Error saving slot scores:`, error); + } + }, + + // Update a player's score and save to file + updatePlayerScore(nick, updates) { + if (!this.playerScores.has(nick)) { + this.playerScores.set(nick, { + totalWins: 0, + biggestWin: 0, + totalSpins: 0, + jackpots: 0, + name: nick + }); + } + + const player = this.playerScores.get(nick); + Object.assign(player, updates); + + // Save to file after each update + this.saveScores(); + }, + + // Weighted random symbol selection + getRandomSymbol() { + const totalWeight = this.symbols.reduce((sum, symbol) => sum + symbol.weight, 0); + let random = Math.random() * totalWeight; + + for (const symbol of this.symbols) { + random -= symbol.weight; + if (random <= 0) { + return symbol; + } + } + return this.symbols[0]; // fallback + }, + + // Spin the slot machine + spinSlots() { + const reel1 = this.getRandomSymbol(); + const reel2 = this.getRandomSymbol(); + const reel3 = this.getRandomSymbol(); + + return { reel1, reel2, reel3 }; + }, + + // Calculate winnings based on the spin result + calculateWinnings(spin) { + const { reel1, reel2, reel3 } = spin; + + // Check for three of a kind (jackpot) + if (reel1.emoji === reel2.emoji && reel2.emoji === reel3.emoji) { + return { + type: 'triple', + amount: reel1.payout, + symbol: reel1, + message: `๐ŸŽ‰ TRIPLE ${reel1.name.toUpperCase()}! ๐ŸŽ‰` + }; + } + + // Check for two of a kind + if (reel1.emoji === reel2.emoji || reel2.emoji === reel3.emoji || reel1.emoji === reel3.emoji) { + let matchSymbol; + if (reel1.emoji === reel2.emoji) matchSymbol = reel1; + else if (reel2.emoji === reel3.emoji) matchSymbol = reel2; + else matchSymbol = reel1; + + const payout = Math.floor(matchSymbol.payout / 3); // Reduced payout for pairs + if (payout > 0) { + return { + type: 'pair', + amount: payout, + symbol: matchSymbol, + message: `โœจ Pair of ${matchSymbol.name}s! โœจ` + }; + } + } + + // No match + return { + type: 'none', + amount: 0, + symbol: null, + message: '๐Ÿ’ธ No match, try again!' + }; + }, + + commands: [ + { + name: 'slots', + description: 'Spin the slot machine!', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + // Initialize player if new + if (!plugin.playerScores.has(from)) { + plugin.updatePlayerScore(from, { + totalWins: 0, + biggestWin: 0, + totalSpins: 0, + jackpots: 0, + name: from + }); + } + + // Spin the slots + const spin = plugin.spinSlots(); + const result = plugin.calculateWinnings(spin); + + // Update player stats + const player = plugin.playerScores.get(from); + const updates = { + totalSpins: player.totalSpins + 1 + }; + + let output = `๐ŸŽฐ ${from}: [${spin.reel1.emoji}|${spin.reel2.emoji}|${spin.reel3.emoji}] `; + + if (result.amount > 0) { + // Player won! + updates.totalWins = player.totalWins + result.amount; + + if (result.amount > player.biggestWin) { + updates.biggestWin = result.amount; + } + + if (result.type === 'triple' && result.symbol.emoji === '๐ŸŽฐ') { + updates.jackpots = player.jackpots + 1; + output += `๐Ÿšจ JACKPOT! ๐Ÿšจ `; + } else if (result.amount >= 50) { + output += `๐Ÿ”ฅ BIG WIN! ๐Ÿ”ฅ `; + } else if (result.type === 'triple') { + output += `๐ŸŽ‰ TRIPLE! ๐ŸŽ‰ `; + } else { + output += `โœจ PAIR! โœจ `; + } + + output += `+${result.amount} pts | Total: ${updates.totalWins}`; + } else { + // Player lost + output += `๐Ÿ’ธ No match | Spins: ${updates.totalSpins} | Total: ${player.totalWins}`; + } + + bot.say(target, output); + plugin.updatePlayerScore(from, updates); + } + }, + + { + name: 'slotsstats', + description: 'Show your slot machine statistics', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + if (!plugin.playerScores.has(from)) { + bot.say(target, `${from}: You haven't played slots yet! Use !slots to start.`); + return; + } + + const player = plugin.playerScores.get(from); + const avgPts = player.totalSpins > 0 ? (player.totalWins / player.totalSpins).toFixed(1) : 0; + + bot.say(target, `๐ŸŽฐ ${from}: ${player.totalWins} total pts | Best: ${player.biggestWin} | Spins: ${player.totalSpins} | Jackpots: ${player.jackpots} | Avg: ${avgPts} pts/spin`); + } + }, + + { + name: 'topslots', + description: 'Show top slot machine players', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + if (plugin.playerScores.size === 0) { + bot.say(target, 'No slot scores recorded yet! Use !slots to start playing.'); + return; + } + + // Get top 3 players and biggest win + const topPlayers = Array.from(plugin.playerScores.values()) + .sort((a, b) => b.totalWins - a.totalWins) + .slice(0, 3); + + const biggestWin = Array.from(plugin.playerScores.values()) + .sort((a, b) => b.biggestWin - a.biggestWin)[0]; + + let output = '๐Ÿ† Top Slots: '; + topPlayers.forEach((player, i) => { + const trophy = i === 0 ? '๐Ÿฅ‡' : i === 1 ? '๐Ÿฅˆ' : '๐Ÿฅ‰'; + output += `${trophy}${player.name}(${player.totalWins}) `; + }); + + if (biggestWin && biggestWin.biggestWin > 0) { + output += `| ๐ŸŽฐ Biggest Win: ${biggestWin.name}(${biggestWin.biggestWin})`; + } + + bot.say(target, output); + } + }, + + { + name: 'slotspayouts', + description: 'Show slot machine payout table', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + + // Create compact payout display + let output = '๐ŸŽฐ Payouts: '; + const sortedSymbols = [...plugin.symbols].sort((a, b) => b.payout - a.payout); + + sortedSymbols.forEach((symbol, i) => { + const pairPayout = Math.floor(symbol.payout / 3); + if (i > 0) output += ' | '; + + if (symbol.emoji === '๐ŸŽฐ') { + output += `${symbol.emoji}x3=${symbol.payout}(JACKPOT!)`; + } else { + output += `${symbol.emoji}x3=${symbol.payout} x2=${pairPayout}`; + } + }); + + bot.say(target, output); + } + }, + + { + name: 'resetslots', + description: 'Reset all slot scores (admin command)', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + + // Simple admin check - you can customize this list + const adminNicks = ['admin', 'owner', 'cancerbot']; // Add your admin nicks here + + if (!adminNicks.includes(from)) { + bot.say(target, `${from}: Access denied - admin only command`); + return; + } + + const playerCount = plugin.playerScores.size; + plugin.playerScores.clear(); + plugin.saveScores(); + + bot.say(target, `๐Ÿ—‘๏ธ ${from}: Reset ${playerCount} slot player scores`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/slots_scores.json b/plugins/slots_scores.json new file mode 100644 index 0000000..ae7a0e8 --- /dev/null +++ b/plugins/slots_scores.json @@ -0,0 +1,23 @@ +{ + "megasconed": { + "totalWins": 57, + "biggestWin": 33, + "totalSpins": 47, + "jackpots": 0, + "name": "megasconed" + }, + "cr0sis": { + "totalWins": 16, + "biggestWin": 5, + "totalSpins": 37, + "jackpots": 0, + "name": "cr0sis" + }, + "aclonedsheeps": { + "totalWins": 3, + "biggestWin": 1, + "totalSpins": 12, + "jackpots": 0, + "name": "aclonedsheeps" + } +} \ No newline at end of file diff --git a/plugins/stories.json b/plugins/stories.json new file mode 100644 index 0000000..79844b3 --- /dev/null +++ b/plugins/stories.json @@ -0,0 +1,29 @@ +{ + "#bakedbeans": { + "title": "Story by cr0sis", + "sentences": [ + { + "author": "StoryBot", + "text": "and then", + "timestamp": 1752341738443 + }, + { + "author": "megasconed", + "text": "and theeen", + "timestamp": 1752341741903 + }, + { + "author": "cr0sis", + "text": "john WAS the demons", + "timestamp": 1752341799537 + }, + { + "author": "megasconed", + "text": "DO IT", + "timestamp": 1752341802504 + } + ], + "lastContribution": 1752341802504, + "maxLength": 20 + } +} \ No newline at end of file diff --git a/plugins/story_plugin.js b/plugins/story_plugin.js new file mode 100644 index 0000000..7c7496c --- /dev/null +++ b/plugins/story_plugin.js @@ -0,0 +1,499 @@ +// plugins/story.js - Collaborative storytelling plugin +const fs = require('fs'); +const path = require('path'); + +module.exports = { + init(bot) { + console.log('๐Ÿ“– Story plugin initialized'); + this.bot = bot; + + // Story data structure + this.stories = new Map(); // channel -> { title, sentences: [{author, text, timestamp}], active, lastContribution, maxLength } + + // Story settings + this.settings = { + maxSentenceLength: 200, + maxStoryLength: 20, // Maximum sentences per story + cooldownTime: 60000, // 1 minute between contributions from same user + maxStoriesPerChannel: 3, // Maximum saved stories per channel + inactivityTimeout: 300000 // 5 minutes of inactivity ends story + }; + + // User cooldowns + this.userCooldowns = new Map(); // "channel:nick" -> timestamp + + // File to save stories + this.storiesFile = path.join(__dirname, 'stories.json'); + + // Load existing stories + this.loadStories(); + + // Story starters for inspiration + this.storyStarters = [ + "In a world where gravity worked backwards, Sarah discovered that her morning coffee was the least of her problems.", + "The last library on Earth had just received its most unusual visitor.", + "Detective Martinez never expected to find a confession letter written in crayon at a crime scene.", + "The GPS kept saying 'turn left at the rainbow,' but there was no rainbow in sight.", + "When the doorbell rang at 3 AM, nobody expected to find a penguin holding a pizza delivery bag.", + "The antique music box in grandmother's attic began playing a song that hadn't been written yet.", + "Every time it rained, the mailbox would fill with letters addressed to people who didn't exist.", + "The new teacher seemed normal until students noticed she never cast a shadow.", + "The fortune cookie contained a message that simply read: 'The dragons know where you hid the keys.'", + "Professor Chen's time machine worked perfectly, except it only traveled to last Tuesday.", + "The vending machine in the subway station started dispensing items that wouldn't be invented for another century.", + "When the power went out citywide, only one house remained mysteriously illuminated.", + "The street musician's violin could make people temporarily forget their own names.", + "Every night at exactly 11:47 PM, the statue in the town square would sneeze.", + "The new apartment came with everything included, except the previous tenant refused to leave the mirror." + ]; + }, + + cleanup(bot) { + console.log('๐Ÿ“– Story plugin cleaned up'); + this.saveStories(); + }, + + // Load stories from file + loadStories() { + try { + if (fs.existsSync(this.storiesFile)) { + const data = fs.readFileSync(this.storiesFile, 'utf8'); + const storiesObject = JSON.parse(data); + + // Convert back to Map and reconstruct story objects + this.stories = new Map(); + for (const [channel, storyData] of Object.entries(storiesObject)) { + this.stories.set(channel, { + title: storyData.title || 'Untitled Story', + sentences: storyData.sentences || [], + active: false, // Always start inactive when loading + lastContribution: storyData.lastContribution || Date.now(), + maxLength: storyData.maxLength || this.settings.maxStoryLength + }); + } + + console.log(`๐Ÿ“š Loaded ${this.stories.size} saved stories`); + } else { + console.log('๐Ÿ“š No existing stories file found, starting fresh'); + } + } catch (error) { + console.error('โŒ Error loading stories:', error); + this.stories = new Map(); + } + }, + + // Save stories to file + saveStories() { + try { + // Only save completed stories (not active ones) + const completedStories = {}; + for (const [channel, story] of this.stories.entries()) { + if (!story.active && story.sentences.length > 0) { + completedStories[channel] = { + title: story.title, + sentences: story.sentences, + lastContribution: story.lastContribution, + maxLength: story.maxLength + }; + } + } + + const data = JSON.stringify(completedStories, null, 2); + fs.writeFileSync(this.storiesFile, data, 'utf8'); + console.log(`๐Ÿ’พ Saved ${Object.keys(completedStories).length} completed stories`); + } catch (error) { + console.error('โŒ Error saving stories:', error); + } + }, + + // Check if user is on cooldown + isOnCooldown(channel, nick) { + const key = `${channel}:${nick}`; + const lastContribution = this.userCooldowns.get(key); + + if (!lastContribution) return false; + + const timeSince = Date.now() - lastContribution; + return timeSince < this.settings.cooldownTime; + }, + + // Set user cooldown + setCooldown(channel, nick) { + const key = `${channel}:${nick}`; + this.userCooldowns.set(key, Date.now()); + }, + + // Get remaining cooldown time + getCooldownRemaining(channel, nick) { + const key = `${channel}:${nick}`; + const lastContribution = this.userCooldowns.get(key); + + if (!lastContribution) return 0; + + const timeSince = Date.now() - lastContribution; + const remaining = this.settings.cooldownTime - timeSince; + return Math.max(0, Math.ceil(remaining / 1000)); + }, + + // Get current story for channel + getCurrentStory(channel) { + return this.stories.get(channel); + }, + + // Start a new story + startStory(channel, title = null, customStarter = null) { + const storyTitle = title || `Story ${Date.now()}`; + + let firstSentence; + if (customStarter) { + firstSentence = customStarter; + } else { + firstSentence = this.storyStarters[Math.floor(Math.random() * this.storyStarters.length)]; + } + + const story = { + title: storyTitle, + sentences: [{ + author: 'StoryBot', + text: firstSentence, + timestamp: Date.now() + }], + active: true, + lastContribution: Date.now(), + maxLength: this.settings.maxStoryLength + }; + + this.stories.set(channel, story); + return story; + }, + + // Add sentence to current story + addSentence(channel, author, text) { + const story = this.getCurrentStory(channel); + if (!story || !story.active) return false; + + // Check if story is at max length + if (story.sentences.length >= story.maxLength) { + this.endStory(channel); + return false; + } + + // Clean up the sentence + const cleanText = text.trim(); + if (cleanText.length === 0) return false; + if (cleanText.length > this.settings.maxSentenceLength) return false; + + // Add the sentence + story.sentences.push({ + author: author, + text: cleanText, + timestamp: Date.now() + }); + + story.lastContribution = Date.now(); + + // Check if story should end (reached max length) + if (story.sentences.length >= story.maxLength) { + this.endStory(channel); + } + + return true; + }, + + // End the current story + endStory(channel) { + const story = this.getCurrentStory(channel); + if (!story) return false; + + story.active = false; + this.saveStories(); + return true; + }, + + // Get story as formatted text + formatStory(story, includeAuthors = false, maxSentences = null) { + if (!story || story.sentences.length === 0) return "No story found."; + + const sentences = maxSentences ? story.sentences.slice(0, maxSentences) : story.sentences; + + let formattedStory = `๐Ÿ“– "${story.title}"\n\n`; + + sentences.forEach((sentence, index) => { + if (includeAuthors && sentence.author !== 'StoryBot') { + formattedStory += `${sentence.text} [${sentence.author}] `; + } else { + formattedStory += `${sentence.text} `; + } + }); + + if (story.active) { + formattedStory += `\n\n๐Ÿ“ Story in progress (${sentences.length}/${story.maxLength} sentences)`; + } else { + formattedStory += `\n\nโœ… Story completed with ${sentences.length} sentences`; + } + + return formattedStory.trim(); + }, + + // Check for inactive stories and end them + checkInactiveStories() { + for (const [channel, story] of this.stories.entries()) { + if (story.active) { + const timeSinceLastContribution = Date.now() - story.lastContribution; + if (timeSinceLastContribution > this.settings.inactivityTimeout) { + this.endStory(channel); + this.bot.say(channel, '๐Ÿ“– Story ended due to inactivity. Use !story to start a new one!'); + } + } + } + }, + + commands: [ + { + name: 'story', + description: 'Start a new collaborative story or add to the current one', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + const channel = context.channel || target; + + // Check for inactive stories + plugin.checkInactiveStories(); + + const currentStory = plugin.getCurrentStory(channel); + + // If no arguments, show current story status + if (args.length === 0) { + if (currentStory && currentStory.active) { + const preview = plugin.formatStory(currentStory, false, 3); + bot.say(target, preview); + bot.say(target, `๐Ÿ’ฌ Add to the story with: !story `); + } else { + bot.say(target, '๐Ÿ“– No active story. Start one with: !story '); + bot.say(target, '๐Ÿ’ก Or use: !storynew for a random starter'); + } + return; + } + + const sentence = args.join(' '); + + // If there's an active story, try to add to it + if (currentStory && currentStory.active) { + // Check cooldown + if (plugin.isOnCooldown(channel, from)) { + const remaining = plugin.getCooldownRemaining(channel, from); + bot.say(target, `${from}: Please wait ${remaining} seconds before contributing again.`); + return; + } + + // Check sentence length + if (sentence.length > plugin.settings.maxSentenceLength) { + bot.say(target, `${from}: Sentence too long! Max ${plugin.settings.maxSentenceLength} characters.`); + return; + } + + // Add the sentence + if (plugin.addSentence(channel, from, sentence)) { + plugin.setCooldown(channel, from); + + // Show last few sentences for context + const recentSentences = currentStory.sentences.slice(-2); + let context = recentSentences.map(s => s.text).join(' '); + + bot.say(target, `๐Ÿ“ ${from} added: "${sentence}"`); + + // Check if story is complete + if (!currentStory.active) { + bot.say(target, `๐ŸŽ‰ Story "${currentStory.title}" is complete! Use !storyread to see the full story.`); + } else { + bot.say(target, `๐Ÿ“– Story continues... (${currentStory.sentences.length}/${currentStory.maxLength})`); + } + } else { + bot.say(target, `${from}: Couldn't add sentence. Story may be complete or sentence invalid.`); + } + } else { + // Start new story with user's sentence + const story = plugin.startStory(channel, `Story by ${from}`, sentence); + plugin.setCooldown(channel, from); + + bot.say(target, `๐Ÿ“– ${from} started a new story: "${sentence}"`); + bot.say(target, `๐Ÿ’ฌ Others can continue with: !story `); + } + } + }, + + { + name: 'storynew', + description: 'Start a new story with a random starter', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const channel = context.channel || target; + + // Check for inactive stories + plugin.checkInactiveStories(); + + const currentStory = plugin.getCurrentStory(channel); + + if (currentStory && currentStory.active) { + bot.say(target, `${from}: There's already an active story! Use !storyend to finish it first.`); + return; + } + + const story = plugin.startStory(channel); + + bot.say(target, `๐Ÿ“– New story started: "${story.sentences[0].text}"`); + bot.say(target, `๐Ÿ’ฌ Continue the story with: !story `); + } + }, + + { + name: 'storyread', + description: 'Read the current or last completed story', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const channel = context.channel || target; + + const story = plugin.getCurrentStory(channel); + + if (!story) { + bot.say(target, 'No story found for this channel. Start one with !story or !storynew'); + return; + } + + const formattedStory = plugin.formatStory(story, true); + + // Split long stories into multiple messages + const maxLength = 400; + if (formattedStory.length <= maxLength) { + bot.say(target, formattedStory); + } else { + const lines = formattedStory.split('\n'); + let currentMessage = ''; + + for (const line of lines) { + if (currentMessage.length + line.length + 1 <= maxLength) { + currentMessage += (currentMessage ? '\n' : '') + line; + } else { + if (currentMessage) { + bot.say(target, currentMessage); + currentMessage = line; + } else { + // Single line too long, split it + bot.say(target, line.substring(0, maxLength)); + currentMessage = line.substring(maxLength); + } + } + } + + if (currentMessage) { + bot.say(target, currentMessage); + } + } + } + }, + + { + name: 'storyend', + description: 'End the current story', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const channel = context.channel || target; + + const story = plugin.getCurrentStory(channel); + + if (!story || !story.active) { + bot.say(target, `${from}: No active story to end.`); + return; + } + + plugin.endStory(channel); + bot.say(target, `๐Ÿ“– ${from} ended the story "${story.title}" with ${story.sentences.length} sentences.`); + bot.say(target, `๐Ÿ“š Use !storyread to read the completed story, or !storynew to start fresh.`); + } + }, + + { + name: 'storystatus', + description: 'Show current story status and stats', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const channel = context.channel || target; + + plugin.checkInactiveStories(); + + const story = plugin.getCurrentStory(channel); + + if (!story) { + bot.say(target, 'No story in this channel. Use !story or !storynew to start one!'); + return; + } + + let statusMsg = `๐Ÿ“Š Story "${story.title}": ${story.sentences.length}/${story.maxLength} sentences`; + + if (story.active) { + statusMsg += ' (Active)'; + + // Show contributors + const contributors = [...new Set(story.sentences.filter(s => s.author !== 'StoryBot').map(s => s.author))]; + if (contributors.length > 0) { + statusMsg += ` | Contributors: ${contributors.join(', ')}`; + } + + // Show cooldown info + if (plugin.isOnCooldown(channel, from)) { + const remaining = plugin.getCooldownRemaining(channel, from); + statusMsg += ` | Your cooldown: ${remaining}s`; + } + } else { + statusMsg += ' (Completed)'; + } + + bot.say(target, statusMsg); + } + }, + + { + name: 'storytitle', + description: 'Set a title for the current story', + execute: function(context, bot) { + const plugin = module.exports; + const target = context.replyTo; + const from = context.nick; + const args = context.args; + const channel = context.channel || target; + + if (args.length === 0) { + bot.say(target, `${from}: Usage: !storytitle `); + return; + } + + const story = plugin.getCurrentStory(channel); + + if (!story) { + bot.say(target, `${from}: No story to rename. Start one first!`); + return; + } + + const newTitle = args.join(' '); + if (newTitle.length > 50) { + bot.say(target, `${from}: Title too long! Max 50 characters.`); + return; + } + + const oldTitle = story.title; + story.title = newTitle; + + bot.say(target, `๐Ÿ“– ${from} renamed the story from "${oldTitle}" to "${newTitle}"`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/zombie_dice_plugin(2).js b/plugins/zombie_dice_plugin(2).js new file mode 100644 index 0000000..697e0ba --- /dev/null +++ b/plugins/zombie_dice_plugin(2).js @@ -0,0 +1,692 @@ +// 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`); + } + } + ] +}; \ No newline at end of file diff --git a/plugins/zombiedice_scores.json b/plugins/zombiedice_scores.json new file mode 100644 index 0000000..16e2e1c --- /dev/null +++ b/plugins/zombiedice_scores.json @@ -0,0 +1,23 @@ +{ + "megasconed": { + "totalBrains": 30, + "bestGame": 17, + "totalGames": 2, + "totalTurns": 11, + "name": "megasconed" + }, + "Loungequi": { + "totalBrains": 0, + "bestGame": 0, + "totalGames": 0, + "totalTurns": 4, + "name": "Loungequi" + }, + "cr0sis": { + "totalBrains": 17, + "bestGame": 10, + "totalGames": 2, + "totalTurns": 13, + "name": "cr0sis" + } +} \ No newline at end of file diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..d6dabac --- /dev/null +++ b/web/index.html @@ -0,0 +1,1288 @@ + + + + + + Pet Battles World Map + + + +
+ +
+ +
+ ๐Ÿ˜๏ธ
Town +
+
+ ๐ŸŒฒ
Forest +
+
+ ๐Ÿ”๏ธ
Mountain +
+
+ ๐ŸŒŠ
Ocean +
+
+ ๐Ÿœ๏ธ
Desert +
+ + +
+ + +
+
+ +
+ + +
+
+
+ + +
+
Player Status
+
+
+ Idle +
+
+
+ Exploring +
+
+
+ Wild Encounter +
+
+
+ In Battle +
+
+ + +
+
World Stats
+
+ Active Players: + 0 +
+
+ Wild Encounters: + 0 +
+
+ Active Battles: + 0 +
+
+ Last Update: + Never +
+
+
+ + +
+
+ +
+
Player Name
+
Current Location
+
+ +
+
๐ŸŽ’ Active Party
+
+ +
+
+ +
+
๐Ÿ“ฆ Inventory
+
+ +
+
+ +
+
๐Ÿ“Š Statistics
+
+ +
+
+
+ + +
+
+
+ + + + + \ No newline at end of file