// Github: https://github.com/shdwjk/Roll20API/blob/master/Facing/Facing.js // By: The Aaron, Arcane Scriptomancer // Contact: https://app.roll20.net/users/104025/the-aaron const Facing = (() => { // eslint-disable-line no-unused-vars const version = '0.1.5'; const lastUpdate = 1560010028; const schemaVersion = 0.3; const defaults = { image: 'https://s3.amazonaws.com/files.d20.io/images/9183999/XcViJVf7-cGOXcZq1KWp-A/thumb.png?1430541914', attributeName: 'facing', scale: 2.5 }; const indicatorLayers = ['map', 'objects' ]; const checkInstall = () => { log('-=> Facing v'+version+' <=- ['+(new Date(lastUpdate*1000))+']'); if( ! _.has(state,'Facing') || state.Facing.version !== schemaVersion) { log(' > Updating Schema to v'+schemaVersion+' <'); switch(state.Facing && state.Facing.version) { case 0.2: state.Facing.config.indicatorLayer = 'map'; /* break; // intentional dropthrough */ /* falls through */ case 0.1: state.Facing.config.centering = false; /* break; // intentional dropthrough */ /* falls through */ case 'UpdateSchemaVersion': state.Facing.version = schemaVersion; break; default: state.Facing = { version: schemaVersion, config: { image: defaults.image, attributeName: defaults.attributeName, relative: true, scale: defaults.scale, centering: false, indicatorLayer: 'map' }, ringed: {} }; break; } } }; const ch = (c) => { var entities = { '<' : 'lt', '>' : 'gt', "'" : '#39', '@' : '#64', '{' : '#123', '|' : '#124', '}' : '#125', '[' : '#91', ']' : '#93', '"' : 'quot', '-' : 'mdash', ' ' : 'nbsp' }; if(_.has(entities,c) ){ return ('&'+entities[c]+';'); } return ''; }; const getCleanImgsrc = (imgsrc) => { let parts = imgsrc.match(/(.*\/images\/.*)(thumb|med|original|max)([^?]*)(\?[^?]+)?$/); if(parts) { return parts[1]+'thumb'+parts[3]+(parts[4]?parts[4]:`?${Math.round(Math.random()*9999999)}`); } return; }; const getRingedPair = (id) => { var ringed; _.find(state.Facing.ringed,(slaveid,masterid) => { if(id === masterid || id === slaveid) { ringed = { master: getObj('graphic',masterid), slave: getObj('graphic',slaveid) }; ringed.attribute = findObjs({ type: 'attribute', name: state.Facing.config.attributeName, characterid: ringed.master && ringed.master.get('represents') })[0] || {set:()=>{}}; return true; } return false; }); return ringed; }; const getRinged = (id) => { let ringed; _.find(state.Facing.ringed,(slaveid,masterid) => { if(id === masterid){ ringed = getObj('graphic',slaveid); return true; } if(id === slaveid) { ringed = getObj('graphic',masterid); return true; } return false; }); return ringed; }; const createRinged = (id) => { // get root obj const master = getObj('graphic',id); let slave = getRinged(id); if(!slave && master) { let layer=( 'gmlayer' === master.get('layer') ? 'gmlayer' : state.Facing.config.indicatorLayer); let dim=(Math.max(master.get('height'),master.get('width'))*state.Facing.config.scale); slave = createObj('graphic',{ imgsrc: state.Facing.config.image, layer: layer, pageid: master.get('pageid'), top: master.get('top'), left: master.get('left'), height: dim, width: dim, rotation: master.get('rotation') }); master.set({ rotation: 0 }); ( findObjs({ type: 'attribute', name: state.Facing.config.attributeName, characterid: master.get('represents') })[0] || (master.get('represents') && createObj('attribute',{ name: state.Facing.config.attributeName, characterid: master.get('represents') })) || { set: ()=>{} }).set({ current: slave.get('rotation') }); if('gmlayer' === layer || 'objects' == state.Facing.config.indicatorLayer) { toBack(slave); } else { toFront(slave); } state.Facing.ringed[master.id]=slave.id; } }; const fixIndicatorLayer = () => { let masters = Object.keys(state.Facing.ringed); const burndown = () => { let id = masters.shift(); let defer = 0; if(id){ let m = getObj('graphic',id); let s = getObj('graphic',state.Facing.ringed[id]); if(s){ let layer = m.get('layer'); let sLayer = s.get('layer'); switch(layer){ case 'gmlayer': if(sLayer !== 'gmlayer'){ s.set({ layer: 'gmlayer'}); toBack(s); defer = 50; } break; default: if(sLayer !== state.Facing.config.indicatorLayer) { s.set({ layer: state.Facing.config.indicatorLayer }); if( 'objects' == state.Facing.config.indicatorLayer) { toBack(s); defer = 50; } else { toFront(s); defer = 1000; } } break; } } setTimeout(burndown,defer); } }; setTimeout(burndown,0); }; const removeRinged = (id) => { var pair=getRingedPair(id); if(pair) { if(id === pair.master.id ) { pair.slave.remove(); } delete state.Facing.ringed[pair.master.id]; } }; const zeroToken = (id) => { var pair=getRingedPair(id); if(pair) { pair.slave.set({ rotation: 0 }); } }; const facingToken = (id) => { var pair=getRingedPair(id); if(pair) { removeRinged(id); } else { createRinged(id); } }; const handleRemoveToken = (obj) => { // special handling for deleting slaves? removeRinged(obj.id); }; const handleTokenChange = (obj,prev) => { let pair = getRingedPair(obj.id); if(pair) { if(pair.master.id === obj.id) { let loc = { top: obj.get('top'), left: obj.get('left'), width: obj.get('width'), height: obj.get('height') }; if(state.Facing.config.centering && ( loc.left !== prev.left || loc.top !== prev.top) && ( loc.width < 70 || loc.height < 70) ){ loc.left = loc.left+35-(loc.width/2); loc.top = loc.top+35-(loc.height/2); } let layer=( 'gmlayer' === pair.master.get('layer') ? 'gmlayer' : state.Facing.config.indicatorLayer ); let dim=(Math.max(pair.master.get('height'),pair.master.get('width'))*state.Facing.config.scale); let rot=pair.master.get('rotation'); if(rot !== prev.rotation ) { if(state.Facing.config.relative) { rot = (pair.slave.get('rotation') + rot + 360) % 360; } } else { rot = pair.slave.get('rotation'); } pair.attribute.set({ current: rot }); pair.slave.set({ layer: layer, top: loc.top, left: loc.left, height: dim, width: dim, rotation: rot }); pair.master.set({ rotation: 0, top: loc.top, left: loc.left }); if('gmlayer' === layer || 'objects' == state.Facing.config.indicatorLayer) { toBack(pair.slave); } else { toFront(pair.slave); } } else { pair.slave.set({ width: prev.width, height: prev.height, top: prev.top, left: prev.left, layer: prev.layer, flipv: prev.flipv, fliph: prev.fliph }); pair.attribute.set({ current: pair.slave.get('rotation') }); } } }; const getConfigOption_RingImage = () => { var text = state.Facing.config.image; return '
'; }; const getConfigOption_AttributeName = () => { var text = state.Facing.config.attributeName; return 'Facing adds a ring below the selected token with a pointer for the direction the token is facing. Rotating the token will rotate the ring and then the token will reset to no rotation. If the token is associated with a character, Facing will maintain an attribute on the character with the current facing stored in it.
' +'Adds or removes the Facing ring below a token.
' +'Aligns the indicator to north.
' +'