Skip to content

Commit 836dda7

Browse files
shdwjkclevett
authored andcommittedSep 3, 2019
GMAura -- adds GM-only auras. Can I get this in 1-click please? (Roll20#856)
* Updated 1 Scripts * Updated 1 Scripts
1 parent 4ddc53a commit 836dda7

File tree

3 files changed

+1153
-0
lines changed

3 files changed

+1153
-0
lines changed
 

‎GMAura/0.1.0/GMAura.js

+568
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,568 @@
1+
// Github: https://github.com/shdwjk/Roll20API/blob/master/GMAura/GMAura.js
2+
// By: The Aaron, Arcane Scriptomancer
3+
// Contact: https://app.roll20.net/users/104025/the-aaron
4+
5+
on('ready',()=>{
6+
7+
const version = '0.1.0';
8+
const lastUpdate = 1567481378;
9+
const schemaVersion = 0.1;
10+
const clearURL = 'https://s3.amazonaws.com/files.d20.io/images/4277467/iQYjFOsYC5JsuOPUCI9RGA/thumb.png?1401938659';
11+
const regex = {
12+
color : {
13+
ops: '([*=+\\-!])?',
14+
transparent: '(transparent)',
15+
html: '#?((?:[0-9a-f]{6})|(?:[0-9a-f]{3}))',
16+
rgb: '(rgb\\(\\s*(?:(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)|(?:\\d+)\\s*,\\s*(?:\\d+)\\s*,\\s*(?:\\d+))\\s*\\))',
17+
hsv: '(hsv\\(\\s*(?:(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)|(?:\\d+)\\s*,\\s*(?:\\d+)\\s*,\\s*(?:\\d+))\\s*\\))'
18+
}
19+
};
20+
const colorReg = new RegExp(`^(?:${regex.color.transparent}|${regex.color.html}|${regex.color.rgb}|${regex.color.hsv})$`,'i');
21+
const colorParams = /\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*\)/;
22+
23+
let auraLookup = {};
24+
25+
26+
const checkInstall = () => {
27+
log('-=> GMAura v'+version+' <=- ['+(new Date(lastUpdate*1000))+']');
28+
29+
if( ! _.has(state,'GMAura') || state.GMAura.version !== schemaVersion) {
30+
log(' > Updating Schema to v'+schemaVersion+' <');
31+
switch(state.GMAura && state.GMAura.version) {
32+
case 0.0:
33+
case 'UpdateSchemaVersion':
34+
state.GMAura.version = schemaVersion;
35+
break;
36+
default:
37+
state.GMAura = {
38+
version: schemaVersion,
39+
lookup: { }
40+
};
41+
break;
42+
}
43+
}
44+
45+
let cleanup = [];
46+
let keys = Object.keys(state.GMAura.lookup);
47+
const burndown = () => {
48+
if(keys.length){
49+
let key = keys.shift();
50+
let g = getObj('graphic',key);
51+
if(g){
52+
state.GMAura.lookup[key].forEach( id => auraLookup[id]=key);
53+
handleTokenChange(g,{left:false,top:false,width:false,height:false});
54+
} else {
55+
cleanup.push(key);
56+
}
57+
setTimeout(burndown,0);
58+
} else {
59+
cleanup.forEach(id => delete state.GMAura.lookup[id]);
60+
}
61+
};
62+
burndown();
63+
};
64+
65+
const processInlinerolls = (msg) => {
66+
if(_.has(msg,'inlinerolls')){
67+
return _.chain(msg.inlinerolls)
68+
.reduce(function(m,v,k){
69+
let ti=_.reduce(v.results.rolls,function(m2,v2){
70+
if(_.has(v2,'table')){
71+
m2.push(_.reduce(v2.results,function(m3,v3){
72+
m3.push(v3.tableItem.name);
73+
return m3;
74+
},[]).join(', '));
75+
}
76+
return m2;
77+
},[]).join(', ');
78+
m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
79+
return m;
80+
},{})
81+
.reduce(function(m,v,k){
82+
return m.replace(k,v);
83+
},msg.content)
84+
.value();
85+
} else {
86+
return msg.content;
87+
}
88+
};
89+
90+
class Color {
91+
static hsv2rgb(h, s, v) {
92+
let r, g, b;
93+
94+
let i = Math.floor(h * 6);
95+
let f = h * 6 - i;
96+
let p = v * (1 - s);
97+
let q = v * (1 - f * s);
98+
let t = v * (1 - (1 - f) * s);
99+
100+
switch (i % 6) {
101+
case 0: r = v, g = t, b = p; break;
102+
case 1: r = q, g = v, b = p; break;
103+
case 2: r = p, g = v, b = t; break;
104+
case 3: r = p, g = q, b = v; break;
105+
case 4: r = t, g = p, b = v; break;
106+
case 5: r = v, g = p, b = q; break;
107+
}
108+
109+
return { r , g , b };
110+
}
111+
112+
static rgb2hsv(r,g,b) {
113+
let max = Math.max(r, g, b),
114+
min = Math.min(r, g, b);
115+
let h, s, v = max;
116+
117+
let d = max - min;
118+
s = max == 0 ? 0 : d / max;
119+
120+
if (max == min) {
121+
h = 0; // achromatic
122+
} else {
123+
switch (max) {
124+
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
125+
case g: h = (b - r) / d + 2; break;
126+
case b: h = (r - g) / d + 4; break;
127+
}
128+
129+
h /= 6;
130+
}
131+
132+
return { h, s, v };
133+
}
134+
135+
static dec2hex (n){
136+
n = (Math.max(Math.min(Math.round(n*255),255), 0)||0);
137+
return `${n<16?'0':''}${n.toString(16)}`;
138+
}
139+
140+
static hex2dec (n){
141+
return Math.max(Math.min(parseInt(n,16),255), 0)/255;
142+
}
143+
144+
static html2rgb(htmlstring){
145+
let s=htmlstring.toLowerCase().replace(/[^0-9a-f]/,'');
146+
if(3===s.length){
147+
s=`${s[0]}${s[0]}${s[1]}${s[1]}${s[2]}${s[2]}`;
148+
}
149+
return {
150+
r: this.hex2dec(s.substr(0,2)),
151+
g: this.hex2dec(s.substr(2,2)),
152+
b: this.hex2dec(s.substr(4,2))
153+
};
154+
}
155+
156+
static parseRGBParam(p){
157+
if(/\./.test(p)){
158+
return parseFloat(p);
159+
}
160+
return parseInt(p,10)/255;
161+
}
162+
static parseHSVParam(p,f){
163+
if(/\./.test(p)){
164+
return parseFloat(p);
165+
}
166+
switch(f){
167+
case 'h':
168+
return parseInt(p,10)/360;
169+
case 's':
170+
case 'v':
171+
return parseInt(p,10)/100;
172+
}
173+
}
174+
175+
static parseColor(input){
176+
return Color.buildColor(input.toLowerCase().match(colorReg));
177+
}
178+
static buildColor(parsed){
179+
const idx = {
180+
transparent: 1,
181+
html: 2,
182+
rgb: 3,
183+
hsv: 4
184+
};
185+
186+
if(parsed){
187+
let c = new Color();
188+
if(parsed[idx.transparent]){
189+
c.type = 'transparent';
190+
} else if(parsed[idx.html]){
191+
c.type = 'rgb';
192+
_.each(Color.html2rgb(parsed[idx.html]),(v,k)=>{
193+
c[k]=v;
194+
});
195+
} else if(parsed[idx.rgb]){
196+
c.type = 'rgb';
197+
let params = parsed[idx.rgb].match(colorParams);
198+
c.r= Color.parseRGBParam(params[1]);
199+
c.g= Color.parseRGBParam(params[2]);
200+
c.b= Color.parseRGBParam(params[3]);
201+
} else if(parsed[idx.hsv]){
202+
c.type = 'hsv';
203+
let params = parsed[idx.hsv].match(colorParams);
204+
c.h= Color.parseHSVParam(params[1],'h');
205+
c.s= Color.parseHSVParam(params[2],'s');
206+
c.v= Color.parseHSVParam(params[3],'v');
207+
}
208+
return c;
209+
}
210+
return new Color();
211+
}
212+
213+
constructor(){
214+
this.type='transparent';
215+
}
216+
217+
clone(){
218+
return Object.assign(new Color(), this);
219+
}
220+
221+
toRGB(){
222+
if('hsv'===this.type){
223+
_.each(Color.hsv2rgb(this.h,this.s,this.v),(v,k)=>{
224+
this[k]=v;
225+
});
226+
this.type='rgb';
227+
} else if ('transparent' === this.type){
228+
this.type='rgb';
229+
this.r=0.0;
230+
this.g=0.0;
231+
this.b=0.0;
232+
}
233+
delete this.h;
234+
delete this.s;
235+
delete this.v;
236+
return this;
237+
}
238+
239+
toHSV(){
240+
if('rgb'===this.type){
241+
_.each(Color.rgb2hsv(this.r,this.g,this.b),(v,k)=>{
242+
this[k]=v;
243+
});
244+
this.type='hsv';
245+
} else if('transparent' === this.type){
246+
this.type='hsv';
247+
this.h=0.0;
248+
this.s=0.0;
249+
this.v=0.0;
250+
}
251+
252+
delete this.r;
253+
delete this.g;
254+
delete this.b;
255+
256+
return this;
257+
}
258+
259+
toHTML(){
260+
switch(this.type){
261+
case 'transparent':
262+
return 'transparent';
263+
case 'hsv': {
264+
return this.clone().toRGB().toHTML();
265+
}
266+
case 'rgb':
267+
return `#${Color.dec2hex(this.r)}${Color.dec2hex(this.g)}${Color.dec2hex(this.b)}`;
268+
}
269+
}
270+
}
271+
272+
const parseColor = (color) => {
273+
let c = Color.parseColor(color).toHTML();
274+
return 'transparent' === c ? '#ff00ff' : c;
275+
};
276+
277+
const AddAura = (token, options) => {
278+
let a = createObj('graphic',
279+
Object.assign({
280+
imgsrc: clearURL,
281+
layer: 'gmlayer',
282+
pageid: token.get('pageid'),
283+
name: '',
284+
showname: false,
285+
width: token.get('width'),
286+
height: token.get('height'),
287+
top: token.get('top'),
288+
left: token.get('left'),
289+
showplayers_name: false,
290+
showplayers_bar1: false,
291+
showplayers_bar2: false,
292+
showplayers_bar3: false,
293+
showplayers_aura1: false,
294+
showplayers_aura2: false,
295+
playersedit_name: true,
296+
playersedit_bar1: true,
297+
playersedit_bar2: true,
298+
playersedit_bar3: true,
299+
playersedit_aura1: true,
300+
playersedit_aura2: true
301+
},options)
302+
);
303+
state.GMAura.lookup[token.id] = state.GMAura.lookup[token.id] || [];
304+
state.GMAura.lookup[token.id].push(a.id);
305+
auraLookup[a.id] = token.id;
306+
};
307+
308+
const handleTokenChange = (obj, prev) => {
309+
if(state.GMAura.lookup.hasOwnProperty(obj.id)){
310+
let changes = {};
311+
let width = obj.get('width');
312+
let height = obj.get('height');
313+
let top = obj.get('top');
314+
let left = obj.get('left');
315+
316+
if(width != prev.width) { changes.width = width; }
317+
if(height != prev.height) { changes.height = height; }
318+
if(top != prev.top) { changes.top = top; }
319+
if(left != prev.left) { changes.left = left; }
320+
321+
if(Object.keys(changes).length){
322+
state.GMAura.lookup[obj.id]
323+
.map( id => getObj('graphic',id))
324+
.filter( g => undefined !== g)
325+
.forEach( g => g.set(changes))
326+
;
327+
}
328+
} else if(auraLookup.hasOwnProperty(obj.id)){
329+
let changes = {};
330+
let width = obj.get('width');
331+
let height = obj.get('height');
332+
let top = obj.get('top');
333+
let left = obj.get('left');
334+
335+
if(width != prev.width) { changes.width = prev.width; }
336+
if(height != prev.height) { changes.height = prev.height; }
337+
if(top != prev.top) { changes.top = prev.top; }
338+
if(left != prev.left) { changes.left = prev.left; }
339+
340+
if(Object.keys(changes).length){
341+
obj.set(changes);
342+
}
343+
}
344+
};
345+
346+
const handleTokenRemove = (obj) => {
347+
if(state.GMAura.lookup.hasOwnProperty(obj.id)){
348+
state.GMAura.lookup[obj.id]
349+
.map( id => getObj('graphic',id))
350+
.filter( g => undefined !== g)
351+
.forEach( g => {
352+
delete auraLookup[g.id];
353+
g.remove();
354+
});
355+
delete state.GMAura.lookup[obj.id];
356+
} else if(auraLookup.hasOwnProperty(obj.id)){
357+
let tid = auraLookup[obj.id];
358+
state.GMAura.lookup[tid] = state.GMAura.lookup[tid].filter(id=>id !== obj.id);
359+
if(0 === state.GMAura.lookup[tid].length){
360+
delete state.GMAura.lookup[tid];
361+
}
362+
}
363+
};
364+
365+
const ch = (c) => {
366+
const entities = {
367+
'<' : 'lt',
368+
'>' : 'gt',
369+
"'" : '#39',
370+
'@' : '#64',
371+
'{' : '#123',
372+
'|' : '#124',
373+
'}' : '#125',
374+
'[' : '#91',
375+
']' : '#93',
376+
'"' : 'quot',
377+
'*' : 'ast',
378+
'/' : 'sol',
379+
' ' : 'nbsp'
380+
};
381+
382+
if( entities.hasOwnProperty(c) ){
383+
return `&${entities[c]};`;
384+
}
385+
return '';
386+
};
387+
388+
const _h = {
389+
outer: (...o) => `<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">${o.join(' ')}</div>`,
390+
title: (t,v) => `<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 130%;">${t} v${v}</div>`,
391+
subhead: (...o) => `<b>${o.join(' ')}</b>`,
392+
minorhead: (...o) => `<u>${o.join(' ')}</u>`,
393+
optional: (...o) => `${ch('[')}${o.join(` ${ch('|')} `)}${ch(']')}`,
394+
required: (...o) => `${ch('<')}${o.join(` ${ch('|')} `)}${ch('>')}`,
395+
header: (...o) => `<div style="padding-left:10px;margin-bottom:3px;">${o.join(' ')}</div>`,
396+
section: (s,...o) => `${_h.subhead(s)}${_h.inset(...o)}`,
397+
paragraph: (...o) => `<p>${o.join(' ')}</p>`,
398+
items: (o) => `<li>${o.join('</li><li>')}</li>`,
399+
ol: (...o) => `<ol>${_h.items(o)}</ol>`,
400+
ul: (...o) => `<ul>${_h.items(o)}</ul>`,
401+
grid: (...o) => `<div style="padding: 12px 0;">${o.join('')}<div style="clear:both;"></div></div>`,
402+
cell: (o) => `<div style="width: 130px; padding: 0 3px; float: left;">${o}</div>`,
403+
inset: (...o) => `<div style="padding-left: 10px;padding-right:20px">${o.join(' ')}</div>`,
404+
pre: (...o) =>`<div style="border:1px solid #e1e1e8;border-radius:4px;padding:8.5px;margin-bottom:9px;font-size:12px;white-space:normal;word-break:normal;word-wrap:normal;background-color:#f7f7f9;font-family:monospace;overflow:auto;">${o.join(' ')}</div>`,
405+
preformatted: (...o) =>_h.pre(o.join('<br>').replace(/\s/g,ch(' '))),
406+
code: (...o) => `<code>${o.join(' ')}</code>`,
407+
attr: {
408+
bare: (o)=>`${ch('@')}${ch('{')}${o}${ch('}')}`,
409+
selected: (o)=>`${ch('@')}${ch('{')}selected${ch('|')}${o}${ch('}')}`,
410+
target: (o)=>`${ch('@')}${ch('{')}target${ch('|')}${o}${ch('}')}`,
411+
char: (o,c)=>`${ch('@')}${ch('{')}${c||'CHARACTER NAME'}${ch('|')}${o}${ch('}')}`
412+
},
413+
bold: (...o) => `<b>${o.join(' ')}</b>`,
414+
italic: (...o) => `<i>${o.join(' ')}</i>`,
415+
font: {
416+
command: (...o)=>`<b><span style="font-family:serif;">${o.join(' ')}</span></b>`
417+
}
418+
};
419+
420+
const showHelp = (who) =>{
421+
let msg = _h.outer(
422+
_h.title('GMAura',version),
423+
_h.header(
424+
_h.paragraph('GMAura creates gm-only auras on selected or specified tokens.'),
425+
_h.paragraph(`Auras are created as invisible tokens on the GM layer with their aura 1 set up with the specified options. Changes to the location and size of the original token will be mimicked by the aura tokens. Changes to the location and size of the aura tokens will be reverted to keep them synchronized to the object layer token. To remove an aura, simply delete its gm layer token. Removal of the object layer token will cause the aura tokens to be cleaned up.`)
426+
),
427+
_h.subhead('Commands'),
428+
_h.inset(
429+
_h.font.command(
430+
`!gm-aura`,
431+
`--help`
432+
),
433+
_h.paragraph('Show this help.')
434+
),
435+
_h.inset(
436+
_h.font.command(
437+
`!gm-aura`,
438+
_h.optional(
439+
`--color ${_h.required('color')}`,
440+
`--c ${_h.required('color')}`
441+
),
442+
_h.optional(
443+
`--radius ${_h.required('radius')}`,
444+
`--r ${_h.required('radius')}`
445+
),
446+
_h.optional(
447+
`--square`,
448+
`--s`
449+
),
450+
_h.optional(
451+
`--ids ${_h.required(`token id`)} ${_h.optional(`token id ...`)}`
452+
)
453+
),
454+
_h.paragraph('This command creates an invisible ura token on the GM layer for each selected or specified token.'),
455+
_h.ul(
456+
`${_h.bold(`--color ${_h.required('color')}`)} -- Sets the color of the created aura. (Default: ${_h.code('#ff00ff')}) Any color format that works for TokenMod works (except ${_h.code('transparent')}).`,
457+
`${_h.bold(`--c ${_h.required('color')}`)} -- Shorthand for ${_h.bold('--color')}.`,
458+
`${_h.bold(`--radius ${_h.required('radius')}`)} -- Sets the radius of the created aura. The number is relative to the page just like regular auras, usually 5 for one square. (Default: ${_h.code('0')}, or just on the token.)`,
459+
`${_h.bold(`--r ${_h.required('radius')}`)} -- Shorthand for ${_h.bold('--radius')}.`,
460+
`${_h.bold(`--square`)} -- Sets the created aura to be square. (Default: ${_h.code('round')})`,
461+
`${_h.bold(`--s`)} -- Shorthand for ${_h.bold('--square')}.`,
462+
`${_h.bold(`--ids ${_h.required(`token id`)} ${_h.optional(`token id ...`)}`)} -- a list of token ids to create auras for.`
463+
),
464+
_h.inset(
465+
_h.preformatted(
466+
`!gm-aura --color #ff0000 --radius 10 --square`
467+
)
468+
),
469+
_h.paragraph(`${_h.bold('Note:')} You can create multi-line commands by enclosing the arguments after ${_h.code('!gm-aura')} in ${_h.code('{{')} and ${_h.code('}}')}.`),
470+
_h.inset(
471+
_h.preformatted(
472+
`!gm-aura {{`,
473+
` --c rgb(.7,.7,.3)`,
474+
` --r 15`,
475+
`}}`
476+
)
477+
),
478+
_h.paragraph(`${_h.bold('Note:')} You can use inline rolls as part of your command`),
479+
_h.inset(
480+
_h.preformatted(
481+
`!gm-aura --radius ${ch('[')}${ch('[')}1d3*5${ch(']')}${ch(']')}`
482+
)
483+
)
484+
),
485+
_h.subhead('Colors'),
486+
_h.inset(
487+
_h.paragraph(`Colors can be specified in multiple formats:`),
488+
_h.inset(
489+
_h.ul(
490+
`${_h.bold('HTML Color')} -- This is 6 or 3 hexadecimal digits, optionally prefaced by ${_h.code('#')}. Digits in a 3 digit hexadecimal color are doubled. All of the following are the same: ${_h.code('#ff00aa')}, ${_h.code('#f0a')}, ${_h.code('ff00aa')}, ${_h.code('f0a')}`,
491+
`${_h.bold('RGB Color')} -- This is an RGB color in the format ${_h.code('rgb(1.0,1.0,1.0)')} or ${_h.code('rgb(256,256,256)')}. Decimal numbers are in the scale of 0.0 to 1.0, integer numbers are scaled 0 to 256. Note that numbers can be outside this range for the purpose of doing math.`,
492+
`${_h.bold('HSV Color')} -- This is an HSV color in the format ${_h.code('hsv(1.0,1.0,1.0)')} or ${_h.code('hsv(360,100,100)')}. Decimal numbers are in the scale of 0.0 to 1.0, integer numbers are scaled 0 to 360 for the hue and 0 to 100 for saturation and value. Note that numbers can be outside this range for the purpose of doing math.`
493+
)
494+
)
495+
)
496+
497+
);
498+
sendChat('',`/w "${who}" ${msg}`);
499+
};
500+
501+
/*
502+
* !gm-aura
503+
* !gm-aura --color #ff0000
504+
* !gm-aura --color #ff0000 -- radius 5
505+
* !gm-aura --color #ff0000 -- radius 5 --square
506+
* !gm-aura --color #ff0000 -- radius 5 --square --ids foo bar baz
507+
*/
508+
on('chat:message', msg => {
509+
if('api' === msg.type && /^!gm-aura(\b|$)/i.test(msg.content) && playerIsGM(msg.playerid) ){
510+
let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
511+
512+
let args = processInlinerolls(msg)
513+
.replace(/<br\/>\n/g, ' ')
514+
.replace(/(\{\{(.*?)\}\})/g," $2 ")
515+
.split(/\s+--/);
516+
517+
if(args.filter(a => /^help/i.test(a)).length){
518+
showHelp(who);
519+
return;
520+
}
521+
522+
let ids = (msg.selected||[]).map(o=>o._id);
523+
524+
let opts = {
525+
aura1_color: '#ff00ff',
526+
aura1_radius: 0.0001,
527+
aura1_square: false
528+
};
529+
530+
args.forEach( arg => {
531+
let cmds = arg.split(/\s+/);
532+
switch(cmds.shift()){
533+
case 'c':
534+
case 'color':
535+
opts.aura1_color = parseColor(cmds[0]);
536+
break;
537+
538+
case 'r':
539+
case 'radius':
540+
opts.aura1_radius = parseInt(cmds[0]) || 0.00001;
541+
break;
542+
543+
case 's':
544+
case 'square':
545+
opts.aura1_square = true;
546+
break;
547+
548+
case 'ids':
549+
ids = [...ids,...cmds];
550+
break;
551+
}
552+
});
553+
554+
[...new Set(ids)]
555+
.map(id=>getObj('graphic',id))
556+
.filter(g=>undefined !== g)
557+
.forEach( t => AddAura(t,opts))
558+
;
559+
}
560+
});
561+
562+
563+
564+
checkInstall();
565+
on('change:graphic', handleTokenChange);
566+
on('destroy:graphic', handleTokenRemove);
567+
568+
});

‎GMAura/GMAura.js

+568
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,568 @@
1+
// Github: https://github.com/shdwjk/Roll20API/blob/master/GMAura/GMAura.js
2+
// By: The Aaron, Arcane Scriptomancer
3+
// Contact: https://app.roll20.net/users/104025/the-aaron
4+
5+
on('ready',()=>{
6+
7+
const version = '0.1.0';
8+
const lastUpdate = 1567481378;
9+
const schemaVersion = 0.1;
10+
const clearURL = 'https://s3.amazonaws.com/files.d20.io/images/4277467/iQYjFOsYC5JsuOPUCI9RGA/thumb.png?1401938659';
11+
const regex = {
12+
color : {
13+
ops: '([*=+\\-!])?',
14+
transparent: '(transparent)',
15+
html: '#?((?:[0-9a-f]{6})|(?:[0-9a-f]{3}))',
16+
rgb: '(rgb\\(\\s*(?:(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)|(?:\\d+)\\s*,\\s*(?:\\d+)\\s*,\\s*(?:\\d+))\\s*\\))',
17+
hsv: '(hsv\\(\\s*(?:(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)\\s*,\\s*(?:\\d*\\.\\d+)|(?:\\d+)\\s*,\\s*(?:\\d+)\\s*,\\s*(?:\\d+))\\s*\\))'
18+
}
19+
};
20+
const colorReg = new RegExp(`^(?:${regex.color.transparent}|${regex.color.html}|${regex.color.rgb}|${regex.color.hsv})$`,'i');
21+
const colorParams = /\(\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*,\s*(\d*\.?\d+)\s*\)/;
22+
23+
let auraLookup = {};
24+
25+
26+
const checkInstall = () => {
27+
log('-=> GMAura v'+version+' <=- ['+(new Date(lastUpdate*1000))+']');
28+
29+
if( ! _.has(state,'GMAura') || state.GMAura.version !== schemaVersion) {
30+
log(' > Updating Schema to v'+schemaVersion+' <');
31+
switch(state.GMAura && state.GMAura.version) {
32+
case 0.0:
33+
case 'UpdateSchemaVersion':
34+
state.GMAura.version = schemaVersion;
35+
break;
36+
default:
37+
state.GMAura = {
38+
version: schemaVersion,
39+
lookup: { }
40+
};
41+
break;
42+
}
43+
}
44+
45+
let cleanup = [];
46+
let keys = Object.keys(state.GMAura.lookup);
47+
const burndown = () => {
48+
if(keys.length){
49+
let key = keys.shift();
50+
let g = getObj('graphic',key);
51+
if(g){
52+
state.GMAura.lookup[key].forEach( id => auraLookup[id]=key);
53+
handleTokenChange(g,{left:false,top:false,width:false,height:false});
54+
} else {
55+
cleanup.push(key);
56+
}
57+
setTimeout(burndown,0);
58+
} else {
59+
cleanup.forEach(id => delete state.GMAura.lookup[id]);
60+
}
61+
};
62+
burndown();
63+
};
64+
65+
const processInlinerolls = (msg) => {
66+
if(_.has(msg,'inlinerolls')){
67+
return _.chain(msg.inlinerolls)
68+
.reduce(function(m,v,k){
69+
let ti=_.reduce(v.results.rolls,function(m2,v2){
70+
if(_.has(v2,'table')){
71+
m2.push(_.reduce(v2.results,function(m3,v3){
72+
m3.push(v3.tableItem.name);
73+
return m3;
74+
},[]).join(', '));
75+
}
76+
return m2;
77+
},[]).join(', ');
78+
m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0;
79+
return m;
80+
},{})
81+
.reduce(function(m,v,k){
82+
return m.replace(k,v);
83+
},msg.content)
84+
.value();
85+
} else {
86+
return msg.content;
87+
}
88+
};
89+
90+
class Color {
91+
static hsv2rgb(h, s, v) {
92+
let r, g, b;
93+
94+
let i = Math.floor(h * 6);
95+
let f = h * 6 - i;
96+
let p = v * (1 - s);
97+
let q = v * (1 - f * s);
98+
let t = v * (1 - (1 - f) * s);
99+
100+
switch (i % 6) {
101+
case 0: r = v, g = t, b = p; break;
102+
case 1: r = q, g = v, b = p; break;
103+
case 2: r = p, g = v, b = t; break;
104+
case 3: r = p, g = q, b = v; break;
105+
case 4: r = t, g = p, b = v; break;
106+
case 5: r = v, g = p, b = q; break;
107+
}
108+
109+
return { r , g , b };
110+
}
111+
112+
static rgb2hsv(r,g,b) {
113+
let max = Math.max(r, g, b),
114+
min = Math.min(r, g, b);
115+
let h, s, v = max;
116+
117+
let d = max - min;
118+
s = max == 0 ? 0 : d / max;
119+
120+
if (max == min) {
121+
h = 0; // achromatic
122+
} else {
123+
switch (max) {
124+
case r: h = (g - b) / d + (g < b ? 6 : 0); break;
125+
case g: h = (b - r) / d + 2; break;
126+
case b: h = (r - g) / d + 4; break;
127+
}
128+
129+
h /= 6;
130+
}
131+
132+
return { h, s, v };
133+
}
134+
135+
static dec2hex (n){
136+
n = (Math.max(Math.min(Math.round(n*255),255), 0)||0);
137+
return `${n<16?'0':''}${n.toString(16)}`;
138+
}
139+
140+
static hex2dec (n){
141+
return Math.max(Math.min(parseInt(n,16),255), 0)/255;
142+
}
143+
144+
static html2rgb(htmlstring){
145+
let s=htmlstring.toLowerCase().replace(/[^0-9a-f]/,'');
146+
if(3===s.length){
147+
s=`${s[0]}${s[0]}${s[1]}${s[1]}${s[2]}${s[2]}`;
148+
}
149+
return {
150+
r: this.hex2dec(s.substr(0,2)),
151+
g: this.hex2dec(s.substr(2,2)),
152+
b: this.hex2dec(s.substr(4,2))
153+
};
154+
}
155+
156+
static parseRGBParam(p){
157+
if(/\./.test(p)){
158+
return parseFloat(p);
159+
}
160+
return parseInt(p,10)/255;
161+
}
162+
static parseHSVParam(p,f){
163+
if(/\./.test(p)){
164+
return parseFloat(p);
165+
}
166+
switch(f){
167+
case 'h':
168+
return parseInt(p,10)/360;
169+
case 's':
170+
case 'v':
171+
return parseInt(p,10)/100;
172+
}
173+
}
174+
175+
static parseColor(input){
176+
return Color.buildColor(input.toLowerCase().match(colorReg));
177+
}
178+
static buildColor(parsed){
179+
const idx = {
180+
transparent: 1,
181+
html: 2,
182+
rgb: 3,
183+
hsv: 4
184+
};
185+
186+
if(parsed){
187+
let c = new Color();
188+
if(parsed[idx.transparent]){
189+
c.type = 'transparent';
190+
} else if(parsed[idx.html]){
191+
c.type = 'rgb';
192+
_.each(Color.html2rgb(parsed[idx.html]),(v,k)=>{
193+
c[k]=v;
194+
});
195+
} else if(parsed[idx.rgb]){
196+
c.type = 'rgb';
197+
let params = parsed[idx.rgb].match(colorParams);
198+
c.r= Color.parseRGBParam(params[1]);
199+
c.g= Color.parseRGBParam(params[2]);
200+
c.b= Color.parseRGBParam(params[3]);
201+
} else if(parsed[idx.hsv]){
202+
c.type = 'hsv';
203+
let params = parsed[idx.hsv].match(colorParams);
204+
c.h= Color.parseHSVParam(params[1],'h');
205+
c.s= Color.parseHSVParam(params[2],'s');
206+
c.v= Color.parseHSVParam(params[3],'v');
207+
}
208+
return c;
209+
}
210+
return new Color();
211+
}
212+
213+
constructor(){
214+
this.type='transparent';
215+
}
216+
217+
clone(){
218+
return Object.assign(new Color(), this);
219+
}
220+
221+
toRGB(){
222+
if('hsv'===this.type){
223+
_.each(Color.hsv2rgb(this.h,this.s,this.v),(v,k)=>{
224+
this[k]=v;
225+
});
226+
this.type='rgb';
227+
} else if ('transparent' === this.type){
228+
this.type='rgb';
229+
this.r=0.0;
230+
this.g=0.0;
231+
this.b=0.0;
232+
}
233+
delete this.h;
234+
delete this.s;
235+
delete this.v;
236+
return this;
237+
}
238+
239+
toHSV(){
240+
if('rgb'===this.type){
241+
_.each(Color.rgb2hsv(this.r,this.g,this.b),(v,k)=>{
242+
this[k]=v;
243+
});
244+
this.type='hsv';
245+
} else if('transparent' === this.type){
246+
this.type='hsv';
247+
this.h=0.0;
248+
this.s=0.0;
249+
this.v=0.0;
250+
}
251+
252+
delete this.r;
253+
delete this.g;
254+
delete this.b;
255+
256+
return this;
257+
}
258+
259+
toHTML(){
260+
switch(this.type){
261+
case 'transparent':
262+
return 'transparent';
263+
case 'hsv': {
264+
return this.clone().toRGB().toHTML();
265+
}
266+
case 'rgb':
267+
return `#${Color.dec2hex(this.r)}${Color.dec2hex(this.g)}${Color.dec2hex(this.b)}`;
268+
}
269+
}
270+
}
271+
272+
const parseColor = (color) => {
273+
let c = Color.parseColor(color).toHTML();
274+
return 'transparent' === c ? '#ff00ff' : c;
275+
};
276+
277+
const AddAura = (token, options) => {
278+
let a = createObj('graphic',
279+
Object.assign({
280+
imgsrc: clearURL,
281+
layer: 'gmlayer',
282+
pageid: token.get('pageid'),
283+
name: '',
284+
showname: false,
285+
width: token.get('width'),
286+
height: token.get('height'),
287+
top: token.get('top'),
288+
left: token.get('left'),
289+
showplayers_name: false,
290+
showplayers_bar1: false,
291+
showplayers_bar2: false,
292+
showplayers_bar3: false,
293+
showplayers_aura1: false,
294+
showplayers_aura2: false,
295+
playersedit_name: true,
296+
playersedit_bar1: true,
297+
playersedit_bar2: true,
298+
playersedit_bar3: true,
299+
playersedit_aura1: true,
300+
playersedit_aura2: true
301+
},options)
302+
);
303+
state.GMAura.lookup[token.id] = state.GMAura.lookup[token.id] || [];
304+
state.GMAura.lookup[token.id].push(a.id);
305+
auraLookup[a.id] = token.id;
306+
};
307+
308+
const handleTokenChange = (obj, prev) => {
309+
if(state.GMAura.lookup.hasOwnProperty(obj.id)){
310+
let changes = {};
311+
let width = obj.get('width');
312+
let height = obj.get('height');
313+
let top = obj.get('top');
314+
let left = obj.get('left');
315+
316+
if(width != prev.width) { changes.width = width; }
317+
if(height != prev.height) { changes.height = height; }
318+
if(top != prev.top) { changes.top = top; }
319+
if(left != prev.left) { changes.left = left; }
320+
321+
if(Object.keys(changes).length){
322+
state.GMAura.lookup[obj.id]
323+
.map( id => getObj('graphic',id))
324+
.filter( g => undefined !== g)
325+
.forEach( g => g.set(changes))
326+
;
327+
}
328+
} else if(auraLookup.hasOwnProperty(obj.id)){
329+
let changes = {};
330+
let width = obj.get('width');
331+
let height = obj.get('height');
332+
let top = obj.get('top');
333+
let left = obj.get('left');
334+
335+
if(width != prev.width) { changes.width = prev.width; }
336+
if(height != prev.height) { changes.height = prev.height; }
337+
if(top != prev.top) { changes.top = prev.top; }
338+
if(left != prev.left) { changes.left = prev.left; }
339+
340+
if(Object.keys(changes).length){
341+
obj.set(changes);
342+
}
343+
}
344+
};
345+
346+
const handleTokenRemove = (obj) => {
347+
if(state.GMAura.lookup.hasOwnProperty(obj.id)){
348+
state.GMAura.lookup[obj.id]
349+
.map( id => getObj('graphic',id))
350+
.filter( g => undefined !== g)
351+
.forEach( g => {
352+
delete auraLookup[g.id];
353+
g.remove();
354+
});
355+
delete state.GMAura.lookup[obj.id];
356+
} else if(auraLookup.hasOwnProperty(obj.id)){
357+
let tid = auraLookup[obj.id];
358+
state.GMAura.lookup[tid] = state.GMAura.lookup[tid].filter(id=>id !== obj.id);
359+
if(0 === state.GMAura.lookup[tid].length){
360+
delete state.GMAura.lookup[tid];
361+
}
362+
}
363+
};
364+
365+
const ch = (c) => {
366+
const entities = {
367+
'<' : 'lt',
368+
'>' : 'gt',
369+
"'" : '#39',
370+
'@' : '#64',
371+
'{' : '#123',
372+
'|' : '#124',
373+
'}' : '#125',
374+
'[' : '#91',
375+
']' : '#93',
376+
'"' : 'quot',
377+
'*' : 'ast',
378+
'/' : 'sol',
379+
' ' : 'nbsp'
380+
};
381+
382+
if( entities.hasOwnProperty(c) ){
383+
return `&${entities[c]};`;
384+
}
385+
return '';
386+
};
387+
388+
const _h = {
389+
outer: (...o) => `<div style="border: 1px solid black; background-color: white; padding: 3px 3px;">${o.join(' ')}</div>`,
390+
title: (t,v) => `<div style="font-weight: bold; border-bottom: 1px solid black;font-size: 130%;">${t} v${v}</div>`,
391+
subhead: (...o) => `<b>${o.join(' ')}</b>`,
392+
minorhead: (...o) => `<u>${o.join(' ')}</u>`,
393+
optional: (...o) => `${ch('[')}${o.join(` ${ch('|')} `)}${ch(']')}`,
394+
required: (...o) => `${ch('<')}${o.join(` ${ch('|')} `)}${ch('>')}`,
395+
header: (...o) => `<div style="padding-left:10px;margin-bottom:3px;">${o.join(' ')}</div>`,
396+
section: (s,...o) => `${_h.subhead(s)}${_h.inset(...o)}`,
397+
paragraph: (...o) => `<p>${o.join(' ')}</p>`,
398+
items: (o) => `<li>${o.join('</li><li>')}</li>`,
399+
ol: (...o) => `<ol>${_h.items(o)}</ol>`,
400+
ul: (...o) => `<ul>${_h.items(o)}</ul>`,
401+
grid: (...o) => `<div style="padding: 12px 0;">${o.join('')}<div style="clear:both;"></div></div>`,
402+
cell: (o) => `<div style="width: 130px; padding: 0 3px; float: left;">${o}</div>`,
403+
inset: (...o) => `<div style="padding-left: 10px;padding-right:20px">${o.join(' ')}</div>`,
404+
pre: (...o) =>`<div style="border:1px solid #e1e1e8;border-radius:4px;padding:8.5px;margin-bottom:9px;font-size:12px;white-space:normal;word-break:normal;word-wrap:normal;background-color:#f7f7f9;font-family:monospace;overflow:auto;">${o.join(' ')}</div>`,
405+
preformatted: (...o) =>_h.pre(o.join('<br>').replace(/\s/g,ch(' '))),
406+
code: (...o) => `<code>${o.join(' ')}</code>`,
407+
attr: {
408+
bare: (o)=>`${ch('@')}${ch('{')}${o}${ch('}')}`,
409+
selected: (o)=>`${ch('@')}${ch('{')}selected${ch('|')}${o}${ch('}')}`,
410+
target: (o)=>`${ch('@')}${ch('{')}target${ch('|')}${o}${ch('}')}`,
411+
char: (o,c)=>`${ch('@')}${ch('{')}${c||'CHARACTER NAME'}${ch('|')}${o}${ch('}')}`
412+
},
413+
bold: (...o) => `<b>${o.join(' ')}</b>`,
414+
italic: (...o) => `<i>${o.join(' ')}</i>`,
415+
font: {
416+
command: (...o)=>`<b><span style="font-family:serif;">${o.join(' ')}</span></b>`
417+
}
418+
};
419+
420+
const showHelp = (who) =>{
421+
let msg = _h.outer(
422+
_h.title('GMAura',version),
423+
_h.header(
424+
_h.paragraph('GMAura creates gm-only auras on selected or specified tokens.'),
425+
_h.paragraph(`Auras are created as invisible tokens on the GM layer with their aura 1 set up with the specified options. Changes to the location and size of the original token will be mimicked by the aura tokens. Changes to the location and size of the aura tokens will be reverted to keep them synchronized to the object layer token. To remove an aura, simply delete its gm layer token. Removal of the object layer token will cause the aura tokens to be cleaned up.`)
426+
),
427+
_h.subhead('Commands'),
428+
_h.inset(
429+
_h.font.command(
430+
`!gm-aura`,
431+
`--help`
432+
),
433+
_h.paragraph('Show this help.')
434+
),
435+
_h.inset(
436+
_h.font.command(
437+
`!gm-aura`,
438+
_h.optional(
439+
`--color ${_h.required('color')}`,
440+
`--c ${_h.required('color')}`
441+
),
442+
_h.optional(
443+
`--radius ${_h.required('radius')}`,
444+
`--r ${_h.required('radius')}`
445+
),
446+
_h.optional(
447+
`--square`,
448+
`--s`
449+
),
450+
_h.optional(
451+
`--ids ${_h.required(`token id`)} ${_h.optional(`token id ...`)}`
452+
)
453+
),
454+
_h.paragraph('This command creates an invisible ura token on the GM layer for each selected or specified token.'),
455+
_h.ul(
456+
`${_h.bold(`--color ${_h.required('color')}`)} -- Sets the color of the created aura. (Default: ${_h.code('#ff00ff')}) Any color format that works for TokenMod works (except ${_h.code('transparent')}).`,
457+
`${_h.bold(`--c ${_h.required('color')}`)} -- Shorthand for ${_h.bold('--color')}.`,
458+
`${_h.bold(`--radius ${_h.required('radius')}`)} -- Sets the radius of the created aura. The number is relative to the page just like regular auras, usually 5 for one square. (Default: ${_h.code('0')}, or just on the token.)`,
459+
`${_h.bold(`--r ${_h.required('radius')}`)} -- Shorthand for ${_h.bold('--radius')}.`,
460+
`${_h.bold(`--square`)} -- Sets the created aura to be square. (Default: ${_h.code('round')})`,
461+
`${_h.bold(`--s`)} -- Shorthand for ${_h.bold('--square')}.`,
462+
`${_h.bold(`--ids ${_h.required(`token id`)} ${_h.optional(`token id ...`)}`)} -- a list of token ids to create auras for.`
463+
),
464+
_h.inset(
465+
_h.preformatted(
466+
`!gm-aura --color #ff0000 --radius 10 --square`
467+
)
468+
),
469+
_h.paragraph(`${_h.bold('Note:')} You can create multi-line commands by enclosing the arguments after ${_h.code('!gm-aura')} in ${_h.code('{{')} and ${_h.code('}}')}.`),
470+
_h.inset(
471+
_h.preformatted(
472+
`!gm-aura {{`,
473+
` --c rgb(.7,.7,.3)`,
474+
` --r 15`,
475+
`}}`
476+
)
477+
),
478+
_h.paragraph(`${_h.bold('Note:')} You can use inline rolls as part of your command`),
479+
_h.inset(
480+
_h.preformatted(
481+
`!gm-aura --radius ${ch('[')}${ch('[')}1d3*5${ch(']')}${ch(']')}`
482+
)
483+
)
484+
),
485+
_h.subhead('Colors'),
486+
_h.inset(
487+
_h.paragraph(`Colors can be specified in multiple formats:`),
488+
_h.inset(
489+
_h.ul(
490+
`${_h.bold('HTML Color')} -- This is 6 or 3 hexadecimal digits, optionally prefaced by ${_h.code('#')}. Digits in a 3 digit hexadecimal color are doubled. All of the following are the same: ${_h.code('#ff00aa')}, ${_h.code('#f0a')}, ${_h.code('ff00aa')}, ${_h.code('f0a')}`,
491+
`${_h.bold('RGB Color')} -- This is an RGB color in the format ${_h.code('rgb(1.0,1.0,1.0)')} or ${_h.code('rgb(256,256,256)')}. Decimal numbers are in the scale of 0.0 to 1.0, integer numbers are scaled 0 to 256. Note that numbers can be outside this range for the purpose of doing math.`,
492+
`${_h.bold('HSV Color')} -- This is an HSV color in the format ${_h.code('hsv(1.0,1.0,1.0)')} or ${_h.code('hsv(360,100,100)')}. Decimal numbers are in the scale of 0.0 to 1.0, integer numbers are scaled 0 to 360 for the hue and 0 to 100 for saturation and value. Note that numbers can be outside this range for the purpose of doing math.`
493+
)
494+
)
495+
)
496+
497+
);
498+
sendChat('',`/w "${who}" ${msg}`);
499+
};
500+
501+
/*
502+
* !gm-aura
503+
* !gm-aura --color #ff0000
504+
* !gm-aura --color #ff0000 -- radius 5
505+
* !gm-aura --color #ff0000 -- radius 5 --square
506+
* !gm-aura --color #ff0000 -- radius 5 --square --ids foo bar baz
507+
*/
508+
on('chat:message', msg => {
509+
if('api' === msg.type && /^!gm-aura(\b|$)/i.test(msg.content) && playerIsGM(msg.playerid) ){
510+
let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname');
511+
512+
let args = processInlinerolls(msg)
513+
.replace(/<br\/>\n/g, ' ')
514+
.replace(/(\{\{(.*?)\}\})/g," $2 ")
515+
.split(/\s+--/);
516+
517+
if(args.filter(a => /^help/i.test(a)).length){
518+
showHelp(who);
519+
return;
520+
}
521+
522+
let ids = (msg.selected||[]).map(o=>o._id);
523+
524+
let opts = {
525+
aura1_color: '#ff00ff',
526+
aura1_radius: 0.0001,
527+
aura1_square: false
528+
};
529+
530+
args.forEach( arg => {
531+
let cmds = arg.split(/\s+/);
532+
switch(cmds.shift()){
533+
case 'c':
534+
case 'color':
535+
opts.aura1_color = parseColor(cmds[0]);
536+
break;
537+
538+
case 'r':
539+
case 'radius':
540+
opts.aura1_radius = parseInt(cmds[0]) || 0.00001;
541+
break;
542+
543+
case 's':
544+
case 'square':
545+
opts.aura1_square = true;
546+
break;
547+
548+
case 'ids':
549+
ids = [...ids,...cmds];
550+
break;
551+
}
552+
});
553+
554+
[...new Set(ids)]
555+
.map(id=>getObj('graphic',id))
556+
.filter(g=>undefined !== g)
557+
.forEach( t => AddAura(t,opts))
558+
;
559+
}
560+
});
561+
562+
563+
564+
checkInstall();
565+
on('change:graphic', handleTokenChange);
566+
on('destroy:graphic', handleTokenRemove);
567+
568+
});

‎GMAura/script.json

+17
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "GMAura",
3+
"script": "GMAura.js",
4+
"version": "0.1.0",
5+
"description": "GMAura creates gm-only auras on selected or specified tokens.\r\rAuras are created as invisible tokens on the GM layer with their aura 1 set up with the specified options. Changes to the location and size of the original token will be mimicked by the aura tokens. Changes to the location and size of the aura tokens will be reverted to keep them synchronized to the object layer token. To remove an aura, simply delete its gm layer token. Removal of the object layer token will cause the aura tokens to be cleaned up.\r\r## Commands\r\r```!gm-aura --help```\r\rShow this help.\r\r```!gm-aura [--color <color> | --c <color>] [--radius <radius> | --r <radius>] [--square | --s] [--ids <token id> [token id ...]]```\r\rThis command creates an invisible ura token on the GM layer for each selected or specified token.\r\r* `--color <color>` -- Sets the color of the created aura. (Default: `#ff00ff`) Any color format that works for TokenMod works (except `transparent`).\r* `--c <color>` -- Shorthand for `--color`.\r* `--radius <radius>` -- Sets the radius of the created aura. The number is relative to the page just like regular auras, usually 5 for one square. (Default: `0`, or just on the token.)\r* `--r <radius>` -- Shorthand for `--radius`.\r* `--square` -- Sets the created aura to be square. (Default: `round`)\r* `--s` -- Shorthand for `--square`.\r* `--ids <token id> [token id ...]` -- a list of token ids to create auras for.\r\r```!gm-aura --color #ff0000 --radius 10 --square ```\r\r**Note:** You can create multi-line commands by enclosing the arguments after !gm-aura in `{{` and `}}`.\r\r```\r!gm-aura {{\r --c rgb(.7,.7,.3)\r --r 15\r}}\r```\r\r**Note:** You can use inline rolls as part of your command\r\r```\r!gm-aura --radius [[1d3*5]]\r```\r\r### Colors\r\rColors can be specified in multiple formats:\r\r* **HTML Color** -- This is 6 or 3 hexidecimal digits, optionally prefaced by `#`. Digits in a 3 digit hexidecimal color are doubled. All of the following are the same: `#ff00aa`, `#f0a`, `ff00aa`, `f0a`\r* **RGB Color** -- This is an RGB color in the format `rgb(1.0,1.0,1.0)` or `rgb(256,256,256)`. Decimal numbers are in the scale of 0.0 to 1.0, integer numbers are scaled 0 to 256. Note that numbers can be outside this range for the purpose of doing math.\r* **HSV Color** -- This is an HSV color in the format `hsv(1.0,1.0,1.0)` or `hsv(360,100,100)`. Decimal numbers are in the scale of 0.0 to 1.0, integer numbers are scaled 0 to 360 for the hue and 0 to 100 for saturation and value. Note that numbers can be outside this range for the purpose of doing math.",
6+
"authors": "The Aaron",
7+
"roll20userid": "104025",
8+
"patreon": "https://www.patreon.com/shdwjk",
9+
"useroptions": {},
10+
"dependencies": {},
11+
"modifies": {
12+
"state.GMAura": "read,write",
13+
"graphic.*": "read,write"
14+
},
15+
"conflicts": [],
16+
"previousversions": []
17+
}

0 commit comments

Comments
 (0)
Please sign in to comment.