// Github: https://github.com/shdwjk/Roll20API/blob/master/TokenMod/TokenMod.js // By: The Aaron, Arcane Scriptomancer // Contact: https://app.roll20.net/users/104025/the-aaron var TokenMod = TokenMod || (function() { 'use strict'; var version = '0.8.23', lastUpdate = 1478805717, schemaVersion = 0.3, observers = { tokenChange: [] }, fields = { // booleans showname: {type: 'boolean'}, showplayers_name: {type: 'boolean'}, showplayers_bar1: {type: 'boolean'}, showplayers_bar2: {type: 'boolean'}, showplayers_bar3: {type: 'boolean'}, showplayers_aura1: {type: 'boolean'}, showplayers_aura2: {type: 'boolean'}, playersedit_name: {type: 'boolean'}, playersedit_bar1: {type: 'boolean'}, playersedit_bar2: {type: 'boolean'}, playersedit_bar3: {type: 'boolean'}, playersedit_aura1: {type: 'boolean'}, playersedit_aura2: {type: 'boolean'}, light_otherplayers: {type: 'boolean'}, light_hassight: {type: 'boolean'}, isdrawing: {type: 'boolean'}, flipv: {type: 'boolean'}, fliph: {type: 'boolean'}, aura1_square: {type: 'boolean'}, aura2_square: {type: 'boolean'}, // bounded by screen size left: {type: 'number', transform: 'screen'}, top: {type: 'number', transform: 'screen'}, width: {type: 'number', transform: 'screen'}, height: {type: 'number', transform: 'screen'}, // 360 degrees rotation: {type: 'degrees'}, light_angle: {type: 'circleSegment'}, light_losangle: {type: 'circleSegment'}, // distance light_radius: {type: 'numberBlank'}, light_dimradius: {type: 'numberBlank'}, light_multiplier: {type: 'numberBlank'}, aura1_radius: {type: 'numberBlank'}, aura2_radius: {type: 'numberBlank'}, // text or numbers bar1_value: {type: 'text'}, bar2_value: {type: 'text'}, bar3_value: {type: 'text'}, bar1_max: {type: 'text'}, bar2_max: {type: 'text'}, bar3_max: {type: 'text'}, bar1: {type: 'text'}, bar2: {type: 'text'}, bar3: {type: 'text'}, bar1_reset: {type: 'text'}, bar2_reset: {type: 'text'}, bar3_reset: {type: 'text'}, // colors aura1_color: {type: 'color'}, aura2_color: {type: 'color'}, tint_color: {type: 'color'}, // Text : special name: {type: 'text'}, statusmarkers: {type: 'status'}, layer: {type: 'layer'}, represents: {type: 'character_id'}, bar1_link: {type: 'attribute'}, bar2_link: {type: 'attribute'}, bar3_link: {type: 'attribute'}, imgsrc: {type: 'image'}, controlledby: {type: 'player'} }, regex = { numberString: /^[+\-]?(0|[1-9][0-9]*)([.]+[0-9]*)?([eE][+\-]?[0-9]+)?$/, stripSingleQuotes: /'([^']+(?='))'/g, stripDoubleQuotes: /"([^"]+(?="))"/g, layers: /^(?:gmlayer|objects|map|walls)$/, statuses: /^(?:red|blue|green|brown|purple|pink|yellow|dead|skull|sleepy|half-heart|half-haze|interdiction|snail|lightning-helix|spanner|chained-heart|chemical-bolt|death-zone|drink-me|edge-crack|ninja-mask|stopwatch|fishing-net|overdrive|strong|fist|padlock|three-leaves|fluffy-wing|pummeled|tread|arrowed|aura|back-pain|black-flag|bleeding-eye|bolt-shield|broken-heart|cobweb|broken-shield|flying-flag|radioactive|trophy|broken-skull|frozen-orb|rolling-bomb|white-tower|grab|screaming|grenade|sentry-gun|all-for-one|angel-outfit|archery-target)$/, colors: /^(transparent|(?:#?[0-9a-fA-F]{6}))$/, imgsrc: /(.*\/images\/.*)(thumb|med|original|max)(.*)$/ }, filters = { hasArgument: function(a) { return a.match(/.+[\|#]/); }, isBoolean: function(a) { return _.has(fields,a) && 'boolean' === fields[a].type; }, isTruthyArgument: function(a) { return _.contains([1,'1','on','yes','true','sure','yup'],a); } }, transforms = { degrees: function(t){ var n = parseFloat(t,10); if(!_.isNaN(n)) { n %= 360; } return n; }, circleSegment: function(t){ var n = Math.abs(parseFloat(t,10)); if(!_.isNaN(n)) { n = Math.min(360,Math.max(0,n)); } return n; }, orderType: function(t){ switch(t){ case 'tofront': case 'front': case 'f': case 'top': return 'tofront'; case 'toback': case 'back': case 'b': case 'bottom': return 'toback'; default: return; } }, keyHash: function(t){ return (t && t.toLowerCase().replace(/\s+/,'_')) || undefined; } }, checkGlobalConfig = function(){ var s=state.TokenMod, g=globalconfig && globalconfig.tokenmod, parsedDots; if(g && g.lastsaved && g.lastsaved > s.globalconfigCache.lastsaved ){ log(' > Updating from Global Config < ['+(new Date(g.lastsaved*1000))+']'); s.playersCanUse_ids = 'playersCanIDs' === g['Players can use --ids']; state.TokenMod.globalconfigCache=globalconfig.tokenmod; } }, checkInstall = function() { log('-=> TokenMod v'+version+' <=- ['+(new Date(lastUpdate*1000))+']'); if( ! _.has(state,'TokenMod') || state.TokenMod.version !== schemaVersion) { log(' > Updating Schema to v'+schemaVersion+' <'); switch(state.TokenMod && state.TokenMod.version) { /* falls through */ case 0.1: case 0.2: delete state.TokenMod.globalConfigCache; state.TokenMod.globalconfigCache = {lastsaved:0}; /* falls through */ case 'UpdateSchemaVersion': state.TokenMod.version = schemaVersion; break; default: state.TokenMod = { version: schemaVersion, globalconfigCache: {lastsaved:0}, playersCanUse_ids: false }; break; } } checkGlobalConfig(); }, observeTokenChange = function(handler){ if(handler && _.isFunction(handler)){ observers.tokenChange.push(handler); } }, notifyObservers = function(event,obj,prev){ _.each(observers[event],function(handler){ handler(obj,prev); }); }, getPlayerIDs = (function(){ let age=0, cache=[], checkCache=function(){ if(_.now()-60000>age){ cache=_.chain(findObjs({type:'player'})) .map((p)=>({ id: p.id, userid: p.get('d20userid'), keyHash: transforms.keyHash(p.get('displayname')) })) .value(); } }, findPlayer = function(data){ checkCache(); let pids=_.reduce(cache,(m,p)=>{ if(p.id===data || p.userid===data || (-1 !== p.keyHash.indexOf(transforms.keyHash(data)))){ m.push(p.id); } return m; },[]); if(!pids.length){ let obj=filterObjs((o)=>(o.id===data && _.contains(['character','graphic'],o.get('type'))))[0]; if(obj){ let charObj = ('graphic'===obj.get('type') && getObj('character',obj.get('represents'))), cb = (charObj ? charObj : obj).get('controlledby'); pids = (cb.length ? cb.split(/,/) : []); } } return pids; }; return function(datum){ return 'all'===datum ? ['all'] : findPlayer(datum); }; }()), 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 ''; }, getConfigOption_PlayersCanIDs = function() { var text = ( state.TokenMod.playersCanUse_ids ? 'ON' : 'OFF' ); return '
'+ 'Players can IDs is currently '+ text+ ''+ 'Toggle'+ ''+ '
'; }, showHelp = function(id) { var who=getObj('player',id).get('_displayname'); sendChat('', '/w "'+who+'" '+ '
'+ '
'+ 'TokenMod v'+version+ '
'+ '
'+ '

TokenMod provides an interface to setting almost all settable properties of a token.

'+ '
'+ 'Commands'+ '
'+ '!token-mod '+ch('<')+'--help|--ignore-selected|--config|--on|--off|--flip|--set'+ch('>')+' '+ch('<')+'parameter'+ch('>')+' '+ch('[')+ch('<')+'parameter'+ch('>')+' ...'+ch(']')+' ... '+ch('[')+'--ids '+ch('<')+'token_id'+ch('>')+' '+ch('[')+ch('<')+'token_id'+ch('>')+' ...'+ch(']')+ch(']')+''+ '
'+ '

This command takes a list of modifications and applies them to the selected tokens (or tokens specified with --ids by a GM or Player depending on configuration). Note that each --option can be specified multiple times and in any order.

'+ '

Note: If you are using multiple '+ch('@')+ch('{')+'target'+ch('|')+'token_id'+ch('}')+' calls in a macro, and need to adjust fewer than the supplied number of token ids, simply select the same token several times. The duplicates will be removed.

'+ '

Note: Anywhere you use |, you can use # instead. Sometimes this make macros easier.

'+ '

Note: You can use the {{ and }} to span multiple lines with your command for easier clarity and editing:'+ '

'+ '
'+
				'!token-mod {{
'+ ch(' ')+ch(' ')+'--on
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+'flipv
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+'fliph
'+ ch(' ')+ch(' ')+'--set
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+'rotation|180
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+'bar1|'+ch('[')+ch('[')+'8d8+8'+ch(']')+ch(']')+'
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+'light_radius|60
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+'light_dimradius|30
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+'name|"My bright token"
'+ '}}
'+ '

'+ '
    '+ '
  • '+ ''+ch('<')+'--help'+ch('>')+' '+ch('-')+' Displays this help.'+ '
  • '+ '
  • '+ ''+ch('<')+'--ignore-selected'+ch('>')+' '+ch('-')+' Prevents modifications to the selected tokens (only modifies tokens passed with --ids).'+ '
  • '+ '
  • '+ ''+ch('<')+'--config'+ch('>')+' '+ch('-')+' Sets Config options. '+ '
  • '+ '
  • '+ ''+ch('<')+'--on'+ch('>')+' '+ch('-')+' Turns on any of the specified parameters (See Booleans below).'+ '
  • '+ '
  • '+ ''+ch('<')+'--off'+ch('>')+' '+ch('-')+' Turns off any of the specified parameters (See Booleans below).'+ '
  • '+ '
  • '+ ''+ch('<')+'--flip'+ch('>')+' '+ch('-')+' Flips the value of any of the specified parameters (See Booleans below).'+ '
  • '+ '
  • '+ ''+ch('<')+'--set'+ch('>')+' '+ch('-')+' Each parameter is treated as a key and value, divided by a | character. Sets the key to the value. If the value has spaces, you must enclose it '+ch("'")+' or '+ch('quot')+'. See below for specific value handling logic.'+ '
  • '+ '
  • '+ ''+ch('<')+'--order'+ch('>')+' '+ch('-')+' changes the ordering of tokens. Specify one of '+ch("'")+'tofront'+ch("'")+', '+ch("'")+'front'+ch("'")+', '+ch("'")+'f'+ch("'")+', '+ch("'")+'top'+ch("'")+' to bring something to the front or '+ch("'")+'toback'+ch("'")+', '+ch("'")+'back'+ch("'")+', '+ch("'")+'b'+ch("'")+', '+ch("'")+'bottom'+ch("'")+' to push it to the back.'+ '
  • '+ '
  • '+ ''+ch('<')+'--ids'+ch('>')+' '+ch('-')+' Each parameter is a Token ID, usually supplied with something like '+ch('@')+ch('{')+'target'+ch('|')+'Target 1'+ch('|')+'token_id'+ch('}')+'. '+ 'By default, only a GM can use this argument. You can enable players to use it as well with --config players-can-ids|on.'+ '
  • '+ '
'+ '
'+ '
'+ 'Specification'+ '
'+ '

--ids takes token ids to operate on, separated by spaces.

'+ '
'+
				'!token-mod --ids -Jbz-mlHr1UXlfWnGaLh -JbjeTZycgyo0JqtFj-r -JbjYq5lqfXyPE89CJVs --on showname showplayers_name'+
			'
'+ '

Usually, you will want to specify these with the '+ch('@')+ch('{')+'target'+ch('}')+' syntax:

'+ '
'+
				'!token-mod --ids '+ch('@')+ch('{')+'target|1|token_id'+ch('}')+' '+ch('@')+ch('{')+'target|2|token_id'+ch('}')+' '+ch('@')+ch('{')+'target|3|token_id'+ch('}')+' --on showname showplayers_name'+
			'
'+ '
'+ 'Booleans'+ '
'+ '

The --on, --off and --flip options only work on properties of a token that are either true or false, checkboxes. Specified properties will only be changed once, priority is given to arguments to --on first, then --off and finally to --flip.

'+ '
'+ '
'+
				'!token-mod --on showname light_hassight --off isdrawing --flip flipv fliph'+
			'
'+ '
'+ '
'+ '

Available Boolean Properties:

'+ '
showname
'+ '
showplayers_name
'+ '
showplayers_bar1
'+ '
showplayers_bar2
'+ '
showplayers_bar3
'+ '
showplayers_aura1
'+ '
showplayers_aura2
'+ '
playersedit_name
'+ '
playersedit_bar1
'+ '
playersedit_bar2
'+ '
playersedit_bar3
'+ '
playersedit_aura1
'+ '
playersedit_aura2
'+ '
light_otherplayers
'+ '
light_hassight
'+ '
isdrawing
'+ '
flipv
'+ '
fliph
'+ '
aura1_square
'+ '
aura2_square
'+ '
'+ch(' ')+'
'+ '

Any of the booleans can be set with the --set command by passing a true or false as the value

'+ '
'+ '
'+
					'!token-mod --set showname|yes isdrawing|no'+
				'
'+ '
'+ '
'+ '
'+ 'Set Syntax Guide'+ '
'+ '

--set takes key value pairs, separated by | characters.

'+ '
'+
				'!token-mod --set key|value key|value key|value'+
			'
'+ '

Note: You can now use inline rolls wherever you like, including rollable tables:

'+ '
'+
				'!token-mod --set bar'+ch('[')+ch('[')+'1d3'+ch(']')+ch(']')+'_value|X statusmarkers|blue:'+ch('[')+ch('[')+'1d9'+ch(']')+ch(']')+'|green:'+ch('[')+ch('[')+'1d9'+ch(']')+ch(']')+' name:'+ch('"')+ch('[')+ch('[')+'1'+ch('[')+'randomName'+ch(']')+ch(']')+ch(']')+

			'
'+ '

Note: You can now use + or - before any number to make an adjustment to the current value:

'+ '
'+
				'!token-mod --set bar1_value|-3 statusmarkers|blue:+1|green:-1'+
			'
'+ '

Note: You can now preface a + or - with a = to explicitly set the number to a negative or positive value:

'+ '
'+
				'!token-mod --set bar1_value|=+3 light_radius|=-10'+
			'
'+ '

There are several types of keys with special value formats:

'+ '
'+ 'Numbers'+ '

Number values can be any floating point number (though most fields will drop the fractional part).

'+ '

Available Numbers Properties:

'+ '
left
'+ '
top
'+ '
width
'+ '
height
'+ '
'+ch(' ')+'
'+ '

It'+ch("'")+'s probably a good idea not to set the location of a token off screen or the width or height to 0.

'+ '
'+ '
'+
					'!token-mod --set top|0 left|0 width|50 height|50'+
				'
'+ '
'+ '
'+ '
'+ 'Numbers or Blank'+ '

Just like the Numbers fields, except you can set them to blank as well.

'+ '

Available Numbers or Blank Properties:

'+ '
light_radius
'+ '
light_dimradius
'+ '
light_multiplier
'+ '
aura1_radius
'+ '
aura2_radius
'+ '
'+ch(' ')+'
'+ '

Here is setting a standard DnD 5e torch, turning off aura1 and setting aura2 to 30. Note that the | is still required for setting a blank value, such as aura1_radius below.

'+ '
'+ '
'+
					'!token-mod --set light_radius|40 light_dimradius|20 aura1_radius| aura2_radius|30'+
				'
'+ '
'+ '
'+ '
'+ 'Degrees'+ '

Any positive or negative number.

'+ '

Available Degrees Properties:

'+ '
rotation
'+ '
'+ch(' ')+'
'+ '

Rotating a token by 180 degrees.

'+ '
'+ '
'+
					'!token-mod --set rotation|180'+
				'
'+ '
'+ '
'+ '
'+ 'Circle Segment'+ '

Any positive or negative number between 0 and 360.

'+ '

Available Circle Segment Properties:

'+ '
light_angle
'+ '
light_losangle
'+ '
'+ch(' ')+'
'+ '

Setting line of sight angle to 90 degrees.

'+ '
'+ '
'+
					'!token-mod --set light_losangle|90'+
				'
'+ '
'+ '
'+ '
'+ 'Color'+ '

Colors can be any hex number color (with or without preceding #), or the word transparent.

'+ '

Available Color Properties:

'+ '
tint_color
'+ '
aura1_color
'+ '
aura2_color
'+ '
'+ch(' ')+'
'+ '

Turning off the tint and setting aura1 to a reddish color.

'+ '
'+ '
'+
					'!token-mod --set tint_color|transparent aura1_color|ff3366'+
				'
'+ '
'+ '
'+ '
'+ 'Text'+ '

These can be pretty much anything. If your value has spaces in it, you need to enclose it in '+ch("'")+' or '+ch('"')+'.

'+ '

Available Text Properties:

'+ '
name
'+ '
bar1_value
'+ '
bar2_value
'+ '
bar3_value
'+ '
bar1_max
'+ '
bar2_max
'+ '
bar3_max
'+ '
bar1
'+ '
bar2
'+ '
bar3
'+ '
bar1_reset
'+ '
bar2_reset
'+ '
bar3_reset
'+ '
'+ch(' ')+'
'+ '

Setting the name to Sir Thomas and bar1 to 23.

'+ '
'+ '
'+
					'!token-mod --set name|'+ch('"')+'Sir Thomas'+ch('"')+' bar1_value|23'+
				'
'+ '
'+ '

bar1, bar2 and bar3 are special. Any value set on them will be set in both the _value and _max fields for that bar. This is most useful for setting hit points.

'+ '
'+ '
'+
					'!token-mod --set bar1|'+ch('[')+ch('[')+'3d6+8'+ch(']')+ch(']')+
				'
'+ '
'+ '

bar1_reset, bar2_reset and bar3_reset are special. Any value set on them will be ignored, instead they well set the _value field for that bar to whatever the matching _max field is set to. This is most useful for resetting hit points or resource counts like spells. (The | is currently still required.)

'+ '
'+ '
'+
					'!token-mod --set bar1_reset| bar3_reset|'+
				'
'+ '
'+ '
'+ '
'+ 'Layer'+ '

There is only one Layer property. It can be one of 4 values, listed below.

'+ '

Available Layer Property:

'+ '
layer
'+ '
'+ch(' ')+'
'+ '

Available Layer Values:

'+ '
gmlayer
'+ '
objects
'+ '
map
'+ '
walls
'+ '
'+ch(' ')+'
'+ '

Moving something to the gmlayer.

'+ '
'+ '
'+
					'!token-mod --set layer|gmlayer'+
				'
'+ '
'+ '
'+ '
'+ 'Status'+ '

There is only one Status property. Status has a somewhat complicated syntax to support the most possible flexibility.

'+ '

Available Status Property:

'+ '
statusmarkers
'+ '
'+ch(' ')+'
'+ '

Status is the only property that supports multiple values, all separated by | as seen below.

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|blue|red|green|padlock|broken-shield'+
				'
'+ '
'+ '

You can optionally preface each status with a + to remind you it is being added. This command is identical:

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|+blue|+red|+green|+padlock|+broken-shield'+
				'
'+ '
'+ '

Each value can be followed by a colon and a number between 0 and 9. (The number following the dead status is ignored because it is special.)

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|blue:0|red:3|green|padlock:2|broken-shield:7'+
				'
'+ '
'+ '

The numbers following a status can be prefaced with a + or -, which causes their value to be applied to the current value. Here'+ch("'")+'s an example showing blue getting incremented by 2, and padlock getting decremented by 1. Values will be bounded between 0 and 9.

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|blue:+2|padlock:-1'+
				'
'+ '
'+ '

You can optionally preface each status with a ? to modify the way + and - on status numbers work. With ? on the front of the status, only selected tokens that have that status will be modified. Additionally, if the status reaches 0, it will be removed. Here'+ch("'")+'s an example showing blue getting decremented by 1. If it reaches 0, it will be removed and no status will be added if it is missing.

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|?blue:-1'+
				'
'+ '
'+ '

By default, status markers will be added, retaining whichever status markers are already present. You can override this behavior by prefacing a value with a - to cause the status to be removed. Below will remove the blue and padlock status.

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|-blue|-padlock'+
				'
'+ '
'+ '

Sometimes it is convenient to have a way to add a status if it is not there, but remove it if it is. This allows marking tokens with markers and clearing them with the same command. You can preface a status with ! to toggle it'+ch("'")+'s state on and off. Here is an example that will add or remove the Rook piece from a token.

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|!white-tower'+
				'
'+ '
'+ '

Sometimes, you might want to clear all status marker as part of setting a new status marker. You can do this by prefacing a status marker with a =. Note that this affects all status markers before as well, so you will want to do this only on the first status marker. Below all status markers are removed and the dead marker is set. (If you want to remove all status markers, just specify the same marker twice with a = and then a -.)

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|=dead'+
				'
'+ '
'+ '

You can set multiple of the same status marker with a bracket syntax. Copies of a status are indexed starting at 1 from left to right. Leaving brackets off will be the same as specifying index 1. Using empty brackets is the same as specifying an index 1 greater than the highest index in use. When setting a status at an index that doesn'+ch("'")+'t exist (say, 8 when you only have 2 of that status) it will be appended to the right as the next index. When removing a status that doesn'+ch("'")+'t exist, it will be ignored. Removing the empty bracket status will remove all statues of that type.

'+ '

Adding the blue status marker with the numbers 7 and 5 in a few different ways:

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|blue:7|blue[]:5
'+ '!token-mod --set statusmarkers|blue[]:7|blue[]:5
'+ '!token-mod --set statusmarkers|blue[1]:7|blue[2]:5
'+ '
'+ '
'+ '

Removing the second blue status marker:

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|-blue[2]'+
				'
'+ '
'+ '

Removing all blue status markers:

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|-blue[]'+
				'
'+ '
'+ '

Available Status Markers:

'+ '
red
'+ '
blue
'+ '
green
'+ '
brown
'+ '
purple
'+ '
pink
'+ '
yellow
'+ '
dead
'+ '
skull
'+ '
sleepy
'+ '
half-heart
'+ '
half-haze
'+ '
interdiction
'+ '
snail
'+ '
lightning-helix
'+ '
spanner
'+ '
chained-heart
'+ '
chemical-bolt
'+ '
death-zone
'+ '
drink-me
'+ '
edge-crack
'+ '
ninja-mask
'+ '
stopwatch
'+ '
fishing-net
'+ '
overdrive
'+ '
strong
'+ '
fist
'+ '
padlock
'+ '
three-leaves
'+ '
fluffy-wing
'+ '
pummeled
'+ '
tread
'+ '
arrowed
'+ '
aura
'+ '
back-pain
'+ '
black-flag
'+ '
bleeding-eye
'+ '
bolt-shield
'+ '
broken-heart
'+ '
cobweb
'+ '
broken-shield
'+ '
flying-flag
'+ '
radioactive
'+ '
trophy
'+ '
broken-skull
'+ '
frozen-orb
'+ '
rolling-bomb
'+ '
white-tower
'+ '
grab
'+ '
screaming
'+ '
grenade
'+ '
sentry-gun
'+ '
all-for-one
'+ '
angel-outfit
'+ '
archery-target
'+ '
'+ch(' ')+'
'+ '

All of these operations can be combine in a single statusmarkers command.

'+ '
'+ '
'+
					'!token-mod --set statusmarkers|blue:3|-dead|red:3'+
				'
'+ '
'+ '
'+ '
'+ 'Image'+ '

You can provide an image to use for the token source image. Images must be in a user library or will be ignored. The full path must be provided.

'+ '

Available Image Properties:

'+ '
imagesrc
'+ '
'+ch(' ')+'
'+ '

Setting the image source to a library image (in this case, the orange ring I use for TurnMarker):

'+ '
'+ '
'+
					'!token-mod --set imgsrc|https://s3.amazonaws.com/files.d20.io/images/4095816/086YSl3v0Kz3SlDAu245Vg/max.png?1400535580'+
				'
'+ '
'+ '
'+ '
'+ 'Character ID'+ '

You can use the '+ch('@')+ch('{')+ch('<')+'character name'+ch('>')+ch('|')+'character_id'+ch('}')+' syntax to specify a character_id directly or use the name of a character (quoted if it contains spaces) or just the shortest part of the name that is unique ('+ch("'")+'Sir Maximus Strongbow'+ch("'")+' could just be '+ch("'")+'max'+ch("'")+'.). Not case sensitive: Max = max = MaX = MAX

'+ '

Available Character ID Properties:

'+ '
represents
'+ '
'+ch(' ')+'
'+ '

Here is setting the represents to the character Bob.

'+ '
'+ '
'+
					'!token-mod --set represents|'+ch('@')+ch('{')+'Bob'+ch('|')+'character_id'+ch('}')+
				'
'+ '
'+ '

Note that setting the represents will clear the links for the bars, so you will probably want to set those again.

'+ '
'+ '
'+ 'Player'+ '

You can specify Players using one of five methods: Player ID, Roll20 ID Number, Player Name Matching, Token ID, Character ID

'+ '
    '+ '
  • Player ID is a unique identifier assigned that player in a specific game. You can only find this id from the API, so this is likely the least useful method.
  • '+ '
  • Roll20 ID Number is a unique identifier assigned to a specific player. You can find it in the URL of their profile page as the number preceeding their name. This is really useful if you play with the same people all the time, or are cloning the same game with the same players, etc.
  • '+ '
  • Player Name Matching is a string that will be matched to the current name of the player in game. Just like with Characters above, it can be quoted if it has spaces and is case insensitive. All players that match a given string will be used.
  • '+ '
  • Token ID will be used to collect the controlledby entries for a token or the associated character if the token represetns one.
  • '+ '
  • Character ID will be used to collect the controlledby entries for a character.
  • '+ '
'+ '

Note that you can use the special string all to denote the All Players special player.

'+ '

Available Player Properties:

'+ '
controlledby
'+ '
'+ch(' ')+'
'+ '

Controlled by supports multiple values, all separated by | as seen below.

'+ '
'+ '
'+
					'!token-mod --set controlledby|aaron|+stephen|+russ'+
				'
'+ '
'+ '

There are 3 operations that can be specified with leading characters: +,-, = (default)

'+ '
    '+ '
  • + will add the player(s) to the controlledby list.
  • '+ '
  • - will remove the player(s) from the controlledby list.
  • '+ '
  • = will set the controlledby list to only the player(s). (Default)
  • '+ '
'+ '

Adding control for roll20 player number 123456:

'+ '
'+ '
'+
					'!token-mod --set controlledby|+123456'+
				'
'+ '
'+ '

Setting control for all players:

'+ '
'+ '
'+
					'!token-mod --set controlledby|all'+
				'
'+ '
'+ '

Adding all the players with k in their name but removing karen:

'+ '
'+ '
'+
					'!token-mod --set controlledby|+k|-karen'+
				'
'+ '
'+ '

Adding the player with player id -JsABCabc123-12:

'+ '
'+ '
'+
					'!token-mod --set controlledby|+-JsABCabc123-12'+
				'
'+ '
'+ '

In the case of a leading character on the name that would be interpreted as an operation, you can use quotes:

'+ '
'+ '
'+
					'!token-mod --set controlledby|"-JsABCabc123-12"'+
				'
'+ '
'+ '

When using Token ID or Characte ID methods, it'+ch("'")+'s a good idea to use an explicit operation:

'+ '
'+ '
'+
                    '!token-mod --set controlledby|=@{target|token_id}'+
				'
'+ '
'+ '

Quotes will also help with names that have spaces, or with nested other quotes:

'+ '
'+ '
'+
					'!token-mod --set controlledby|+'+ch("'")+'Bob "tiny" Slayer'+ch("'")+
				'
'+ '
'+ '

You can remove all controlling players by using a blank list or explicitly setting equal to nothing:

'+ '
'+ '
'+
					'!token-mod --set controlledby|
'+ '!token-mod --set controlledby|='+ '
'+ '
'+ '

A specified action that doesn'+ch("'")+'t match any player(s) will be ignored. If there are no players named Tim, this won'+ch("'")+'t change the list:

'+ '
'+ '
'+
					'!token-mod --set controlledby|tim'+
				'
'+ '
'+ '

If you wanted to force an empty list and set tim if tim is available, you can chain this with blanking the list:

'+ '
'+ '
'+
					'!token-mod --set controlledby||tim'+
				'
'+ '
'+ '

Using controlledby with represents

'+ '

When a token represents a character, the controlledby property that is adjusted is the one on the character. This works as you would want it to,so if you are changing the represents as part of the same command, it will adjust the location that will be correct after all commands are run.

'+ '

Set the token to represent the character with rook in the name and assign control to players matching bob:

'+ '
'+ '
'+
					'!token-mod --set represents|rook controlledby|bob'+
				'
'+ '
'+ '

Remove the represent setting for the token and then give bob controll of that token (useful for one-offs from npcs or monsters):

'+ '
'+ '
'+
					'!token-mod --set represents| controlledby|bob'+
				'
'+ '
'+ '
'+ '
'+ 'Attribute Name'+ '

These are resolved from the represented character id. If the token doesn'+ch("'")+'t represent a character, these will be ignored. If the Attribute Name specified doesn'+ch("'")+'t exist for the represented character, the link is unchanged. You can clear a link by passing a blank Attribute Name.

'+ '

Available Attribute Name Properties:

'+ '
bar1_link
'+ '
bar2_link
'+ '
bar3_link
'+ '
'+ch(' ')+'
'+ '

Here is setting the represents to the character Bob and setting bar1 to be the npc hit points attribute.

'+ '
'+ '
'+
					'!token-mod --set represents|'+ch('@')+ch('{')+'Bob'+ch('|')+'character_id'+ch('}')+' bar1_link|npc_HP'+
				'
'+ '
'+ '

Here is clearing the link for bar3:

'+ '
'+ '
'+
					'!token-mod --set bar3_link|'+
				'
'+ '
'+ '
'+ 'Configuration'+ '
'+ '

--config takes option value pairs, separated by | characters.

'+ '
'+
				'!token-mod --config option|value option|value'+
			'
'+ '

There is currently one configuration option:

'+ '
'+ '
    '+ '
  • '+ '
    '+ ( state.TokenMod.playersCanUse_ids ? 'ON' : 'OFF' )+ '
    ' +'players-can-ids '+ch('-')+' Determines if players can use --ids. Specifying a value which is true allows players to use --ids. Omitting a value flips the current setting.' +'
  • '+ '
'+ '
'+ getConfigOption_PlayersCanIDs()+ '
'+ 'API Notifications'+ '
'+ '

API Scripts can register for the following notifications:

'+ '

Token Changes -- Register your function by passing it to TokenMod.ObserveTokenChange(). When TokenMod changes a token, it will call your function with the Token as the first argument and the previous properties as the second argument.

'+ '

Example script that notifies when a token'+ch("'")+'s status markers are changed by TokenMod:

'+ '
'+ '
'+
				'on('+ch("'")+'ready'+ch("'")+',function(){
'+ ch(' ')+ch(' ')+'if('+ch("'")+'undefined'+ch("'")+' !== typeof TokenMod && TokenMod.ObserveTokenChange){
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+'TokenMod.ObserveTokenChange(function(obj,prev){
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+ch(' ')+ch(' ')+'if(obj.get('+ch("'")+'statusmarkers'+ch("'")+') !== prev.statusmarkers){
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+ch(' ')+ch(' ')+ch(' ')+ch(' ')+'sendChat('+ch("'")+'Observer Token Change'+ch("'")+','+ch("'")+'Token: '+ch("'")+'+obj.get('+ch("'")+'name'+ch("'")+')+'+ch("'")+' has changed status markers!'+ch("'")+');
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+ch(' ')+ch(' ')+'}
'+ ch(' ')+ch(' ')+ch(' ')+ch(' ')+'});
'+ ch(' ')+ch(' ')+'}
'+ '});'+ '
'+ '
'+ '
'+ '
' ); }, getRelativeChange = function(current,update) { var cnum,unum; if( update && _.has(update,0) && ('=' === update[0]) ){ return parseFloat(_.rest(update).join('')); } cnum = current && (_.isNumber(current) ? current : ( _.isString(current) ? (current.match(regex.numberString) ? parseFloat(current,10) : NaN) : NaN) ); unum = update && (_.isNumber(update) ? update : ( _.isString(update) ? (update.match(regex.numberString) ? parseFloat(update,10) : NaN) : NaN) ); if(!_.isNaN(unum) && !_.isUndefined(unum) ) { if(!_.isNaN(cnum) && !_.isUndefined(cnum) ) { switch(update[0]) { case '+': case '-': return cnum+unum; default: return unum; } } else { return unum; } } return update; }, parseArguments = function(a) { var args=a.split(/[\|#]/), cmd=args.shift(), retr={}, t,t2; if(_.has(fields,cmd)) { retr[cmd]=[]; switch(fields[cmd].type) { case 'boolean': retr[cmd].push(filters.isTruthyArgument(args.shift())); break; case 'text': retr[cmd].push(args.shift().replace(regex.stripSingleQuotes,'$1').replace(regex.stripDoubleQuotes,'$1')); break; case 'numberBlank': if( '=' === args[0][0] ) { t='='; args[0]=_.rest(args[0]).join(''); } else { t=''; } t2=args[0].match(regex.numberString) ? args[0] : '' ; if( ''===t2 || !_.isNaN(parseFloat(t2)) ) { retr[cmd].push(t+t2); } break; case 'number': if( '=' === args[0][0] ) { t='='; args[0]=_.rest(args[0]).join(''); } else { t=''; } t2=args[0].match(regex.numberString) ? args[0] : undefined ; if(!_.isNaN(parseFloat(t2)) ) { retr[cmd].push(t+t2); } break; case 'degrees': if( '=' === args[0][0] ) { t='='; args[0]=_.rest(args[0]); } else { t=''; } retr[cmd].push(t+(_.contains(['-','+'],args[0][0]) ? args[0][0] : '') + Math.abs(transforms.degrees(args.shift()))); break; case 'circleSegment': if( '=' === args[0][0] ) { t='='; args[0]=_.rest(args[0]); } else { t=''; } retr[cmd].push(t+(_.contains(['-','+'],args[0][0]) ? args[0][0] : '') + transforms.circleSegment(args.shift())); break; case 'layer': retr[cmd].push((args.shift().match(regex.layers)||[]).shift()); if(0 === (retr[cmd][0]||'').length) { retr = undefined; } break; case 'image': t=args.shift().match(regex.imgsrc); if(t){ retr[cmd].push(t[1]+'thumb'+t[3]); } else { retr = undefined; } break; case 'color': retr[cmd].push((args.shift().match(regex.colors)||[]).shift()); if(0 === (retr[cmd][0]||'').length) { retr = undefined; } else if('transparent' !== retr[cmd][0] && '#' !== retr[cmd][0].substr(0,1) ) { retr[cmd][0]='#'+retr[cmd][0]; } break; case 'character_id': if('' === args[0]){ retr[cmd].push(''); } else { t=getObj('character', args[0]); if(t) { retr[cmd].push(args[0]); } else { // try to find a character with this name t2=findObjs({type: 'character',archived: false}); t=_.chain([ args[0].replace(regex.stripSingleQuotes,'$1').replace(regex.stripDoubleQuotes,'$1') ]) .map(function(n){ var l=_.filter(t2,function(c){ return c.get('name').toLowerCase() === n.toLowerCase(); }); return ( 1 === l.length ? l : _.filter(t2,function(c){ return -1 !== c.get('name').toLowerCase().indexOf(n.toLowerCase()); })); }) .flatten() .value(); if(1 === t.length) { retr[cmd].push(t[0].id); } else { retr=undefined; } } } break; case 'attribute': retr[cmd].push(args.shift().replace(regex.stripSingleQuotes,'$1').replace(regex.stripDoubleQuotes,'$1')); break; case 'player': _.each(args, function(p){ let parts = p.match(/^([\+-=]?)(.*)$/), pids = (parts ? getPlayerIDs(parts[2].replace(regex.stripSingleQuotes,'$1').replace(regex.stripDoubleQuotes,'$1')):[]); if(pids.length){ _.each(pids,(pid)=>{ retr[cmd].push({ pid: pid, operation: parts[1] || '=' }); parts[1]='+'; }); } else if(_.contains(['','='],p)){ retr[cmd].push({ pid:'', operation:'=' }); } }); break; case 'status': _.each(args, function(a) { var s = a.split(/:/), statparts = s.shift().match(/^(\S+?)(\[(\d*)\]|)$/)||[], index = ( '[]' === statparts[2] ? statparts[2] : ( undefined !== statparts[3] ? Math.max(parseInt(statparts[3],10)-1,0) : 0 ) ), stat=statparts[1]||'', op = (_.contains(['-','+','=','!','?'],stat[0]) ? stat[0] : false), numraw = s.shift() || '', numop = (_.contains(['-','+'],numraw[0]) ? numraw[0] : false), num = Math.max(0,Math.min(9,Math.abs(parseInt(numraw,10)))) || 0; stat = ( op ? stat.substring(1) : stat); if(stat.match(regex.statuses)) { retr[cmd].push({ status: stat, number: num, index: index, sign: numop, operation: op || '+' }); } }); break; default: retr=undefined; break; } } return retr; }, expandMetaArguments = function(memo,a) { var args=a.split(/[\|#]/), cmd=args.shift(); switch(cmd) { case 'bar1': case 'bar2': case 'bar3': args=args.join('|'); memo.push(cmd+'_value|'+args); memo.push(cmd+'_max|'+args); break; default: memo.push(a); break; } return memo; }, parseOrderArguments = function(list,base) { return _.chain(list) .map(transforms.orderType) .reject(_.isUndefined) .union(base) .value(); }, parseSetArguments = function(list,base) { return _.chain(list) .filter(filters.hasArgument) .reduce(expandMetaArguments,[]) .map(parseArguments) .reject(_.isUndefined) .reduce(function(memo,i){ _.each(i,function(v,k){ switch(k){ case 'statusmarkers': if(_.has(memo,k)) { memo[k]=_.union(v,memo[k]); } else { memo[k]=v; } break; default: memo[k]=v; break; } }); return memo; },base) .value(); }, decomposeStatuses = function(statuses){ return _.reduce(statuses.split(/,/).reverse(),function(memo,st,idx){ var parts=st.split(/@/), entry = { mark: parts[0], num: parseInt(parts[1],10) || 0, idx: idx }; if(parts[0].length) { memo[parts[0]] = ( memo[parts[0]] && memo[parts[0]].push(entry) && memo[parts[0]]) || [entry] ; } return memo; },{}); }, composeStatuses = function(statuses){ return _.chain(statuses) .reduce(function(m,s){ _.each(s,function(sd){ m.push(sd); }); return m; },[]) .sortBy(function(s){ return s.idx; }) .map(function(s){ return ('dead'===s.mark ? 'dead' : ( s.mark+(s.num>0 ? '@'+s.num : ''))); }) .value() .reverse() .join(','); }, applyModListToToken = function(modlist, token) { var mods={}, delta, cid,prev, repChar, controlList = (modlist.set && modlist.set.controlledby) ? (function(){ let list; repChar = getObj('character', modlist.set.represents || token.get('represents')); list = (repChar ? repChar.get('controlledby') : token.get('controlledby')); return (list ? list.split(/,/) : []); }()) : [], current=decomposeStatuses(token.get('statusmarkers')||''), statusCount=(token.get('statusmarkers')||'').split(/,/).length; _.each(modlist.order,function(f){ switch(f){ case 'tofront': toFront(token); break; case 'toback': toBack(token); break; } }); _.each(modlist.on,function(f){ mods[f]=true; }); _.each(modlist.off,function(f){ mods[f]=false; }); _.each(modlist.flip,function(f){ mods[f]=!token.get(f); }); _.each(modlist.set,function(f,k){ switch(k) { case 'controlledby': _.each(f, function(cb){ switch(cb.operation){ case '=': controlList=[cb.pid]; break; case '+': controlList=_.union(controlList,[cb.pid]); break; case '-': controlList=_.without(controlList,cb.pid); break; } }); if(repChar){ repChar.set('controlledby',controlList.join(',')); } else { mods[k]=controlList.join(','); } break; case 'statusmarkers': _.each(f, function (sm){ switch(sm.operation){ case '!': if('[]' !== sm.index && _.has(current,sm.status) ){ if( _.has(current[sm.status],sm.index) ) { current[sm.status]= _.filter(current[sm.status],function(e,idx){ return idx !== sm.index; }); } } else { current[sm.status] = current[sm.status] || []; current[sm.status].push({ mark: sm.status, num: Math.max(0,Math.min(9,getRelativeChange(0, sm.sign+sm.number))), index: statusCount++ }); } break; case '?': if('[]' !== sm.index && _.has(current,sm.status) && _.has(current[sm.status],sm.index)){ current[sm.status][sm.index].num = (Math.max(0,Math.min(9,getRelativeChange(current[sm.status][sm.index].num, sm.sign+sm.number)))); if(0 === current[sm.status][sm.index].num) { current[sm.status]= _.filter(current[sm.status],function(e,idx){ return idx !== sm.index; }); } } break; case '+': if('[]' !== sm.index && _.has(current,sm.status) && _.has(current[sm.status],sm.index)){ current[sm.status][sm.index].num = (Math.max(0,Math.min(9,getRelativeChange(current[sm.status][sm.index].num, sm.sign+sm.number)))); } else { current[sm.status] = current[sm.status] || []; current[sm.status].push({ mark: sm.status, num: Math.max(0,Math.min(9,getRelativeChange(0, sm.sign+sm.number))), index: statusCount++ }); } break; case '-': if('[]' !== sm.index && _.has(current,sm.status)){ if( _.has(current[sm.status],sm.index )) { current[sm.status]= _.filter(current[sm.status],function(e,idx){ return idx !== sm.index; }); } } else { current[sm.status]= _.first(current[sm.status],-1); } break; case '=': current = {}; current[sm.status] = []; current[sm.status].push({ mark: sm.status, num: Math.max(0,Math.min(9,getRelativeChange(0, sm.sign+sm.number))), index: statusCount++ }); break; } }); break; case 'represents': mods[k]=f[0]; mods.bar1_link=''; mods.bar2_link=''; mods.bar3_link=''; break; case 'bar1_link': case 'bar2_link': case 'bar3_link': if( '' === f[0] ) { mods[k]=''; } else { cid=mods.represents || token.get('represents') || ''; if('' !== cid) { delta=findObjs({type: 'attribute', characterid: cid, name: f[0]})[0]; if(delta) { mods[k]=delta.id; mods[k.split(/_/)[0]+'_value']=delta.get('current'); mods[k.split(/_/)[0]+'_max']=delta.get('max'); } } } break; case 'left': case 'top': case 'width': case 'height': delta=getRelativeChange(token.get(k),f[0]); if(_.isNumber(delta)) { mods[k]=delta; } break; case 'rotation': delta=getRelativeChange(token.get(k),f[0]); if(_.isNumber(delta)) { mods[k]=(delta%360); } break; case 'light_angle': case 'light_losangle': delta=getRelativeChange(token.get(k),f[0]); if(_.isNumber(delta)) { mods[k] = Math.min(360,Math.max(0,delta)); } break; case 'light_radius': case 'light_dimradius': case 'light_multiplier': case 'aura2_radius': case 'aura1_radius': delta=getRelativeChange(token.get(k),f[0]); if(_.isNumber(delta) || '' === delta) { mods[k]=delta; } break; case 'bar1_reset': case 'bar2_reset': case 'bar3_reset': delta = token.get(k.replace(/_reset$/,'_max')); if(!_.isUndefined(delta)) { mods[k.replace(/_reset$/,'_value')]=delta; } break; case 'bar1_value': case 'bar2_value': case 'bar3_value': case 'bar1_max': case 'bar2_max': case 'bar3_max': case 'name': delta=getRelativeChange(token.get(k),f[0]); if(_.isNumber(delta) || _.isString(delta)) { mods[k]=delta; } break; default: mods[k]=f[0]; break; } }); mods.statusmarkers=composeStatuses(current); prev=JSON.parse(JSON.stringify(token)); token.set(mods); notifyObservers('tokenChange',token,prev); }, handleConfig = function(config, id) { var args, cmd, who=getObj('player',id).get('_displayname'); if(config.length) { while(config.length) { args=config.shift().split(/[\|#]/); cmd=args.shift(); switch(cmd) { case 'players-can-ids': if(args.length) { state.TokenMod.playersCanUse_ids = filters.isTruthyArgument(args.shift()); } else { state.TokenMod.playersCanUse_ids = !state.TokenMod.playersCanUse_ids; } sendChat('','/w "'+who+'" '+ '
'+ getConfigOption_PlayersCanIDs()+ '
' ); break; default: sendChat('', '/w "'+who+'" '+ '
'+ 'Error: '+ 'No configuration setting for ['+cmd+']'+ '
' ); break; } } } else { sendChat('','/w "'+who+'" '+ '
'+ '
'+ 'TokenMod v'+version+ '
'+ getConfigOption_PlayersCanIDs()+ '
' ); } }, handleInput = function(msg_orig) { var msg = _.clone(msg_orig), args, cmds, ids=[], ignoreSelected = false, modlist={ flip: [], on: [], off: [], set: {}, order: [] }; if (msg.type !== "api") { return; } if(_.has(msg,'inlinerolls')){ msg.content = _.chain(msg.inlinerolls) .reduce(function(m,v,k){ var ti=_.reduce(v.results.rolls,function(m2,v2){ if(_.has(v2,'table')){ m2.push(_.reduce(v2.results,function(m3,v3){ m3.push(v3.tableItem.name); return m3; },[]).join(', ')); } return m2; },[]).join(', '); m['$[['+k+']]']= (ti.length && ti) || v.results.total || 0; return m; },{}) .reduce(function(m,v,k){ return m.replace(k,v); },msg.content) .value(); } args = msg.content .replace(/\n/g, ' ') .replace(/(\{\{(.*?)\}\})/g," $2 ") .split(/\s+--/); switch(args.shift()) { case '!token-mod': while(args.length) { cmds=args.shift().match(/([^\s]+[\|#]'[^']+'|[^\s]+[\|#]"[^"]+"|[^\s]+)/g); switch(cmds.shift()) { case 'help': showHelp(msg.playerid); return; case 'config': if(playerIsGM(msg.playerid)) { handleConfig(cmds,msg.playerid); } return; case 'flip': modlist.flip=_.union(_.filter(cmds,filters.isBoolean),modlist.flip); break; case 'on': modlist.on=_.union(_.filter(cmds,filters.isBoolean),modlist.on); break; case 'off': modlist.off=_.union(_.filter(cmds,filters.isBoolean),modlist.off); break; case 'set': modlist.set=parseSetArguments(cmds,modlist.set); break; case 'order': modlist.order=parseOrderArguments(cmds,modlist.order); break; case 'ignore-selected': ignoreSelected=true; break; case 'ids': ids=_.union(cmds,ids); break; } } modlist.off=_.difference(modlist.off,modlist.on); modlist.flip=_.difference(modlist.flip,modlist.on,modlist.off); if( !playerIsGM(msg.playerid) && !state.TokenMod.playersCanUse_ids ) { ids=[]; } if(!ignoreSelected) { ids=_.union(ids,_.pluck(msg.selected,'_id')); } if(ids.length){ _.chain(ids) .uniq() .map(function(t){ return getObj('graphic',t); }) .reject(_.isUndefined) .each(function(t) { applyModListToToken(modlist,t); }); } break; } }, registerEventHandlers = function() { on('chat:message', handleInput); }; return { CheckInstall: checkInstall, ObserveTokenChange: observeTokenChange, RegisterEventHandlers: registerEventHandlers }; }()); on("ready",function(){ 'use strict'; TokenMod.CheckInstall(); TokenMod.RegisterEventHandlers(); });