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:
commit
a3ed25f8dd
39 changed files with 12360 additions and 0 deletions
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal 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
129
CONVERSATION_SUMMARY.md
Normal 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
135
README.md
Normal 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
296
bot_template.js
Normal 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
423
botmain.js
Normal 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
299
complete_irc_bot.js
Normal 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
509
index.html
Normal 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>
|
||||
73
notplugins/example_plugin.js
Normal file
73
notplugins/example_plugin.js
Normal 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
409
notplugins/pigs_plugin.js
Normal 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
2031
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
7
package.json
Normal file
7
package.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"dependencies": {
|
||||
"express": "^5.1.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"ws": "^8.18.3"
|
||||
}
|
||||
}
|
||||
37
plugin_template.js
Normal file
37
plugin_template.js
Normal 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}!`);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
773
plugins/babble_plugin_creative(3).js
Normal file
773
plugins/babble_plugin_creative(3).js
Normal 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
38
plugins/basic.js
Normal 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}`);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
19
plugins/bots_reply_plugin.js
Normal file
19
plugins/bots_reply_plugin.js
Normal 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
337
plugins/coinflip_plugin.js
Normal 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`);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
11
plugins/coinflip_scores.json
Normal file
11
plugins/coinflip_scores.json
Normal 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
339
plugins/combo_plugin.js
Normal 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'}`);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
166
plugins/conspiracy_generator_plugin(1).js
Normal file
166
plugins/conspiracy_generator_plugin(1).js
Normal 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)];
|
||||
}
|
||||
};
|
||||
156
plugins/drunk_historian_plugin.js
Normal file
156
plugins/drunk_historian_plugin.js
Normal 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
597
plugins/duck_hunt_plugin.js
Normal 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);
|
||||
}
|
||||
}
|
||||
};
|
||||
3
plugins/duck_hunt_scores.json
Normal file
3
plugins/duck_hunt_scores.json
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"Monqui": 5
|
||||
}
|
||||
262
plugins/emojify_plugin(2).js
Normal file
262
plugins/emojify_plugin(2).js
Normal 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
397
plugins/hilo_plugin.js
Normal 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
47
plugins/hilo_scores.json
Normal 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
215
plugins/markov_debug.js
Normal 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
20
plugins/pigs_scores.json
Normal 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
|
||||
}
|
||||
}
|
||||
668
plugins/pigsmp_plugin_fixed.js
Normal file
668
plugins/pigsmp_plugin_fixed.js
Normal 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`);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
37
plugins/pigsmp_scores.json
Normal file
37
plugins/pigsmp_scores.json
Normal 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
517
plugins/quiplash_basic.js
Normal 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(', ')}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
455
plugins/scratchcard_plugin(1).js
Normal file
455
plugins/scratchcard_plugin(1).js
Normal 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`);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
34
plugins/scratchcard_scores.json
Normal file
34
plugins/scratchcard_scores.json
Normal 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
328
plugins/slots_plugin.js
Normal 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
23
plugins/slots_scores.json
Normal 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
29
plugins/stories.json
Normal 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
499
plugins/story_plugin.js
Normal 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}"`);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
692
plugins/zombie_dice_plugin(2).js
Normal file
692
plugins/zombie_dice_plugin(2).js
Normal 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`);
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
23
plugins/zombiedice_scores.json
Normal file
23
plugins/zombiedice_scores.json
Normal 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
1288
web/index.html
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue