/* Easy Experience Script: This script will create a character called ExperienceThresholds that it uses for most of its functionality. If there is already an ExperienceThresholds character present, it will not make one; please make sure that you do not have a self-made ExperienceThresholds character (highly unlikely I would think ;) ) ---------------------------------------------------------------------------------------- Use the following chat commands to utilize this script: !xp challenge @{token_id/character_id}: Adds a character's npc-xp attribute value to the Session XP tally in the ExperienceThresholds character. !xp miscXP ###: Adds a manually entered XP value to the Session XP tally in ExperienceThresholds. !xp session: Divides the Session XP by the number of PCs (defined as characters that have a player-name entry), adds that xp to each PC's current experience, and then sets the Session XP max field to the current field before resetting the current field to 0. Will also send a chat message congratulating the character on leveling up if this brings that character's current experience above or equal to the experience|max value. ---------------------------------------------------------------------------------------- The script will also automatically set the experience|max value of all PCs based on their current level, and will update this whenever their level changes. Experience Thresholds can be updated by changing the threshold values (2-20) in ExperienceThresholds. */ var EASYEXPERIENCE = EASYEXPERIENCE || (function() { 'use strict'; var version = '0.1.0', lastUpdate = 1465601482, schemaVersion = 1.0, ExperienceThresholds, defaults = { css: { button: { 'border': '1px solid #cccccc', 'border-radius': '1em', 'background-color': '#006dcc', 'margin': '0 .1em', 'font-weight': 'bold', 'padding': '.1em 1em', 'color': 'white' } } }, templates = {}, PCs, ch = function (c) { var entities = { '<' : 'lt', '>' : 'gt', "'" : '#39', '@' : '#64', '{' : '#123', '|' : '#124', '}' : '#125', '[' : '#91', ']' : '#93', '"' : 'quot', '-' : 'mdash', ' ' : 'nbsp' }; if(_.has(entities,c) ){ return ('&'+entities[c]+';'); } return ''; }, checkInstall = function() { log('-=> EasyExperience v'+version+' <=- ['+(new Date(lastUpdate*1000))+']'); if( ! _.has(state,'EASYEXPERIENCE') || state.EASYEXPERIENCE.version !== schemaVersion) { log(' > Updating Schema to v'+schemaVersion+' <'); state.EASYEXPERIENCE = { version: schemaVersion, config: { }, policies: { global: { recoveryUpdatesMaximum: false }, byAttribute: { }, byCharacter: { } } }; } if(!state.EASYEXPERIENCE.PCs){ setDefaults(); } loadSettings(); buildTemplates(); if(!state.PAGENAVIGATOR.configButton){ state.PAGENAVIGATOR.configButton = makeButton( '!xp config', 'Options', '#191970', '#fff8dc' ); } }, //UTILITY FUNCTIONS: makeButton, buildTemplates, createThresholds /*Builds templates for use in all other functions*/ buildTemplates = function() { templates.cssProperty =_.template( '<%=name %>: <%=value %>;' ); templates.style = _.template( 'style="<%='+ '_.map(css,function(v,k) {'+ 'return templates.cssProperty({'+ 'defaults: defaults,'+ 'templates: templates,'+ 'name:k,'+ 'value:v'+ '});'+ '}).join("")'+ ' %>"' ); templates.button = _.template( ' href="<%= command %>"><%= label||"Button" %>' ); }, /*Makes the API buttons used throughout the script*/ makeButton = function(command, label, backgroundColor, color){ return templates.button({ command: command, label: label, templates: templates, defaults: defaults, css: { color: color, 'background-color': backgroundColor } }); }, createThresholds = function(choice){ ExperienceThresholds = findObjs({ type: "character", name: "ExperienceThresholds" })[0]; if (!ExperienceThresholds) { ExperienceThresholds = createObj("character", { name: "ExperienceThresholds" }); let levels = state.EASYEXPERIENCE.prog[choice]; _.each(levels, (xp,lvl) => { createObj('attribute',{ name: lvl, current: xp, characterid: ExperienceThresholds.id }); }); }else{ sendChat('Easy Experience', '/w gm There is already a character called ExperienceThresholds in the game. Please delete this old instance ' +'of the thresholds and reselect your desired experience progression in the'+state.PAGENAVIGATOR.configButton+'menu.'); }; }, //FUNCTIONS FOR WORKING WITH THE STATE: loadSettings, setDefaults, configPlayers, configProgression, update, //Set PCs to eligible/ineligible for XP awards configPlayers = function(cid, status){ state.EASYEXPERIENCE.config.PCs[cid].playing = status; outputConfig(); }, //Configure the progression system used. Options are - Pathfinder:Slow, Medium, Fast; PFS Standard; D&D 5e configProgression = function(choice){ state.EASYEXPERIENCE.prog.selected = choice; createThresholds(choice); outputConfig(); }, //Update all the list of character ids for all PCs, active PCs, and MIA PCs update = function(){ var characters = playerCharacters(), PCs = {}, current = state.EASYEXPERIENCE.config.PCs; _.each(characters, function(obj){ if(!state.EASYEXPERIENCE.config.PCs[obj.cid]){ PCs[obj.cid] = { id: obj.cid, playing: 'active' }; }else{ PCs[obj.cid] = { id: obj.cid, playing: state.EASYEXPERIENCE.config.PCs[obj.cid].playing }; } }); state.EASYEXPERIENCE.config.PCs = PCs; loadSettings(); outputConfig(); }, //Load the character ids stored in the state to an array of full roll20 character objects loadSettings = function(){ PCs = _.map(state.EASYEXPERIENCE.config.PCs, function(obj){ return getObj('character', obj.id); }); }, //If config.PCs or config.prog in the state is not defined, create them using predefined values setDefaults = function(){ var tempAttr, count=0, cid; tempAttr = playerCharacters(); if(!state.EASYEXPERIENCE.config.PCs){ state.EASYEXPERIENCE.config.PCs = {}; _.each(tempAttr, function(n){ state.EASYEXPERIENCE.config.PCs[n.cid] = { id: n.cid, playing: 'active' }; }); }; if(!state.EASYEXPERIENCE.prog){ state.EASYEXPERIENCE.prog = {}; state.EASYEXPERIENCE.prog.selected = 'pfmedium'; state.EASYEXPERIENCE.prog.pfmedium ={ "Session XP": 0, "2": 2000, "3": 5000, "4": 9000, "5": 15000, "6": 23000, "7": 35000, "8": 51000, "9": 75000, "10": 105000, "11": 155000, "12": 220000, "13": 315000, "14": 445000, "15": 635000, "16": 890000, "17": 1300000, "18": 1800000, "19": 2550000, "20": 3600000 }; state.EASYEXPERIENCE.prog.pfslow ={ "Session XP": 0, "2": 3000, "3": 7500, "4": 14000, "5": 23000, "6": 35000, "7": 53000, "8": 77000, "9": 115000, "10": 160000, "11": 235000, "12": 330000, "13": 475000, "14": 665000, "15": 955000, "16": 1350000, "17": 1900000, "18": 2700000, "19": 3850000, "20": 5350000 }; state.EASYEXPERIENCE.prog.pffast ={ "Session XP": 0, "2": 1300, "3": 3300, "4": 6000, "5": 10000, "6": 15000, "7": 23000, "8": 34000, "9": 50000, "10": 71000, "11": 105000, "12": 145000, "13": 210000, "14": 295000, "15": 425000, "16": 600000, "17": 850000, "18": 1200000, "19": 1700000, "20": 2400000 }; state.EASYEXPERIENCE.prog.fifth ={ "Session XP": 0, "2": 300, "3": 900, "4": 2700, "5": 6500, "6": 14000, "7": 23000, "8": 34000, "9": 48000, "10": 64000, "11": 85000, "12": 100000, "13": 120000, "14": 140000, "15": 165000, "16": 195000, "17": 225000, "18": 265000, "19": 305000, "20": 355000 }; state.EASYEXPERIENCE.prog.pfs ={ "Session XP": 0, "2": 3, "3": 6, "4": 9, "5": 12, "6": 15, "7": 18, "8": 21, "9": 24, "10": 27, "11": 30, "12": 33, "13": 36, "14": 39, "15": 42, "16": 45, "17": 48, "18": 51, "19": 54, "20": 57 }; }; }, //FUNCTIONS FOR DETERMINING PLAYERS AND THRESHOLDS: playerCharacters, getThresholds, createThresholds playerCharacters = function() { /* start the chain with all the attribute objects named 'player-name' */ return _.chain(filterObjs((o) => { return (o.get('type')==='attribute' && o.get('name')==='player-name' && o.get('current').length>0); })) //.tap((o)=>{log('chain started '+o.length);}) /* IN: Array of Attribute Objects */ /* extract the characterid from each */ .reduce((m,o)=>{ let obj={}; obj.cid=o.get('characterid'); obj['player-name']=o; m.push(obj); return m; },[]) //.tap((o)=>{log('after .reduce '+ o.length);}) /* IN: Array of Objects with * Character ID in property cid * attribute in [attributeName] */ /* add characters to the objects */ .map((o)=>{ o.char=getObj('character',o.cid); return o; }) //.tap((o)=>{log('after character added '+o.length);}) /* IN: Array of Objects with * Character ID in property cid * attribute in [attributeName] * character in property char */ /* remove any entries that didn't have Characters */ .reject( (o)=> {return _.isUndefined(o.char);} ) //.tap((o)=>{log('after .reject '+o.length);}) /*IN: Array of Objects cleaned of undefined characters * with character ID's, 'player-name' attribute in ['player-name] * and character in property char **** * add the experience attribute to a property called xp for all characters*/ .map( (o)=>{ o.xp = findObjs({ type: 'attribute', name: 'experience', characterid: o.cid })[0]; return o; }) /* IN: Array of Character Objects */ /* Unwrap Chain and return the array */ .value(); }, getThresholds = function(){ return _.chain(filterObjs((o) => { return (o.get('type')==='attribute' && o.get('name')==='Session XP'); })) .reduce((m,o)=>{ let obj={}; obj.cid=o.get('characterid'); obj['Session XP']=o; m.push(obj); return m; },[]) .map((o)=>{ let attrs=filterObjs( (a)=>{ return a.get('type')==='attribute' && a.get('characterid')===o.cid && _.contains(['2','3','4','5','6','7','8','9','10','11','12','13','14','15','16','17','18','19','20'],a.get('name')); }); o.attrs=o.attrs||{}; _.each(attrs, (a)=>{ o.attrs[a.get('name')]=a; }); return o; }) .value(); }, //RECORDING OR APPLYING XP: applyXP, recordXP, characterXP, levelUP applyXP = function(){ var activePCs = _.map(PCs, function(obj){ if(state.EASYEXPERIENCE.config.PCs[obj.id].playing === 'active'){return obj} }), thresholds = getThresholds(), numPCs, sessionXP, errors, eachTrack = 0; activePCs = _.reject(activePCs, function(obj){ return _.isUndefined(obj) }); sessionXP = parseInt(thresholds[0]['Session XP'].get('current')); if(sessionXP>0){ numPCs = parseInt(activePCs.length); errors = 0; _.each(activePCs, function(obj){ if(!obj.xp){ sendChat('EasyExperience Script', "/w gm Error MSG: Character" + obj.get('name') + " does not have an " +"experience attribute, please set that character's experience to some value (even if it is 0). No experience was awarded to " +"any character."); errors++; } }); if(errors>0){ return } _.each(activePCs, function(obj){ var level = findObjs({ type: 'attribute', name: 'level', characterid: obj.cid })[0]; if(obj.xp){ if(parseInt(obj.xp.get('max'))===0 || !obj.xp.get('max')){ levelUP(level); } obj.xp.set('current', parseInt(obj.xp.get('current')) + (sessionXP/numPCs)); if(parseInt(obj.xp.get('current'))>=parseInt(obj.xp.get('max'))){ sendChat('Congratulations! ' + obj.char.get('name'), obj.char.get('name') + ' has leveled up!'); } } }); sendChat('EasyExperience Script', 'The GM has ended the session. All players have been awarded ' + (sessionXP/numPCs) + ' XP.') thresholds[0]['Session XP'].set('max', thresholds[0]['Session XP'].get('current')); thresholds[0]['Session XP'].set('current', 0); }else{ sendChat('Experience Tracker', '/w gm There is currently no experience in the Session XP attribute of the tracker. No XP was applied ' +'and your previous Session XP was not overwritten.'); return; } }, recordXP = function(xpAward){ var thresholds=getThresholds(), currXP; currXP=parseInt(thresholds[0]['Session XP'].get('current')); thresholds[0]['Session XP'].set('current', currXP + xpAward); sendChat('EasyExperience Script', '/w gm Status Update: ' + xpAward + ' XP was added to the Session XP tally. There is now ' + (currXP + xpAward) + ' XP recorded in the tally.' ); }, characterXP = function(chr, award){ var numChr = chr.length, currXP; _.each(chr, function(obj){ currXP = findObjs({ type: 'attribute', name: 'experience', characterid: obj.id })[0]; log(currXP); if(currXP){ currXP.set('current', parseInt(currXP.get('current')) + (award/numChr)); if(parseInt(currXP.get('current'))>=parseInt(currXP.get('max'))){ sendChat('Congratulations! ' + obj.get('name'), ' You have leveled up!'); } } }); }, levelUP = function(level){ var thresholds = getThresholds(), currXP = findObjs({ type: 'attribute', name: 'experience', characterid: level.get('characterid') }), player = findObjs({ type:'attribute', name: 'player-name', characterid: level.get('characterid') }), nextLevel; if(player[0].get('current').length===0){ return; }; if(level.get('current')<20){ if(level.get('current')>0){ if(currXP[0]){ nextLevel = level.get('current') + 1; currXP[0].set('max', thresholds[0].attrs[nextLevel].get('current')); return } sendChat('EasyExperience', "/w gm The character sheet for " + player[0].get('current') + "'s character has not generated an experience attribute yet, please ensure the current xp on all characters is set to something (even if it is 0)"); return } sendChat('EasyExperience', "/w gm " + player[0].get('current') + "'s character level has been set to 0, please raise that character's level to a valid level (at least 1)"); return } sendChat('EasyExperience', "/w gm " + player[0].get('current') + "'s character level has been set to 20, there are no experience thresholds defined for epic level play and the EasyExperience script can no longer handle keeping track of your experience thresholds. The script can still track experience gains without a problem. If you would like to have thresholds added, please contact the author: Scott C. via Roll20"); return }, //DIALOG GENERATION: outputConfig and showHelp outputConfig = function(){//allows the gm to set experience thresholds to the slow, medium, or fast pathfinder progression; set players to active or inactive var playerButton, progressButton = makeButton( '!xp-config progression ?{What experience progression would you like to use?|Pathfinder Slow, pfslow|Pathfinder Medium, ' +'pfmedium|Pathfinder Fast, pffast|Pathfinder Society, pfs|D&D 5e, fifth|Custom Progression, custom}', state.EASYEXPERIENCE.prog.selected, '#CDAE88', 'black' ), playerMsg = '', playerButton, updateButton = makeButton( '!xp-config addplayer', 'Update Players', '#CDAE88', 'black' ); _.each(PCs,function(obj){ if(state.EASYEXPERIENCE.config.PCs[obj.id].playing === 'active'){ playerButton = makeButton( '!xp-config player '+obj.id+' MIA', 'Active', '#228b22' ); }else{ playerButton = makeButton( '!xp-config player '+obj.id+' active', 'MIA', '#d3d3d3' ); } playerMsg+= obj.get('name') +'
Set PCs to active or MIA:
' +playerMsg+'Make awarding XP simple.
' +'PC vs. NPC: The script determines if a character is a PC or an NPC based on the value of the character'+ch("'")+'s player-name ' +'attribute. If the attribute is empty, it is an NPC. If it has any value, it is a PC.' +'
Use !xp followed by one of these commands to call the script:
You can also set PCs to active or inactive for xp awards
' +'