megabot/plugins/story_plugin.js
megaproxy a3ed25f8dd 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>
2025-07-17 19:03:45 +00:00

499 lines
No EOL
20 KiB
JavaScript

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