Bots Home
|
Create an App
The Drinking Game
Author:
batorboy4vw
Description
Source Code
Launch Bot
Current Users
Created by:
Batorboy4vw
/* * Copyright 2019 batorboy4vw * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * * Veronique Wilde's The Drinking Game * * Note: Despite the name of the bot this game has nothing to do with alcohol * consumption or drinking. But it does draw its insiration from games like * https://thechuggernauts.com/sir-david-attenborough-drinking-game/ * * Change Log * * 2.1.0 * - Added vanity icons for batorboy * - Fix autostart bug that caused the bot to always start when loaded. * - Phrase matching is now case insensitive. * - Different points per phrase * - Different matching algorighms per phrase * - Private shows for admins. * * 2.0.0 * - Complete re-write of the first bot. */ const VERSION = '2.1.0-RC1' // Title used at various display points. const GAME_TITLE = "Veronique Wilde's The Drinking Game" // The users that have tipped at least once since the bot has started. let tippers = {} const symbols = { none: 'none', kiss: '\u{1f48b}', heart: '\u{2665}', diamond: '\u{2666}', star: '\u{2605}', rose: '\u{1f339}', crown: '\u{1F451}', bear: '\u{1F43B}' } // Configuration options when the bot is started/ cb.settings_choices = [ {name:'grammar_nazi', type:'choice', choice1: 'enable', choice2:'disable', defaultValue: 'disable', label:"Grammar Nazi "}, {name:'censor', type:'choice', choice1:'yes', choice2:'no', defaultValue: 'yes', label:"Censor whore and bitch"}, {name:'match_mode', type:'choice', choice1:'exact', choice2:'lax', defaultValue:'exact', label:"Ignored..."}, {name:'multi_match', type:'choice', choice1:'yes', choice2:'no', defaultValue:'no', label:'Multi Match. Allow multiple points per phrase.'}, {name:'cheat_mode', type:'choice', choice1:'on', choice2:'off', defaultValue:'off', label:'Cheat Mode. Players can give themselves points if they enter one of the phrases.'}, {name:'admins', type:'str', required: false, maxLength:2048, label:'Admins. Users that are allowed to issue commands to the bot.', defaultValue:'batorboy4vw'}, {name:'tags', type:'str', maxLength:2048, label: 'Tags used when the bot sets the room title. Can be changed with the /vw tags command.', defaultValue:'cute sexy feet legs stockings bigboobs squirt shaved lovense young dirty natural heels bigtits dildo panties ass pussy blonde'}, {name:'subject', label:"Can admins change the room subject/tags?", type:'choice', choice1:'yes', choice2:'no', defaultValue:'yes'}, {name:'verbose', type:'choice', choice1:'yes', choice2:'no', defaultValue:'no', label:'Verbose. Display /vw commands in chat'}, {name:'welcome', label:'Welcome new users', type:'choice', choice1:'yes', choice2:'no', defaultValue:'yes'}, {name:'welcome_message', label:'Welcome message', type:'str', defaultValue:'Welcome to my room {user}. | Please :followtip99 | Thank you :hearts3'}, {name:'icon', label:"Display a vanity icon by batorboy's name in chat", type:'choice', choice1:'None', choice2:'Bear', choice3:'Kiss', choice4:'Heart', choice5:'Star', choice6:'Diamond', choice7:'Rose', choice8:'Crown', defaultValue:'Bear'}, {name:'autostart', label:'Autostart. Start game when bot loads?', type:'choice', choice1:'yes', choice2:'no', defaultValue:'yes'}, {name:'phrase0', label:'Phrase 1', required:false, type:'str', defaultValue:'hi bb'}, {name:'phrase0_mode', label:'Phrase 1 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase0_score', label:'Phrase 1 Score', required:false, type:'int', minValue:1, defaultValue:'3'}, {name:'phrase1', label:'Phrase 2', required:false, type:'str', defaultValue:'bb'}, {name:'phrase1_mode', label:'Phrase 2 Mode', required:false, type:'choice', defaultValue:'lax', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase1_score', label:'Phrase 2 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase2', label:'Phrase 3', required:false, type:'str', defaultValue:'show feet'}, {name:'phrase2_mode', label:'Phrase 3 Mode', required:false, type:'choice', defaultValue:'any', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase2_score', label:'Phrase 3 Score', required:false, type:'int', minValue:1, defaultValue:'2'}, {name:'phrase3', label:'Phrase 4', required:false, type:'str', defaultValue:'show ass'}, {name:'phrase3_mode', label:'Phrase 4 Mode', required:false, type:'choice', defaultValue:'any', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase3_score', label:'Phrase 4 Score', required:false, type:'int', minValue:1, defaultValue:'2'}, {name:'phrase4', label:'Phrase 5', required:false, type:'str', defaultValue:'show boobs'}, {name:'phrase4_mode', label:'Phrase 5 Mode', required:false, type:'choice', defaultValue:'any', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase4_score', label:'Phrase 5 Score', required:false, type:'int', minValue:1, defaultValue:'2'}, {name:'phrase5', label:'Phrase 6', required:false, type:'str', defaultValue:'show pussy'}, {name:'phrase5_mode', label:'Phrase 6 Mode', required:false, type:'choice', defaultValue:'any', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase5_score', label:'Phrase 6 Score', required:false, type:'int', minValue:1, defaultValue:'2'}, {name:'phrase6', label:'Phrase 7', required:false, type:'str', defaultValue:'stand up'}, {name:'phrase6_mode', label:'Phrase 7 Mode', required:false, type:'choice', defaultValue:'lax', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase6_score', label:'Phrase 7 Score', required:false, type:'int', minValue:1, defaultValue:'2'}, {name:'phrase7', label:'Phrase 8', required:false, type:'str', defaultValue:'turn around'}, {name:'phrase7_mode', label:'Phrase 8 Mode', required:false, type:'choice', defaultValue:'lax', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase7_score', label:'Phrase 8 Score', required:false, type:'int', minValue:1, defaultValue:'2'}, {name:'phrase8', label:'Phrase 9', required:false, type:'str', defaultValue:'open bobs'}, {name:'phrase8_mode', label:'Phrase 9 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase8_score', label:'Phrase 9 Score', required:false, type:'int', minValue:1, defaultValue:'20'}, {name:'phrase9', label:'Phrase 10', required:false, type:'str', defaultValue:'pm'}, {name:'phrase9_mode', label:'Phrase 10 Mode', required:false, type:'choice', defaultValue:'lax', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase9_score', label:'Phrase 10 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase10', label:'Phrase 11', required:false, type:'str', defaultValue:'pm me'}, {name:'phrase10_mode', label:'Phrase 11 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase10_score', label:'Phrase 11 Score', required:false, type:'int', minValue:1, defaultValue:'2'}, {name:'phrase11', label:'Phrase 12', required:false, type:'str', defaultValue:'pm pls'}, {name:'phrase11_mode', label:'Phrase 12 Mode', required:false, type:'choice', defaultValue:'any', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase11_score', label:'Phrase 12 Score', required:false, type:'int', minValue:1, defaultValue:'2'}, {name:'phrase12', label:'Phrase 13', required:false, type:'str', defaultValue:'pm for pvt'}, {name:'phrase12_mode', label:'Phrase 13 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase12_score', label:'Phrase 13 Score', required:false, type:'int', minValue:1, defaultValue:'5'}, {name:'phrase13', label:'Phrase 14', required:false, type:'str', defaultValue:'hru'}, {name:'phrase13_mode', label:'Phrase 14 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase13_score', label:'Phrase 14 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase14', label:'Phrase 15', required:false, type:'str', defaultValue:'mama mia'}, {name:'phrase14_mode', label:'Phrase 15 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase14_score', label:'Phrase 15 Score', required:false, type:'int', minValue:1, defaultValue:'5'}, {name:'phrase15', label:'Phrase 16', required:false, type:'str', defaultValue:'i love you'}, {name:'phrase15_mode', label:'Phrase 16 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase15_score', label:'Phrase 16 Score', required:false, type:'int', minValue:1, defaultValue:'5'}, {name:'phrase16', label:'Phrase 17', required:false, type:'str', defaultValue:''}, {name:'phrase16_mode', label:'Phrase 17 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase16_score', label:'Phrase 17 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase17', label:'Phrase 18', required:false, type:'str', defaultValue:''}, {name:'phrase17_mode', label:'Phrase 18 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase17_score', label:'Phrase 18 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase18', label:'Phrase 19', required:false, type:'str', defaultValue:''}, {name:'phrase18_mode', label:'Phrase 19 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase18_score', label:'Phrase 19 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase19', label:'Phrase 20', required:false, type:'str', defaultValue:''}, {name:'phrase19_mode', label:'Phrase 20 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase19_score', label:'Phrase 20 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase20', label:'Phrase 21', required:false, type:'str', defaultValue:''}, {name:'phrase20_mode', label:'Phrase 21 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase20_score', label:'Phrase 21 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase21', label:'Phrase 22', required:false, type:'str', defaultValue:''}, {name:'phrase21_mode', label:'Phrase 22 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase21_score', label:'Phrase 22 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase22', label:'Phrase 23', required:false, type:'str', defaultValue:''}, {name:'phrase22_mode', label:'Phrase 23 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase22_score', label:'Phrase 23 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase23', label:'Phrase 24', required:false, type:'str', defaultValue:''}, {name:'phrase23_mode', label:'Phrase 24 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase23_score', label:'Phrase 24 Score', required:false, type:'int', minValue:1, defaultValue:'1'}, {name:'phrase24', label:'Phrase 25', required:false, type:'str', defaultValue:''}, {name:'phrase24_mode', label:'Phrase 25 Mode', required:false, type:'choice', defaultValue:'exact', choice1:'lax', choice2:'exact', choice3:'any'}, {name:'phrase24_score', label:'Phrase 25 Score', required:false, type:'int', minValue:1, defaultValue:'1'} ]; const NUM_PHRASES = 25 // Watch phrases for the grammar nazi const your = ['your so', 'your the', 'your a', 'your an', "your very", "your beautiful", "your gorgeous", "your cute", "your sexy"] // Data for the help message that is displayed in response to '/vw help'. // This will be converted to a monospace font and the first column padded // with non-breaking spaces. const help_data = [ ['about','displays the game title and version'], ['start','starts a new game with new phrases lists'], ['stop','stops the current game'], ['score','show the current game scores'], ['status','display bot options'], ['list', 'list phrases assigned to players'], ['phrases', 'list all phrases'], ['private', 'start or stop a private show'], ['tags','set the room #tags'], ['subject', 'change the room subject'], ['add','add new phrases, players, or admins'], ['remove','remove phrases, players or admins'], ['grammar','turn the Grammar Nazi on/off'], ['match','select the phrase matching mode'], ['cheat','turn cheat mode on/off'], ['censor','the the whore/bitch censor on/off'], ['timer','start, stop, or list times'], ['welcome','enable the welcome notice to new users'], ['help','get help about commands'] ] // The actual help message that will be displayed. const help = `The following commands are available. ${pad(help_data)} To get additional help with a command pass the command name to '/vw help' EG: /vw help timer ` /* * Game states. Once a game has been started the states will only alternate * between STOPPED and STARTED. The WAITING and INITIALIZED states are * mainly for runing the bot in the ADK testing framework. (Google it). */ const WAITING = 'waiting' // bot has been loaded const INITIALIZED = 'INITIALIZED' // the bot has been loaded and init() has run const STARTED = 'started' // game has started const STOPPED = 'stopped' // game has stopped // The messages that are sent to the room owner when I enter the room. const vw_messages = [ "You are the most awesome woman in the world.", "You are so cute and adorable it makes my heart melt", "You have very sexy knees.", "I will tell you a secret... your Little Bear wants to kiss you.", "Veronique Wilde has the cutest dimples ever.", "All hail my Greek Goddess Aphrodite", "You are so beautiful it takes my breath away." ] Array.prototype.remove = function(item) { let i = this.indexOf(item) if (i === -1) { return false } this.splice(i, 1) return true } /* * Information about each player in the game. */ class Player { constructor(name, isAdmin) { this.name = name this.points = 0 this.phrases = [] this.admin = isAdmin cb.log(`Created ${name} is admin ${isAdmin}`) } // Reset the points and delete all phrases. reset() { this.points = 0 this.phrases.length = 0 } // Adds a new phrase to the players. add(phrase) { let index = this.phrases.findIndex(p => p.text === phrase.text) if (index !== -1) { return false } this.phrases.push(phrase) return true } remove(s) { let index = this.phrases.findIndex(p => s === p.text) if (index === -1) { return false } this.phrases.splice(index, 1) return true } toString() { cb.log(`Converting ${this.name} to string.`) cb.log(`Player has ${this.phrases.length} phrases.`) let phrase_list = this.phrases.map(s => `"${s.text}"`).join(', ') let s = `${this.name} (${this.phrases.length}) [${phrase_list}]` cb.log(s) return s } score(user, text) { cb.log(`Scoring player ${this.name}`) // let matcher = game.match_mode() let score = 0 for (let i = 0; i < this.phrases.length; ++i) { let phrase = this.phrases[i] if (phrase.match(text)) { if (tippers[user] == null) { this.points += phrase.points let points = phrase.points === 1 ? 'point' : 'points' notify_players(`${phrase.points} ${points} for ${this.name} [${phrase.text}]`) } else { this.points += phrase.points + 1 let points = phrase.points === 1 ? 'point' : 'points' notify_players(`${phrase.points} ${points} for ${this.name} + tipper's bonus! [${phrase.text}]`) } if (game.match_once) { return } } } } } let matchers = { // The text and phrase must be the same. // eg. exact('hi bb', 'hi bb') === true exact: function(user_text) { return this.text === user_text }, // The text must appear somewhere in the phrase // eg lax('kiss me', 'please kiss me bb') === true lax: function(user_text) { return user_text.indexOf(this.text) !== -1 }, // The words in the phrase can appear anywhere as long // as they are in the same order // eg any('i love you', 'i really do love you') === true any: function(user_text) { let words = this.text if (typeof words === 'string') { words = this.text.split(' ') } let index = 0 for (let i = 0; i < words.length; ++i) { index = user_text.indexOf(words[i], index) if (index === -1) { return false } } return true } } class Phrase { constructor(points, match, text) { this.points = parseInt(points) this.match = matchers[match] this.mode = match if (this.match == null) { notify_admins('Invalid matching mode ' + match) this.match = matchers.exact this.mode = 'exact' } this.text = text } } class Command { constructor(name, f, help) { this.name = name this.f = f this.helpText = help // cb.log(`Created command ${name} with ${typeof f}`) } execute(user, args) { // cb.log(`Command.execute : command executed by ${user}`) this.f(user, args) } help(user) { if (this.help == null) { notify(user, 'There is not help available for this command') return } notify(user, help) } } class AddCommand extends Command { constructor() { super('add', function(user, args) { if (args.length === 0) { notify(user, 'Invalid add command.') return } let subcommand = args.shift() if (args.length === 0) { notify(user, `Insufficient args for add ${subcommand}`) return } switch(subcommand) { case 'phrase': if (args.length < 3) { notify(user, 'Not enough parameters. Use: /vw phrase add 1 lax hi bb') return } let points = parseInt(args.shift()) if (isNaN(points)) { notify(user, 'Invalid points value.') return } let mode = args.shift() let invalid_mode = true switch (mode) { case 'lax': case 'any': case 'exact': invalid_mode = false break; default: notify(user, `${mode} is an invalid scoring mode. Must be one of 'lax', 'exact', or 'any'`) break } if (invalid_mode) { return } let phrase = args.join(' ') // if (game.phrases.includes(phrase)) { // notify(user, 'That phrase is already included.') // return // } if (!game.hasPhrase(phrase)) { let p = new Phrase(points, mode, phrase) game.phrases.push(p) if (game.started) { let player = game.players.next() player.add(p) } } notify_admins(`Adding phrase "${phrase}"`) break case 'admin': while (args.length > 0) { let name = args.shift() let player = game.get(name) if (player == null) { //notify(user, 'No such player') game.players.add(new Player(name,true)) notify_admins(`Created a new player ${name} and made them an admin`) return } if (player.admin) { notify(user, 'The player is already an admin') return } player.admin = true notify_admins(`Made ${name} an admin.`) } break case 'player': while (args.length > 0) { let name = args.shift() if (game.isPlayer(name)) { notify(user, `${name} is already playing`) return } game.players.add(new Player(user, false)) notify_admins(`Added player ${name}`) } break case 'help': this.help(user) break default: notify(user, `Invalid option "${subcommand}" for add command.`) break } }) } help(user) { let msg = `Add phrases, players, or admins /vw add phrase the phrase to be added /vw add player name [name ...] /vw add admin name [name ...] It is possible to add more than one player or admin at a time by providing a list of name separated by a single space.` notify(user, msg) } } class RemoveCommand extends Command { constructor() { super('remove', function(user, args) { if (args.length === 0) { notify(user, 'Invalid remove command.') return } let subcommand = args.shift() if (args.length === 0) { notify(user, `Insufficient args for remove ${subcommand}`) return } switch(subcommand) { case 'phrase': let phrase = args.join(' ') let index = game.phrases.findIndex(p => p.text === phrase) if (index === -1) { notify(user, `No such phrase "${phrase}`) return } game.phrases.splice(index, 1) notify_admins(`The phrase "${phrase} has been removed from play.`) /* if (game.phrases.remove(phrase)) { // game.phrases.remove(phrase) notify_admins(`Removed phrase '${phrase}`) game.players.list.forEach(p => p.remove(phrase)) } else { notify(user, 'No such phrase.') } */ break case 'admin': while (args.length > 0) { let name = args.shift() if (name === cb.room_slug) { if (user === name) { notify(user, 'You are the room owner and will alwasy be an admin') } else { notify(user, `The model can not be removed from the admins list. The model has been notified.`) notify(cb.room_slug, `${user} just tried to remove your admin status.`) } return } let player = game.get(name) if (player == null) { notify(user, `No player named ${name}`) } else if (player.admin) { player.admin = false notify_admins(`Removed admin status from ${name}`) } else { notify(user, `Player ${name} is not an admin.`) } } break case 'player': while (args.length > 0) { let name = args.shift() if (game.players.remove(name)) { notify_admins(`Removed player ${name}`) } else { notify(user, `There is no player named ${name}`) } } break case 'help': this.help(user) break default: notify(user, `Invalid option "${subcommand}" for remove command.`) break } }) } help(user) { notify(user, `Remove phrases, players, or admins /vw remove phrase the phrase to be removed /vw remove player name [name ...] /vw remove admin name [name ...] It is possible to remove more than one player or admin at a time by providing a list of name separated by a single space.`) } } class OptionCommand extends Command { constructor(name, prop, description) { super(name, function(user,args) { if (args.length === 0) { notify(user, 'Invalid command. No argument provided.') return } let mode = args[0] if (mode === 'on') { if(game[this.prop]) { notify(user, `The option ${name} is already enabled.`) } else { game[this.prop] = true notify_admins('Enabled ' + name + ' mode') } } else if (mode === 'off') { if (game[this.prop]) { game[this.prop] = false notify_admins('Disabled ' + name + ' mode') } else { notify(user, `The option ${name} is not enabled`) } } else if (mode === 'help') { notify(user, this.helpText) } else { notify(user, "Invalid option. Must be one of 'on' or 'off'") } }) this.prop = prop == null ? name : prop this.helpText = `${description} /vw ${name} on /vw ${name} off` } } class MatchCommand extends Command { constructor() { super('match', function(user,args) { if (args.length === 0) { notify(user, "Invalid command.") return } let mode = args.shift() if (mode === 'exact') { game.exact_match = true notify_admins('Using exact match mode (phrase must be an exact match. This does not actually do anything anymore.') } else if (mode === 'lax') { game.exact_match = false notify_admins('Using lax match mode (phrase can appear anywhere. . This does not actually do anything anymore.') } else if (mode === 'once') { game.match_once = true notify_admins('Match once mode enabled. Players will only get one point per phrase') } else if (mode === 'many') { game.match_once = false notify_admins('Multi match mode enabled. Players can get more than one point per phrase.') } else if (mode === 'help') { this.help(user) } else { notify(user, `Invalid option '${mode}'. Must be one of 'exact', 'lax', 'once', or 'many'`) } }) } help(user) { // let msg = `Controls how scoring is performed on text users enter into chat. There are four modes 'exact' and 'lax' control the matching mode. In 'exact' match mode the user must enter the text exactly as it appears. In 'lax' match mode the phrase can appear anywhere in the text entered by the user. The modes 'once' and 'many' control multi-match mode. The 'once' mode means players can only score one point per phrase. The 'many' mode allows players to score more than one point per phrase. // /vw match exact // /vw match lax // /vw match once // /vw match many` let msg = `Controls how scoring is performed on text users enter into chat. There are two modes 'many' and 'once'. The the match mode is set to 'once' players can only score one point per phrase. The 'many' mode allows players to score more than one point per phrase. /vw match once /vw match many` notify(user, msg) } } // class Timer { // constuctor() { let timer_ids = [] let timer_data = [] // } function setTimer(user, duration, unit, message) { let scale = 1 switch(unit) { case 'm': case 'min': case 'minute': case 'minutes': scale = 60000 break; case 's': case 'sec': case 'second': case 'seconds': scale = 1000 break; case 'h': case 'hour': case 'hours': scale = 3600000 break; default: notify(user, "Invalid timer duration: " + unit) return } let n = parseInt(duration, 10) if (isNaN(n)) { notify(user,"Invalid timer duration: " + duration) return } let handler = function() { notify_admins(message) } let id = cb.setTimeout(handler, n * scale) timer_data[id] = `${id} - ${duration} ${unit} '${message}'` timer_ids.push(id) cb.log(`Timer IDs: ${timer_ids.length}`) cb.log(timer_ids) notify_admins(`Timer ${id} started for ${duration} ${unit}`) let cleanup = function() { cb.log(`Cleanup for ${id}`) if (timer_ids.includes(id)) { cb.log(`Removing id ${id} from timers list`) timer_ids = cbjs.arrayRemove(timer_ids, id) } if (timer_data[id] != null) { cb.log(`Removing timer data for ${id}`) delete timer_data[id] } } let delay = (n * scale) + 100 cb.setTimeout(cleanup, delay) return id } function cancelTimer(user, id) { cb.log(`cancel timer ${id}`) if (typeof id === 'string') { id = parseInt(id) if (isNaN(id)) { notify(user, `Invalid timer ID`) return } } cb.log('attempting to cancel timer.') cb.cancelTimeout(id) timer_ids = cbjs.arrayRemove(timer_ids, id) if (timer_data[id] != null) { delete timer_data[id] } cb.log(`There are ${timer_ids.length} timers still running.`) notify_admins(`Timer ${id} cancelled.`) } function listTimers(user) { if (timer_ids.length === 0) { notify(user, 'There are no running timers.') return } cb.log(timer_ids) let msg = `The following timers are running:` for (var i = 0; i < timer_ids.length; ++i) { let id = timer_ids[i] let info = timer_data[id] if (info != null) { msg = msg + '\n' + info } } notify_admins(msg) } // let timer = new Timer() class TimerCommand extends Command { constructor() { super('timer', function (user, args) { if (args.length === 0) { notify(user, 'Invalid command. No args provided.') return } let cmd = args.shift() if (cmd === 'list') { listTimers(user) } else if (cmd === 'start' || cmd === 'set') { if (args.length < 3) { notify(user, 'Invalid timer set command. Not enough args') return } let duration = args.shift() let unit = args.shift() let msg = args.join(' ') setTimer(user, duration, unit, msg) } else if (cmd === 'cancel' || cmd === 'stop') { if (args.length === 0) { notify(user, 'No timer id provided.') return } args.forEach(id => cancelTimer(user, id)) } else if (cmd === 'help') { this.help(user) } else { notify(user, `Invalid option "${cmd} to the timer command. Must be one of 'set', 'cancel', or 'list'`) } }) } help(user) { let msg = `Start, cancel, or list timers. When a timer is started a 'timer id' will be displayed. This id can be used to cancel the timer if needed. /vw timer start {duration} {unit} {message} /vw timer cancel {id} /vw timer list The duration can be one of 's', 'sec', 'second', 'seconds', 'm', 'min', 'minute', 'minutes', 'h', 'hour', 'hours' For example, all these commands will display the message 'Time up' in 10 minutes: /vw timer start 10 minutes Time up /vw timer start 10 m Time up /vw timer start 600 sec Time up ` notify(user, msg) } } class TagCommand extends Command { constructor() { super('tags', function(user,args) { if ((user !== cb.room_slug) && !game.admins_can_tag) { notify(cb.room_slug, `${user} attempted to change the room tags`) notify(user, 'The model does not allow you to do that.') return } if (args.length === 1 && args[0] === 'help') { this.help(user) return } let tags = game.default_tags if (args.length > 0) { tags = args.map(x => '#'+x).join(' ') } cb.changeRoomSubject(tags) if (user !== cb.room_slug) { notify_admins(`${user} changed the room tags`) } }) } help(user) { let msg = `Set new tags in the room subject. If no tags are specified with the command then the default set of tags specified in the bot startup screen will be used /vw tags /vw tags sexy hot cute feet stockings NOTE You do not need to include the '#' before the tag names as I will do that for you.` notify(user, msg) } } class SubjectCommand extends Command { constructor() { super('subject', function(user,args) { if ((user !== cb.room_slug) && !game.admins_can_tag) { notify(cb.room_slug, `${user} attempted to change the room subject`) notify(user, 'The model does not allow you to do that.') return } if (args.length === 0) { notify(user, 'No room subject provided.') return } cb.changeRoomSubject(args.join(' ')) if (user !== cb.room_slug) { notify_admins(`${user} changed the room subject`) } }) this.helpText = `Change the room subject /vw subject Lovense - make me wet with your tips.` } } class IconCommand extends Command { constructor() { super('icon', function (user,args) { if (args.length === 0) { notify(user, "No icon provided.") return } let key = args[0].toLowerCase() if (key === 'off' || key === 'no' || key === 'none' || key === 'disable') { game.icon = 'none' notify_admins("Disabled batorboy's vanity icon.") return } let symbol = symbols[key] if (symbol == null) { let valid_symbols = Object.keys(symbols).map(s => `"${s}"`).join(', ') notify(user, `Invalid symbol name: ${args[0]}. Use one of: ${valid_symbols}`) return } game.icon = symbol; //args[0] notify_admins(`Changed the vanity icon to ${symbol}`) }) } help(user) { let symbol_list = Object.keys(symbols).map(k => [k, symbols[k]]) symbol_list.push(['None', 'Disable the vanity icon.']) let msg = `Change or disable the vanity icon displayed by batorboy's name in chat. Use one of the following icon names: ${symbol_list.join('\n')}` notify(user, msg) } } class PrivateCommand extends Command { constructor() { super('private', function(user, args) { if (args.length === 0) { this.help(user) return } let cmd = args.shift() cb.log('private cmd: ' + cmd) switch (cmd) { case 'start': if (cb.limitCam_isRunning()) { notify(user, 'A private show has already been started.') } else { let viewers = game.admins().map(p => p.name).filter(name => name !== cb.room_slug) if (viewers.length === 0) { notify(user, 'There are no admins available to view a private show. Your camera WILL NOT be hidden.') return } let msg = 'A private show is in progress. Please check back later.' if (args.length > 0) { msg = args.join(' ') } cb.limitCam_start(msg, viewers) viewers.forEach( viewer => notify(viewer, 'You are now in a private show with ' + cb.room_slug)) let viewer_list = viewers.join(', ') notify(cb.room_slug, `You are now in a private show with ${viewer_list}`) notify(cb.room_slug, `Other viewers will see the message "${msg}"`) this.start = Date.now() } break case 'stop': if (!cb.limitCam_isRunning()) { notify(user, "A private show is not in progress!") return } cb.limitCam_stop() // let duration = Math.floor((Date.now() - this.start) / 60000) // let unit = 'minutes' // if (duration === 1) { // unit = 'minute' // } // notify_admins(`The private show has ended. Duration: ${duration} ${unit}`) let duration = new Date(Date.now() - this.start) let h = duration.getUTCHours() let m = duration.getUTCMinutes() let s = duration.getUTCSeconds() if (h < 10) h = '0' + h if (m < 10) m = '0' + m if (s < 10) s = '0' + s let length = h + ":" + m + ":" + s notify_admins(`The private show has ended. Duration ${length}`) break case 'help': this.help(user) break default: notify(user, `Invalid option. Type '/vw private help' for help.`) break } }) this.start = 0 } help(user) { let msg = `Start or stop a private show for all the admins in the room. You can specify the message that will be displayed with the 'start' command, otherwise a default message will be displayed. /vw private start /vw private start A private show is in progress. Please check back later. /vw private stop To control who can view the private show use the /vw [add|remove] admin command /vw remove admin batorboy4vw /vw add admin abigtipper /vw private A private show for a big tipper. When a private show ends the duration of the show will be displayed. ` notify(user, msg) } } class Commands { constructor() { this.dict = {} } add(command) { this.dict[command.name] = command } get(name) { return this.dict[name] } dispatch(name, user, args) { let command = this.dict[name] if (command == null) { cb.log(`ERROR: Unknown command ${name}`) return } command.execute(user,args) } } class Players { constructor() { this.list = [] this.i = 0 } get length() { return this.list.length } add(player) { if (this.list.find(p => p.name === player.name) == null) { this.list.push(player) } } remove(name) { let i = 0 for (; i < this.list.length; ++i) { let p = this.list[i] if (name === p.name) { this.list.splice(i, 1) return true } } return false } find(name) { cb.log(`Player.find ${name}`) return this.list.find(p => p.name === name) } getWinner() { let winners = [this.list[0]] cb.log(`Players.getWinner(): ${winners[0].name} has ${winners[0].points}`) for (let i = 1; i < this.list.length; ++i) { let p = this.list[i] if (p.points > winners[0].points) { cb.log(`${p.name} has ${p.points} points`) winners.length = 0 winners.push(p) } else if (p.points === winners[0].points) { winners.push(p) } } return winners } clear() { this.list = [] this.i = 0 } next() { if (this.list.length === 0) { return null } if (this.i >= this.list.length) { this.i = 0 } return this.list[this.i++] } print() { let msg = this.list.map(p => p.toString()).join('\n') notify_players(msg) } score(user, text) { this.list.forEach(p => p.score(user, text)) } reset() { this.list.forEach(p => p.reset()) } } class Game { constructor() { this.players = new Players() this.commands = new Commands() this.state = WAITING this.phrases = [] // Game play modes. this.exact_match = true this.cheat_mode = true this.verbose = false this.grammar_nazi = false this.match_once = true this.admins_can_tag = true this.censor = true this.welcome = false this.icon = true this.exact_match_mode = function(phrase, text) { return phrase === text } this.lax_match_mode = function(phrase, text) { return phrase.indexOf(text) !== -1 } } get started() { return this.state === STARTED } status() { let info = [ ['Game state', this.state], ['Cheat mode', this.cheat_mode ? 'enabled' : 'disabled'], // ['Match mode', this.exact_match ? 'exact' : 'lax'], ['Multi match', this.match_once ? 'yes' : 'no'], ['Grammar nazi', this.grammar_nazi ? 'enabled' : 'disabled'], ['Verbose', this.verbose ? 'yes' : 'no'], ['Censor', this.censor ? 'enabled' : 'disabled'], ['Welcome', this.welcome ? 'yes' : 'no'], ['Players', this.players.length] ] let msg = GAME_TITLE + ' v' + VERSION + '\n' + pad(info) if (this.admins_can_tag) { msg += '\nAdmins can change the room subject.' } else { msg += '\nAdmins can not change the room subject.' } notify_admins(msg) } start(user) { if (this.state === STARTED) { notify(user, 'The game has already started') return false } if (this.players.length < 2) { notify(user, 'There are not enough players.') } this.state = STARTED // this.players.reset() this.players.list.forEach(p => p.reset()) this.deal() notify_players(`We are now playing ${GAME_TITLE}`) return true } stop(user) { if (this.state !== STARTED) { notify(user, 'The game has not been started.') return } this.state = STOPPED let winners = this.players.getWinner() if (winners.length === 0) { notify_admins('This is a bug... the winners array is empty.') } else if (winners.length > 1) { if (winners[0].points === 0) { notify_admins('That game was a dud. No points for anyone!') } else { let msg = 'We have a tie between ' + winners.map(p => p.name).join(' and ') notify_admins(msg) } } else { let winner = winners[0] notify_players(`The winner is ${winner.name} with ${winner.points} points`) } } match_mode() { if (this.exact_match) { return this.exact_match_mode } return this.lax_match_mode } deal() { let clone = this.phrases.slice(0) this.players.i = 0 let current = this.players.next() while (clone.length > 0) { let n = random(clone.length) let item = clone.splice(n, 1) // cb.log(`Dealt '${item[0].text}' to ${current.name}`) current.add(item[0]) current = this.players.next() } } isPlayer(name) { return this.players.find(name) != null } add(player) { //this.players[player.name] = player this.players.add(player) } get(name) { cb.log(`Game.get ${name}`) return this.players.find(name) } isAdmin(name) { let p = this.get(name) if (p == null) { cb.log(`Game.isAdmin - no such player ${name}`) return false } return p.admin } admins() { return this.players.list.filter(p => p.admin) } dispatch(user, name, args) { cb.log(`Game.dispatch ${name}`) let command = this.commands.get(name) if (command == null) { cb.log('ERROR: no such command.') notify_admins('ERROR: no such command ' + name) return } cb.log(`Game.dispatch type of command is ${typeof command}`) cb.log(`Game.players is ${this.players}`) command.execute(user, args) } printPlayers() { cb.log(`Game.printPlayers ${typeof this.players}`) this.players.print() } score(user, text) { cb.log(`Game.score(${text})`) this.players.score(user, text) } scores() { let data = this.players.list.map(p => [p.name, p.points]) let msg = 'Game Scores\n' + pad(data) notify_players(msg) } hasPhrase(phrase) { let found = this.phrases.find(p => p.text === phrase) return found != null } } class HelpCommand extends Command { constructor() { super('help', function(user,args) { if (args.length === 0) { cb.log('No args passed to help.') notify(user, help) return } let name = args.shift() let cmd = game.commands.get(name) if (cmd == null) { notify(user, `No such command: ${name}`) return } cmd.help(user) }) this.helpText = 'Displays this help message.' } } /* * Mark a message as spam. Messages with X-Spam = true will not be * displayed in chat. */ function spam(message) { message['X-Spam'] = true } let game = new Game() /* * Handler for the '/vw phrases' command. * * Prints the list of all phrases with their point values and matching mode. * */ function printPhrases(user, args) { //notify(user, cb.settings.phrases) //notify(user, game.phrases.map(s=>`"${s}"`).join(', ')) let msg = 'Phrase List. Format is {points} - {match mode} : {phrase}\n' msg += game.phrases.map(p => `${p.points} - ${p.mode} : ${p.text}`).join('\n') notify_players(msg) } function run_test(user, args) { let n = parseInt(args[0]) let duration = new Date(n) let h = duration.getUTCHours() let m = duration.getUTCMinutes() let s = duration.getUTCSeconds() if (h < 10) h = '0' + h if (m < 10) m = '0' + m if (s < 10) s = '0' + s let length = h + ":" + m + ":" + s notify_admins(`Duration ${length}`) } /* * Bot setup. */ function init() { if (game.state !== WAITING) { // We have already been initialized. return } game.state = INITIALIZED //game.phrases = cb.settings.phrases.split(',').map(s => s.trim().toLowerCase()) game.phrases = [] for (let i = 0; i < NUM_PHRASES; ++i) { let phrase = cb.settings['phrase'+i] if (phrase == null) { notify_admins(`There is a typo in the cb.settings for phrase${i}`) } else if (phrase.length > 1) { let points = parseInt(cb.settings[`phrase${i}_score`]) let mode = cb.settings[`phrase${i}_mode`] game.phrases.push(new Phrase(points, mode, phrase)) } } // Set game modes from the cb.settings. We convert these to booleans at init() time so the // bot is not doing string comparisons all the time. game.exact_match = cb.settings.match_mode === 'exact' game.censor = cb.settings.censor === 'yes' game.cheat_mode = cb.settings.cheat_mode === 'on' game.grammar_nazi = cb.settings.grammar_nazi === 'enable' game.match_once = cb.settings.multi_match === 'no' game.verbose = cb.settings.verbose === 'yes' game.admins_can_tag = cb.settings.subject === 'yes' game.default_tags = cb.settings.tags.split(' ').map(x => '#' + x).join(' ') game.welcome = cb.settings.welcome === 'yes' game.icon = symbols[cb.settings.icon.toLowerCase()] //=== 'yes' if (game.cheat_mode) { cb.log('cheat mode on') } else { cb.log('cheat mode off') cb.log(cb.settings.cheat_mode) } // The default players. The room owner is included of course, along with // anyone listed in the setup UI. game.players.add(new Player(cb.room_slug, true)) cb.settings.admins.split(' ').forEach( name => { game.players.add(new Player(name, true)) }) // Register the commands handlers for the /vw command that are recognized in chat. game.commands.add(new Command('start', (user,args) => game.start(user), 'Starts the game')) game.commands.add(new Command('stop', (user,args) => game.stop(user), 'Stops the game.')) game.commands.add(new Command('list', (user,args) => game.printPlayers()), 'Lists phrases assigned to each player.') game.commands.add(new Command('about', (user,args) => notify(user, `${GAME_TITLE} v${VERSION}`), 'Displays the bot version.')) game.commands.add(new Command('status', () => game.status()), 'Display game status.') game.commands.add(new Command('score', () => game.scores(), 'Display player scores.')) game.commands.add(new Command('phrases', printPhrases, 'list all phrases')) game.commands.add(new OptionCommand('grammar', 'grammar_nazi'), 'Turn the Grammar Nazi on or off.') game.commands.add(new OptionCommand('verbose'), 'Controls whether the /vw command will be appear in chat.') game.commands.add(new OptionCommand('censor'), 'Turns on/off the whore/bitch censor.') game.commands.add(new OptionCommand('cheat', 'cheat_mode'), 'Turn on/off cheat mode that allows players to give themselves points.') game.commands.add(new OptionCommand('welcome'),'Turn on/off welcome messages when users enter the room') game.commands.add(new IconCommand()) game.commands.add(new MatchCommand()) game.commands.add(new AddCommand()) game.commands.add(new RemoveCommand()) game.commands.add(new TimerCommand()) game.commands.add(new TagCommand()) game.commands.add(new SubjectCommand()) game.commands.add(new PrivateCommand()) game.commands.add(new HelpCommand()) game.commands.add(new Command('test', run_test, 'testing time conversions')) if (cb.settings.autostart === 'yes') { game.start(cb.room_slug) } } let censored_words = ['whore', 'bitch', 'bicth'] let censor = function(phrase) { if (phrase === 'cunt') { return true } for (let i = 0; i < censored_words.length; ++i) { if (phrase.includes(censored_words[i])) { return true } } return false } function grammar_nazi(user, message) { let found = false your.forEach(function(term) { if (message.indexOf(term) !== -1) { found = true } }) if (found) { notify(user, "*you're*") } } const onMessage = function(message) { cb.log('Handling messge') let user = message['user'] let text = message['m'] let args = text.split(' ') if (args[0] === '/vw') { cb.log('Handling /vw message') if (!game.verbose) { spam(message) // Ignore command messages } if (!game.isAdmin(user)) { cb.log(`${user} is not an admin.`) notify_admins(`${user} attempted to use the restricted command "${text}"`) message['m'] = "Oops, I am not allowed to do that." return message } // Discard the '/vw'' args.shift() if (args.length === 0) { cb.log('No more tokens for command') //notify(user, 'ERROR - empty command') notify(user, help) } else { let command_name = args.shift() cb.log(`dispatching ${command_name}`) game.dispatch(user, command_name, args) } return message } if (censor(text)) { message['m'] = "[This message has been censored]" spam(message) notify_admins(`The user ${user} has been censored for "${text}"`) return message } if (game.grammar_nazi) { grammar_nazi(user, text) } if (game.started && (!game.isPlayer(user) || game.cheat_mode)) { game.score(user, text.toLowerCase()) } if (user === 'batorboy4vw' && game.icon !== 'none') { message['m'] = game.icon + ' ' + message['m'] } return message } const onEnter = function(user) { cb.log('onEnter') let name = user['user'] if (name === 'batorboy4vw') { // Send a random love note when I enter the room let msg = random_element(vw_messages) notify(cb.room_slug, msg) notify(name, msg) } if (game.welcome) { cb.log('sending welcome message') let msg = cb.settings.welcome_message.replace(/{user}/g, name) let parts = msg.split('|') //msg = msg.replace(/{br}/g, '\n') cb.log(parts.join('\n')) cb.sendNotice(parts.join('\n'), name, '#CCF5EB', '', 'bold'); //notify(name, `Welcome ${name}`) } else { cb.log('Welcome message not enabled.') } } const onTip = function(tip) { cb.log("onTip") let user = tip.from_user let amount = parseInt(tip.amount) if (tippers[user] == null) { tippers[user] = amount } else { tippers[user] = tippers[user] + amount } } cb.onMessage(onMessage) cb.onEnter(onEnter) cb.onTip(onTip) // Send a notification to a specific user. function notify(user, message) { // Send the notice after a short delay so it appears in the chat window after the text // the user entered. cb.log("Sending a private notice to " + user) // schedule(() => cb.sendNotice('[PRIVATE] ' + message, user)) cb.sendNotice('[PRIVATE] ' + message, user) } function notify_admins(message) { game.admins().forEach(admin => { // schedule(() => cb.sendNotice('[ADMINS] ' + message, admin.name)) cb.sendNotice('[ADMINS] ' + message, admin.name) }) } function notify_players(message) { game.players.list.forEach(p => { // schedule(() => cb.sendNotice('[PLAYER] ' + message, p.name)) cb.sendNotice('[PLAYER] ' + message, p.name) }) } /* * Set a timer to perform 'action' in 'delay' milliseconds. */ function schedule(action, delay) { delay = delay ? delay : 200 cb.setTimeout(action, delay) } // Pick a random integer between 0 and n-1 function random(n) { return Math.floor(Math.random() * n) } // Selects a random elemnt from an array. function random_element(list) { let index = random(list.length) return list[index] } function isGrey(message) { return !(message.tipped_tons_recently || message.tipped_alot_recently || message.tipped_recently || message.has_tokens) } // Convert Latin letters and numbers into characters from the Monospace Mathematical // symbols. Also replaces whitespace with non-breaking spaces. function transpose(code) { const zero = 48 //'0'.charCodeAt(0) const nine = 57 //'9'.charCodeAt(0) const A = 65 //'A'.charCodeAt(0) const Z = 90 //'Z'.charCodeAt(0) const a = 97 //'a'.charCodeAt(0) const z = 122 //'z'.charCodeAt(0) const MONO_A = 120432; // Start of uppercase A-Z const MONO_a = 120458; // Start of lowercase a-z const MONO_0 = 120802; // Start of digits 0-9 const space = '\u{2007}' // 0x2007 is a non-breaking "figure" space (the width of digits 0..9) const tab = space.repeat(4) if (zero <= code && code <= nine) { return String.fromCodePoint(MONO_0 + code - zero) } if (A <= code && code <= Z) { return String.fromCodePoint(MONO_A + code - A) } if (a <= code && code <= z) { return String.fromCodePoint(MONO_a + code - a) } // Return space characters with non-breaking spaces if (code === 32) return space // Return anything else we don't recognize as I can not find // monospace punctuation return String.fromCodePoint(code) } // Converts the input string to monospaced characters. function monospace(s) { let result = '' for (let i = 0; i < s.length; ++i) { result += transpose(s.codePointAt(i)) } return result } function pad(list) { // First find the longest prefix. // cb.log(`padding list with ${list.length} elements`) let longest = 0 for (let i = 0; i < list.length; ++i) { if (list[i] == null) { cb.log(`Why is element ${i} undefined?`) } else { let item = list[i][0] if (item.length > longest) { longest = item.length } } } // Add one extra space ++longest // Now build the string. let result = '' for (let i = 0; i < list.length; ++i) { let item = list[i] let cmd = item[0] let n = longest - cmd.length let padding = ' '.repeat(n) // cb.log(`Adding ${n} spaces to ${cmd}`) if (i > 0) { result += '\n' } result += monospace(cmd + padding + ': ' + item[1]) } return result } cb.setTimeout(init, 500)
© Copyright Chaturbate 2011- 2024. All Rights Reserved.