Roll20 uses cookies to improve your experience on our site. Cookies enable you to enjoy certain features, social sharing functionality, and tailor message and display ads to your interests on our site and others. They also help us understand how our site is being used. By continuing to use our site, you consent to our use of cookies. Update your cookie preferences .
×
Create a free account

Bump script with UDL

1614232634

Edited 1614232696
Ken
Pro
Is there any chance of getting an update to Bump to work with Updated Dynamic Lighting? Currently running bump version 0.2.19 I don't know if its possible to copy the vision settings from the original token to the bump token created on the object layer. As of right now, when a player (not gm) uses bump (for example - invisibility spell), none of the vision qualities of the token transfer to the bump token (the token put on the object layer) - so the player effectively can't see. 
1614261969
The Aaron
Roll20 Production Team
API Scripter
OH!  good point!  I can fix that.
1614262677
The Aaron
Roll20 Production Team
API Scripter
Ok, here's a preliminary version.&nbsp; I still need to test with it a bit, but it looks like the UDL is still some what broken, so YMMV. // Github: <a href="https://github.com/shdwjk/Roll20API/blob/master/Bump/Bump.js" rel="nofollow">https://github.com/shdwjk/Roll20API/blob/master/Bump/Bump.js</a> // By: The Aaron, Arcane Scriptomancer // Contact: <a href="https://app.roll20.net/users/104025/the-aaron" rel="nofollow">https://app.roll20.net/users/104025/the-aaron</a> /* global GroupInitiative TokenMod */ const Bump = (() =&gt; { // eslint-disable-line no-unused-vars const scriptName = "Bump"; const version = '0.2.20'; const lastUpdate = 1614262310; const schemaVersion = 0.5; const clearURL = '<a href="https://s3.amazonaws.com/files.d20.io/images/4277467/iQYjFOsYC5JsuOPUCI9RGA/thumb.png?1401938659" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/4277467/iQYjFOsYC5JsuOPUCI9RGA/thumb.png?1401938659</a>'; const checkerURL = '<a href="https://s3.amazonaws.com/files.d20.io/images/16204335/MGS1pylFSsnd5Xb9jAzMqg/med.png?1455260461" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/16204335/MGS1pylFSsnd5Xb9jAzMqg/med.png?1455260461</a>'; const regex = { colors: /(transparent|(?:#?[0-9a-fA-F]{3}(?:[0-9a-fA-F]{3})?))/ }; const mirroredProps = [ 'name', 'left', 'top', 'width', 'height', 'rotation', 'flipv', 'fliph', 'bar1_value', 'bar1_max', 'bar2_value', 'bar2_max', 'bar3_value', 'bar3_max', 'tint_color', 'lastmove', 'controlledby', 'light_hassight', 'light_radius', 'light_dimradius', 'light_angle', 'light_losangle','lastmove', 'represents','bar1_link','bar2_link','bar3_link', //UDL settings "has_bright_light_vision", "has_night_vision", "night_vision_tint", "night_vision_distance", "emits_bright_light", "bright_light_distance", "emits_low_light", "low_light_distance", "has_limit_field_of_vision", "limit_field_of_vision_center", "limit_field_of_vision_total", "has_limit_field_of_night_vision", "limit_field_of_night_vision_center", "limit_field_of_night_vision_total", "has_directional_bright_light", "directional_bright_light_total", "directional_bright_light_center", "has_directional_low_light", "directional_low_light_total", "directional_low_light_center", "dim_light_opacity" ]; const mirroredPropsNoBar = [ 'name', 'left', 'top', 'width', 'height', 'rotation', 'flipv', 'fliph', 'bar1_value', 'bar2_value', 'bar3_value', 'tint_color', 'lastmove', 'controlledby', 'light_hassight', 'light_radius', 'light_dimradius', 'light_angle', 'light_losangle','lastmove', 'represents', //UDL settings "has_bright_light_vision", "has_night_vision", "night_vision_tint", "night_vision_distance", "emits_bright_light", "bright_light_distance", "emits_low_light", "low_light_distance", "has_limit_field_of_vision", "limit_field_of_vision_center", "limit_field_of_vision_total", "has_limit_field_of_night_vision", "limit_field_of_night_vision_center", "limit_field_of_night_vision_total", "has_directional_bright_light", "directional_bright_light_total", "directional_bright_light_center", "has_directional_low_light", "directional_low_light_total", "directional_low_light_center", "dim_light_opacity" ]; const defaults = { css: { button: { 'border': '1px solid #cccccc', 'border-radius': '1em', 'background-color': '#006dcc', 'margin': '0 .1em', 'font-weight': 'bold', 'padding': '.1em 1em', 'color': 'white' } } }; const filterObj = (o) =&gt; Object.keys(o).reduce( (m,k) =&gt; (undefined === o[k] ? m : Object.assign({},m,{[k]:o[k]})), {}); const mergeObj = (...a) =&gt; Object.keys(a).reduce((m,k)=&gt;Object.assign(m,filterObj(a[k])),{}); const css = (rules) =&gt; `style="${Object.keys(rules).map(k=&gt;`${k}:${rules[k]};`).join('')}"`; const makeButton = (command, label, backgroundColor, color) =&gt; `&lt;a ${css(mergeObj(defaults.css.button,{color,'background-color':backgroundColor}))} href="${command}"&gt;${label}&lt;/a&gt;`; const isPlayerToken = (obj) =&gt; { let players = obj.get('controlledby') .split(/,/) .filter(s=&gt;s.length); if( players.includes('all') || players.filter((p)=&gt;!playerIsGM(p)).length ) { return true; } if('' !== obj.get('represents') ) { players = (getObj('character',obj.get('represents')) || {get: ()=&gt;''} ) .get('controlledby').split(/,/) .filter(s=&gt;s.length); return players.includes('all') || players.filter((p)=&gt;!playerIsGM(p)).length ; } return false; }; const keyForValue = (obj,v) =&gt; Object.keys(obj).find( key =&gt; obj[key] === v); const cleanupObjectReferences = () =&gt; { const allIds = findObjs({type:'graphic'}).map( o =&gt; o.id); const ids = [ ...Object.keys(state[scriptName].mirrored), ...Object.values(state[scriptName].mirrored) ] .filter( id =&gt; !allIds.includes(id) ); ids.forEach( id =&gt; { if(state[scriptName].mirrored.hasOwnProperty(id)){ if(!ids.includes(state[scriptName].mirrored[id])){ let obj = getObj('graphic',state[scriptName].mirrored[id]); if(obj){ obj.remove(); } } } else { let iid = keyForValue(state[scriptName].mirrored,id); delete state[scriptName].mirrored[iid]; if(!ids.includes(iid)){ createMirrored(iid, false, 'gm'); } } }); }; const simpleObj = (o)=&gt;JSON.parse(JSON.stringify(o)); const fixupSlaveBars = () =&gt; { let ids = Object.values(state[scriptName].mirrored); const burndown = () =&gt;{ if(ids.length){ let id = ids.shift(); let slave = getObj('graphic',id); if(slave){ if(state[scriptName].config.noBars) { slave.set({ bar1_max: '', bar2_max: '', bar3_max: '', bar1_link: '', bar2_link: '', bar3_link: '' }); } let prev = simpleObj(slave); handleTokenChange(slave,prev); } setTimeout(burndown,0); } }; burndown(); }; const checkGlobalConfig = () =&gt; { var s=state[scriptName], g=globalconfig &amp;&amp; globalconfig.bump; if(g &amp;&amp; g.lastsaved &amp;&amp; g.lastsaved &gt; s.globalconfigCache.lastsaved ){ log(' &gt; Updating from Global Config &lt; ['+(new Date(g.lastsaved*1000))+']'); if(g['Visible Color'].match(regex.colors)) { s.config.layerColors.gmlayer =g['Visible Color'].match(regex.colors)[1]; } if(g['Invisible Color'].match(regex.colors)) { s.config.layerColors.objects=g['Invisible Color'].match(regex.colors)[1]; } s.config.autoPush = 'autoPush' === g['Auto Push']; s.config.autoSlave = 'autoSlave' === g['Auto Slave']; s.config.autoUnslave = 'autoUnslave' === g['Auto Unslave']; s.config.noBars = 'noBars' === g['No Bars on Slave']; state[scriptName].globalconfigCache=globalconfig.bump; } }; const checkInstall = () =&gt; { log(`-=&gt; ${scriptName} v${version} &lt;=- [${new Date(lastUpdate*1000)}]`); if( ! state.hasOwnProperty(scriptName) || state[scriptName].version !== schemaVersion) { log(' &gt; Updating Schema to v'+schemaVersion+' &lt;'); switch(state[scriptName] &amp;&amp; state[scriptName].version) { case 0.1: state[scriptName].config.autoSlave = false; /* falls through */ case 0.2: case 0.3: delete state[scriptName].globalConfigCache; state[scriptName].globalconfigCache = {lastsaved:0}; /* falls through */ case 0.4: state[scriptName].lastHelpVersion=version; state[scriptName].config.noBars = false; state[scriptName].config.autoUnslave = false; /* falls through */ case 'UpdateSchemaVersion': state[scriptName].version = schemaVersion; break; default: state[scriptName] = { version: schemaVersion, lastHelpVersion: version, globalconfigCache: {lastsaved:0}, config: { layerColors: { 'gmlayer' : '#008000', 'objects' : '#800080' }, autoPush: false, autoSlave: false, noBars: false, autoUnslave: false }, mirrored: {} }; break; } } checkGlobalConfig(); cleanupObjectReferences(); assureHelpHandout(); }; const ch = (c) =&gt; { const entities = { '&lt;' : 'lt', '&gt;' : 'gt', "'" : '#39', '@' : '#64', '{' : '#123', '|' : '#124', '}' : '#125', '[' : '#91', ']' : '#93', '"' : 'quot', '*' : 'ast', '/' : 'sol', ' ' : 'nbsp' }; if( entities.hasOwnProperty(c) ){ return `&amp;${entities[c]};`; } return ''; }; const _h = { outer: (...o) =&gt; `&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;${o.join(' ')}&lt;/div&gt;`, title: (t,v) =&gt; `&lt;div style="font-weight: bold; border-bottom: 1px solid black;font-size: 130%;"&gt;${t} v${v}&lt;/div&gt;`, subhead: (...o) =&gt; `&lt;b&gt;${o.join(' ')}&lt;/b&gt;`, minorhead: (...o) =&gt; `&lt;u&gt;${o.join(' ')}&lt;/u&gt;`, optional: (...o) =&gt; `${ch('[')}${o.join(` ${ch('|')} `)}${ch(']')}`, required: (...o) =&gt; `${ch('&lt;')}${o.join(` ${ch('|')} `)}${ch('&gt;')}`, header: (...o) =&gt; `&lt;div style="padding-left:10px;margin-bottom:3px;"&gt;${o.join(' ')}&lt;/div&gt;`, section: (s,...o) =&gt; `${_h.subhead(s)}${_h.inset(...o)}`, paragraph: (...o) =&gt; `&lt;p&gt;${o.join(' ')}&lt;/p&gt;`, group: (...o) =&gt; `${o.join(' ')}`, items: (o) =&gt; `&lt;li&gt;${o.join('&lt;/li&gt;&lt;li&gt;')}&lt;/li&gt;`, ol: (...o) =&gt; `&lt;ol&gt;${_h.items(o)}&lt;/ol&gt;`, ul: (...o) =&gt; `&lt;ul&gt;${_h.items(o)}&lt;/ul&gt;`, grid: (...o) =&gt; `&lt;div style="padding: 12px 0;"&gt;${o.join('')}&lt;div style="clear:both;"&gt;&lt;/div&gt;&lt;/div&gt;`, cell: (o) =&gt; `&lt;div style="width: 130px; padding: 0 3px; float: left;"&gt;${o}&lt;/div&gt;`, inset: (...o) =&gt; `&lt;div style="padding-left: 10px;padding-right:20px"&gt;${o.join(' ')}&lt;/div&gt;`, join: (...o) =&gt; o.join(' '), pre: (...o) =&gt;`&lt;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;"&gt;${o.join(' ')}&lt;/div&gt;`, preformatted: (...o) =&gt;_h.pre(o.join('&lt;br&gt;').replace(/\s/g,ch(' '))), code: (...o) =&gt; `&lt;code&gt;${o.join(' ')}&lt;/code&gt;`, attr: { bare: (o)=&gt;`${ch('@')}${ch('{')}${o}${ch('}')}`, selected: (o)=&gt;`${ch('@')}${ch('{')}selected${ch('|')}${o}${ch('}')}`, target: (o)=&gt;`${ch('@')}${ch('{')}target${ch('|')}${o}${ch('}')}`, char: (o,c)=&gt;`${ch('@')}${ch('{')}${c||'CHARACTER NAME'}${ch('|')}${o}${ch('}')}` }, bold: (...o) =&gt; `&lt;b&gt;${o.join(' ')}&lt;/b&gt;`, italic: (...o) =&gt; `&lt;i&gt;${o.join(' ')}&lt;/i&gt;`, font: { command: (...o)=&gt;`&lt;b&gt;&lt;span style="font-family:serif;"&gt;${o.join(' ')}&lt;/span&gt;&lt;/b&gt;` } }; const getMirroredPair = (id) =&gt; { if(state[scriptName].mirrored.hasOwnProperty(id)){ return { master: getObj('graphic',id), slave: getObj('graphic',state[scriptName].mirrored[id]) }; } let iid = keyForValue(state[scriptName].mirrored,id); if(iid){ return { master: getObj('graphic',iid), slave: getObj('graphic',id) }; } }; const getMirrored = (id) =&gt; { if(state[scriptName].mirrored.hasOwnProperty(id)){ return getObj('graphic',state[scriptName].mirrored[id]); } let iid = keyForValue(state[scriptName].mirrored,id); if(iid){ return getObj('graphic',iid); } }; const createMirrored = (id, push, who) =&gt; { // get root obj var master = getObj('graphic',id), slave = getMirrored(id), baseObj, layer; if(!slave &amp;&amp; master) { layer=((state[scriptName].config.autoPush || push || 'gmlayer' === master.get('layer')) ? 'objects' : 'gmlayer'); if(state[scriptName].config.autoPush || push) { master.set({layer: 'gmlayer'}); } baseObj = { imgsrc: clearURL, layer: layer, pageid: master.get('pageid'), aura1_color: state[scriptName].config.layerColors[layer], aura1_square: true, aura1_radius: 0.000001, light_otherplayers: (isPlayerToken(master) ? master.get('light_otherplayers') : false), showname: (isPlayerToken(master) ? master.get('showname') : false), showplayers_name: false, showplayers_bar1: false, showplayers_bar2: false, showplayers_bar3: false, showplayers_aura1: false, showplayers_aura2: false }; (state[scriptName].config.noBars ? mirroredPropsNoBar : mirroredProps ).forEach( p =&gt; baseObj[p]=master.get(p) ); slave = createObj('graphic',baseObj); state[scriptName].mirrored[master.id]=slave.id; } else { if(!slave) { sendChat('',`/w "${who}" `+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ `&lt;b&gt;Error:&lt;/b&gt; Couldn${ch("'")}t find a token for id: ${id}`+ '&lt;/div&gt;' ); } } }; const setMasterLayer = (obj,layer) =&gt; { obj.set({ layer: layer }); }; const setSlaveLayer = (obj,layer) =&gt; { obj.set({ layer: layer, aura1_color: state[scriptName].config.layerColors[layer] }); }; const bumpToken = (id,who) =&gt; { var pair=getMirroredPair(id); if(pair &amp;&amp; pair.master &amp;&amp; pair.slave) { switch(pair.master.get('layer')){ case 'gmlayer': setMasterLayer(pair.master,'objects'); if(state[scriptName].config.autoUnslave){ removeMirrored(pair.master.id); } else { setSlaveLayer(pair.slave,'gmlayer'); } break; default: setMasterLayer(pair.master,'gmlayer'); setSlaveLayer(pair.slave,'objects'); break; } } else if(state[scriptName].config.autoSlave) { createMirrored(id, false, who); } }; const removeMirrored = (id) =&gt; { var pair=getMirroredPair(id); if(pair) { pair.slave.remove(); delete state[scriptName].mirrored[pair.master.id]; } }; const handleRemoveToken = (obj) =&gt; { // special handling for deleting slaves? removeMirrored(obj.id); }; const unpackSM = (stats) =&gt; stats.split(/,/).reduce((m,v) =&gt; { let p = v.split(/@/); let n = parseInt(p[1] || '0', 10); if(p[0].length) { m[p[0]] = Math.max(n, m[p[0]] || 0); } return m; },{}); const packSM = (o) =&gt; Object.keys(o) .map(k =&gt; ('dead' === k || o[k]&lt;1 || o[k]&gt;9) ? k : `${k}@${parseInt(o[k])}` ).join(','); const handleTokenChange = (obj,prev) =&gt; { var pair = getMirroredPair(obj.id); if(pair &amp;&amp; obj) { // status markers if(obj.id === pair.slave.id &amp;&amp; obj.get('statusmarkers') !== prev.statusmarkers){ let sSM = unpackSM(obj.get('statusmarkers')); let mSM = unpackSM(pair.master.get('statusmarkers')); Object.keys(sSM).forEach( s =&gt; { if(sSM[s]) { mSM[s] = Math.min(Math.max((mSM[s]||0) + sSM[s],0),9); } else if(mSM.hasOwnProperty(s)) { delete mSM[s]; } else { mSM[s]=0; } }); pair.slave.set('statusmarkers',''); pair.master.set('statusmarkers',packSM(mSM)); } const propList = (state[scriptName].config.noBars ? mirroredPropsNoBar : mirroredProps ); (pair.master.id === obj.id ? pair.slave : pair.master).set(propList.reduce((m,p) =&gt; { m[p]=obj.get(p); return m; },{})); if(pair.slave.id === obj.id) { pair.slave.set(Object.assign({ showplayers_name: false, showplayers_bar1: false, showplayers_bar2: false, showplayers_bar3: false, showplayers_aura1: false, showplayers_aura2: false },(propList.reduce((m,p) =&gt; { m[p]=pair.slave.get(p); return m; },{})))); } else { pair.master.set(propList.reduce((m,p) =&gt; { m[p]=pair.master.get(p); return m; },{})); pair.slave.set({ showplayers_name: false, showplayers_bar1: false, showplayers_bar2: false, showplayers_bar3: false, showplayers_aura1: false, showplayers_aura2: false }); } if(obj.get('layer') !== prev.layer) { if(pair.master.id === obj.id) { setSlaveLayer(pair.slave,prev.layer); } else { setMasterLayer(pair.master,prev.layer); setSlaveLayer(pair.slave,obj.get('layer')); } } } }; const makeConfigOption = (config,command,text) =&gt; { var onOff = (config ? 'On' : 'Off' ), color = (config ? '#5bb75b' : '#faa732' ); return '&lt;div style="'+ 'border: 1px solid #ccc;'+ 'border-radius: .2em;'+ 'background-color: white;'+ 'margin: 0 1em;'+ 'padding: .1em .3em;'+ '"&gt;'+ '&lt;div style="float:right;"&gt;'+ makeButton(command,onOff,color)+ '&lt;/div&gt;'+ text+ '&lt;div style="clear:both;"&gt;&lt;/div&gt;'+ '&lt;/div&gt;'; }; const makeConfigOptionColor = (config,command,text) =&gt; { var color = ('transparent' === config ? "background-image: url('"+checkerURL+"');" : "background-color: "+config+";"), buttonText ='&lt;div style="border:1px solid #1d1d1d;width:40px;height:40px;display:inline-block;'+color+'"&gt;&amp;nbsp;&lt;/div&gt;'; return '&lt;div style="'+ 'border: 1px solid #ccc;'+ 'border-radius: .2em;'+ 'background-color: white;'+ 'margin: 0 1em;'+ 'padding: .1em .3em;'+ '"&gt;'+ '&lt;div style="float:right;"&gt;'+ makeButton(command,buttonText)+ '&lt;/div&gt;'+ text+ '&lt;div style="clear:both;"&gt;&lt;/div&gt;'+ '&lt;/div&gt;'; }; const getConfigOption_GMLayerColor = () =&gt; { return makeConfigOptionColor( state[scriptName].config.layerColors.gmlayer, '!bump-config --gm-layer-color|?{What aura color for when the master token is visible? (transparent for none, #RRGGBB for a color)|'+state[scriptName].config.layerColors.gmlayer+'}', '&lt;b&gt;GM Layer (Visible) Color&lt;/b&gt; is the color the overlay turns when it is on the GM Layer, thus indicating that the Bumped token is visible to the players on the Object Layer.' ); }; const getConfigOption_ObjectsLayerColor = () =&gt; { return makeConfigOptionColor( state[scriptName].config.layerColors.objects, '!bump-config --objects-layer-color|?{What aura color for when the master token is invisible? (transparent for none, #RRGGBB for a color)|'+state[scriptName].config.layerColors.objects+'}', '&lt;b&gt;Objects Layer (Invisible) Color&lt;/b&gt; is the color the overlay turns when it is on the Objects Layer, thus indicating that the Bumped token is invisible to the players on the GM Layer.' ); }; const getConfigOption_AutoPush = () =&gt; { return makeConfigOption( state[scriptName].config.autoPush, '!bump-config --toggle-auto-push', '&lt;b&gt;Auto Push&lt;/b&gt; automatically moves a bumped token to the GM Layer when it gets added to Bump.' ); }; const getConfigOption_AutoSlave = () =&gt; { return makeConfigOption( state[scriptName].config.autoSlave, '!bump-config --toggle-auto-slave', '&lt;b&gt;Auto Slave&lt;/b&gt; causes tokens that are not in Bump to be put into Bump when ever !bump is run with them selected.' ); }; const getConfigOption_AutoUnslave = () =&gt; { return makeConfigOption( state[scriptName].config.autoUnslave, '!bump-config --toggle-auto-unslave', '&lt;b&gt;Auto Unslave&lt;/b&gt; causes tokens that are in Bump to be unslaved when they are moved back to the Token Layer.' ); }; const getConfigOption_NoBars = () =&gt; { return makeConfigOption( state[scriptName].config.noBars, '!bump-config --toggle-no-bars', '&lt;b&gt;No Bars on Slave&lt;/b&gt; causes slave tokens to only mirror the current value on the bars, not the max, preventing them from having bars.' ); }; const getAllConfigOptions = () =&gt; { return getConfigOption_GMLayerColor() + getConfigOption_ObjectsLayerColor() + getConfigOption_AutoPush() + getConfigOption_AutoSlave() + getConfigOption_AutoUnslave() + getConfigOption_NoBars(); }; const assureHelpHandout = (create = false) =&gt; { const helpIcon = "<a href="https://s3.amazonaws.com/files.d20.io/images/127392204/tAiDP73rpSKQobEYm5QZUw/thumb.png?15878425385" rel="nofollow">https://s3.amazonaws.com/files.d20.io/images/127392204/tAiDP73rpSKQobEYm5QZUw/thumb.png?15878425385</a>"; // find handout let props = {type:'handout', name:`Help: ${scriptName}`}; let hh = findObjs(props)[0]; if(!hh) { hh = createObj('handout',Object.assign(props, {inplayerjournals: "all", avatar: helpIcon})); create = true; } if(create || version !== state[scriptName].lastHelpVersion){ hh.set({ notes: helpParts.helpDoc({who:'handout',playerid:'handout'}) }); state[scriptName].lastHelpVersion = version; log(' &gt; Updating Help Handout to v'+version+' &lt;'); } }; const helpParts = { helpBody: (context) =&gt; _h.join( _h.header( _h.paragraph('Bump provides a way to invisibly manipulate tokens on the GM Layer from the Objects Layer, and vice versa.') ), _h.subhead('Commands'), _h.inset( _h.font.command( `!bump-slave`, _h.optional( `--push`, `--help` ) ), _h.paragraph( 'Adds the selected tokens to Bump, creating their slave tokens.'), _h.ul( `${_h.bold('--push')} -- If the selected token is on the Objects Layer, it will be pushed to the GM Layer.`, `${_h.bold('--help')} -- Shows the Help screen.` ), _h.font.command( `!bump`, _h.optional( `--help` ) ), _h.paragraph( `Using !bump on a token in Bump causes it to swap with it${ch("'")}s counterpart on the other layer.`), _h.ul( `${_h.bold('--help')} -- Shows the Help screen.` ), _h.font.command( `!bump-unslave`, _h.optional( `--help` ) ), _h.paragraph( 'Removes the selected tokens from Bump, removing their slave tokens.') ), _h.subhead('Description'), _h.inset( _h.paragraph('When a token is added to Bump a slave token is created that mimics everything about the master token. The slave token is only visible to the GM and has a color on it to show if the master token is on the GM Layer or the Objects layer. Moving the slave token will move the master token and vice versa. The slave token represents the same character as the master token and changes on the slave token will be reflected on the master token.'), _h.paragraph('Non-GM players can also use bump on characters they control. They will be able to see their bumped token as a transparent aura, but other players will not. This is useful for invisible characters.'), _h.paragraph(`While changes on the slave token will be reflected on the master token, Status Markers have a special behavior. Status Markers are always cleared from the slave token to prevent it from being visible, with the following changes being made to the master token. Adding a Status Marker on a slave token that is not on the master token will cause that Status Marker to be added to the master token. Adding a Status Marker to the slave token that is on the master token causes it to be removed from the master token. Adding a Status Marker with a number on the slave token will cause that number to be added to the master token${ch("'")}s Status Marker. Note that non-GM players will not be able to see Status Markers.`) ), ( playerIsGM(context.playerid) ? _h.group( _h.subhead('Configuration'), getAllConfigOptions() ) : '' ) ), helpDoc: (context) =&gt; _h.join( _h.title(scriptName, version), helpParts.helpBody(context) ), helpChat: (context) =&gt; _h.outer( _h.title(scriptName, version), helpParts.helpBody(context) ) }; const showHelp = (playerid) =&gt; { const who=(getObj('player',playerid)||{get:()=&gt;'API'}).get('_displayname'); let context = { who, playerid }; sendChat('', '/w "'+who+'" '+ helpParts.helpChat(context)); }; const handleInput = (msg) =&gt; { var args, who; if (msg.type !== "api") { return; } who=(getObj('player',msg.playerid)||{get:()=&gt;'API'}).get('_displayname'); args = msg.content.split(/\s+/); switch(args.shift()) { case '!bump': if(!msg.selected || args.includes('--help')) { showHelp(msg.playerid); return; } msg.selected.forEach( (s) =&gt; bumpToken(s._id,who) ); break; case '!bump-slave': if(!msg.selected || args.includes('--help')) { showHelp(msg.playerid); return; } msg.selected.forEach( (s) =&gt; createMirrored(s._id, args.includes('--push'), who) ); break; case '!bump-unslave': if(!msg.selected || args.includes('--help')) { showHelp(msg.playerid); return; } msg.selected.forEach( (s) =&gt; removeMirrored(s._id) ); break; case '!bump-config': if(args.includes('--help')) { showHelp(msg.playerid); return; } if(!playerIsGM(msg.playerid)){ break; } if(!args.length) { sendChat('','/w "'+who+'" '+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ '&lt;div style="font-weight: bold; border-bottom: 1px solid black;font-size: 130%;"&gt;'+ 'Bump v'+version+ '&lt;/div&gt;'+ getAllConfigOptions()+ '&lt;/div&gt;' ); return; } args.forEach((a) =&gt; { var opt=a.split(/\|/), omsg=''; switch(opt.shift()) { case '--gm-layer-color': if(opt[0].match(regex.colors)) { state[scriptName].config.layerColors.gmlayer=opt[0]; } else { omsg='&lt;div&gt;&lt;b&gt;Error:&lt;/b&gt; Not a valid color: '+opt[0]+'&lt;/div&gt;'; } sendChat('','/w "'+who+'" '+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ omsg+ getConfigOption_GMLayerColor()+ '&lt;/div&gt;' ); break; case '--objects-layer-color': if(opt[0].match(regex.colors)) { state[scriptName].config.layerColors.objects=opt[0]; } else { omsg='&lt;div&gt;&lt;b&gt;Error:&lt;/b&gt; Not a valid color: '+opt[0]+'&lt;/div&gt;'; } sendChat('','/w "'+who+'" '+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ omsg+ getConfigOption_ObjectsLayerColor()+ '&lt;/div&gt;' ); break; case '--toggle-auto-push': state[scriptName].config.autoPush=!state[scriptName].config.autoPush; sendChat('','/w "'+who+'" '+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ getConfigOption_AutoPush()+ '&lt;/div&gt;' ); break; case '--toggle-auto-slave': state[scriptName].config.autoSlave=!state[scriptName].config.autoSlave; sendChat('','/w "'+who+'" '+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ getConfigOption_AutoSlave()+ '&lt;/div&gt;' ); break; case '--toggle-auto-unslave': state[scriptName].config.autoUnslave=!state[scriptName].config.autoUnslave; sendChat('','/w "'+who+'" '+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ getConfigOption_AutoUnslave()+ '&lt;/div&gt;' ); break; case '--toggle-no-bars': state[scriptName].config.noBars=!state[scriptName].config.noBars; fixupSlaveBars(); sendChat('','/w "'+who+'" '+ '&lt;div style="border: 1px solid black; background-color: white; padding: 3px 3px;"&gt;'+ getConfigOption_NoBars()+ '&lt;/div&gt;' ); break; default: sendChat('','/w "'+who+'" '+ '&lt;div&gt;&lt;b&gt;Unsupported Option:&lt;/div&gt; '+a+'&lt;/div&gt;'); } }); break; } }; const handleTurnOrderChange = () =&gt; { Campaign().set({ turnorder: JSON.stringify( JSON.parse( Campaign().get('turnorder') || '[]').map( (turn) =&gt; { let iid = keyForValue(state[scriptName].mirrored,turn.id); if(iid) { turn.id = iid; } return turn; }) ) }); }; const registerEventHandlers = () =&gt; { on('chat:message', handleInput); on('change:graphic', handleTokenChange); on('destroy:graphic', handleRemoveToken); on('change:campaign:turnorder', handleTurnOrderChange); if('undefined' !== typeof GroupInitiative &amp;&amp; GroupInitiative.ObserveTurnOrderChange){ GroupInitiative.ObserveTurnOrderChange(handleTurnOrderChange); } if('undefined' !== typeof TokenMod &amp;&amp; TokenMod.ObserveTokenChange){ TokenMod.ObserveTokenChange(handleTokenChange); } }; return { CheckInstall: checkInstall, Notify_TurnOrderChange: handleTurnOrderChange, RegisterEventHandlers: registerEventHandlers }; })(); on('ready', () =&gt; { Bump.CheckInstall(); Bump.RegisterEventHandlers(); });
1614263567

Edited 1614276575
Ken
Pro
Awesome. Thank you. I'll play around and test it after work and see what happens. Edit: Home from work early (got everything done quicker, lol) I don't know if it is in your manual for bump and I just overlooked it, but figured this out while testing. Bump pulls the settings from the linked token on the character sheet, not the actual token on the map. After testing and playing around with settings - this is what I found.&nbsp; Works correctly - vision toggle, night vision toggle, night vision distance, night vision tint, limit field of vision (total), limit field of vision (center), emit light (toggle), emit light (bright light), emit light (distance), low-light (toggle), low-light brightness (slider), directional light (total), directional light (center) Player permissions (general - name, token bars, auras) set to edit only on bump token. Pretty sure this is correct (otherwise what is the point of going invisible) Not working - notes for each Night Vision Effect - always set to none (doesn't copy sharpen or dimming) Low-Light (distance) - does not copy number from original token emits light (total) - does not reflect accurate number (guessing because it is an autocalc field?) light multiplier (under advanced) - does not copy number from original token Additionally - there was one or two times when I noticed the bars on the original token had reverted to standard (instead of compact). I neglected to make a note of which trial...don't think it was user error (as I was not changing that toggle - standard/compact). Hope that helps.