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

Sorting and arranging selected tokens on the map

Hello, I've got a game where I'm using tokens to represent d10 dice using rollable tokens that players can move around on the map like they would on a tabletop.  Each token has 10 faces representing the different values on the die, and I have a script that will randomly assign a face to each selected die to randomize and "roll" those dice.  It's working great. My next goal is to figure out how to easily arrange and sort those "dice" tokens on the map.  After selecting multiple tokens, I would like a script that would move the tokens into a line next to the top leftmost of the tokens, and sort the tokens in descending order based on their currentside property.   I feel like I've seen an API or script that does something similar to this before, maybe sorting by token name instead of the currentside property, but my search fu is failing and I cannot find it.   Does anyone know of such a script, or have guidance on how to write it? Thanks in advance!
1707402906
keithcurtis
Forum Champion
Marketplace Creator
API Scripter
I started such a script once, but the math defeated me. :/ I would love an Arrange script.
1707429659

Edited 1707429686
The Aaron
Roll20 Production Team
API Scripter
Here's one you can start with.  MarshalTokens -- groups selected tokens together into a tight formation: on('ready',()=>{ const positioner = (x,y) => { const shell = (n) => Math.ceil(Math.sqrt(n)); const xyForN = (n) => { let s = shell(n); let sm = s-1; let sSeq=n-(Math.pow(sm,2)); let pSeq=Math.ceil(sSeq/2); return { x: (sSeq%2 ? pSeq : s)-1, y: (sSeq%2 ? s : pSeq)-1 }; }; let startx = x; let starty = y; let count=0; return (obj)=>{ let coord = xyForN(++count); obj.set({ top: starty+(coord.y*70), left: startx+(coord.x*70) }); }; }; const normalizer = (n) => { return (Math.max(0,Math.floor((n-35)/70))*70)+35; }; on('chat:message',function(msg){ if('api'===msg.type && /^!marshal-tokens(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){ let tokens = [...new Set([ ...msg.content.split(/\s+/).slice(1), ...((msg.selected && msg.selected.map(s=>s._id)) || []) ])] .map(id=>getObj('graphic',id)) .filter(g=>undefined !== g) ; let firstToken = (tokens[0]||{get:()=>0}); let x=normalizer(firstToken.get('left')); let y=normalizer(firstToken.get('top')); let poser = positioner(x,y); tokens.forEach(poser); } }); }); You call it with: !marshal-tokens For your changes, add this after the filter call (might need to swap a and b to get desc order but I think that's right): .sort((a, b) => b.get('currentSide') - a.get('currentSide')) Then replace the positioner with one that just puts them in a line: const asALine = (x,y) => { let offset = x; return (obj) => { obj.set({ top: y, left: offset }); offset += obj.get('width'); }; }; and you get something like (which I didn't test yet): on('ready',()=>{ const asALine = (x,y) => { let offset = x; return (obj) => { obj.set({ top: y, left: offset }); offset += obj.get('width'); }; }; const normalizer = (n) => { return (Math.max(0,Math.floor((n-35)/70))*70)+35; }; on('chat:message',function(msg){ if('api'===msg.type && /^!marshal-in-line(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){ let tokens = [...new Set([ ...msg.content.split(/\s+/).slice(1), ...((msg.selected && msg.selected.map(s=>s._id)) || []) ])] .map(id=>getObj('graphic',id)) .filter(g=>undefined !== g) .sort((a, b) => b.get('currentSide') - a.get('currentSide')) ; let firstToken = (tokens[0]||{get:()=>0}); let x=normalizer(firstToken.get('left')); let y=normalizer(firstToken.get('top')); let poser = asALine(x,y); tokens.forEach(poser); } }); }); call with: !marshal-in-line
1707432067

Edited 1707432116
The Aaron
Roll20 Production Team
API Scripter
k, I tried it!
Awesome, thank you so much! I've made a few tweaks for my own needs.  I allow players to run it too, and I loop through the selected tokens once to find the leftmost token and use that as the source for X and Y rather than the location of the die which rolled highest.  Here's the version I'm using currently: on('ready',()=>{   const asALine = (x,y) => {     let offset = x;     return (obj) => {       obj.set({         top: y,         left: offset       });       offset += obj.get('width');     };   };   const normalizer = (n) => {     return (Math.max(0,Math.floor((n-30)/70))*70)+35;   };      on('chat:message',function(msg){     if('api'===msg.type && /^!sort-dice(\b\s|$)/i.test(msg.content) ){     let x=0;     let y=0;       let tokens = [...new Set([         ...msg.content.split(/\s+/).slice(1),         ...((msg.selected && msg.selected.map(s=>s._id)) || [])       ])]         .map(id=>getObj('graphic',id))         .filter(g=>undefined !== g)         .sort((a, b) => b.get('currentSide') - a.get('currentSide'))         ;         for (var t in tokens){             if(x==0 || x>tokens[t].get('left') ) {                 x = tokens[t].get('left');                 y = tokens[t].get('top');             }           }                  x=normalizer(x);         y=normalizer(y);                let poser = asALine(x,y);       tokens.forEach(poser);     }   }); Thank you again, that is perfect!
1707496406

Edited 1707627762
The Aaron
Roll20 Production Team
API Scripter
Nice!  Here's how I would make those same changes, in case you're curious: on('ready',()=>{ const asALine = (x,y) => { let offset = x; return (obj) => { obj.set({ top: y, left: offset }); offset += obj.get('width'); }; }; const normalizer = (n) => { return (Math.max(0,Math.floor((n-35)/70))*70)+35; }; on('chat:message',function(msg){ if('api'===msg.type && /^!marshal-in-line(\b\s|$)/i.test(msg.content) && playerIsGM(msg.playerid)){ let x = Number.MAX_SAFE_INTEGER; let y = 0; let tokens = [...new Set([ ...msg.content.split(/\s+/).slice(1), ...((msg.selected && msg.selected.map(s=>s._id)) || []) ])] .map(id=>getObj('graphic',id)) .filter(g=>undefined !== g) .sort((a, b) => b.get('currentSide') - a.get('currentSide')) .map(o=>{ if(o.get('left')<x){ x = o.get('left'); y = o.get('top'); } return o; }) ; x=normalizer(x); y=normalizer(y); let poser = asALine(x,y); tokens.forEach(poser); } }); });
That's great!  Thanks! One note:  for anyone who wants to try this in the future, there's a typo near the end: y=normalizer(x); should be y=normalizer(y); Thanks again.
1707627785
The Aaron
Roll20 Production Team
API Scripter
Good catch! Corrected.