// 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)=>`${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)=>`
`;
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 '';
},
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
'+
'
'+
'!cal --set|<date> (GM Only) -- Use --set to adjust the current day. The current day is what all relative days are calcualted from and is highlighted yellow in the display. Anywhere you can omit the day in a command, the current day is used instead. If you specify an invalid day, the current day is used.'+
' '+
'!cal --set|023-1116
'+
'!cal --set|+1w
'+
'
'+
'
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.
'+
'
'+
'- Note: This is the only command that will treat a bare number as a year.
!cal --show-year|1116 is equivolent to !cal --show-year|001-1116. '+
'
'+
'
--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
'+
'
'+
'!cal --show -- show'+ch("'")+'s the current date. '+
'!cal --show|+1d -- show'+ch("'")+'s tomorrow. '+
'!cal --show|-1w -- show'+ch("'")+'s a week ago. '+
'!cal --show|1-1116|10-1116|16-1116 -- show'+ch("'")+'s the 1st, 10th, and 16th days of the year 1116. '+
'!cal --show-week -- show'+ch("'")+'s the current week. '+
'!cal --show-week|+w -- show'+ch("'")+'s next week. '+
'!cal --show-year -- show'+ch("'")+'s the current year. '+
'!cal --between|023-1116|043-1116 -- shows all the days from the 23rd to the 43rd of the year 1116. '+
'!cal --between|023-1116|043-1116 --details --sparse -- Shows the days with notes from the 23rd to the 43rd of the year 1116. '+
'
'+
'
Whisper Examples
'+
'
'+
'!wcal --show -- Whisper the current day. Whispering is the only time that notes other than public notes are displayed. '+
'!cal --show --whisper -- Idential to the above. '+
'
'+
'
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
'+
'
'+
'!cal --note|all|Hi everyone -- adds a note on the current day for all players. '+
'!cal --note|Secret note. -- adds a note for just the GM. '+
'!cal --note|tom|Tom'+ch("'")+'s note. -- adds a note for all players with tom in there name (ignoreing spaces and capitalization). '+
'!cal --note|sally|tom|bob|Small group note. -- adds a note for 3 or more players. '+
'!cal --note|10-1116|Bomb goes off. -- adds a GM note on the 10th of the year 1116. '+
'!cal --note|10-1116|all|A good day to be off planet. -- adds a note for all players on the 10th of the year 1116. '+
'!cal --remove-note|0 -- removes the first note on the current day. '+
'!cal --remove-note|all -- removes all the notes on the current day. '+
'
'+
'
'+
'
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 ``;
},
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 ``;
},
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();
});