// Github: https://github.com/shdwjk/Roll20API/blob/master/MutantYearZero/MutantYearZero.js // By: The Aaron, Arcane Scriptomancer // Contact: https://app.roll20.net/users/104025/the-aaron var MutantYearZero = MutantYearZero || (function() { 'use strict'; var version = '0.1.8', lastUpdate = 1455584304, schemaVersion = 0.4, symbols = { biohazard: '&#'+'9763;', radioactive: '&#'+'9762;', explosion: '&#'+'128165;', push: '&#'+'10150;' }, myzBackground = 'https://s3.amazonaws.com/files.d20.io/images/16289834/HuG1AuH9EWsSI2tLJZubBA/original.png?1455490062', colors = { green: '#3ea62a', lightGreen: '#89D878', yellow: '#ddcf43', lightYellow: '#FFF699', black: '#1d1d1d', lightBlack: '#8F8E8E' }, defaults = { css: { button: { 'border': '1px solid #cccccc', 'border-radius': '1em', 'background-color': '#006dcc', 'margin': '0 .1em', 'font-weight': 'bold', 'padding': '.1em .4em', 'color': 'white' }, errorMsg: { 'font-size': '.8em', 'border': '.2em solid #990000', 'border-radius': '1em', 'background-color': '#cccccc', 'margin': '0 .1em', 'padding': '.1em .6em', 'overflow': 'hidden' }, errorMsgLabel: { 'font-weight': 'bold', 'background-color': '#990000', 'color': '#ffffff', 'display': 'inline-block', 'padding': '.1em .6em', 'border-radius': '0 0 .75em 0', 'margin': '-.1em .2em 0em -.6em' }, optionalMsg: { 'font-size': '.6em', 'border': '.1em solid #a2521f', 'border-radius': '.5em', 'background-color': '#c3701b', 'margin': '0 .1em .2em .1em', 'padding': '.1em .4em', 'overflow': 'hidden' }, optionalMsgLabel: { 'font-weight': 'bold', 'background-color': '#a2521f', 'color': '#ffffff', 'display': 'inline-block', 'padding': '.1em .6em .1em .4em', 'border-radius': '0 0 .75em 0', 'margin': '-.1em .2em 0em -.4em' }, myzMsg: { 'font-size': '1.25em', 'border': '.2em solid #8b3f1f', 'border-radius': '.5em', 'background-color': '#2c2119', 'background-image': 'url('+myzBackground+')', 'background-repeat': 'no-repeat', 'background-position': 'top', 'background-size': '100%', 'margin': '0 0', 'padding': '.3em', 'overflow': 'hidden' }, myzMsgLabelContainer: { 'max-width': '10em', 'float': 'left', 'font-size': '1.5em', 'margin-right': '.2em' }, myzMsgLabel: { 'border': '.1em solid #a2521f', 'border-radius': '.25em', 'font-weight': 'normal', 'padding': '.1em', 'margin-bottom': '.1em', 'text-align': 'right' }, myzDie: { 'display': 'inline-block', 'border': '1px solid black', 'border-radius': '.3em', 'padding': '.2em 0 0 0', 'text-align': 'center', 'width': '1.3em', 'height': '1.1em', 'float': 'left', 'margin-right': '.1em', 'margin-bottom': '.1em' } } }, reportingModes = { 'off': 'None', 'gm': 'GM', 'gm+player': 'GM & Player', 'public': 'Public' }, templates = {}, 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" %>' ); templates.errorMsg = _.template( '
>'+ '
>'+ '<%= label %>'+ '
'+ '<%= text %>'+ '
' ); templates.optionalMsg = _.template( '
>'+ '<% if(label){ %>'+ '
>'+ '<%= label %>'+ '
'+ '<% } %>'+ '<%= text %>'+ '
' ); templates.die = _.template( '
>'+ '<%= text %>'+ '
' ); templates.outputLabel = _.template( '
>'+ '<%= labelText %>'+ '
' ); templates.output = _.template( '
>'+ '
>'+ '<%= results %>'+ '
'+ '<%= diceImages %>'+ '
' ); }, makeErrorMsg = function(msg){ return templates.errorMsg({ text: msg, label: 'Error', templates: templates, defaults: defaults, css: {} }); }, makeOptionalMsg = function(label,msg){ return templates.optionalMsg({ text: msg, label: label, templates: templates, defaults: defaults, css: {} }); }, makeOptionalText = function (optional){ return '
'+_.map(optional,function(o){ return makeOptionalMsg(o.label,o.msg); }).join('')+'
'; }, makeLabel = function(text,color){ return templates.outputLabel({ labelText: text, templates: templates, defaults: defaults, css: { 'background-color': color } }); }, makeOutput = function (results,diceImages){ return templates.output({ results: results, diceImages: diceImages, templates: templates, defaults: defaults, css: {} }); }, makeButton = function(command, label, backgroundColor, color){ return templates.button({ command: command, label: label, templates: templates, defaults: defaults, css: { color: color, 'background-color': backgroundColor } }); }, checkInstall = function() { log('-=> MutantYearZero v'+version+' <=- ['+(new Date(lastUpdate*1000))+']'); if( ! _.has(state,'MutantYearZero') || state.MutantYearZero.version !== schemaVersion) { log(' > Updating Schema to v'+schemaVersion+' <'); switch(state.MutantYearZero && state.MutantYearZero.version) { case 0.1: state.MutantYearZero.config = { reportMode: 'public' }; /* break; // intentional drop through */ case 0.2: state.MutantYearZero.playerRolls={}; state.MutantYearZero.sequence=0; /* break; // intentional drop through */ case 0.3: state.MutantYearZero.config.gmCanPush=false; /* break; // intentional drop through */ case 'UpdateSchemaVersion': state.MutantYearZero.version = schemaVersion; break; default: state.MutantYearZero = { version: schemaVersion, config: { reportMode: 'public', gmCanPush: false }, sequence: 0, playerRolls: {} }; break; } } buildTemplates(); }, 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 ''; }, makeConfigOption = function(config,command,text) { var onOff = (config ? 'On' : 'Off' ), color = (config ? '#5bb75b' : '#faa732' ); return '
'+ '
'+ makeButton(command,onOff,color)+ '
'+ text+ '
'+ '
'; }, makeConfigOptionSelection = function(config,values,command,text) { var buttons=_.map(values,function(v,k){ return makeButton( command+' '+k, v, (config === k ? '#5bb75b' : '#faa732' ) ); }); return '
'+ text+ '
'+ buttons.join('') + '
'+ '
'+ '
'; }, getConfigOption_GMCanPush = function() { return makeConfigOption( state.MutantYearZero.config.gmCanPush, '!myz-config --toggle-gm-can-push', 'GM Can Push determines if the GM is allowed to push a player'+ch("'")+' roll.' ); }, getConfigOption_ErrorReporting = function() { return makeConfigOptionSelection( state.MutantYearZero.config.reportMode, reportingModes, '!myz-config --set-report-mode', 'Report Mode determines when and how invalid push attempts are reported.' ); }, getAllConfigOptions = function() { return getConfigOption_GMCanPush() + getConfigOption_ErrorReporting(); }, showHelp = function(playerid) { var who=getObj('player',playerid).get('_displayname'); sendChat('','/w "'+who+'" '+ '
'+ '
'+ 'MutantYearZero v'+version+ '
'+ '
'+ '

MutantYearZero automates the rolling of Skill, Base (Stat), and Gear dice using Mutant: Year Zero dice mechanics.

'+ '
'+ 'Commands'+ '
'+ '!myz '+ch('[')+'Skill Roll'+ch(']')+' '+ch('[')+'Base Roll'+ch(']')+' '+ch('[')+'Gear Roll'+ch(']')+' '+ch('[')+'--'+ch('<')+'Label'+ch('>')+ch('|')+ch('<')+'Message'+ch('>')+' ...'+ch(']')+''+ '
'+ '

Performs a Mutant Year Zero Roll.

'+ '
    '+ '
  • '+ ''+ch('[')+'Skill Roll'+ch(']')+' '+ch('-')+' An inline dice expression rolling the d6s. 6s are counted as a success ('+symbols.radioactive+'). Example: '+ch('[')+ch('[')+'3d6'+ch(']')+ch(']')+ '
  • '+ '
  • '+ ''+ch('[')+'Base Roll'+ch(']')+' '+ch('-')+' An inline dice expression rolling the d6s. 6s are counted as a success ('+symbols.radioactive+'). 1s are counted as Trama ('+symbols.biohazard+'). Example: '+ch('[')+ch('[')+'4d6'+ch(']')+ch(']')+ '
  • '+ '
  • '+ ''+ch('[')+'Gear Roll'+ch(']')+' '+ch('-')+' An inline dice expression rolling the d6s ('+symbols.radioactive+'). 1s are counted as Gear Degradation ('+symbols.explosion+'). Example: '+ch('[')+ch('[')+'2d6'+ch(']')+ch(']')+ '
  • '+ '
  • '+ ''+ch('[')+'--'+ch('<')+'Label'+ch('>')+ch('|')+ch('<')+'Message'+ch('>')+' ...'+ch(']')+' '+ch('-')+' An optional set of text to be shown above the die roll. Label may be omitted to just provid a text field. You can specify as many optional descriptions as you like.'+ '
    '+ '
    '+
                                '!myz '+ch('[')+ch('[')+'5d6'+ch(']')+ch(']')+' '+ch('[')+ch('[')+'3d6'+ch(']')+ch(']')+' '+ch('[')+ch('[')+'4d6'+ch(']')+ch(']')+' --Player|Buzzard --Skill|Know the Zone (Wits)'+
                            '
    '+ '
    '+ '
  • '+ '
'+ '
'+ '
'+ '
'+ '!wmyz '+ch('[')+'Skill Roll'+ch(']')+' '+ch('[')+'Base Roll'+ch(']')+' '+ch('[')+'Gear Roll'+ch(']')+''+ '
'+ '

Identical to !myz except that the results are whispered to the player rolling and the GM.

'+ '
'+ '
'+ ( playerIsGM(playerid) ? 'Configuration' + getAllConfigOptions() : '' )+ '
' ); }, getDiceCounts = function(msg,idx) { var rolls = {}; if( msg.inlinerolls && msg.inlinerolls[idx] && msg.inlinerolls[idx].results && msg.inlinerolls[idx].results.rolls[0] ) { _.each(msg.inlinerolls[idx].results.rolls,function(res){ rolls=_.reduce(_.map(res.results,function(r){ return r.v; }).sort() || [], function(m,r){ m[r]=(m[r]||0)+1; return m; },rolls); }); } return rolls; }, getDiceArray = function(c) { return _.reduce(c,function(m,v,k){ _.times(v,function(){m.push(k);}); return m; },[]); }, makeDiceImages = function(dice,bgcolor,color){ bgcolor=bgcolor||'black'; color=color||'white'; return _.map(dice,function(r){ return templates.die({ text: r, templates:templates, defaults:defaults, css: { 'background-color': bgcolor, 'color': color } }); }).reverse().join(''); }, getRollableDiceCount = function(dice){ return _.filter(dice,function(v){return v.match(/^\d+$/);}).length; }, makeRerollExpression = function(dice){ var cnt = getRollableDiceCount(dice); return ' '+ch('[')+ch('[')+cnt+'d6'+ch(']')+ch(']')+' '; }, validatePlayerRollHash = function(playerid, hash, skill,base,gear){ var obj=state.MutantYearZero.playerRolls[playerid]; return (obj && obj.hash === hash && obj.dice.skillDice === skill && obj.dice.baseDice === base && obj.dice.gearDice === gear ); }, getCountsForRoll = function(playerid,hash){ if(hash && _.has(state.MutantYearZero.playerRolls,playerid)){ return state.MutantYearZero.playerRolls[playerid].counts; } return { success: 0, trauma: 0, damage: 0, pushes: 0 }; }, getOptionalForRoll = function(playerid,hash){ if(hash && _.has(state.MutantYearZero.playerRolls,playerid)){ return state.MutantYearZero.playerRolls[playerid].optional; } return []; }, recordPlayerRollHash = function(playerid, obj){ state.MutantYearZero.playerRolls[playerid]=obj; }, reportBadPushCounts = function(playerid){ var player=getObj('player',playerid); switch(state.MutantYearZero.config.reportMode){ case 'public': sendChat(player.get('displayname'),'/direct '+ makeErrorMsg('Attempting to push with modified dice counts.') ); break; case 'gm+player': if(!playerIsGM(playerid)){ sendChat('','/w "'+player.get('displayname')+'" '+ makeErrorMsg('Do not adjust the number of dice when pushing.') ); } /* break; // intentional drop thru */ case 'gm': if(playerIsGM(playerid)){ sendChat('','/w gm '+ makeErrorMsg('Cannot adjust the number of dice in a Push attempt.') ); } else { sendChat('','/w gm '+ makeErrorMsg(player.get('displayname')+' attempted to Push with an altered number of dice.') ); } break; default: break; } }, reportBadPush = function(playerid){ var player=getObj('player',playerid); switch(state.MutantYearZero.config.reportMode){ case 'public': sendChat(player.get('displayname'),'/direct '+ makeErrorMsg('Cannot Push old rolls. Please use the current result'+ch("'")+'s Push button.') ); break; case 'gm+player': if(!playerIsGM(playerid)){ sendChat('','/w "'+player.get('displayname')+'" '+ makeErrorMsg('Cannot Push old rolls. Please use the current result'+ch("'")+'s Push button.') ); } /* break; // intentional drop thru */ case 'gm': if(playerIsGM(playerid)){ sendChat('','/w gm '+makeErrorMsg('Cannot Push old rolls.')); } else { sendChat('','/w gm '+makeErrorMsg(player.get('displayname')+' attempted to Push from an old roll.')); } break; default: break; } }, reportNotAllowed = function(playerid,ownerid){ var player=getObj('player',playerid), owner=getObj('player',ownerid); switch(state.MutantYearZero.config.reportMode){ case 'public': sendChat(player.get('displayname'),'/direct '+ makeErrorMsg('Cannot Push '+owner.get('displayname')+ch("'")+'s roll.') ); break; case 'gm+player': if(!playerIsGM(playerid)){ sendChat('','/w "'+player.get('displayname')+'" '+ makeErrorMsg('Cannot Push '+owner.get('displayname')+ch("'")+'s roll.') ); } /* break; // intentional drop thru */ case 'gm': if(playerIsGM(playerid)){ sendChat('','/w gm '+ makeErrorMsg('Cannot Push '+owner.get('displayname')+ch("'")+'s roll because GM Can Push is not enabled. See '+makeButton('!myz-config','Configuration')+' for details.') ); } else { sendChat('','/w gm '+makeErrorMsg(player.get('displayname')+' attempted to Push '+owner.get('displayname')+ch("'")+'s roll.')); } break; default: break; } }, handleInput = function(msg_orig) { var msg = _.clone(msg_orig), args, optional, who, rollIndices=[], skillDice, // skill: green baseDice, // base(stat): yellow, trauma on 1s gearDice, // gear: black, degradation on 1s skillDiceArray, baseDiceArray, gearDiceArray, successes=0, trauma=0, gearDamage=0, pushedValues, pushes, push=false, pushButton, w=false, cmd, hash, matches, owner, output; if (msg.type !== "api") { return; } if(_.has(msg,'inlinerolls')){ rollIndices=_.map(msg.content.match(/\$\[\[(\d+)\]\]/g),function(i){ return i.match(/\d+/)[0]; }); msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ m['$[['+k+']]']=v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } who=getObj('player',msg.playerid).get('_displayname'); optional = msg.content.split(/\s+--/); args = optional.shift().split(/\s+/); optional = _.map(optional,function(o){ var s=o.split(/\|/), k=s.shift(); s=s.join('|'); return { label: k, msg: s }; }); cmd=args.shift(); matches=cmd.match(/^(!\S+)\[([^\]]+)\]/); if(matches){ cmd=matches[1]; hash=parseInt(matches[2],10); } switch(cmd) { case '!wmyz': w=true; /* break; */ // Intentional drop through case '!myz': if( 0 === args.length || _.contains(args,'--help')) { showHelp(msg.playerid); return; } push=!!hash; if( push && ( _.has(state.MutantYearZero.playerRolls,msg.playerid) && ( hash !== state.MutantYearZero.playerRolls[msg.playerid].hash)) ) { owner = _.find(state.MutantYearZero.playerRolls,function(v){ return hash === v.hash; }); if(owner){ owner=owner.playerid; if(!(playerIsGM(msg.playerid) && state.MutantYearZero.config.gmCanPush)){ reportNotAllowed(msg.playerid,owner); return; } } else { reportBadPush(msg.playerid); return; } } owner = owner || msg.playerid; skillDice=getDiceCounts(msg,rollIndices[0]); baseDice=getDiceCounts(msg,rollIndices[1]); gearDice=getDiceCounts(msg,rollIndices[2]); if(push && ( _.has(state.MutantYearZero.playerRolls,owner) && ( hash === state.MutantYearZero.playerRolls[owner].hash) ) && !validatePlayerRollHash(owner,hash, getDiceArray(skillDice).length, getDiceArray(baseDice).length, getDiceArray(gearDice).length ) ){ reportBadPushCounts(msg.playerid); return; } pushedValues=getCountsForRoll(owner,hash); successes=pushedValues.success + (skillDice['6']||0) + (baseDice['6']||0) + (gearDice['6']||0) ; trauma=pushedValues.trauma + (baseDice['1']||0); gearDamage=pushedValues.damage + (gearDice['1']||0); pushes=pushedValues.pushes+1; optional = (optional.length && optional) || getOptionalForRoll(owner,hash); skillDiceArray=_.map(getDiceArray(skillDice),function(v){ switch(v){ case '6': return symbols.radioactive; default: return v; } }); baseDiceArray=_.map(getDiceArray(baseDice),function(v){ switch(v){ case '1': return symbols.biohazard; case '6': return symbols.radioactive; default: return v; } }); gearDiceArray=_.map(getDiceArray(gearDice),function(v){ switch(v){ case '1': return symbols.explosion; case '6': return symbols.radioactive; default: return v; } }); // record push hash=(++state.MutantYearZero.sequence); recordPlayerRollHash(owner,{ hash: hash, playerid: owner, dice: { skillDice: getRollableDiceCount(skillDiceArray), baseDice: getRollableDiceCount(baseDiceArray), gearDice: getRollableDiceCount(gearDiceArray) }, counts: { success: successes, trauma: trauma, damage: gearDamage, pushes: pushes }, optional: optional }); pushButton = (_.reduce([skillDiceArray,baseDiceArray,gearDiceArray],function(m,dice){ return m+getRollableDiceCount(dice);},0) ? makeButton( '!'+(w?'w':'')+'myz['+hash+'] '+ makeRerollExpression(skillDiceArray)+ makeRerollExpression(baseDiceArray)+ makeRerollExpression(gearDiceArray), symbols.push+' '+pushes ) : '' ); output = makeOutput([ makeLabel( successes+' '+symbols.radioactive, colors.lightGreen), makeLabel( trauma+' '+symbols.biohazard, colors.lightYellow), makeLabel( gearDamage+' '+symbols.explosion, colors.lightBlack), pushButton ].join(''), [ makeOptionalText(optional), makeDiceImages(skillDiceArray,colors.green), makeDiceImages(baseDiceArray,colors.yellow, colors.black), makeDiceImages(gearDiceArray,colors.black) ].join('') ); who=getObj('player',owner).get('displayname'); if(w){ sendChat(msg.who, '/w gm '+output); if(!playerIsGM(owner)){ sendChat(who, '/w "'+who+'" '+output); } } else { sendChat(who, '/direct '+output); } break; case '!myz-config': if(!playerIsGM(msg.playerid)){ return; } args = _.rest(msg.content.split(/\s+--/)); if(_.contains(args,'help')) { showHelp(msg.playerid); return; } if(!args.length) { sendChat('','/w "'+who+'" '+ '
'+ '
'+ 'MutantYearZero v'+version+ '
'+ 'Configuration'+ getAllConfigOptions()+ '
' ); return; } _.each(args,function(a){ var opt=a.split(/\s+/); switch(opt.shift()) { case 'toggle-gm-can-push': state.MutantYearZero.config.gmCanPush=!state.MutantYearZero.config.gmCanPush; sendChat('','/w '+who+' ' +'
' +getConfigOption_GMCanPush() +'
' ); break; case 'set-report-mode': if(_.has(reportingModes,opt[0])){ state.MutantYearZero.config.reportMode=opt[0]; sendChat('','/w '+who+' ' +'
' +getConfigOption_ErrorReporting() +'
' ); } else { sendChat('','/w '+who+' ' +'
Unsupported Reporting Mode Option:
'+opt[0]+'' ); } break; default: sendChat('','/w '+who+' ' +'
Unsupported Option:
'+a+'' ); } }); break; } }, registerEventHandlers = function() { on('chat:message', handleInput); }; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers }; }()); on('ready',function() { 'use strict'; MutantYearZero.CheckInstall(); MutantYearZero.RegisterEventHandlers(); });