/* * Version 0.1.11 * Made By Robin Kuiper * Skype: RobinKuiper.eu * Discord: Atheos#1095 * Roll20: https://app.roll20.net/users/1226016/robin * Roll20 Thread: * Github: https://github.com/RobinKuiper/Roll20APIScripts * Reddit: https://www.reddit.com/user/robinkuiper/ */ /* * TODO * Check styling variables * Check styling of the different menus * Color to the buttons! * Check all commands */ var LazyExperience = LazyExperience || (function() { 'use strict'; let playerid; // Styling for the chat responses. const style = "overflow: hidden; background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px;", buttonStyle = "background-color: #000; border: 1px solid #292929; border-radius: 3px; padding: 5px; color: #fff; text-align: center; float: right;", buttonStyle2 = "background-color: #000; border: 1px solid #292929; border-radius: 3px; padding: 5px; color: #fff; text-align: center;", listStyle = 'list-style: none; padding: 0; margin: 0;', script_name = 'Lazy Experience', state_name = 'LAZYEXPERIENCE', markers = ['blue', 'brown', 'green', 'pink', 'purple', 'red', 'yellow', '-', 'all-for-one', 'angel-outfit', 'archery-target', 'arrowed', 'aura', 'back-pain', 'black-flag', 'bleeding-eye', 'bolt-shield', 'broken-heart', 'broken-shield', 'broken-skull', 'chained-heart', 'chemical-bolt', 'cobweb', 'dead', 'death-zone', 'drink-me', 'edge-crack', 'fishing-net', 'fist', 'fluffy-wing', 'flying-flag', 'frozen-orb', 'grab', 'grenade', 'half-haze', 'half-heart', 'interdiction', 'lightning-helix', 'ninja-mask', 'overdrive', 'padlock', 'pummeled', 'radioactive', 'rolling-tomb', 'screaming', 'sentry-gun', 'skull', 'sleepy', 'snail', 'spanner', 'stopwatch','strong', 'three-leaves', 'tread', 'trophy', 'white-tower'], handleInput = (msg) => { if (msg.type != 'api' || !playerIsGM(msg.playerid)) return; // Split the message into command and argument(s) let args = msg.content.split(' '); let command = args.shift().substring(1); let extracommand = args.shift(); if (command == state[state_name].config.command) { switch(extracommand){ case 'help': sendHelpMenu(); break; case 'reset': state[state_name] = {}; setDefaults(true); sendConfigMenu(); break; case 'menu': sendMenu(); break; case 'resetxp': resetExperience(); sendMenu(); break; case 'refresh': refreshPlayers(); sendConfigMenu(); break; case 'refreshcharacters': playerid = args.shift(); refreshCharacters(); sendPlayerConfigMenu(playerid); break; case 'player': let todo = args.shift(); switch(todo){ case 'remove': playerid = args.shift(); let sure = (args.shift() === 'yes'); if(sure){ delete state[state_name].players[playerid]; sendConfigMenu(); } break; default: playerid = args.shift(); state[state_name].players[playerid].active = (todo === 'true'); sendPlayerConfigMenu(playerid); break; } break; case 'setcharactive': let characterid = args.shift(); playerid = args.shift(); let active = args.shift(); state[state_name].players[playerid].characters.forEach((character, i, array) => { if((character.id === characterid)){ state[state_name].players[playerid].characters[i].active = (active === 'true'); } }); sendPlayerConfigMenu(playerid) break; case 'config': if(args.length > 0){ let setting = args.shift(); if(setting === 'player'){ playerid = args.shift(); sendPlayerConfigMenu(playerid); }else{ setting = setting.split('|'); let key = setting.shift(); let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; value = (key === 'marker' && value === '-') ? state[state_name].config[key] : value; if(key == 'extra_players'){ state[state_name][key] = value; }else{ state[state_name].config[key] = value; } sendConfigMenu(); } }else{ sendConfigMenu(); } break; case 'add': let who = args.shift(); let experience = args.shift()*1; if(who === 'session'){ setExperience(experience); }else{ setExperience(experience, who); } sendMenu(); break case 'show': makeAndSendMenu('Current Experience: ' + getExperience(), script_name + ' Help', 'gm') break; case 'end': let xpSharers = getExperienceSharers(); let session_experience = (xpSharers > 0) ? (Math.floor(getExperience()/xpSharers)) : 0; let send_message_text = 'Session Ended'; let send_gm_text = 'Session Experience: '+session_experience if(!state[state_name].config.directxp){ let character_experiences = ''; for(playerid in state[state_name].players){ let player = state[state_name].players[playerid]; if(player.active){ player.characters.forEach((character, i) => { if(!character){ state[state_name].players[playerid].characters.splice(i, 1); return; } if(character.active){ let total_experience = character.experience+session_experience let send_character_text = 'You get '+character.experience+' extra experience, bringing your total this session to ' + total_experience + ' experience.'; character_experiences += handleLongString(character.name) + ': ' + character.experience + ' | Total: ' + total_experience + '
'; if(state[state_name].config.updatesheet){ let new_experience = updateExperienceOnSheet(total_experience, character.id); send_character_text += '

Your sheet has been updated, your total experience is now '+new_experience+'

'; } makeAndSendMenu(send_character_text, '', character.name.split(' ').shift()); } }); } } send_message_text += (session_experience > 0) ? '
Everyone gets '+session_experience+' experience.' : '
There is no session experience.'; send_message_text += (state[state_name].config.updatesheet) ? '

Your character sheets have been updated.

' : ''; send_gm_text += '

'+character_experiences; } makeAndSendMenu(send_message_text); makeAndSendMenu(send_gm_text, '', 'gm'); resetExperience(); break; default: sendHelpMenu(); break; } } }, updateExperienceOnSheet = (experience, characterid) => { if(characterid){ let new_experience = getAttrByName(characterid, state[state_name].config.experience_attribute_name, 'current')*1 + experience*1; let attributes = {}; attributes[state[state_name].config.experience_attribute_name] = new_experience; setAttrs(characterid, attributes); return new_experience; }else{ for(let playerid in state[state_name].players){ state[state_name].players[playerid].characters.forEach((character, i) => { let new_experience = getAttrByName(character.id, state[state_name].config.experience_attribute_name, 'current')*1 + experience*1; let attributes = {}; attributes[state[state_name].config.experience_attribute_name] = new_experience; setAttrs(character.id, attributes); }) } } }, getExperienceSharers = () => { let xpSharers = state[state_name].extra_players*1; if(state[state_name].players.length === 0){ return xpSharers; } for(let playerid in state[state_name].players){ if(state[state_name].players[playerid].active){ state[state_name].players[playerid].characters.forEach((character) => { if(character.active){ xpSharers++; } }); } } return xpSharers; }, getPlayers = (object) => { let players = [] findObjs({ _type: 'player' }).forEach((player) => { if(!playerIsGM(player.get('id'))){ if(object){ players.push({ name: player.get('_displayname'), id: player.get('_id') }) }else{ players.push(player.get('_id')); } } }); return players; }, getPlayerCharacters = (playerid) => { return findObjs({ _type: 'character' }).filter(character => { let controlling = character.get('controlledby').split(','); return (controlling.includes(playerid) || controlling.includes('all')); }).map(character => { return { name: character.get('name'), id: character.get('id'), active: true, experience: 0 }; }); }, getExperience = () => { return state[state_name].session_experience; }, setExperience = (experience, characterid) => { let playerid; if(characterid){ playerid = findObjs({ _id: characterid, _type: 'character' }).shift().get('controlledby'); state[state_name].players[playerid].characters.forEach((character, i) =>{ if(character.id === characterid){ state[state_name].players[playerid].characters[i].experience += experience; } }) }else{ state[state_name].session_experience += experience; } // Give XP Directly? if(state[state_name].config.directxp){ let pp_experience = (characterid) ? experience : experience/getExperienceSharers(); let send_message_text = (characterid) ? 'You have been awarded ' + pp_experience + ' experience' : 'Everyone is awarded '+pp_experience+' experience.'; // Update Sheet with new experience? if(state[state_name].config.updatesheet){ updateExperienceOnSheet(pp_experience, characterid); send_message_text += 'Your character sheet has been updated.'; } let whisper = (characterid) ? getObjects(state[state_name].players[playerid].characters, 'id', characterid).shift().name.split(' ')[0] : ''; makeAndSendMenu(send_message_text, '', whisper); } }, //return an array of objects according to key, value, or key and value matching getObjects = (obj, key, val) => { var objects = []; for (var i in obj) { if (!obj.hasOwnProperty(i)) continue; if (typeof obj[i] == 'object') { objects = objects.concat(getObjects(obj[i], key, val)); } else //if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not) if (i == key && obj[i] == val || i == key && val == '') { // objects.push(obj); } else if (obj[i] == val && key == ''){ //only add if the object is not already in the array if (objects.lastIndexOf(obj) == -1){ objects.push(obj); } } } return objects; }, resetExperience = () => { state[state_name].session_experience = 0; for(let playerid in state[state_name].players){ let player = state[state_name].players[playerid]; player.characters.forEach((character, i) => { if(!character){ state[state_name].players[playerid].characters.splice(i, 1); return; } state[state_name].players[playerid].characters[i].experience = 0; }) } }, handleStatusmarkerChange = (obj, prev) => { // Check if the statusmarkers string is different from the previous statusmarkers string. if(obj.get('statusmarkers') !== prev.statusmarkers){ // Create arrays from the statusmarkers strings. var prevstatusmarkers = prev.statusmarkers.split(","); var statusmarkers = obj.get('statusmarkers').split(","); if(!prevstatusmarkers.includes(state[state_name].config.marker) && statusmarkers.includes(state[state_name].config.marker)){ /*var experience = findObjs({ _type: "attribute", name: state[state_name].config.experience_attribute_name, characterid: obj.get('represents') }).shift().get('current');*/ let experience = getAttrByName(obj.get('represents'), state[state_name].config.npc_experience_attribute_name, 'current') if(experience > 0){ let yesButton = makeButton('Yes', '!' + state[state_name].config.command + ' add session '+experience, buttonStyle2); makeAndSendMenu('' + obj.get('name') + ' just died, do you want to add '+experience+' xp to the threshold?
'+yesButton, '', 'gm'); } } } }, sendMenu = () => { let addXPButton = makeButton(getTexts().add_session_xp, '!' + state[state_name].config.command + ' add session ?{Experience}', buttonStyle2); let endButton = makeButton('End Session', '!' + state[state_name].config.command + ' end', buttonStyle2); let resetXPButton = makeButton('Reset Experience', '!' + state[state_name].config.command + ' resetxp', buttonStyle2); //let addXPSelectedButton = makeButton('Add XP: Selected', '!' + state[state_name].config.command + ' add session ?{selectedExperience}', buttonStyle2); let playerListItems = []; for(var playerid in state[state_name].players){ let player = state[state_name].players[playerid]; if(player.active){ let characterListItems = []; let characterDropdown = '?{Character'; let characterIds = []; player.characters.forEach((character, i) => { if(!character){ state[state_name].players[playerid].characters.splice(i, 1); return; } if(character.active){ let name = handleLongString(character.name); characterListItems.push(name + ': ' + character.experience); characterDropdown += '|'+character.name+','+character.id; characterIds.push(character.id); } }); characterDropdown += '}'; characterDropdown = (characterIds.length === 1) ? characterIds[0] : characterDropdown; let addExperienceButton = (characterIds.length > 0) ? makeButton(getTexts().add_xp, '!' + state[state_name].config.command + ' add '+characterDropdown+' ?{Experience}', buttonStyle + ' float: right; font-size: 10pt;', (player.characters.length > 0)) : ''; playerListItems.push('' + player.name + '
' + makeList(characterListItems, listStyle + 'margin-left: 5px;', 'font-size: 10pt;') + '
' + addExperienceButton); } } let contents = 'Session Experience: '+getExperience()+'
Experience Divisors: ' + getExperienceSharers() + '
'+makeList(playerListItems, listStyle + ' overflow: hidden;', 'overflow: hidden;')+'
'+addXPButton+'
'+endButton+'
'+resetXPButton makeAndSendMenu(contents, script_name + ' menu', 'gm'); }, handleLongString = (str, max=8) => { return (str.length > max) ? str.slice(0, 8) + '...' : str; }, ucFirst = (string) => { return string.charAt(0).toUpperCase() + string.slice(1); }, getTexts = () => { return (state[state_name].config.directxp) ? { add_xp: 'Give xp', add_session_xp: 'Give Session Experience' } : { add_xp: 'Add xp', add_session_xp: 'Add Session Experience' } }, sendConfigMenu = (first) => { let commandButton = makeButton('!'+state[state_name].config.command, '!' + state[state_name].config.command + ' config command|?{Command (without !)}', buttonStyle); let markerDropdown = '?{Marker'; markers.forEach((marker) => { markerDropdown += '|'+ucFirst(marker).replace('-', ' ')+','+marker }) markerDropdown += '}'; let markerButton = makeButton(state[state_name].config.marker, '!' + state[state_name].config.command + ' config marker|'+markerDropdown, buttonStyle); let experienceAttributeButton = makeButton(state[state_name].config.experience_attribute_name, '!' + state[state_name].config.command + ' config experience_attribute_name|?{Attribute}', buttonStyle); let npcExperienceAttributeButton = makeButton(state[state_name].config.npc_experience_attribute_name, '!' + state[state_name].config.command + ' config npc_experience_attribute_name|?{Attribute}', buttonStyle); let extraPlayersButton = makeButton(state[state_name].extra_players, '!' + state[state_name].config.command + ' config extra_players|?{Players}', buttonStyle); let updateSheetButton = makeButton(state[state_name].config.updatesheet, '!' + state[state_name].config.command + ' config updatesheet|'+!state[state_name].config.updatesheet, buttonStyle); let directXPButton = makeButton(state[state_name].config.directxp, '!' + state[state_name].config.command + ' config directxp|'+!state[state_name].config.directxp, buttonStyle); let listItems = [ 'Command: ' + commandButton, 'Marker: ' + markerButton, 'Player XP Attribute: ' + experienceAttributeButton, 'NPC XP Attribute: ' + npcExperienceAttributeButton, 'Extra Players: ' + extraPlayersButton, 'Give Xp Instant: ' + directXPButton, 'Update Sheets: ' + updateSheetButton, ]; let playerListItems = []; if(Object.keys(state[state_name].players).length > 0){ for(var playerid in state[state_name].players){ let player = state[state_name].players[playerid]; let activeStyle = (player.active) ? '' : 'text-decoration: line-through;'; playerListItems.push(''+player.name+' ' + makeButton('Config', '!' + state[state_name].config.command + ' config player '+playerid, buttonStyle)); } }else{ playerListItems.push('No players found.'); } let refreshPlayersButton = makeButton('Refresh Players', '!' + state[state_name].config.command + ' refresh', buttonStyle + ' width: 100%'); let resetXPButton = makeButton('Reset Experience', '!' + state[state_name].config.command + ' resetxp', buttonStyle + ' width: 100%'); let resetButton = makeButton('Reset Config', '!' + state[state_name].config.command + ' reset', buttonStyle + ' width: 100%'); let title_text = (first) ? script_name + ' First Time Setup' : script_name + ' Config'; let contents = makeList(listItems, listStyle + ' overflow:hidden;', 'overflow: hidden')+'
Players

'+makeList(playerListItems, listStyle + ' overflow:hidden;', 'overflow: hidden')+'
'+refreshPlayersButton+'

You can always come back to this config by typing `!'+state[state_name].config.command+' config`.


'+resetXPButton+'
'+resetButton; makeAndSendMenu(contents, title_text, 'gm'); }, sendPlayerConfigMenu = (playerid) => { let player = state[state_name].players[playerid]; let activeButton = makeButton((player.active) ? 'Active' : 'Not Active', '!' + state[state_name].config.command + ' player '+!player.active+' '+playerid, buttonStyle2); let characterListItems = []; let characterDropdown = '?{Character'; player.characters.forEach((character, i) => { if(!character){ state[state_name].players[playerid].characters.splice(i, 1); return; } let mainButton = makeButton((character.active) ? 'Active' : 'Not Active', '!' + state[state_name].config.command + ' setcharactive ' + character.id + ' ' + playerid + ' ' + !character.active, buttonStyle); let main = (character.main) ? 'Main' : mainButton; characterListItems.push(''+handleLongString(character.name)+'
Experience: '+character.experience+'
' + main); characterDropdown += '|'+character.name+','+character.id; }); characterDropdown += '}'; let addExperienceButton = makeButton('Add Experience', '!' + state[state_name].config.command + ' add '+characterDropdown+' ?{Experience}', buttonStyle2, (player.characters.length > 0)); let listItems = [ activeButton, addExperienceButton, ]; let refreshCharactersButton = makeButton('Refresh Characters', '!' + state[state_name].config.command + ' refreshcharacters '+playerid, buttonStyle + ' width: 100%'); let removeButton = makeButton('Remove', '!' + state[state_name].config.command + ' player remove ' + playerid + ' ?{Are you sure?|Yes,yes|No,no}', buttonStyle + ' width: 100%;'); let backButton = makeButton('Back', '!' + state[state_name].config.command + ' config', buttonStyle + ' width: 100%'); let contents = 'Experience: '+player.experience+'
'+makeList(listItems, listStyle + ' overflow:hidden;', 'overflow: hidden')+'
Characters'+makeList(characterListItems, listStyle + ' overflow:hidden;', 'overflow: hidden;')+refreshCharactersButton+'
'+backButton+'
'+removeButton; makeAndSendMenu(contents, player.name + ' - Config', 'gm'); }, sendHelpMenu = (first) => { let configButton = makeButton('Config', '!' + state[state_name].config.command + ' config', buttonStyle + ' width: 100%;') let listItems = [ '!'+state[state_name].config.command+' help - Shows this menu.', '!'+state[state_name].config.command+' config - Shows the configuration menu.', ] let contents = 'Commands:'+makeList(listItems, listStyle)+'
'+configButton; makeAndSendMenu(contents, script_name + ' Help', 'gm') }, makeAndSendMenu = (contents, title, whisper) => { title = (title && title != '') ? makeTitle(title) : ''; whisper = (whisper && whisper !== '') ? '/w ' + whisper + ' ' : ''; sendChat(script_name, whisper + '
'+title+contents+'
', null, {noarchive:true}); }, makeTitle = (title) => { return '

'+title+'

'; }, makeButton = (title, href, style, disabled) => { let disableStyle = (disabled) ? 'pointer-events: none' : ''; return ''+title+''; }, makeList = (items, listStyle, itemStyle) => { let list = ''; return list; }, pre_log = (message) => { log('---------------------------------------------------------------------------------------------'); log(message); log('---------------------------------------------------------------------------------------------'); }, checkInstall = () => { if(!_.has(state, state_name)){ state[state_name] = state[state_name] || {}; } setDefaults(); refreshPlayers(); log(script_name + ' Ready! Command: !'+state[state_name].config.command); if(state[state_name].config.debug){ sendChat('', script_name + ' Ready!', null, {noarchive:true}); } }, getPlayerById = (playerid) => { return findObjs({ _type: 'player', _id: playerid }).shift() }, refreshPlayers = () => { let saved_players = Object.keys(state[state_name].players).map((playerid, i) => { return playerid }); // Player added? array_diff(saved_players, getPlayers()).forEach((playerid) => { let player = getPlayerById(playerid); state[state_name].players[playerid] = { name: player.get('_displayname'), id: playerid, active: true, experience: 0, characters: getPlayerCharacters(playerid) }; }); // Player removed? array_diff(getPlayers(), saved_players).forEach((playerid) => { delete state[state_name].players[player]; }); }, refreshCharacters = () => { for(let playerid in state[state_name].players){ let characterids = getPlayerCharacters(playerid).map((character) => { return character.id }); let saved_characterids = (state[state_name].players[playerid].characters.length > 0) ? state[state_name].players[playerid].characters.map((character) => { return character.id }) : []; // Player added? array_diff(saved_characterids, characterids).forEach((characterid) => { state[state_name].players[playerid].characters.push(getObjects(getPlayerCharacters(playerid), 'id', characterid).shift()); }); // Player removed? array_diff(characterids, saved_characterids).forEach((characterid) => { state[state_name].players[playerid].characters.forEach((character, i) => { if(character.id === characterid){ delete state[state_name].players[playerid].characters[i]; } }) }); } }, array_diff = (a, b) => { return b.filter(function(i) {return a.indexOf(i) < 0;}); }, setDefaults = (reset) => { const defaults = { config: { command: 'xp', marker: 'dead', experience_attribute_name: 'experience', npc_experience_attribute_name: 'npc_xp', directxp: false, updatesheet: false }, players: {}, extra_players: 0, session_experience: 0 }; getPlayers(true).forEach((player) => { defaults.players[player.id] = { name: player.name, id: player.id, active: true, experience: 0, characters: getPlayerCharacters(player.id) }; }); if(!state[state_name].config){ state[state_name].config = defaults.config; }else{ if(!state[state_name].config.hasOwnProperty('command')){ state[state_name].config.command = defaults.config.command; } if(!state[state_name].config.hasOwnProperty('marker')){ state[state_name].config.marker = defaults.config.marker; } if(!state[state_name].config.hasOwnProperty('experience_attribute_name')){ state[state_name].config.experience_attribute_name = defaults.config.experience_attribute_name; } if(!state[state_name].config.hasOwnProperty('npc_experience_attribute_name')){ state[state_name].config.npc_experience_attribute_name = defaults.config.npc_experience_attribute_name; } if(!state[state_name].config.hasOwnProperty('directxp')){ state[state_name].config.directxp = defaults.config.directxp; } if(!state[state_name].config.hasOwnProperty('updatesheet')){ state[state_name].config.updatesheet = defaults.config.updatesheet; } } if(!state[state_name].hasOwnProperty('players') || typeof state[state_name].players !== 'object' || state[state_name].players === {}){ state[state_name].players = defaults.players; } if(!state[state_name].hasOwnProperty('extra_players')){ state[state_name].extra_players = defaults.extra_players; } if(!state[state_name].hasOwnProperty('session_experience')){ state[state_name].session_experience = defaults.session_experience; } if(!state[state_name].config.hasOwnProperty('firsttime') || state[state_name].config.firsttime && !reset){ sendConfigMenu(true); state[state_name].config.firsttime = false; } }, registerEventHandlers = () => { on('chat:message', handleInput); on('change:graphic:statusmarkers', handleStatusmarkerChange); // Handle condition descriptions when tokenmod changes the statusmarkers on a token. if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(function(obj,prev){ handleStatusmarkerChange(obj,prev); }); } if('undefined' !== typeof DeathTracker && DeathTracker.ObserveTokenChange){ DeathTracker.ObserveTokenChange(function(obj,prev){ handleStatusmarkerChange(obj,prev); }); } }; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers }; })(); on('ready', () => { 'use strict'; LazyExperience.CheckInstall(); LazyExperience.RegisterEventHandlers(); });