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
This post has been closed. You can still view previous posts, but you can't post any new replies.

[Idea] Generic dynamic lighting support

1455790909
Lucian
Pro
API Scripter
Hey guys, I had an idea, and before I spend ages wasting my time on something impossible, I thought I'd put it out there for people to shoot down with reasons why it'll never work :-) I know that Stephen S. has already made a set of Dungeon Tiles that work with his dynamic lighting script - it's a really cool system that makes building dungeons super quick. The downsides are that a) you have to manually set up all the artwork thanks to the Roll20 security restrictions (boo!) and b) it only works with his tiles at the moment AIUI. But it set me thinking, why shouldn't there be a generic system? Place a dungeon tile or map (from anywhere) Enter recording mode Draw the dynamic lighting layer by hand. The script records what you draw and saves it keyed against the tile somehow Exit recording mode Now whenever you drop that tile, the script can automatically redraw the dynamic lighting layer for you. It will also track changes in scale, rotation and position as you mess about with your dungeon (This the good bit)  Drop one or more pre-recorded tiles, select them all Run the "export" command It spits out a whole load of JSON describing the DL paths for those tiles Pass the JSON over to whoever else wants to use the same tiles They dump the same set of tiles on the canvas, select them, and run the import command, along with the JSON string you provided Hey presto, you can now distribute dynamic lighting templates with your dungeon tiles. I guess there's a good reason why this won't work, because otherwise Aaron would presumably have written it already... so what doesn't work? :-) Lucian
1455793170
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
Acccess to tags.... would go a long way to fixing this... and tags coming with the URL (or some other metadata solution.) There is no good way to realate what the API knows about a tile... to a URL.... unless its a common URL.
1455793432

Edited 1455793545
Ziechael
Forum Champion
Sheet Author
API Scripter
From my pathetically limited understanding of the API the main issue I can see here is that the API cannot access images in this context unless they are in your personal image library (there is a well  supported suggestion about allowing access to the marketplace that would potentially support your awesome idea). [edit] must refresh before posting in future ;)
1455794239
Lucian
Pro
API Scripter
Hey, I might be wrong, but I think you are misunderstanding what I'm trying to achieve here. The image tags/URL issue is, of course, a complete blocker if you want to be able to get a script to create/edit map tiles - you can't create graphics on the canvas that aren't in the library, which is a total PITA. But that's not what I'm proposing. What I'm proposing is to have the script respond to the user dumping marketplace tiles onto the canvas. All it needs to be able to do is to recognise the URL of the marketplace tile, which AIUI, remains constant between users, and then match that up with the appropriate set of dynamic lighting paths. In effect, I'm turning Stephen's script on its head to work around the restriction. Do you see what I mean? Cheers, Lucian
1455794688
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
"All it needs to be able to do is to recognise the URL of the marketplace tile, which AIUI, remains constant between users, and then match that up with the appropriate set of dynamic lighting paths. " That's a fair point... 
1455828808
Lithl
Pro
Sheet Author
API Scripter
This sounds totally doable. IIRC there can be some complications with rotating or resizing DL paths, and the export/import would require that the tiles are either in someone's library or rely on people getting the exact same image from the web, but it sounds like it could be a cool script.
1455829625
The Aaron
Pro
API Scripter
Rotation : the lines must be redrawn for a rotated tile, rotation of paths is not respected on the DL Layer. Tiles : Everyone should have the same image url for Marketplace assets (less the cache buster value on the end), so a script could key into that particular detail to match up the DL paths.
1455829746
Lucian
Pro
API Scripter
Brian said: This sounds totally doable. IIRC there can be some complications with rotating or resizing DL paths, and the export/import would require that the tiles are either in someone's library or rely on people getting the exact same image from the web, but it sounds like it could be a cool script. Just writing it now and it works pretty nicely. You teach it where the light-blocking layer for a given graphic is, and it stores that keyed against the URL of the marketplace image. When you drop another copy of that marketplace image onto the canvas, it looks up to find if it already knows that URL, and if so, draws a copy of the paths that it has stored at the appropriate offsets from the new tile's position. The main use will be for marketplace content, really, until Roll20 expose tags or something similar to the API. Naturally, you will need to have purchased/otherwise have access to the relevant marketplace content for it to work, but you don't need to upload it to your library manually (and indeed, doing so will prevent the import function from working, since the URL will change!) Of course, there's nothing stopping you from using the basic functionality for content that you have uploaded to your library, you just won't be able to share the light-blocking paths with anyone else at the moment... Resizing is relatively easy - you just need to store the size of the tile when the light-blocking layer was drawn, and scale the offsets appropriately. Rotation, on the other hand, is going to be a PITA because the DL layer doesn't support it :-( I will have to delete all the paths, calculate the transformations by hand, and then redraw them. Given that my mathz skillz are... weak... I think I may be spending some time on google looking for rotation algorithms :-) Lucian
1455829818
Lucian
Pro
API Scripter
Sigh, Ninja-ed on my own thread by Aaron. How does he do it!!??
1455834445
The Aaron
Pro
API Scripter
(Magic!  =D)
1455836979
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
_.each(findObjs({type:'path', controlledby: obj.id}), function(e) { e.remove(); }); Tucking the obj.id into the paths controlledby is the best way I have found to handle the removal prior to redraw (or on delete of the graphic.) Path control by can only be editted by the API after create (no user interface.)
1455839894
The Aaron
Pro
API Scripter
Generalized rotation of a path isn't too hard.  Here's some code I whipped up (functions for rotation bolded): on('ready',function(){     "use strict";     var bounds = function(p){         return _.reduce(p,function(m,p){             m.minX=Math.min(p[1],m.minX);             m.minY=Math.min(p[2],m.minY);             m.maxX=Math.max(p[1],m.maxX);             m.maxY=Math.max(p[2],m.maxY);             return m;         },{minX:Infinity,maxX:0,minY:Infinity,maxY:0});     },     center = function(bounds){         return {             x: bounds.minX + ((bounds.maxX-bounds.minX)/2),             y: bounds.minY + ((bounds.maxY-bounds.minY)/2)         };     },     rotate = function(p,theta){         var b=bounds(p),             c=center(b),             sinT=Math.sin( theta*Math.PI/180.0),             cosT=Math.cos( theta*Math.PI/180.0),             newBounds={                 minX:Infinity,                 minY:Infinity,                 maxX:0,                 maxY:0             },             points =_.chain(p)                 .map(function(point){                     var pointPrime=_.clone(point);                     pointPrime[1]=cosT*(point[1]-c.x) - sinT*(point[2]-c.y) + c.x;                     pointPrime[2]=sinT*(point[1]-c.x) + cosT*(point[2]-c.y) + c.y;                     newBounds.minX=Math.min(pointPrime[1],newBounds.minX);                     newBounds.minY=Math.min(pointPrime[2],newBounds.minY);                     newBounds.maxX=Math.max(pointPrime[1],newBounds.maxX);                     newBounds.maxY=Math.max(pointPrime[2],newBounds.maxY);                     return pointPrime;                 })                 .map(function(p){                     p[1]-=newBounds.minX;                     p[2]-=newBounds.minY;                     return p;                 })                 .value(),             newCenter=center(newBounds);         return {             path: points,             bounds: newBounds,             center: newCenter,             offset: {                 x: newCenter.x-c.x,                 y: newCenter.y-c.y             }         };     };     on('chat:message',function(msg){         var match=msg.content.match(/^!rotate-path\s+(\S+)/),             path,details,angle;         if (msg.type !== "api") {             return;         }         if(match && msg.selected){             angle=parseFloat(match[1]);             path=getObj('path',msg.selected[0]._id);             if(path){                 details=rotate(JSON.parse(path.get('path')),angle);                 createObj('path', {                     pageid: path.get('pageid'),                     fill: path.get('fill'),                     stroke: path.get('stroke'),                     rotation: path.get('rotation'),                     layer: path.get('layer'),                     stroke_width: path.get('stroke_width'),                     width: details.bounds.maxX-details.bounds.minX,                     height: details.bounds.maxY-details.bounds.minY,                     path: JSON.stringify(details.path),                     top: path.get('top')+details.offset.y,                     left: path.get('left')+details.offset.x,                     scaleX: 1,                     scaleY: 1                 });             }         }     }); }); Throw that in the API and you can type: !rotate-path <angle> and it will make an identical copy of the selected path but with the points all rotated. Functions of note: bounds:  Takes an array of path points (JSON.parse(path.get('path')) to get the points) and returns the bounds as an object with properties minX,minY,maxX,maxY. center: Takes a bounds object and returns the center point. rotate: takes an array of path points (same as bounds) and an angle in degrees and returns an object with the new path, the center of the path (origin top left corner), the bounds of the path (as above) and the offset of the center from the original path object. If you're going to make paths you rotate about to the right angle as tiles are moved, I suggest always starting from a "gold master" path for the tile and rotating that to prevent rounding errors from starting to distort your path. Also... SPIRALGRAPHS !!!
1455840968
Lucian
Pro
API Scripter
Haha, thanks! I'd pretty much got there myself, although it's a tiny bit more complicated because obviously the origin of the rotation is the centre of the tile for which the paths describe the DL boundaries rather than centre of the path itself. Your code is prettier than mine (obviously), although I did save myself a second pass through the points to reset the bounds by rotating the origin and bottom right corners first to calculate the offsets before I went through and rotate the points. Not that it would probably make any difference to the speed in practice, unless you had a *very* big path :-) Cheers, Lucian
1455841792
The Aaron
Pro
API Scripter
Ah, but did you make a spiralgraph????  =D Sounds good!  =D
1455848043

Edited 1455848112
Lithl
Pro
Sheet Author
API Scripter
Aaron, you should probably note that your code only works on polygons and polylines, not ovals or freehand drawings. Of course, freehand drawings aren't permitted on the DL layer and ovals don't work properly on the DL layer, so it's certainly sufficient in this case, but it can produce... interesting things with other paths: Interestingly, if you rotate the oval or freehand a full 360 degrees, it does  return to its original shape. Of course, the reason for the wackiness is because with the exception of the first point in an oval's path array, all of the elements are  Bézier curve control points rather than simple points; with the exception of the first and last points in a freehand drawing, all of the points in its path are  quadratic curve control points . Rotating the control points would probably  be sufficient to correctly rotate ovals and freehand drawings, but since the former are near-useless on the DL layer, the latter are not permitted on the DL layer, and this script isn't really necessary for paths not  on the DL layer, I don't think it's a huge issue.
1455848752
The Aaron
Pro
API Scripter
I like the 315 degrees!  Looks like a stylized leaf..  =D
1455856461
Lucian
Pro
API Scripter
So here's first cut of what I've written. Still has lots of holes, haven't implemented flipV and flipH, but rotation and scaling basically work. Haven't put it up on GitHub yet but interested in anyone's feedback in the meantime.... // Github:    // By:       Lucian Holland // Contact:   var DynamicLightRecorder = DynamicLightRecorder || (function() {     'use strict';     var version = '0.1',         lastUpdate = 1455059736,         schemaVersion = 0.1,              checkInstall = function() {         log('-=> DynamicLightRecorder v'+version+' <=-  ['+(new Date(lastUpdate*1000))+']');         if( ! _.has(state,'DynamicLightRecorder') || state.DynamicLightRecorder.version !== schemaVersion) {             log('  > Updating Schema to v'+schemaVersion+' <');             switch(state.DynamicLightRecorder && state.DynamicLightRecorder.version) {                                  default:                     state.DynamicLightRecorder = {                         version: schemaVersion,                         tilePaths: {},                         tileTemplates: {},                         config: {                         }                     };                     break;             }         }     },          handleInput = function(msg) {        if (msg.type !== "api" ) {             return;         }         var args = msg.content.split(/\s+--/);         switch(args.shift()) {             case '!dl-attach':                 attach(msg.selected, !_.isEmpty(args) && args.shift() === 'overwrite');                 break;             case '!dl-dump':                 sendChat('DynamicLightRecorder', JSON.stringify(state.DynamicLightRecorder));                 break;             case '!dl-wipe':                 sendChat('DynamicLightRecorder', 'Wiping all data');                 state.DynamicLightRecorder.tilePaths = {};                 state.DynamicLightRecorder.tileTemplates = {};                 break;             case '!dump-obj':                 sendChat('', JSON.stringify(_.map(msg.selected, function(obj){ return getObj(obj._type, obj._id)})));                 break;             default:             //Do nothing         }     },          attach = function(selection, overwrite) {                  if (!selection || _.isEmpty(selection) || selection.length < 2) {             sendChat('DynamicLightRecorder', 'You must have one map tile and at least one path select to run attach');             return;         }                           var objects = _.map(selection, function(object) {             return getObj(object._type, object._id);         });                  var tiles = [];         var paths = [];         _.each(objects, function(object) {             if(object.get('_type') === 'path') {                 paths.push(object);             }             else {                 tiles.push(object);             }         });                  if (_.isEmpty(tiles) || tiles.length !== 1) {             sendChat('DynamicLightRecorder', 'You must have exactly one map tile to run attach');             return;         }                           var tile = tiles[0];         if (tile.get('_subtype') !== 'token' || !tile.get('imgsrc') || tile.get('imgsrc').indexOf('marketplace') === -1 || tile.get('layer') !== 'map') {             sendChat('DynamicLightRecorder', 'Selected tile must be from marketplace and must be on the map layer.');             return;         }                  if (state.DynamicLightRecorder.tileTemplates[tile.get('imgsrc')] && !overwrite) {            sendChat('DynamicLightRecorder', 'Tile already has dynamic lighting paths recorded. Call with --overwrite to replace them');            return;         }                  var template = {             top: tile.get('top'),             left: tile.get('left'),             width: tile.get('width'),             height: tile.get('height'),             flipv: tile.get('flipv'),             fliph: tile.get('fliph'),             rotation: tile.get('rotation'),             paths: _.map(paths, function(path) {                 var savedPath = {                     path: path.get('_path'),                     offsetY: path.get('top') - tile.get('top'),                     offsetX: path.get('left') - tile.get('left'),                     width: path.get('width'),                     height: path.get('height'),                     layer: 'walls'                 };                 return savedPath;             })         };         state.DynamicLightRecorder.tileTemplates[tile.get('imgsrc')] = template;              },          handleTokenChange = function(token, previous) {         var paths = getPathsForToken(token);         var template = state.DynamicLightRecorder.tileTemplates[token.get('imgsrc')];         if (template) {             drawTokenPaths(token, paths, template);         }     },          getPathsForToken = function(token) {         var tile = state.DynamicLightRecorder.tilePaths[token.id];         if (!tile) {             return [];         }                  var paths = _.map(tile.pathIds, function(pathId) {             var path = getObj('path', pathId);             if (!path) {                 log('Warning, path with id [' + pathId + '] that should have been attached to token ' + JSON.stringify(token) + ' was not present.');             }             return path;         });         return paths;     },          SIZE_ATTRIBUTE_LOOKUP = {         'height': ['top', 'scaleY'],         'width': ['left', 'scaleX']     },              rotatePoint = function(point, centre, angle) {         angle = angle % 360;         var s = Math.sin(angle * Math.PI / 180.0);         var c = Math.cos(angle * Math.PI / 180.0);         // translate point back to origin:         var x = point[0] - centre[0];         var y = point[1] - centre[1];         // rotate point         var xnew = (x * c) - (y * s);         var ynew = (x * s) + (y * c);         // translate point back:         x = xnew + centre[0];         y = ynew + centre[1];         return [x,y];     },          scalePaths = function(token, template, paths) {         _.each(_.keys(SIZE_ATTRIBUTE_LOOKUP), function(attribute) {             var scaleFactor = token.get(attribute) / template[attribute];             _.chain(paths).compact().each(function(path) {                 path.set(SIZE_ATTRIBUTE_LOOKUP[attribute][1], scaleFactor);                 var positionAttribute = SIZE_ATTRIBUTE_LOOKUP[attribute][0];                 var existingOffset = path.get(positionAttribute) - token.get(positionAttribute);                 var scaledOffset = existingOffset * scaleFactor;                 path.set(positionAttribute, token.get(positionAttribute) + scaledOffset);             });         });      },          buildRotatedPaths = function(token, template) {                  var angle = token.get('rotation');         angle -= template.rotation;         var tokenCentre = [token.get('left'), token.get('top')];         return _.map(template.paths, function(path) {                 var existingCentre = [token.get('left') + path.offsetX, token.get('top') + path.offsetY];                 var newCentreOfPath = rotatePoint(existingCentre, tokenCentre, angle);                 var points = JSON.parse(path.path);                 log(points);                 var bottomRight = _.reduce(points, function(result, point) {                             result[0] = Math.max(point[1], result[0]);                             result[1] = Math.max(point[2], result[1]);                             return result;                         }, [0,0]);                                  var pointsCentre = [bottomRight[0]/2, bottomRight[1]/2];                                  var transformedOrigin = rotatePoint([0,0], pointsCentre, angle);                 var transformedBottomRight = rotatePoint(bottomRight, pointsCentre, angle);                                  var xMin = Math.min(transformedOrigin[0], transformedBottomRight[0]);                 var yMin = Math.min(transformedOrigin[1], transformedBottomRight[1]);                                  var xMax = Math.max(transformedOrigin[0], transformedBottomRight[0]);                 var yMax = Math.max(transformedOrigin[1], transformedBottomRight[1]);                                  var xAdjustment = Math.min(0, xMin);                 var yAdjustment = Math.min(0, yMin);                                  var newHeight = yMax - yAdjustment;                 var newWidth = xMax - xAdjustment;                                  var newPoints = _.map(points, function(point) {                     var result = rotatePoint(_.rest(point, 1), pointsCentre, angle);                     //Adjust to reset the top-left of the final shape to 0,0                     result[0] -= xAdjustment;                     result[1] -= yAdjustment;                     result.unshift(point[0]);                     return result;                 });                                  var newPath = {                         layer: 'walls',                         path: JSON.stringify(newPoints),                         left: newCentreOfPath[0],                         top: newCentreOfPath[1],                         pageid: token.get('_pageid'),                         width: newWidth,                         height: newHeight                     };                      return createObj('path', newPath);             });     },          drawTokenPaths = function(token, existingPaths, template) {         _.invoke(_.compact(existingPaths), 'remove');         var paths;         if(token.get('rotation') !== template.rotation) {             paths = buildRotatedPaths(token, template);         }         else {             paths = _.map(template.paths, function(templatePath) {                 var attributes = _.clone(templatePath);                 attributes.pageid = token.get('_pageid');                 attributes.left = token.get('left') + attributes.offsetX;                 attributes.top = token.get('top') + attributes.offsetY;                 return createObj('path', attributes);             });         }                  scalePaths(token, template, paths);         state.DynamicLightRecorder.tilePaths[token.id].pathIds = _.pluck(paths, 'id');     },          handleNewToken = function(token) {         var template = state.DynamicLightRecorder.tileTemplates[token.get('imgsrc')];         if (!template) {             return;         }                  var tileObject = {             tileId: token.id         };         state.DynamicLightRecorder.tilePaths[token.id] = tileObject;         drawTokenPaths(token, [], template);        },          handleDeleteToken = function(token) {         log('deleting paths for token ' + token.id);         var paths = getPathsForToken(token);         _.invoke(_.compact(paths), 'remove');         delete state.DynamicLightRecorder.tilePaths[token.id];     },          registerEventHandlers = function() {         on('chat:message', handleInput);         on('change:token', handleTokenChange);         on('add:token', handleNewToken);         on('destroy:graphic', handleDeleteToken);     };     return {         RegisterEventHandlers: registerEventHandlers,         CheckInstall: checkInstall     }; }());     on("ready",function(){     'use strict';         DynamicLightRecorder.CheckInstall();         DynamicLightRecorder.RegisterEventHandlers(); });
1455856625
The Aaron
Pro
API Scripter
(Started with GroupInitiative as a base, eh?  =D) Looking forward to trying it out after I'm done gaming for the night. =D
1455874918
Lucian
Pro
API Scripter
The Aaron said: (Started with GroupInitiative as a base, eh?  =D) Looking forward to trying it out after I'm done gaming for the night. =D Haha, Busted! ;-) Imitation is the sincerest form of flattery and all that... L.
1455880311

Edited 1455882085
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
I would drop the state tracking for this relationship (graphic to path.) Its less overhead and that "controlby" for paths is just sitting there :P unleveraged for anything.... only API can ever change it. It also makes Transmogrifier efforts easier down the road.  Edit... We are always going to have a know URL....  h t t p s://s3.amazonaws.com/files.d20.io/marketplace/4378/max.png? 1339090409  That part after the "?" Can be used too. And will also survive the Transmogrifier, 1) Known URL is dropped on the map ( obj.get('imgsrc').split('?')[0]  === "I know that one" ) 2) obj.set('imgsrc', obj.get('imgsrc').split('?')[0] + obj.id);   //I now have the "create id" encoded in the imgsrc. 3) createObj('path', {pathstuff: stuff, controlby:  obj.get('imgsrc').split('?')[1]} ); //path is related to the "create id". I now have the URL, path data that comes from knowing the URL, the "create id", and paths related to the "create id". And this will survive theTransmogrifier.... 
1455882357
Lucian
Pro
API Scripter
Stephen S. said: I would drop the state tracking for this relationship (graphic to path.) Its less overhead and that "controlby" for paths is just sitting there :P unleveraged for anything.... only API can ever change it. It also makes Transmogrifier efforts easier down the road.  Ah, I understand what you are saying now - I hadn't worked out what you meant before. That makes a lot of sense, I will change that for the token->path relationship for the next version. I'm thinking that I might continue to store the templates in state, though, since these aren't specific to an individual token. I guess I could make a character entry to store them, but given that I want to write import/export as JSON anyway I'm not sure that there's much benefit. What do you think? controlledby instead of state for token->path fliph/flipv support import/export door token support any other ideas? Cheers, Lucian
1455882755
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
For flip... given.fliph ? given.width - eachPoint[0] : eachPoint[0], given.flipv ? given.height - eachPoint[1] : eachPoint[1] (where "eachPoint[0]" = path x for that point.... same for path y....  you just got to do that before any rotation math starts) Using that "controlby" and "imgsrc" trick lets you skip going back to state... so I would think its a tad faster (you already had to grab the graphic obj and the path obj, might as well have the data already there.) Also if someone deletes your wall path or a GM moves your wall path... you can see it has a controlby and undelete and/or snap it back into place.
1455883917

Edited 1455883972
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
Doors...... In that past I have used token actions (relate the door tile to a recordsheet) and just use image swap.... open or closed. Aaron's coaching on this is basically "You haven't considered everything a user might want to do... like 'cracking the door open to peek.'" Aaron is COMPLETELY WRONG. I have considered it... I am just lazy, apathetic, and concerned only for my ideas and not what the consumer actually wants. But since you might write this... I am thinking Aaron has a small point, even if by accident.  I am thinking now a variaition of the "bump" script.. were you have clear PNG that represents the door hinge rotation or % open for sliding doors. You can use "aura" to make this token always visible to the GM and players that can open the door (whoever has a key or a thief that has picked the lock.) The rotation of that "bump" image = % open for the door.  This would also work well for secret doors.
1455884565
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
import/export Thinking we need to have it in SVG format... so you can go from GIMP (or whatever) to path format. But all external data storage and maybe even manipulation is SVG...&nbsp; <a href="https://www.dashingd3js.com/svg-basic-shapes-and-d" rel="nofollow">https://www.dashingd3js.com/svg-basic-shapes-and-d</a>... But not sure.
1455892173
Lucian
Pro
API Scripter
Stephen S. said: 1) Known URL is dropped on the map ( obj.get('imgsrc').split('?')[0] &nbsp;=== "I know that one" ) 2) obj.set('imgsrc',&nbsp;obj.get('imgsrc').split('?')[0] + obj.id); &nbsp; //I now have the "create id" encoded in the imgsrc. 3) createObj('path', {pathstuff: stuff, controlby: &nbsp;obj.get('imgsrc').split('?')[1]} ); //path is related to the&nbsp;"create id". I now have the URL, path data that comes from knowing the URL, the "create id", and paths related to the "create id". OK, I'm a bit confused now. Won't step 2 fail (you can't set marketplace URLs on objects through the API)? And &nbsp;I'm not sure what the point of having the object id encoded in the img src of the token is, given that you have it on the token already? It seems easier just to have a lookup from imgsrc (with or without the cache-buster query string, depending on whether we want to preserve the links with new versions of images) to a set of path data and then use that to draw the new paths whenever a recognised image is dropped on the canvas. But TBH I'm not sure that I'm really understanding what you're suggesting here. The other question is, what do you do about deleting tokens? Part of the reason for having a token-&gt;path lookup in state was that I wanted to be able to delete the wall paths easily when the connected token was deleted. If I only store the relationship on the wall paths (in controlledby) then I have to iterate through all the paths on a page looking for references to a token whenever a token is deleted... or am I missing something here? Cheers, Lucian
1455895782
The Aaron
Pro
API Scripter
Regarding 2, you're right. &nbsp;That won't work (currently) because of the failure when setting a url that contains a marketplace image. &nbsp;Stephen's thing about this is that it survives transfer via the transmogrifier. &nbsp;Probably encoding it in the GM notes for the token would be a safer way to go to hook things back up post transmogrification. &nbsp; I'd probably favor putting the data in those places, but using it out of the state (or possibly building a lookup from it at startup). &nbsp;You could then have a relink command to fix up your state in a new game (or just do it automatically at startup, see DryErase's checkInstall()...). Regarding flattery, I just thought it was funny that I could identify which script you used. &nbsp;(and which version! =D) &nbsp;As Stephen well knows, I'm pleased to help and flattered at the imitation. =D &nbsp;(Stephen also know's he's pushing me to up my game by doing a better job of functional decomposition and encapsulation than I am... the jerk! =D) &nbsp;In my editor (Vim, the GREATEST EDITOR OF ALL TIME, BRIAN! =D), when I save an API script, it automatically updates the lastUpdate variable to the current unix timestamp. &nbsp;Since your editor doesn't do that, I just looked for that timestamp in my code to find your base model. =D &nbsp;
1455895874
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
var test_script = test_script || (function() { 'use strict'; var test_imsrc = '<a href="https://s3.amazonaws.com/files.d20.io/marketplace/67110/jhGxwsa3wP8c7SyjgaAD4A/max.jpg?1427316258" rel="nofollow">https://s3.amazonaws.com/files.d20.io/marketplace/67110/jhGxwsa3wP8c7SyjgaAD4A/max.jpg?1427316258</a>', //from a tile pack I created on the market. //change to any market asset URL you have access to. spam_log_urls = function() { //just to get the URL for hard coding. var currentPageGraphics = findObjs({ pageid: Campaign().get('playerpageid'), type: 'graphic' }); _.each(currentPageGraphics, function(obj) { log('imgsrc: ' + obj.get('imgsrc')); }); }, create_path = function (obj) { createObj('path', { left: obj.get('left'), top: obj.get('top'), width: obj.get('width'), height: obj.get('height'), pageid: obj.get('pageid'), layer: 'gmlayer', stroke_width: 2, stroke: '#ff0000', controlledby: obj.get('controlledby'), path: '[["M", 0, 0],["L", '+obj.get('width')+', 0],["L", '+obj.get('width')+', '+obj.get('height')+'],["L", 0, '+obj.get('height')+'],["L", 0, 0]]' }); }, remove_path = function (obj) { _.each(findObjs({type:'path', controlledby: obj.get('controlledby')}), function(e) { e.remove(); }); }, on_change_graphic = function (obj){ if( obj.get('imgsrc') === test_imsrc ){ if( '' === obj.get('controlledby') ){ obj.set('controlledby',obj.id); } remove_path(obj); create_path(obj); } }, on_destroy_graphic = function (obj){ remove_path(obj); }, registerEventHandlers = function() { on('change:graphic', on_change_graphic); on('destroy:graphic', on_destroy_graphic); }, ready_module = function () { spam_log_urls(); registerEventHandlers(); }; return { ready_module: ready_module }; }()); on('ready',function(){ 'use strict'; test_script.ready_module(); });
1455896375
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
That should survive the&nbsp;Transmogrifier.
1455898098

Edited 1455903579
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
" Probably encoding it in the GM notes for the token would be a safer way to go to hook things back up post transmogrification." For the graphic... We could look for a change to the controlledby (obj,prev) to make sure you keep the controlledby current with the "createid" (part of the delimited string... if anyone ever asigned control.) A lot of people toouch the GM notes... very few play around with the controlledby.... and the path&nbsp;controlledby can only be changed by the API. EDIT: Changing the "controlledby" only "adds"..... So if I set&nbsp;controlledby to the createId.... and then give me control (player) or all controll... the&nbsp;createId remains in the&nbsp;delimited string. You can't break it... without aggressive API code.. to jack up the controlledby. No.. it breaks it.... &nbsp;code below cares for that.
1455898660
The Aaron
Pro
API Scripter
That's cool. =D
1455903371
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
var test_script = test_script || (function() { 'use strict'; var test_imsrc = '<a href="https://s3.amazonaws.com/files.d20.io/marketplace/67110/jhGxwsa3wP8c7SyjgaAD4A/max.jpg?1427316258" rel="nofollow">https://s3.amazonaws.com/files.d20.io/marketplace/67110/jhGxwsa3wP8c7SyjgaAD4A/max.jpg?1427316258</a>', //from a tile pack I created on the market. //change to any market asset URL you have access to. spam_log_urls = function() { //just to get the URL for hard coding. var currentPageGraphics = findObjs({ pageid: Campaign().get('playerpageid'), type: 'graphic' }); _.each(currentPageGraphics, function(obj) { log('imgsrc: ' + obj.get('imgsrc')); }); }, create_path = function (obj) { createObj('path', { left: obj.get('left'), top: obj.get('top'), width: obj.get('width'), height: obj.get('height'), pageid: obj.get('pageid'), layer: 'gmlayer', stroke_width: 2, stroke: '#ff0000', controlledby: obj.get('controlledby'), path: '[["M", 0, 0],["L", '+obj.get('width')+', 0],["L", '+obj.get('width')+', '+obj.get('height')+'],["L", 0, '+obj.get('height')+'],["L", 0, 0]]' }); }, remove_path = function (obj) { var createid=obj.get('controlledby').match(/createid([^,]*)/),cb; if( null === createid ) { return; } createid=createid[0]; _.each(findObjs({type:'path', pageid: obj.get('pageid')}), function(e) { cb = e.get('controlledby').match(/createid([^,]*)/); if( null !== cb ){ cb = cb[0]; if( cb === createid ){ e.remove(); } } }); }, on_change_graphic = function (obj){ if( obj.get('imgsrc') === test_imsrc ){ if( '' === obj.get('controlledby') ){ obj.set('controlledby','createid'+obj.id); } remove_path(obj); create_path(obj); } }, on_destroy_graphic = function (obj){ remove_path(obj); }, on_change_graphic_controlledby = function (obj,prev){ var createid=prev.controlledby.match(/createid([^,]*)/); if( null === createid ) { return; } obj.set('controlledby',obj.get('controlledby')+','+createid[0]); }, registerEventHandlers = function() { on('change:graphic', on_change_graphic); on('destroy:graphic', on_destroy_graphic); on('change:graphic:controlledby', on_change_graphic_controlledby); }, ready_module = function () { spam_log_urls(); registerEventHandlers(); }; return { ready_module: ready_module }; }()); on('ready',function(){ 'use strict'; test_script.ready_module(); });
1455903533
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
That keeps the controlledby straight.... Had to get a "nudge" from Aaron to get that exmple working.... it can be further simplifed. But that should keep graphic and path relationship without state and without user corruption of the controlledby.
1455907392
Lucian
Pro
API Scripter
Aha, I think I finally understand. I couldn't see why you would both setting the controlledby value on the graphic to the object id - why not just use the object id? But now I realise it's because the object ids change after transmogrification, thus breaking all the links. I'll look at incorporating this into the script at some point. Also, I'm afraid I don't get what you mean about the PNG for doors. What would actually be visible in the end? Could you explain a bit more? The main thing I wanted was an easy way to handle making doors and then opening/closing them in play. I was thinking of doing the same thing as I have done with the map tiles (saving a DL pattern against an image URL), but adding some code support for rotating about one end rather than around the centre so they open properly. It can use the same DL management code as I do for the map tiles to keep the light blocking lined up. I guess that should enable some funky behaviours like "cracking the door open" relatively straightforwardly. Lucian
1455907919
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
1455908444

Edited 1455908472
Stephen S.
Pro
Marketplace Creator
Sheet Author
API Scripter
You can get really smooth and fun door interaction... The issue is "what is the user intercface?" Token action is a "click"..... so you can do that on a "door" token and just image swap and move the path... Or in the example above a "click" = a door opening over time. But then people like Aaron chime in and say.... "what if I just want to open the door 25%?" Now...people like him aren't fun, but they don't go away either, so you have to do something. Walls Layer----------------------------------- P = [proper path] walls Layer----------------------------------- Objects Layer--------------------------------- C = [clear PNG with "aura" so people with control can see it.] Objects Layer--------------------------------- Map Layer------------------------------------- D = [door image] Map Layer------------------------------------- P... that path will always align with the door (D) C... player/GM can rotate... but not move (always centered over the door) D... will rotate open or closed based on the roation of C (the control)
1455908662
Lucian
Pro
API Scripter
Ahhhh!. I see what you mean now, the clear PNG is like a handle, and then you use the API to translate the rotation/movement of the handle into movement of the door. Nice. Definitely up for trying to do that. Lucian
1455909549

Edited 1455909568
The Aaron
Pro
API Scripter
For bonus points, center the clear png on the hinge so it feels natural when rotating it. (Start its rotation to match the door do the rotate handle is where you world naturally think it should be. ) If Stephen were doing this I'd suggest one on the objects and one on the GM layer just to hear him try and weasel out of it! &nbsp;:)
1455909858
Lucian
Pro
API Scripter
Hey guys, have posted a proper script thread with a github link now&nbsp; <a href="https://app.roll20.net/forum/post/2987563/script-d" rel="nofollow">https://app.roll20.net/forum/post/2987563/script-d</a>... &nbsp;- perhaps we can move any further discussion there. Is there a way to close a thread here without deleting the original post? Lucian
1455909922
The Aaron
Roll20 Production Team
API Scripter
Nope.... but I can. =D &nbsp;Done!