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

[Script,Snippet] Player Token Swapping -- Great for Echo Knights or abilities that let master and familiar trade places, etc.

1659715600
The Aaron
Roll20 Production Team
API Scripter
Here's a little script I threw together for one of my players who's playing an Echo Knight.  It lets him swap his Echo and character easily. !swap @{SomeCharacter|character_id} @{SomeOtherCharacter|character_id} It will find the first token representing the supplied character on the player's current page, and swap their positions.  It only works (currently) for characters the player can control (or any characters for the GM).  It will send a notice to the GM about the swapping, with a button to swap back, if the player isn't a GM.  It also works with token ids, should you want to use those.  I will probably expand this to ask for consent from players/gms if trying to swap with something you don't control (Looking at you, transposition abilities), and possibly add some optional status marker swapping (support for elevation changes). Code: on('ready',() => { const getPageForPlayer = (playerid) => { let player = getObj('player',playerid); if(playerIsGM(playerid)){ return player.get('lastpage') || Campaign().get('playerpageid'); } let psp = Campaign().get('playerspecificpages'); if(psp[playerid]){ return psp[playerid]; } return Campaign().get('playerpageid'); }; const playerCanControl = (obj, playerid='any') => { const playerInControlledByList = (list, playerid) => list.includes('all') || list.includes(playerid) || ('any'===playerid && list.length); let players = obj.get('controlledby') .split(/,/) .filter(s=>s.length); if(playerInControlledByList(players,playerid)){ return true; } if('' !== obj.get('represents') ) { players = (getObj('character',obj.get('represents')) || {get: function(){return '';} } ) .get('controlledby').split(/,/) .filter(s=>s.length); return playerInControlledByList(players,playerid); } return false; }; const getTokenForPlayerAndID = (pid,id) => { let t = getObj('graphic',id); if(!t){ let c = getObj('character',id); if(c){ t = findObjs({ type: 'graphic', pageid: getPageForPlayer(pid), represents: c.id })[0]; } } if(t && (playerIsGM(pid) || playerCanControl(t,pid))){ return t; } return; }; on('chat:message',msg=>{ if('api'===msg.type && /^!swap(\b\s|$)/i.test(msg.content)){ let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); let args = msg.content.split(/\s+/).slice(1); let t1 = getTokenForPlayerAndID(msg.playerid, args[0]); let t2 = getTokenForPlayerAndID(msg.playerid, args[1]); if(t1 && t2) { let o1 = { left: t2.get('left'), top: t2.get('top') }; let o2 = { left: t1.get('left'), top: t1.get('top') }; t1.set(o1); t2.set(o2); if(!playerIsGM(msg.playerid)){ sendChat('TokenSwap', `/w gm <code>${who}</code> just swapped <b>${t1.get('name')}</b> and <b>${t2.get('name')}</b>. <a href="!swap ${t1.id} ${t2.id}">Swap Back</a>`); } } else { sendChat('Swap', `/w "${who}" <div>Couldn't swap tokens.${!t1?` No token for ID: [${args[0]}]`:''}${!t1?` No token for ID: [${args[0]}]`:''}</div>`); } } }); });
!swap @{selected|character_id} @{target|character_id} Works nicely if you wish to use this script on the fly 
1659887355

Edited 1659887378
The Aaron
Roll20 Production Team
API Scripter
Here's a little gif I made of it for the player in question.
I like this api. One of my players is a Battle Master fighter with the Bait & Switch maneuver. How would I add this to his scriptcard macro so that when he chooses The Bait & Switch part it will swap the 2 tokens? Here is the macro. !scriptcard {{ --#bodyfontface|Helvetica --/#bodyfontsize|12px --#bodyfontcolor|#220000 --#buttonfontsize|9px --#buttonbackground|#58180d --#buttonfontface|Tahoma --#titlecardbackground|#f6efd6 --#titlefontcolor|#58180d --#titlefontsize|1.5em --#titlefontshadow|0 --#oddrowbackground|#f6efd6 --#evenrowbackground|#f6efd6 --#subtitlefontcolor|#58180d --#title|Combat Maneuver --#sourcetoken|@{selected|token_id} --#targettoken|@{target|token_id} --&TTokenId|@{target|token_id} --=SaveDC|8 + [*S:strength_mod] + [*S:pb] --=TargetSave|1d20 + [*T:strength_save_bonus] --=TargetWisSave|1d20 + [*T:wisdom_save_bonus] --=SuperiorDice|1d8 --=Dices|@{selected|repeating_resource_$0_resource_left}-1 --#leftsub|Dices Left: [$Dices] --=DiceTotal|@{selected|repeating_resource_$0_resource_left|max} --#rightsub|Total Dice: [$DiceTotal] --?[$Dices] -eq -1|NoDice --@setattr|_charid [*S:character_id] _silent _sel _mod _repeating_resource_$0_resource_left|-1 --=Maneuver|?{Which One?|Trip,1|Parry,2|DistractingStrike,3|Maneuvering,4|Goading,5|Commander's Strike,6|Bait & Switch,7} --C[$Maneuver.Total]|1:Trip|2:Parry|3:DistractingStrike|4:Maneuvering|5:Goading|6:Commander|7:Bait --:Trip| --?[$TargetSave.Total] -lt [$SaveDC]|TARGET_FAILED --:TARGET_PASSED| --+|[c]**Trip**[/c] --+Action Type:| After you hit with an weapon attack. --+Plus:|[$SuperiorDice] damage. --+Strength Save DC:|[$SaveDC] VS Enemy Save [$TargetSave] --+Passed Save:| Target remains standing. --^Final| --:TARGET_FAILED| --+|[c]**Trip**[/c] --+Action Type:| After you hit with an weapon attack. --+Plus:|[$SuperiorDice] damage. --+Strength Save DC:|[$SaveDC] VS Enemy Save [$TargetSave] --+Failed Save:| Target is knocked prone. --@token-mod| _ids @{target|token_id} _set statusmarkers#Prone::243679 --^Final| --:Parry| --+|[c]**Parry**[/c] --=Parry|[$SuperiorDice] + [*S:dexterity_mod] --+Action Type:| Reaction --+Parry:| You parry [$Parry] damage from the incoming attack. --^Final| --:DistractingStrike| --+|[c]**Distracting Strike**[/c] --+Action Type:| Part of an attack. --+Plus:| [$SuperiorDice] damage. --+Effect:| The next attack roll against the target by an attacker other than you has advantage if the attack is made before the start of your next turn. --^Final| --:Maneuvering| --+|[c]**Maneuvering Attack**[/c] --+Action Type:| Part of an attack. --+Plus:| [$SuperiorDice] damage. --+Effect:| Choose a friendly creature who can see or hear you. That creature can use its reaction to move up to half its speed without provoking opportunity attacks from the target of your attack. --^Final| --:Goading| --?[$TargetWisSave.Total] -lt [$SaveDC]|TARGETWIS_FAILED --:TARGETWIS_PASSED| --+|[c]**Goading Attack**[/c] --+Action Type:| After you hit with an weapon attack. --+Plus:|[$SuperiorDice] damage. --+Wisdom Save DC:|[$SaveDC] VS Enemy Save [$TargetWisSave] --+Passed Save:| The enemy doesn't fall for your trick. --^Final| --:TARGETWIS_FAILED| --+|[c]**Goading Attack**[/c] --+Action Type:| After you hit with an weapon attack. --+Plus:|[$SuperiorDice] damage. --+Wisdom Save DC:|[$SaveDC] VS Enemy Save [$TargetWisSave] --+Failed Save:| The target has disadvantage on all attack rolls against targets other than you until the end of your next turn --^Final| --:Commander| --+|[c]**Commander's Strike**[/c] --+Action Type:| Attack action & Bonus action! --+Plus:| [$SuperiorDice] Damage --+Effect:| You can forgo one of your attacks and use a bonus action to direct an ally to Strike. --+Ally:| Must be able to hear or see you and have a reaction to make one weapon attack. --^Final| --:Bait| --+| [c]**Bait & Switch**[/c] --+Action Type:| Move Action --+Plus:| [$SuperiorDice] to Targets or Yours AC. Until the start of your next turn. --+Effect:| When you are within 5' of a Creature, you can switch spots with them. --+Requirement:| You must have 5' of movement left and target must be willing. --+| This movement doesn't provoke attacks of opportunity. --^Final| --:NoDice| --+| [c] **SORRY!!! You are out of Superior Dice!** [/c] --:Final| }}
1659957174

Edited 1659957188
David M.
Pro
API Scripter
Snow, since you've already added a #sourcetoken and #targettoken, try adding the following somewhere in your Bait code block. (Note:  untested air coding) --@swap|[*S:t-id] [*T:t-id]
1659962735
The Aaron
Roll20 Production Team
API Scripter
Hmm. Currently, that's going to fail as the script only allows swapping of tokens the player who calls it can control, unless that player is a GM, in which case it allows any swap. When another script calls it, the "player" is the API, which controls no tokens and is not a GM.  I'll see about some modifications to allow this to work. 
1659963883
David M.
Pro
API Scripter
Aaron, here's the version I got from you that we are using in my game. Less flexible ( only accepts token_ids) and n o protections, but it still sends the GM a message if a player uses it. on('ready',function(){ "use strict"; on('chat:message',function(msg){ let args, who; if (msg.type === "api" && msg.content.indexOf("!token-swap")==0) { try { who = getObj('player',msg.playerid).get('_displayname'); args = msg.content.split(/\s+/); let t1 = getObj('graphic',args[1]); let t2 = getObj('graphic',args[2]); if(!t1 || !t2) { sendChat('TokenSwap', `/w "${who}" Please provide 2 token ids to swap: <code>!token-swap ID1 ID2</code>`); return; } let p1={ top: t1.get('top'), left: t1.get('left') }; let p2={ top: t2.get('top'), left: t2.get('left') }; t1.set(p2); t2.set(p1); if(!playerIsGM(msg.playerid)){ sendChat('TokenSwap', `/w gm <code>${who}</code> just swapped <b>${t1.get('name')}</b> and <b>${t2.get('name')}</b>. <a href="!token-swap ${t1.id} ${t2.id}">Swap Back</a>`); } } catch(err) { sendChat('TokenSwap',`/w "${who}" `+ 'Unhandled exception: ' + err.message); } } }); });
1659967128
The Aaron
Roll20 Production Team
API Scripter
That one will get an unhandled exception, but this version should work: on('ready',function(){ "use strict"; on('chat:message',function(msg){ if (msg.type === "api" && msg.content.indexOf("!token-swap")==0) { try { let who = (getObj('player',msg.playerid)||{get:()=>'API'}).get('_displayname'); let args = msg.content.split(/\s+/); let t1 = getObj('graphic',args[1]); let t2 = getObj('graphic',args[2]); if(!t1 || !t2) { sendChat('TokenSwap', `/w "${who}" Please provide 2 token ids to swap: <code>!token-swap ID1 ID2</code>`); return; } let p1={ top: t1.get('top'), left: t1.get('left') }; let p2={ top: t2.get('top'), left: t2.get('left') }; t1.set(p2); t2.set(p1); if(!playerIsGM(msg.playerid)){ sendChat('TokenSwap', `/w gm <code>${who}</code> just swapped <b>${t1.get('name')}</b> and <b>${t2.get('name')}</b>. <a href="!token-swap ${t1.id} ${t2.id}">Swap Back</a>`); } } catch(err) { sendChat('TokenSwap',`/w "${who}" `+ 'Unhandled exception: ' + err.message); } } }); }); Though it will report API as the person doing the swap to the gm.
1659975127
David M.
Pro
API Scripter
Roger that. My player calls it directly from a token action rather than the api so it hasn't come up. I'll update it for future-proofing :) So Snow, if you use Aaron's latest snippet, the Scriptcard line to try would be  --@token-swap|[*S:t-id] [*T:t-id]
Thank you David & The Aaron. It works great.