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 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2025-07-17 19:03:45 +00:00
commit a3ed25f8dd
39 changed files with 12360 additions and 0 deletions

39
.gitignore vendored Normal file
View file

@ -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/

129
CONVERSATION_SUMMARY.md Normal file
View file

@ -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.*

135
README.md Normal file
View file

@ -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! 🦆🏖️

296
bot_template.js Normal file
View file

@ -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;

423
botmain.js Normal file
View file

@ -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;

299
complete_irc_bot.js Normal file
View file

@ -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;

509
index.html Normal file
View file

@ -0,0 +1,509 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Server Music Player</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 100%);
min-height: 100vh;
color: white;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.container {
max-width: 800px;
width: 100%;
background: rgba(0, 0, 0, 0.8);
border-radius: 20px;
padding: 40px;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.3);
backdrop-filter: blur(10px);
border: 2px solid rgba(255, 255, 255, 0.1);
}
h1 {
font-size: 3em;
margin-bottom: 10px;
text-align: center;
background: linear-gradient(45deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.subtitle {
text-align: center;
margin-bottom: 40px;
opacity: 0.8;
font-size: 1.1em;
}
.player-container {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 30px;
margin-bottom: 30px;
text-align: center;
}
.track-info {
margin-bottom: 30px;
}
.track-title {
font-size: 2.2em;
margin-bottom: 10px;
color: #4ecdc4;
word-break: break-word;
}
.track-details {
font-size: 1.1em;
opacity: 0.8;
margin-bottom: 5px;
}
.audio-player {
width: 100%;
margin: 20px 0;
border-radius: 10px;
background: rgba(0, 0, 0, 0.5);
}
.controls {
display: flex;
justify-content: center;
gap: 15px;
margin: 30px 0;
flex-wrap: wrap;
}
.control-btn {
background: linear-gradient(45deg, #4ecdc4, #44a08d);
color: white;
padding: 15px 25px;
border: none;
border-radius: 25px;
font-weight: bold;
cursor: pointer;
transition: all 0.3s ease;
font-size: 1em;
min-width: 120px;
}
.control-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3);
background: linear-gradient(45deg, #44a08d, #4ecdc4);
}
.control-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.status {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
text-align: center;
}
.status.loading {
color: #ffd700;
}
.status.error {
color: #ff6b6b;
}
.status.success {
color: #4ecdc4;
}
.playlist {
background: rgba(255, 255, 255, 0.1);
border-radius: 15px;
padding: 20px;
margin-top: 30px;
max-height: 400px;
overflow-y: auto;
}
.playlist h3 {
color: #4ecdc4;
margin-bottom: 15px;
text-align: center;
}
.track-item {
background: rgba(255, 255, 255, 0.1);
border-radius: 8px;
padding: 15px;
margin-bottom: 10px;
cursor: pointer;
transition: all 0.3s ease;
border-left: 4px solid transparent;
}
.track-item:hover {
background: rgba(255, 255, 255, 0.2);
border-left-color: #4ecdc4;
}
.track-item.current {
background: rgba(78, 205, 196, 0.3);
border-left-color: #4ecdc4;
}
.visualizer {
height: 60px;
background: rgba(0, 0, 0, 0.3);
border-radius: 10px;
margin: 20px 0;
display: flex;
align-items: end;
justify-content: center;
gap: 2px;
padding: 10px;
}
.bar {
width: 4px;
background: linear-gradient(to top, #4ecdc4, #667eea);
border-radius: 2px;
transition: height 0.1s ease;
height: 5px;
}
.settings {
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
padding: 20px;
margin: 20px 0;
}
.settings h4 {
color: #4ecdc4;
margin-bottom: 15px;
}
.setting-item {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 15px;
}
.setting-input {
background: rgba(255, 255, 255, 0.1);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 5px;
padding: 8px 12px;
color: white;
width: 200px;
}
.setting-input:focus {
outline: none;
border-color: #4ecdc4;
}
@media (max-width: 768px) {
.container { padding: 20px; }
h1 { font-size: 2em; }
.track-title { font-size: 1.5em; }
.controls { flex-direction: column; align-items: center; }
.control-btn { width: 200px; }
.setting-item { flex-direction: column; gap: 10px; }
.setting-input { width: 100%; }
}
</style>
</head>
<body>
<div class="container">
<h1>🎵 Server Music Player</h1>
<p class="subtitle">Random FLAC player from server directory</p>
<div class="settings">
<h4>⚙️ Server Configuration</h4>
<div class="setting-item">
<label>Server URL:</label>
<input type="text" id="serverUrl" class="setting-input" value="http://localhost:3000" placeholder="http://localhost:3000">
</div>
<div class="setting-item">
<label>Music Directory:</label>
<input type="text" id="musicDir" class="setting-input" value="/music" placeholder="/path/to/music">
</div>
<button class="control-btn" onclick="connectToServer()">🔗 Connect</button>
</div>
<div class="status" id="status">
Click "Connect" to start browsing your music library
</div>
<div class="player-container" id="playerContainer" style="display: none;">
<div class="track-info">
<div class="track-title" id="trackTitle">No track loaded</div>
<div class="track-details" id="trackPath">Path: N/A</div>
<div class="track-details" id="trackSize">Size: N/A</div>
<div class="track-details" id="trackFormat">Format: FLAC</div>
</div>
<div class="visualizer" id="visualizer"></div>
<audio id="audioPlayer" class="audio-player" controls>
Your browser does not support the audio element.
</audio>
<div class="controls">
<button class="control-btn" onclick="previousTrack()">⏮️ Previous</button>
<button class="control-btn" onclick="getRandomTrack()">🎲 Random</button>
<button class="control-btn" onclick="nextTrack()">⏭️ Next</button>
<button class="control-btn" onclick="refreshLibrary()">🔄 Refresh</button>
</div>
</div>
<div class="playlist" id="playlist" style="display: none;">
<h3>🎵 Music Library</h3>
<div id="trackList"></div>
</div>
</div>
<script>
let musicLibrary = [];
let currentTrackIndex = 0;
let audioPlayer = null;
let serverUrl = 'http://localhost:3000';
let musicDirectory = '/music';
function updateStatus(message, type = 'info') {
const status = document.getElementById('status');
status.textContent = message;
status.className = `status ${type}`;
}
async function connectToServer() {
serverUrl = document.getElementById('serverUrl').value;
musicDirectory = document.getElementById('musicDir').value;
updateStatus('Connecting to server...', 'loading');
try {
const response = await fetch(`${serverUrl}/api/tracks?dir=${encodeURIComponent(musicDirectory)}`);
if (!response.ok) {
throw new Error(`Server responded with ${response.status}`);
}
musicLibrary = await response.json();
if (musicLibrary.length === 0) {
updateStatus('No FLAC files found in the specified directory', 'error');
return;
}
updateStatus(`Connected! Found ${musicLibrary.length} tracks`, 'success');
document.getElementById('playerContainer').style.display = 'block';
document.getElementById('playlist').style.display = 'block';
setupPlayer();
generatePlaylist();
getRandomTrack();
} catch (error) {
updateStatus(`Connection failed: ${error.message}`, 'error');
console.error('Connection error:', error);
}
}
function setupPlayer() {
audioPlayer = document.getElementById('audioPlayer');
audioPlayer.addEventListener('loadstart', () => {
document.getElementById('trackTitle').textContent = 'Loading...';
});
audioPlayer.addEventListener('loadedmetadata', () => {
updateTrackInfo();
animateVisualizer();
});
audioPlayer.addEventListener('ended', () => {
nextTrack();
});
audioPlayer.addEventListener('play', () => {
animateVisualizer();
});
audioPlayer.addEventListener('pause', () => {
stopVisualizer();
});
createVisualizer();
}
function generatePlaylist() {
const trackList = document.getElementById('trackList');
trackList.innerHTML = '';
musicLibrary.forEach((track, index) => {
const trackItem = document.createElement('div');
trackItem.className = 'track-item';
trackItem.onclick = () => playTrack(index);
trackItem.innerHTML = `
<div><strong>${track.name}</strong></div>
<div style="font-size: 0.9em; opacity: 0.7;">${track.path}</div>
<div style="font-size: 0.8em; opacity: 0.6;">Size: ${(track.size / (1024 * 1024)).toFixed(1)} MB</div>
`;
trackList.appendChild(trackItem);
});
}
async function playTrack(index) {
if (index < 0 || index >= musicLibrary.length) return;
currentTrackIndex = index;
const track = musicLibrary[index];
updateStatus('Loading track...', 'loading');
try {
// Get the audio stream URL from server
const streamUrl = `${serverUrl}/api/stream/${encodeURIComponent(track.path)}`;
audioPlayer.src = streamUrl;
updateTrackInfo();
updatePlaylistHighlight();
updateStatus('Track loaded successfully', 'success');
} catch (error) {
updateStatus(`Failed to load track: ${error.message}`, 'error');
}
}
function updateTrackInfo() {
const track = musicLibrary[currentTrackIndex];
document.getElementById('trackTitle').textContent = track.name;
document.getElementById('trackPath').textContent = `Path: ${track.path}`;
document.getElementById('trackSize').textContent = `Size: ${(track.size / (1024 * 1024)).toFixed(1)} MB`;
document.getElementById('trackFormat').textContent = 'Format: FLAC';
}
function updatePlaylistHighlight() {
const trackItems = document.querySelectorAll('.track-item');
trackItems.forEach((item, index) => {
item.classList.toggle('current', index === currentTrackIndex);
});
}
function getRandomTrack() {
if (musicLibrary.length === 0) return;
let randomIndex;
do {
randomIndex = Math.floor(Math.random() * musicLibrary.length);
} while (randomIndex === currentTrackIndex && musicLibrary.length > 1);
playTrack(randomIndex);
}
function nextTrack() {
if (musicLibrary.length === 0) return;
const nextIndex = (currentTrackIndex + 1) % musicLibrary.length;
playTrack(nextIndex);
}
function previousTrack() {
if (musicLibrary.length === 0) return;
const prevIndex = currentTrackIndex > 0 ? currentTrackIndex - 1 : musicLibrary.length - 1;
playTrack(prevIndex);
}
function refreshLibrary() {
connectToServer();
}
function createVisualizer() {
const visualizer = document.getElementById('visualizer');
visualizer.innerHTML = '';
for (let i = 0; i < 50; i++) {
const bar = document.createElement('div');
bar.className = 'bar';
visualizer.appendChild(bar);
}
}
function animateVisualizer() {
const bars = document.querySelectorAll('.bar');
function animate() {
if (audioPlayer && !audioPlayer.paused) {
bars.forEach((bar, index) => {
const height = Math.random() * 40 + 5;
bar.style.height = height + 'px';
bar.style.animationDelay = `${index * 0.05}s`;
});
setTimeout(() => requestAnimationFrame(animate), 100);
}
}
animate();
}
function stopVisualizer() {
const bars = document.querySelectorAll('.bar');
bars.forEach(bar => {
bar.style.height = '5px';
});
}
// Keyboard shortcuts
document.addEventListener('keydown', (e) => {
if (e.target.tagName !== 'INPUT') {
switch(e.code) {
case 'Space':
e.preventDefault();
if (audioPlayer && audioPlayer.src) {
if (audioPlayer.paused) {
audioPlayer.play();
} else {
audioPlayer.pause();
}
}
break;
case 'ArrowRight':
e.preventDefault();
nextTrack();
break;
case 'ArrowLeft':
e.preventDefault();
previousTrack();
break;
case 'KeyR':
e.preventDefault();
getRandomTrack();
break;
}
}
});
</script>
</body>
</html>

View file

@ -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`);
}
}
]
};

409
notplugins/pigs_plugin.js Normal file
View file

@ -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)
}
]
};

2031
package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

7
package.json Normal file
View file

@ -0,0 +1,7 @@
{
"dependencies": {
"express": "^5.1.0",
"sqlite3": "^5.1.7",
"ws": "^8.18.3"
}
}

37
plugin_template.js Normal file
View file

@ -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}!`);
}
}
]
};

View file

@ -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 = ['<START>', ...words, '<END>'];
// 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 !== '<START>' && currentWord !== '<END>') {
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 === '<START>' || randomWord === '<END>') {
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 !== '<START>' && randomWord !== '<END>') {
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 === '<END>') {
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 !== '<START>' && randomWord !== '<END>') {
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 !== '<START>') {
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 <START>, 50% chance to start with random word
if (Math.random() < 0.5) {
callback(null, '<START>');
return;
}
// Build query for random word that appears as word1
let query = 'SELECT word1 FROM word_pairs WHERE word1 != "<START>" AND word1 != "<END>"';
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, '<START>'); // 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 != "<START>" AND word2 != "<END>"';
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 <nick> or !babblecreative <nick>`);
}
);
}
},
{
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 <message to learn>`);
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`);
}
});
});
}
}
]
};

38
plugins/basic.js Normal file
View file

@ -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}`);
}
}
]
};

View file

@ -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');
}
}
};

337
plugins/coinflip_plugin.js Normal file
View file

@ -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 <heads/tails> [bet]
if (args.length === 0) {
bot.say(target, `Usage: !flip <heads/tails> [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`);
}
}
]
};

View file

@ -0,0 +1,11 @@
{
"megasconed": {
"totalWins": 0,
"totalLosses": 3,
"totalFlips": 3,
"biggestWin": 0,
"winStreak": 0,
"bestStreak": 0,
"name": "megasconed"
}
}

339
plugins/combo_plugin.js Normal file
View file

@ -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'}`);
}
}
]
};

View file

@ -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)];
}
};

View file

@ -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)];
}
};

597
plugins/duck_hunt_plugin.js Normal file
View file

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

View file

@ -0,0 +1,3 @@
{
"Monqui": 5
}

View file

@ -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 <message>`);
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!`);
}
}
]
};

397
plugins/hilo_plugin.js Normal file
View file

@ -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 <opponent> <bet>
if (args.length < 2) {
bot.say(target, `Usage: !hilo <opponent> <bet> - 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);
}
};

47
plugins/hilo_scores.json Normal file
View file

@ -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"
}
}

215
plugins/markov_debug.js Normal file
View file

@ -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}`);
}
}
}
]
};

20
plugins/pigs_scores.json Normal file
View file

@ -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
}
}

View file

@ -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`);
}
}
]
};

View file

@ -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"
}
}

517
plugins/quiplash_basic.js Normal file
View file

@ -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 <your funny response>`);
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 <number> (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 <number>`);
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 <number>`);
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 <your response>`);
return;
}
if (args.length === 0) {
bot.say(target, 'Usage: !answer <your funny response>');
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(', ')}`);
}
}
}
]
};

View file

@ -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`);
}
}
]
};

View file

@ -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"
}
}

328
plugins/slots_plugin.js Normal file
View file

@ -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`);
}
}
]
};

23
plugins/slots_scores.json Normal file
View file

@ -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"
}
}

29
plugins/stories.json Normal file
View file

@ -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
}
}

499
plugins/story_plugin.js Normal file
View file

@ -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 <your sentence>`);
} else {
bot.say(target, '📖 No active story. Start one with: !story <your opening sentence>');
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 <next sentence>`);
}
}
},
{
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 <your sentence>`);
}
},
{
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 <new title>`);
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}"`);
}
}
]
};

View file

@ -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`);
}
}
]
};

View file

@ -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"
}
}

1288
web/index.html Normal file

File diff suppressed because it is too large Load diff