// Github: https://github.com/shdwjk/Roll20API/blob/master/ImperialCalendar/ImperialCalendar.js // By: The Aaron, Arcane Scriptomancer // Contact: https://app.roll20.net/users/104025/the-aaron var ImperialCalendar = ImperialCalendar || (function() { 'use strict'; var version = '0.1.2', lastUpdate = 1490707355, schemaVersion = 0.2, Uw = (w)=>`&${w};`, Un = (n)=>`&#${n};`, Unx = (n)=>`&#x${n};`, colorHex = (c1,c2)=>`${Unx('2b23')}`, colorDot = (c1,c2,s1)=>`${Uw('nbsp')}`, colors = { gm: { dark: '#5e322f', light: '#ea6566' }, player: { dark: '#506872', light: '#8ac8e1' }, all: { dark: '#3d6135', light: '#5bbd58' } }, noteDot = { gm: colorDot(colors.gm.dark,colors.gm.light,0.6), player: colorDot(colors.player.dark,colors.player.light,0.6), all: colorDot(colors.all.dark,colors.all.light,0.6) }, images = { headerTransparent: 'https://s3.amazonaws.com/files.d20.io/images/27543499/ys8wHPa69UFPqkxeqGNQEg/original.png?1484703811' }, regex = { dateSpec: /^([\+-])?\s*(\d+(?:-\d+)?)?\s*([dDwWyY])?$/ }, esRE = function (s) { var escapeForRegexp = /(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g; return s.replace(escapeForRegexp,"\\$1"); }, HE = (function(){ var entities={ //' ' : '&'+'nbsp'+';', '<' : '&'+'lt'+';', '>' : '&'+'gt'+';', "'" : '&'+'#39'+';', '@' : '&'+'#64'+';', '{' : '&'+'#123'+';', '|' : '&'+'#124'+';', '}' : '&'+'#125'+';', '[' : '&'+'#91'+';', ']' : '&'+'#93'+';', '"' : '&'+'quot'+';' }, re=new RegExp('('+_.map(_.keys(entities),esRE).join('|')+')','g'); return function(s){ return s.replace(re, function(c){ return entities[c] || c; }); }; }()), 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 ''; }, outputChat = function(msg){ sendChat('',msg); }, checkInstall = function() { log('-=> ImperialCalendar v'+version+' <=- ['+(new Date(lastUpdate*1000))+']'); if( ! _.has(state,'ImperialCalendar') || state.ImperialCalendar.version !== schemaVersion) { log(' > Updating Schema to v'+schemaVersion+' <'); state.ImperialCalendar = { version: schemaVersion, config: { holidayName: 'Holiday', dayNames: [ 'Wonday', 'Tuday', 'Thirday', 'Forday', 'Fiday', 'Sixday', 'Senday' ], showToolTip: true }, current: 407340, // days starting at 0 noteSeed: 1, notes: { } }; } }, getConfigOption_DayNames = function(){ let stcc=state.ImperialCalendar.config, mkDay=(n,c,idx)=>`
${n} :: ${c}
`; return `
Day Names
${mkDay('Holiday',stcc.holidayName,'H')}${mkDay('Monday',stcc.dayNames[0],0)}${mkDay('Tuesday',stcc.dayNames[1],1)}${mkDay('Wednesday',stcc.dayNames[2],2)}${mkDay('Thursday',stcc.dayNames[3],3)}${mkDay('Friday',stcc.dayNames[4],4)}${mkDay('Saturday',stcc.dayNames[5],5)}${mkDay('Sunday',stcc.dayNames[6],6)}
`; }, getConfigOption_ShowToolTip = function(){ var text = (state.ImperialCalendar.config.showToolTip ? 'On' : 'Off' ); return '
'+ 'Showing Tool Tips is currently '+ text+ ' '+ ''+ 'Toggle'+ ''+ '
'; }, getAllConfigOptions = function() { return getConfigOption_DayNames()+ getConfigOption_ShowToolTip(); }, showHelp = function(who) { outputChat('/w "'+who+'" '+ '
'+ '
'+ 'ImperialCalendar v'+version+ '
'+ '
'+ '

ImperialCalendar provides a fully featured calendar as used in Traveller, Starts without Number, and other scifi games. It supports notes (public, private, and player specific) and various display formats.

'+ '

Commands

'+ '

!cal [<--help|--details|--sparse|--whisper|--set|--note|--remove-note|--show|--show-week|--show-4week|--show-5week|--show-year>|<parameter> ...]

'+ '

!wcal -- An alias for !cal --whisper;

'+ '

Date Format

'+ '

ImperialCalendar uses dates of the format <day>-<year>, such as 023-1116. The leading 0s are optional, so 001-0001 is the same as 1-1. Anywhere you can use a date, you can also use a relative date by using a number prefixed by either + or -. These numbers will be used as a day offset from the current day. You can further suffice these numbers with one of d,w,y to specify days (default), weeks, or years. (D,W,Y also work.) In most cases, the + is also optional. In fact, you can leave off the number and only specify a suffice. +7d is the same as 7d, +7, d, D, +d, +D, +1w, etc.

'+ '

Display

'+ '

There are two display modes: Calendar and Journal.

'+ '

Calendar

'+ '

Shows you days on a grid. Hover on a day to see it'+ch("'")+'s notes (days with notes have a green background). Click a day to see the journal for that day.

'+ '

Journal

'+ '

Shows you the notes for the day or days. Notes have a button allowing the owner to delete them. There are buttons by each day for adding notes of each permission type.

'+ '

Setting the Date

'+ ''+ '

Show Commands

'+ '

--show[|<date>[|<date> ..]] -- use --show to show a day.

'+ '

--show-week[|<date>] -- Show the week (Monday-Sunday or the Holiday week) surrounding the provided date or the current date.

'+ '

--show-4week[|<date>] -- As above, but shows also 1 week prior and 2 weeks hence from the date.

'+ '

--show-5week[|<date>] -- As above, but shows 2 weeks prior and 2 weeks hence.

'+ '

--show-year[|<date>] -- Shows the full year including the date.

'+ ''+ '

--between|<date>|<date> -- Shows all the days between the two dates. Relative dates are still realtive to the current day.

'+ '

Modifiers

'+ '

--details -- Causes a list of days and notes to be shown, rather than a grid caldendar. (This is the default for --show)

'+ '

--sparse -- Causes a detail view to omit any days without notes.

'+ '

Examples

'+ ''+ '

Whisper Examples

'+ ''+ '

Note commands

'+ '

--note[|day][[|who] ...]|<note text> -- adds a note. If you omit the day, it is added for the current day. You can specify who can see a note. Use gm or private for a gm only note (also the default, you can just leave it off), all for a public note, or some part of a player'+ch("'")+'s name to restrict it to just the matching players. You can specify multiple | separated players.

'+ '

--remove-note[|day]|<all|index> -- removes a note. If you omit the date, it removes from the current day. If you specify all, it will remove all the notes you created on that day (or all notes if you are the gm). If you specify a number, it will remove that index from the specified day (counting from the beginning, starting at 0).

'+ '

Examples

'+ ''+ '
'+ 'Configuration'+ getAllConfigOptions()+ '
' ); }, dayName = function(i){ return (_.isNumber(i) ? state.ImperialCalendar.config.dayNames[((i%7)+7)%7] : state.ImperialCalendar.config.holidayName ); }, dayToDate = function(num){ let y=Math.floor(num/365), d=((num%365)+365)%365, dow = (!d ? 'H' : (d-1)%7); return { year: y, doy: d+1, dow: dow, woy: Math.floor((d-1)/7), dayname: (!d ? 'Holiday' : dayName(dow)) }; }, generateNoteID = function(){ return ++state.ImperialCalendar.noteSeed; }, parseDate = function(date){ let p=(date+'').match(regex.dateSpec), day=state.ImperialCalendar.current; if(p){ let rel=!!(p[1]&&p[1].length)||(p[3]&&p[3].length), dir=(rel&&parseInt((p[1]||'')+'1',10))||1, scale=({d:1, D:1, w:7, W:7, y:365, Y:365}[p[3]])||1, d=p[2]||'1', dParts=d.match(/(\d+)-(\d+)/); day=(rel?day:0)+(dir*(dParts?((parseInt(dParts[2],10)||0)*365+((parseInt(dParts[1],10)-1)||0)):(d*scale))); } return day; }, zeroPad = function (num,d) { let neg = (num<0); d=d || 2; return ('0000000'+Math.abs(num)).slice(-d)+(neg?'-':''); }, getNotes = function(range,opts){ return _.chain(range) .reduce((m,d)=>{ if(_.has(state.ImperialCalendar.notes,d)){ m[d]= _.filter(state.ImperialCalendar.notes[d],(n)=>(opts.whisper && (opts.isGM || _.contains(n.permissions,opts.whoID) || n.owner===opts.whoID)) || _.contains(n.permissions,'all') ); if(!m[d].length){ delete m[d]; } } return m; },{}) .value(); }, formatDate = function(num){ let d=dayToDate(num); return `${zeroPad(d.doy,3)}-${zeroPad(d.year,4)}`; }, keyFormat = function(text) { return (text && text.toLowerCase().replace(/\s+/,'')) || undefined; }, matchKey = function (keys,subject){ return subject && !_.isUndefined(_.find(keys,(o)=>(-1 !== subject.indexOf(o)))); }, argHandlers = { 'details': _.constant(true), 'sparse': _.constant(true), 'whisper': _.constant(true), 'set': (args)=>parseDate(args[0].length ? args[0].shift() : ''), 'show': (args)=>{ return _.chain(args) .map((v)=>{ return v.length ? _.map(v,parseDate) : state.ImperialCalendar.current; }) .flatten() .sortBy(_.identity) .uniq() .value() ; }, 'show-week': (args)=>{ let day= parseDate( args[0].length ? args[0].shift() : state.ImperialCalendar.current ), d=dayToDate(day); return (d.doy !== 1 ? [day-d.dow,day+(6-d.dow)] : [day,day]); }, 'show-4week': (args)=>{ let day= parseDate( args[0].length ? args[0].shift() : state.ImperialCalendar.current ), d=dayToDate(day); return (d.doy !== 1 ? [day-d.dow-7,day+(6-d.dow)+14] : [day-7,day+14]); }, 'show-5week': (args)=>{ let day= parseDate( args[0].length ? args[0].shift() : state.ImperialCalendar.current ), d=dayToDate(day); return (d.doy !== 1 ? [day-d.dow-14,day+(6-d.dow)+14] : [day-14,day+14]); }, 'show-year': (args)=>{ let day= parseDate(args[0].length ? (args[0][0].match(/^\d*$/) ? '1-'+args[0][0] : args[0][0] ) : state.ImperialCalendar.current ), d=dayToDate(day), d0=d.year*365; return [d0,d0+364]; }, 'between': (args)=>[parseDate( args[0].length ? args[0].shift() : state.ImperialCalendar.current ), parseDate( args[0].length ? args[0].shift() : state.ImperialCalendar.current )], 'note': (args)=>_.map(args,(arg)=>{ let retr={ text:arg.pop(), day: false, permissions:[] }, playerKeys=[]; _.each(arg,(a)=>{ if(!retr.day && regex.dateSpec.test(a)){ retr.day = parseDate(a); } else { a=a.toLowerCase(); switch(a){ case 'gm': case 'private': retr.permissions=[]; break; case 'all': case 'public': retr.permissions=['all']; break; default: playerKeys.push(keyFormat(a)); break; } } }); if(playerKeys.length && !_.contains(retr.permission, 'all') ){ let playerids=_.chain(filterObjs((o)=>{ return o && o.get('type') ==='player' && matchKey(playerKeys,keyFormat(o.get('displayname'))); })) .map(p=>p.id) .uniq() .value(); retr.permissions=_.union(retr.permissions,playerids); } retr.day=retr.day||state.ImperialCalendar.current; return retr; }), 'remove-note': (args)=>_.map(args,(arg)=>{ let retr; if(2 === arg.length && /^(all|ID:\d+|\d+)$/i.test(arg[1])){ retr={ day: parseDate(arg[0]), id: (/^ID:/.test(arg[1]) ? parseInt(arg[1].substring(3),10) : undefined ), index: ('all'===arg[1] ? 'all' : ( /^\d+$/.test(arg[1]) ? parseInt(arg[1],10) : undefined)) }; } return retr; }) }, paraphraseString = function(str,maxLen){ if(str.len<=maxLen){ return str; } return str.substring(0,maxLen)+'...'; }, parseArgs = function(args,defaults){ return _.chain(args) .reduce((m,a)=>{ let sp=a.split(/\|/), aName = sp.shift(); m[aName] = (m[aName]||[]); m[aName].push(sp); return m; },{}) .pick(_.keys(argHandlers)) .mapObject((v,k)=>argHandlers[k](v)) .defaults(defaults) .value(); }, buttonMaker = function(command,text,tip,style){ let tipExtra=(tip && state.ImperialCalendar.config.showToolTip ? `class="showtip tipsy" title="${HE(HE(tip))}"` :''); return `${text}`; }, formatNote = function(note,opts){ let noteType =(note.permissions.length ? (_.contains(note.permissions,'all')? 'all':'player'): 'gm'), symbol = noteDot[noteType], bullet = (s)=>`${s}`, row = (b,btn,txt,clr) => `${b} ${btn} ${txt}`, button = (opts.details ? `${buttonMaker(`!wcal -?{Remove note "${paraphraseString(note.text,10)}", are you sure?|Yes,-|No,-no-}remove-note|${note.day}|ID:${note.id}`,Unx('20e0'),'Delete')}` : ''); return row(bullet(symbol),button,note.text,colors[noteType].dark); }, formatDayFull = function(day,notes,opts){ let d=dayToDate(day), dayColor=(day===state.ImperialCalendar.current ? '#fef900' : 'white'), addAll=buttonMaker(`!wcal --note|${day}|all|?{Note text (public)}`,'A','Add
Public
Note',`background-color:${colors.all.dark};border:1px solid ${colors.all.light};`), addPlayer=buttonMaker(`!wcal --note|${day}|?{List of Players separated by ${Unx('2502')} }|?{Note text (players)}`,'P','Add
Player
Note',`background-color:${colors.player.dark};border:1px solid ${colors.player.light};`), addGM=buttonMaker(`!wcal --note|${day}|gm|?{Note text (private)}`,'G','Add
Private
Note',`background-color:${colors.gm.dark};border:1px solid ${colors.gm.light};`), buttons=`${addAll}${addPlayer}${addGM}`; return `
${formatDate(day)} - ${d.dayname} ${buttons}
${ _.isArray(notes) ? _.map(notes,(n)=>formatNote(n,opts)).join('') :'' }
`; }, formatDaySmall = function(day,notes,opts){ let d=dayToDate(day), borderColor=(day===state.ImperialCalendar.current ? '#fef900' : '#666'), dayHeader=`${formatDate(day)}`, tipExtra=( (_.isArray(notes) && notes.length) ? `class="showtip tipsy" title="${HE(HE(`${dayHeader}${_.map(notes,(n)=>formatNote(n,opts)).join('')}Click for Full Access`))}"` : ''), styleExtra = (_.isArray(notes) && notes.length) ? `background-color:${colors.all.dark};` : ''; return `
${zeroPad(d.doy,3)}
`; }, formatDayEmpty = function(){ return `
 
`; }, formatWeek = function(days){ return `
${days.join('')}
`; }, createHolidayWeek = function(day,notes,opts){ let d=dayToDate(day); return [`
${d.dayname}
${formatDaySmall(day,notes,opts)}
`]; }, formatYear = function(year){ return `
Year ${year.year}
${year.weeks.join('')}
`; }, showDays = function(begin,end,opts){ let range; if(_.isArray(begin)){ range = begin; opts=end; } else { let dateLow = (_.isNumber(begin)&&!_.isNaN(begin))? begin : state.ImperialCalendar.current , dateHigh = (_.isNumber(end)&&!_.isNaN(end))?end:begin; if(dateHigh`
${inner}
`, contentWrapper = (inner)=>`
${inner}
`, header=`
Imperial Calendar
`, bData=dayToDate(begin); // details is a list if(opts.details){ if(opts.sparse) { range=_.keys(notes); } message=_.map(range,(d)=>formatDayFull(d,notes[d],opts)).join(''); } else { let years={}; message = _.chain(range) .reduce((years,d)=>{ let day=dayToDate(d), pad=false; if(!years[day.year]){ pad=true; years[day.year]=(years[day.year]||{ year:day.year, weeks:[], accum:[] }); } if(1 === day.doy){ years[day.year].weeks.push(createHolidayWeek(d,notes[d],opts)); } else { if(pad && day.dow){ _.times(day.dow,() => years[day.year].accum.push(formatDayEmpty())); } years[day.year].accum.push(formatDaySmall(d,notes[d],opts)); if(7===years[day.year].accum.length){ years[day.year].weeks.push(formatWeek(years[day.year].accum)); years[day.year].accum=[]; } } return years; },{}) .map((y)=>{ if(y.accum.length){ _.times(7-y.accum.length,()=>y.accum.push(formatDayEmpty())); y.weeks.push(formatWeek(y.accum)); y.accum=[]; } return formatYear(y); }) .value() .join(''); } outputChat(`${preface} ${mainWrapper(`${header} ${contentWrapper(message)}`)}`); }, handleInput = function(msg) { var args, who, msgDefs={ whisper:false, details:false }; if (msg.type !== "api") { return; } who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); args = msg.content.split(/\s+--/); switch(args.shift()) { case '!wcal': msgDefs.whisper=true; /* falls through */ case '!cal': if(_.contains(args,'help')){ showHelp(who); return; } args=parseArgs(args,msgDefs); args.who=who; args.whoID=msg.playerid; args.isGM=playerIsGM(msg.playerid); // change events _.each(args, (v,k)=>{ switch(k){ case 'set': if(args.isGM) { state.ImperialCalendar.current=v; args.show=_.sortBy(_.union((args.show||[]),[v]),_.identity); args.details=true; args.whisper=true; } break; case 'note': _.each(v,(n)=>{ n.owner=args.whoID; n.id=generateNoteID(); state.ImperialCalendar.notes[n.day]=state.ImperialCalendar.notes[n.day]||[]; state.ImperialCalendar.notes[n.day].push(n); args.show=_.sortBy(_.union((args.show||[]),[n.day]),_.identity); }); args.details=true; args.whisper=true; break; case 'remove-note': _.each(v,(n)=>{ if(_.has(state.ImperialCalendar.notes,n.day)){ if('all' === n.index) { if(args.isGM){ delete state.ImperialCalendar.notes[n.day]; args.show=_.sortBy(_.union((args.show||[]),[n.day]),_.identity); } else { let lenPre=state.ImperialCalendar.notes[n.day].length; _.each(_.reduce(state.ImperialCalendar.notes[n.day],(m,d,i)=>{ if(d.owner===args.whoID){ m.push(i); } return m; },[]).sort().reverse(),(idx)=>state.ImperialCalendar.notes[n.day].splice(idx,1)); if(lenPre !== state.ImperialCalendar.notes[n.day].length){ args.show=_.sortBy(_.union((args.show||[]),[n.day]),_.identity); } } } else if(_.has(n,'id')) { let note = _.find(state.ImperialCalendar.notes[n.day],(o)=>o.id===n.id ); if(note && (args.isGM || note.owner===args.whoID)){ state.ImperialCalendar.notes[n.day]=_.without(state.ImperialCalendar.notes[n.day],note); args.show=_.sortBy(_.union((args.show||[]),[n.day]),_.identity); } } else if(_.has(state.ImperialCalendar.notes[n.day],n.index) && (args.isGM || state.ImperialCalendar.notes[n.day][n.index].owner===args.whoID) ) { state.ImperialCalendar.notes[n.day].splice(n.index,1); args.show=_.sortBy(_.union((args.show||[]),[n.day]),_.identity); } } }); args.details=true; args.whisper=true; break; } }); if( !_.intersection(['show','show-week','show-4week','show-5week','show-year','between'],_.keys(args)).length ){ args.show=[state.ImperialCalendar.current]; args.details=true; } // display events _.each(args, (v,k)=>{ switch(k){ case 'show': args.details=true; showDays(v,args); break; case 'show-week': case 'show-year': case 'show-4week': case 'show-5week': case 'between': showDays(v[0],v[1],args); break; } }); break; case '!cal-config': if(!playerIsGM(msg.playerid)){ return; } if(_.contains(args,'help')){ showHelp(who); return; } if(!args.length) { sendChat('','/w gm '+ '
'+ '
'+ 'ImperialCalendar v'+version+ '
'+ getAllConfigOptions()+ '
' ); return; } _.each(args,function(a){ var opt=a.split(/\|/), omsg=''; switch(opt.shift()) { case 'toggle-tool-tips': state.ImperialCalendar.config.showToolTip = !state.ImperialCalendar.config.showToolTip; sendChat('','/w gm '+ '
'+ getConfigOption_ShowToolTip()+ '
' ); break; case 'set-day-name': if('H'===opt[0]){ state.ImperialCalendar.config.holidayName=opt[1]; } else { let key=parseInt(opt[0],10); if(_.contains(_.keys(state.ImperialCalendar.config.dayNames),key)){ state.ImperialCalendar.config.dayNames[key]=opt[1]; } } sendChat('','/w gm '+ '
'+ getConfigOption_DayNames()+ '
' ); break; } }); // Commands // Settings // Weekday names break; } }, registerEventHandlers = function() { on('chat:message', handleInput); }; return { CheckInstall: checkInstall, RegisterEventHandlers: registerEventHandlers }; }()); on('ready',function() { 'use strict'; ImperialCalendar.CheckInstall(); ImperialCalendar.RegisterEventHandlers(); });