const fs = require('fs'); const RandomOrg = require('random-org'); const { Client, Intents } = require('discord.js'); console.log("Lady Luck"); console.log("A Discord dicebot with rules ad hoceries"); console.log("The Stranjer\n\n"); var auth = JSON.parse(fs.readFileSync('auth.json', 'utf8')); var wc = JSON.parse(fs.readFileSync('wc.json', 'utf8')); var random = new RandomOrg({ apiKey: auth.random, endpoint: 'https://api.random.org/json-rpc/2/invoke' }); var d10s = []; if (fs.existsSync('stored_results.json')) { d10s = JSON.parse(fs.readFileSync('stored_results.json', 'utf8')); } var d10sold = d10s; const isAdmin = member => member.permissions.has("ADMINISTRATOR"); const randInt = (min,max) => min + Math.round(Math.random() * (max - min)); function d10() { const ret = d10s.pop(); return typeof ret == 'undefined' ? randInt(1, 10) : ret; } function unique(value, index, self) { return self.indexOf(value) === index; } function d10RefillCheck() { if (d10s.length < 100) { console.log(`d10s have ${d10s.length} left. Refilling...`); random.generateIntegers({ min: 1, max: 10, n: 100 }).then(function (result) { d10s = d10s.concat(result.random.data); console.log(`d10s now at ${d10s.length}.`); }); } } function dav20Roll(pool, difficulty, options) { const optionsArray = options ? options.toLowerCase().split('').filter(unique) : []; var outcome = { results: [], errors: [], pool: parseInt(pool), botching: !optionsArray.some(opt => opt == 'n'), willpower: optionsArray.some(opt => opt == 'w'), specialty: optionsArray.some(opt => opt == 's'), difficulty: parseInt(difficulty) }; if (isNaN(outcome.pool) || outcome.pool > 25 || outcome.pool < 1) { outcome.errors.push("Pool must be a number between 1 and 25"); } if (isNaN(outcome.difficulty) || outcome.difficulty > 10 || outcome.difficulty < 1) { outcome.errors.push("Must assign a difficulty between 1 and 10"); } for (var i = 0; i < pool; i++) { outcome.results.push(d10()); } d10RefillCheck(); outcome.botches = outcome.botching ? outcome.results.filter(res => res == 1).length : 0; outcome.hits = outcome.results.filter(res => res >= difficulty).length; outcome.successes = outcome.hits - outcome.botches; outcome.successes += outcome.specialty ? outcome.results.filter(res => res == 10).length : 0; outcome.successes = outcome.willpower ? Math.max(outcome.successes + 1, 1) : outcome.successes; return outcome; } function nwodRoll(pool, options='') { var again = 10; var rote = false; var botching = false; for (const char of options) { switch (char) { case '8': again = 8; break; case '9': again = 9; break; case 'r': rote = true break; case 'b': botching = true; break; case 'n': again = 11; break; } } var outcome = { errors: [], successes: 0, results: [], pool: parseInt(pool), again: again, rote: rote, botching: botching }; if (isNaN(outcome.pool)) { outcome.errors.push("Pool must be a number"); } if (isNaN(outcome.again)) { outcome.errors.push("Exploding dice value must be a number") } else if (outcome.again < 8) { outcome.errors.push("Exploding dice may not be below 8"); } if (outcome.pool > 25) { outcome.errors.push("Cannot roll more than 25 dice"); } if (outcome.errors.length) { return outcome; } if (pool < 1) { var result = null; while (true) { result = d10(); outcome.results.push(result); if (result != 10) { break; } outcome.successes++; } d10RefillCheck(); return outcome; } for (i = 0; i < pool; i++) { var free_rerolls = rote ? 1 : 0; // will start out 0 if rote action isn't enabled do { var result = d10(); if (result >= 8) { outcome.successes++; } else if (result == 1 && botching) { outcome.successes--; } outcome.results.push(result); } while (result >= outcome.again || free_rerolls-- > 0); } d10RefillCheck(); return outcome; } function suxxToWords(suxx) { return suxx ? suxx + " success" + (suxx == 1 ? '' : 'es') : 'No success'; } function againToWords(again) { return again == 10 ? '' : (again < 10 ? " (" + again + "-Again)" : " (No rerolls)"); } function dav20ToText(outcome) { if (outcome.errors.length > 0) { return "Fate cannot adjudicate your request because: " + outcome.errors.join("; "); } const prettyResults = outcome.results.map(function (res) { if (res >= outcome.difficulty) { return `**${res}**`; } if (outcome.botching && res == 1) { return `~~${res}~~`; } return `${res}`; }); var outcomeType = ''; if (outcome.successes > 1) { outcomeType = `${outcome.successes} Successes!`; } else if (outcome.successes == 1) { outcomeType = `Single successes!`; } else if (outcome.botches > 0 && outcome.hits == 0) { outcomeType = 'Botch!'; } else { outcomeType = 'Failure...'; } notes = []; if (!outcome.botching) { notes.push("No Botches"); } if (outcome.willpower) { notes.push("Using willpower"); } if (outcome.specialty) { notes.push("Using Specialty"); } return `**Outcome:** ${outcomeType}\n**Pool:** ${outcome.pool}\n**Difficulty:** ${outcome.difficulty}\n**Results:** ${prettyResults.join(', ')}\n**Options:** ${notes.length > 0 ? notes.join(', ') : 'None'}`; } function nwodToText(outcome) { if (outcome.errors.length > 0) { return "Fate cannot adjuciate your request because: " + outcome.errors.join("; "); } else if (outcome.pool < 1) { return `Rolling a chance die with ${suxxToWords(outcome.successes)}. _Individual results:_ ${outcome.results.join(', ')}`; } var newResults = outcome.results; for (const i in newResults) { if (newResults[i] >= outcome.again) { newResults[i] = "**_" + newResults[i] + "_**"; } else if (newResults[i] >= 8) { newResults[i] = "**" + newResults[i] + "**"; } else if (outcome.botching && newResults[i] == 1) { newResults[i] == "~~1~~"; } } notes = []; if (outcome.again == 8 || outcome.again == 9) { notes.push(outcome.again + "-Again"); } if (outcome.botching) { notes.push("Ones Botch"); } if (outcome.rote) { notes.push("Rote Action"); } if (outcome.again == 11) { notes.push("No Rerolls"); } return `Rolling ${outcome.pool}${notes.length > 0 ? ' (' + notes.join(", ") + ')' : ''}; ${suxxToWords(outcome.successes)}. _Individual results:_ ${newResults.join(', ')}`; } function generateTableContent(initTable) { var ret = "**Initiative Table**\n\n```\n"; var chars = initTable.characters; var keys = Object.keys(chars); keys.sort(function (a, b) { return chars[b] - chars[a] }); var padLength = chars[keys[0]].toString().length; for (const charIndex in keys) { charName = keys[charIndex]; var forcesText = (initTable.forces[charName] ? " (forced by " + initTable.forces[charName] + ")" : ""); ret += chars[charName].toString().padStart(padLength) + " : " + charName + forcesText + "\n"; } ret += "```"; return ret; } function nwodInitForceToText(msg, val, name) { if (!/^\d+$/.test(val)) { msg.reply("Can only force the init table if given a number"); return; } if (!name) { name = msg.member ? msg.member.nickname : msg.author.username; } var channelId = msg["channel"].id; if (!initTables[channelId]) { initTables[channelId] = { characters: {}, forces: {} }; } initTables[channelId].characters[name] = val; initTables[channelId].forces[name] = name; var tableContent = generateTableContent(initTables[channelId]); if (initTables[channelId].msg) { initTables[channelId].msg.delete(); } msg.channel.send(tableContent).then(function(tableMsg) { initTables[channelId].msg = tableMsg; }); } function nwodInitToText(msg, offset, name) { if (!/^\d+$/.test(offset)) { offset = 0; } else { offset = Number(offset); } if (!name) { name = msg.member ? msg.member.nickname : msg.author.username; } var roll = d10(); var channelId = msg["channel"].id; var content = `Rolling initiative for ${name}: ${roll} (roll outcome) + ${offset} = ${roll + offset}`; msg.reply(content).then(function (notificationMsg) { if (!initTables[channelId]) { initTables[channelId] = { characters: {}, forces: {} }; } initTables[channelId].characters[name] = roll + offset; var tableContent = generateTableContent(initTables[channelId]); if (initTables[channelId].msg) { initTables[channelId].msg.delete(); } msg.channel.send(tableContent).then(function(tableMsg) { initTables[channelId].msg = tableMsg; }); }); } function nwodInitClear(msg) { var channelId = msg["channel"].id; msg.reply("Cleared initiative table"); initTables[channelId] = { characters: {}, forces: {} }; } function on_ready(client) { console.log(` logged in as ${this.user.tag}!`); } function wcRem(msg, channel_id) { var guild = msg.guild; if (guild == null) { msg.reply("This command must be used in a guild."); return; } if (!isAdmin(msg.member)) { msg.reply("Only administrators may use this command."); return; } var channel = guild.channels.cache.find(chan => chan.id == channel_id); if (channel == null) { msg.reply("Channel does not exist on this server"); return; } if (wc[guild.id] == null) { wc[guild.id] = { "listen_channels" : [], "ooc_channel" : null }; } if (wc[guild.id].listen_channels.includes(channel_id)) { wc[guild.id].listen_channels = wc[guild.id].listen_channels.filter(function (val, ind) { return val != channel_id; }); msg.reply("The channel " + channel.name + " has been removed."); fs.writeFile('wc.json', JSON.stringify(wc), function () {}); } else { msg.reply("The channel " + channel.name + " isn't in the wordcount list"); } } function wcAdd(msg, channel_id) { var guild = msg.guild; if (guild == null) { msg.reply("this command must be used in a guild."); return; } if (!isAdmin(msg.member)) { msg.reply("only administrators may use this command."); return; } if (wc[guild.id] == null) { wc[guild.id] = { "listen_channels" : [], "ooc_channel" : null }; } var channel = guild.channels.cache.find(chan => chan.id == channel_id); if (channel == null) { msg.reply("channel does not exist on this server"); return; } if (wc[guild.id].listen_channels.includes(channel_id)) { msg.reply("That channel is already added."); } else { wc[guild.id].listen_channels.push(channel_id); msg.reply("Added " + channel.name); fs.writeFile('wc.json', JSON.stringify(wc), function () {}); } } function wcList(msg) { var guild = msg.guild; if (guild == null) { msg.reply("This command must be used in a guild."); return; } if (!isAdmin(msg.member)) { msg.reply("Only administrators may use this command."); return; } if (wc[guild.id] == null) { wc[guild.id] = { "listen_channels" : [], "ooc_channel" : null }; } var channels = guild.channels.cache.filter(chan => wc[guild.id].listen_channels.includes(chan.id)).array(); if (channels.length == 0) { msg.reply("This server has no wordcount channels."); return; } var reply = "The wordcount channels are:\n\n"; for (i = 0; i < channels.length; i++) { reply += channels[i].name + "\n"; } msg.reply(reply); } function wcOOC(msg, channel_id) { var guild = msg.guild; if (guild == null) { msg.reply("this command must be used in a guild."); return; } if (!isAdmin(msg.member)) { msg.reply("only administrators may use this command."); return; } if (wc[guild.id] == null) { wc[guild.id] = { "listen_channels" : [], "ooc_channel" : null }; } var channel = guild.channels.cache.find(chan => chan.id == channel_id); if (channel == null) { msg.reply("channel does not exist on this server"); return; } wc[guild.id].ooc_channel = channel.id; msg.reply("OOC award channel set to " + channel.name); } function wordCount(prose) { let length = prose.match(/[\w'’]+/gi); length = length == null ? 0 : length.length; return length; } function wcRA(msg, role_id) { var guild = msg.guild; if (guild == null) { msg.reply("this command must be used in a guild."); return; } if (!isAdmin(msg.member)) { msg.reply("only administrators may use this command."); return; } if (wc[guild.id] == null) { wc[guild.id] = { "listen_channels" : [], "ooc_channel" : null, "roles" : [] }; } guild.roles.fetch(role_id).then(role => { if (role == null) { msg.reply("role does not exist on this server"); return; } if (wc[guild.id].roles == null) { wc[guild.id].roles = []; } if (wc[guild.id].roles.includes(role_id)) { msg.reply("That role is already added."); } else { wc[guild.id].roles.push(role_id); msg.reply("Added " + role.name); fs.writeFile('wc.json', JSON.stringify(wc), function () {}); } }); } function wcRR(msg, role_id) { var guild = msg.guild; if (guild == null) { msg.reply("this command must be used in a guild."); return; } if (!isAdmin(msg.member)) { msg.reply("only administrators may use this command."); return; } if (wc[guild.id] == null) { wc[guild.id] = { "listen_channels" : [], "ooc_channel" : null, "roles" : [] }; } guild.roles.fetch(role_id).then(role => { wc[guild.id].roles = wc[guild.id].roles.filter(function (val, ind) { return val != role_id }); msg.reply("Removed role " + (role ? role.name : role_id) + "."); fs.writeFile('wc.json', JSON.stringify(wc), function () {}); }); } function wordCountConsider(msg) { var guild = msg.guild; if (guild == null) { return; } var channel = msg.channel; if (wc[guild.id] == null || !wc[guild.id].listen_channels.includes(channel.id.toString())) { return; } var roles = msg.member.roles.cache.array(); if (wc[guild.id].roles == null || msg.member.roles.cache.every(function (role) { return !wc[guild.id].roles.includes(role.id); })) { return; } var ooc_channel = guild.channels.cache.find(chan => chan.id == wc[guild.id].ooc_channel); if (ooc_channel == null) { return; } if (wc[guild.id].word_count_reward == null) { wc[guild.id].word_count_reward = 50; } if (wc[guild.id].users == null) { wc[guild.id].users = {}; } if (wc[guild.id].users[msg.author.id] == null) { wc[guild.id].users[msg.author.id] = { word_count: 0, bonus_points: 0 }; } var wordCountCalc = wordCount(msg.content); var wordCountTotal = wc[guild.id].users[msg.author.id].word_count + wordCountCalc; var reward = Math.floor(wordCountTotal / wc[guild.id].word_count_reward); wc[guild.id].users[msg.author.id].word_count = wordCountTotal - (wc[guild.id].word_count_reward * reward); wc[guild.id].users[msg.author.id].bonus_points += reward; if (!wc[guild.id].users[msg.author.id].last_sent) { wc[guild.id].users[msg.author.id].last_sent = 0; } var now = Math.floor(Date.now() / 1000); if (reward > 0 && now > wc[guild.id].users[msg.author.id].last_sent) { ooc_channel.send(msg.author.toString() + " wrote a post with " + wordCountCalc + " words, earning " + reward + " Bonus Points. This user's total is now " + wc[guild.id].users[msg.author.id].bonus_points + "."); wc[guild.id].users[msg.author.id].last_sent = now + 3600; } fs.writeFile('wc.json', JSON.stringify(wc), function () {}); } function wcForce(msg, user_id, bp_total, wc_total) { if (user_id == null || isNaN(user_id)) { msg.reply("You must specify the user whose total you are forcing."); return; } if (bp_total == null || isNaN(bp_total) ) { msg.reply("Must submit a bonus point total, and it must be a number"); return; } var guild = msg.guild; if (guild == null) { msg.reply("this command must be used in a guild."); return; } if (!isAdmin(msg.member)) { msg.reply("only administrators may use this command."); return; } var user = guild.members.cache.array().filter(member => member.user.id == user_id)[0]; if (user == null) { msg.reply("that user isn't on this server."); return; } if (wc[guild.id].users == null) { wc[guild.id].users = {}; } if (wc[guild.id].users[user.id] == null) { wc[guild.id].users[user.id] = { bonus_points: 0, word_count: 0 }; } wc[guild.id].users[user.id].bonus_points = parseInt(bp_total); if (wc_total != null && !isNaN(wc_total)) { wc[guild.id].users[user.id].word_count = parseInt(wc_total); } msg.reply(user.toString() + " now has " + wc[guild.id].users[user.id].bonus_points + " Bonus Points and a word count cache of " + wc[guild.id].users[user.id].word_count + ".") fs.writeFile('wc.json', JSON.stringify(wc), function () {}); } function wcShow(msg, user_id) { var guild = msg.guild; if (guild == null) { msg.reply("this command must be used in a guild."); return; } var user = user_id == null || isNaN(user_id) ? msg.author : guild.members.cache.array().filter(member => member.user.id == parseInt(user_id))[0].user; if (wc[guild.id] == null) { wc[guild.id] = { }; } if (wc[guild.id].users == null) { wc[guild.id].users = {}; } if (wc[guild.id].users[user.id] == null) { wc[guild.id].users[user.id] = { bonus_points: 0, word_counts: 0 }; } var bonus_points = wc[guild.id].users[user.id].bonus_points; var word_count = wc[guild.id].users[user.id].word_count; if (bonus_points == null || bonus_points == 0) { bonus_points = "no"; } if (word_count == null || word_count == 0) { word_count = "no"; } msg.reply(user.toString() + " has " + wc[guild.id].users[user.id].bonus_points + " Bonus Points and is " + wc[guild.id].users[user.id].word_count + " words toward their next one."); } function handle_message(msg) { const words = msg.content.split(/\s+/); const command = words[0].toLowerCase(); switch (command) { case '!nwod': const nWoDoutcome = nwodToText(nwodRoll(words[1], words[2])); console.log(`New nWoD dice roll from ${msg.author.username}#${msg.author.discriminator}. Outcome: ${nWoDoutcome}`); msg.reply(nWoDoutcome); break; case '!dav20': const dav20outcome = dav20ToText(dav20Roll(words[1], words[2], words[3])); console.log(`New dav20 roll from ${msg.author.username}#${msg.author.discriminator}. Outcome: ${dav20outcome}`); msg.reply(dav20outcome); break; case '!init': nwodInitToText(msg, words[1], words[2]); break; case '!initforce': nwodInitForceToText(msg, words[1], words[2]); break; case '!initclear': nwodInitClear(msg); break; case '!wc-add': wcAdd(msg, words[1]); break; case '!wc-rem': wcRem(msg, words[1]); break; case '!wc-ooc': wcOOC(msg, words[1]); break; case '!wc-ra': wcRA(msg, words[1]); break; case '!wc-rr': wcRR(msg, words[1]); break; case '!wc-list': wcList(msg); break; case '!wc-force': wcForce(msg, words[1], words[2], words[3]); break; case '!wc': wcShow(msg, words[1]); break; } wordCountConsider(msg); } var clients = []; for (const token of auth.token) { const client = new Client({ intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES] }); process.stdout.write("Logging in now..."); client.on('message', handle_message); client.on('ready', on_ready); client.login(token); } d10RefillCheck(); initTables = {}; var minute = 1000 * 60; setInterval(function () { if (d10sold == d10s) { return; } fs.writeFileSync('stored_results.json', JSON.stringify(d10s)); d10sold = d10s; console.log("Updating stored results"); }, minute);