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] AddCustomTurn -- An easy way to add (and remove) custom turns which increment or decrement, and have auto delete features.

1640281054
The Aaron
Roll20 Production Team
API Scripter
I think this script will do some of what you want.  You can roll in with GroupInitiative and it will adjust turns automatically, or you can add turns and run: !dup-turn to add the extra turns to everyone. Right now, it adds a fixed number of turns (defaults to 2), but can add a number based on arguments or an attribute on characters.  If you're javascript savvy, you could make the changes to support what you want, otherwise, I can try and fit it in over the holidays. /* global GroupInitiative */ on('ready', () => { const handleGroupInit = true; const defaultCount = 2; const defaultAttrName = 'apm'; const turnMultiple = 10; const multiplyTurn = (token_id,count) => { let to = JSON.parse(Campaign().get('turnorder')||'[]'); let turn = {pr:-10000}; to = to.filter((o) => { if(o.id !== token_id){ return true; } if( o.id === token_id ) { if(o.pr>turn.pr) { turn = o; } } return false; }); if(turn.id){ [...Array(parseInt(count)).keys()].forEach((n)=>{ to.push(Object.assign({},turn,{pr:(parseInt(turn.pr)-(turnMultiple*n))})); }); } Campaign().set({ turnorder: JSON.stringify(to.sort((a,b)=>parseFloat(b.pr)-parseFloat(a.pr))) }); }; on('chat:message', (msg) => { if( 'api'===msg.type && /^!dup-turn/.test(msg.content) ){ let args = msg.content.split(/\s+/); let count = args[1]||defaultCount; let attrname = args[2]||defaultAttrName; if(msg.selected){ msg.selected.forEach((m)=>{ let n = count; let token = getObj('graphic',m._id); if(token){ n = parseInt(getAttrByName(token.get('represents'),attrname)); } multiplyTurn(m._id,n||count); }); } } }); const maxPRLookup = (to) => { let lookup = {}; to.forEach(o=>{ let npr = parseFloat(o.pr); if(!lookup[o.id] || lookup[o.id]<npr){ lookup[o.id]=npr; } }); return lookup; }; const handleGroupInitChange = (obj,prev) => { let lookup = maxPRLookup(JSON.parse(prev||'[]')); let curlist = maxPRLookup(JSON.parse(obj||'[]')); let updateList = Object.keys(curlist).filter(id=>curlist[id]!==lookup[id]); updateList.forEach(id=>{ let n; let token = getObj('graphic',id); if(token){ n = parseInt(getAttrByName(token.get('represents'),defaultAttrName)); } multiplyTurn(id,n||defaultCount); }); }; if(handleGroupInit && 'undefined' !== typeof GroupInitiative && GroupInitiative.ObserveTurnOrderChange){ GroupInitiative.ObserveTurnOrderChange(handleGroupInitChange); } });
1640302870

Edited 1640305693
I'm sadly not javascript savvy but don't rush over it or anything, if you can help thats amazing otherwise I'll try and muddle through. thanks for the help you've provided already *edit: I actually got it to work in what i assume is not the most elegant way but still works: !setattr --sel --silent --apm|[[ceil([[@{selected|initiative|max}d6+@{selected|reaction|max}&{tracker}]]/10)]] !dup-turn Only think it still doesn't work on is tokens without a character sheet but thats for another day.
1640357490
The Aaron
Roll20 Production Team
API Scripter
I think this version should give you the full packing of turns without needing to create attributes: /* global GroupInitiative */ on('ready', () => { const handleGroupInit = true; const defaultCount = 2; const defaultAttrName = 'apm'; const turnMultiple = 10; const maxFitTurns = true; const multiplyTurn = (token_id,count) => { let to = JSON.parse(Campaign().get('turnorder')||'[]'); let turn = {pr:-10000}; to = to.filter((o) => { if(o.id !== token_id){ return true; } if( o.id === token_id ) { if(o.pr>turn.pr) { turn = o; } } return false; }); if(maxFitTurns){ count = Math.floor(turn.pr / turnMultiple) + 1; } if(turn.id){ [...Array(parseInt(count)).keys()].forEach((n)=>{ to.push(Object.assign({},turn,{pr:(parseInt(turn.pr)-(turnMultiple*n))})); }); } Campaign().set({ turnorder: JSON.stringify(to.sort((a,b)=>parseFloat(b.pr)-parseFloat(a.pr))) }); }; on('chat:message', (msg) => { if( 'api'===msg.type && /^!dup-turn/.test(msg.content) ){ let args = msg.content.split(/\s+/); let count = args[1]||defaultCount; let attrname = args[2]||defaultAttrName; if(msg.selected){ msg.selected.forEach((m)=>{ let n = count; let token = getObj('graphic',m._id); if(token){ n = parseInt(getAttrByName(token.get('represents'),attrname)); } multiplyTurn(m._id,n||count); }); } } }); const maxPRLookup = (to) => { let lookup = {}; to.forEach(o=>{ let npr = parseFloat(o.pr); if(!lookup[o.id] || lookup[o.id]<npr){ lookup[o.id]=npr; } }); return lookup; }; const handleGroupInitChange = (obj,prev) => { let lookup = maxPRLookup(JSON.parse(prev||'[]')); let curlist = maxPRLookup(JSON.parse(obj||'[]')); let updateList = Object.keys(curlist).filter(id=>curlist[id]!==lookup[id]); updateList.forEach(id=>{ let n; let token = getObj('graphic',id); if(token){ n = parseInt(getAttrByName(token.get('represents'),defaultAttrName)); } multiplyTurn(id,n||defaultCount); }); }; if(handleGroupInit && 'undefined' !== typeof GroupInitiative && GroupInitiative.ObserveTurnOrderChange){ GroupInitiative.ObserveTurnOrderChange(handleGroupInitChange); } });
Thanks SO much dude, this has been driving me mad for ages and you've sorted it! The Aaron said: I think this version should give you the full packing of turns without needing to create attributes: /* global GroupInitiative */ on('ready', () => { const handleGroupInit = true; const defaultCount = 2; const defaultAttrName = 'apm'; const turnMultiple = 10; const maxFitTurns = true; const multiplyTurn = (token_id,count) => { let to = JSON.parse(Campaign().get('turnorder')||'[]'); let turn = {pr:-10000}; to = to.filter((o) => { if(o.id !== token_id){ return true; } if( o.id === token_id ) { if(o.pr>turn.pr) { turn = o; } } return false; }); if(maxFitTurns){ count = Math.floor(turn.pr / turnMultiple) + 1; } if(turn.id){ [...Array(parseInt(count)).keys()].forEach((n)=>{ to.push(Object.assign({},turn,{pr:(parseInt(turn.pr)-(turnMultiple*n))})); }); } Campaign().set({ turnorder: JSON.stringify(to.sort((a,b)=>parseFloat(b.pr)-parseFloat(a.pr))) }); }; on('chat:message', (msg) => { if( 'api'===msg.type && /^!dup-turn/.test(msg.content) ){ let args = msg.content.split(/\s+/); let count = args[1]||defaultCount; let attrname = args[2]||defaultAttrName; if(msg.selected){ msg.selected.forEach((m)=>{ let n = count; let token = getObj('graphic',m._id); if(token){ n = parseInt(getAttrByName(token.get('represents'),attrname)); } multiplyTurn(m._id,n||count); }); } } }); const maxPRLookup = (to) => { let lookup = {}; to.forEach(o=>{ let npr = parseFloat(o.pr); if(!lookup[o.id] || lookup[o.id]<npr){ lookup[o.id]=npr; } }); return lookup; }; const handleGroupInitChange = (obj,prev) => { let lookup = maxPRLookup(JSON.parse(prev||'[]')); let curlist = maxPRLookup(JSON.parse(obj||'[]')); let updateList = Object.keys(curlist).filter(id=>curlist[id]!==lookup[id]); updateList.forEach(id=>{ let n; let token = getObj('graphic',id); if(token){ n = parseInt(getAttrByName(token.get('represents'),defaultAttrName)); } multiplyTurn(id,n||defaultCount); }); }; if(handleGroupInit && 'undefined' !== typeof GroupInitiative && GroupInitiative.ObserveTurnOrderChange){ GroupInitiative.ObserveTurnOrderChange(handleGroupInitChange); } });
1640366003
The Aaron
Roll20 Production Team
API Scripter
No problem!  Merry Christmas / Happy Holidays. =D
To anyone who might find it useful, I've written a simple macro to use this script with from the macro bar. !act -1 ?{Number of Turns|0} {{   --?{ConditionName|0}   --delete-on-zero   --after }} It will prompt you for how many turns the condition will last for, and then the name of the condition. Could expand on the macro to do more... but I just wanted something to cover 90% of my circumstances and this does that :)
Hey Aaron, I'm wondering if the AddCustomTurn would be easy to update for the Turn Tracker update that just happened.  I added the  !fix-turn-order  scriptlet that you created to my macro, which will basically fix the turn order each time I'm using an old script  by Sky  solely for the 'Green Dot' and !eot functions. But the Custom Turns are behaving weirdly now -- I'm also using your ' Add Invisible Custom Turn ' scriptlet -- sometimes they will increase/decrease appropriately, and other times they will change the turn order value in odd ways (such as an increasing number will change from 2 and become 20, then 21, 22 -- using the original number in the tens digit). I need to go through and pare down my script list and reconfigure some of my macros... this is going to take a while! Thanks for anything you're able to do!
1645661481
The Aaron
Roll20 Production Team
API Scripter
AddCustomTurn shouldn't have an issue with the update as custom turns have no token and thus no page associated with them.  Add Invisible Custom Turn needed a fix, I updated the script at the link you pasted a few weeks ago (and made a slightly more efficient change just now).  Are you using that version? Here's a fixed version of Sky's script (untested): // VERSION INFO var SimpleInitiative_Author = "Sky"; var SimpleInitiative_Version = "1.4.0"; var SimpleInitiative_LastUpdated = 1530594275; // VARIABLE DECLARATIONS var SHOW_GREEN_DOT = true; var ANNOUNCE_NEW_TURN = true; var PC_COLOR = "#073763"; var NPC_COLOR = "#440000"; var PLAYER_COLOR = true; var HIDE_NPC_NAMES = false; var PULL_GM_TO_TOKEN = true; // FUNCTION DECLARATIONS var HandleTurnOrderChange = HandleTurnOrderChange || {}; var AnnounceNewTurn = AnnounceNewTurn || {}; var getBrightness = getBrightness || {}; var getHex2Dec = getHex2Dec || {}; // HANDLERS on("ready", function () { log("-=> SimpleInitiative v" + SimpleInitiative_Version + " <=- [" + (new Date(SimpleInitiative_LastUpdated * 1000)) + "]"); //log (Date.now().toString().substr(0, 10)); }); on("change:campaign:turnorder", function (obj, prev) { HandleTurnOrderChange(obj, prev, HIDE_NPC_NAMES, PULL_GM_TO_TOKEN); }); on("chat:message", function(msg) { if (msg.type !== "api") return; if (msg.content.split(" ")[0] == "!eot") { if (!Campaign().get("turnorder")) return; var turn_order = JSON.parse(Campaign().get("turnorder")); if (!turn_order.length) return; if (turn_order.length == 1) return; var current = turn_order.shift(); var next = turn_order.shift(); if (next.formula == "+1") next.pr = next.pr + 1; turn_order.unshift(next); if (!playerIsGM(msg.playerid)) { if (getObj("graphic", current.id).get("represents") != "") { if (!getObj("character", getObj("graphic", current.id).get("represents")).get("controlledby").includes(msg.playerid)) return; } } turn_order.push({id: current.id, pr: current.pr, _pageid: current.get('pageid'), custom: current.custom, formula: current.formula}); Campaign().set("turnorder", JSON.stringify(turn_order)); if (current.id != -1 && SHOW_GREEN_DOT) getObj("graphic", current.id).set("status_green", false); if (next.id != -1 && SHOW_GREEN_DOT) getObj("graphic", next.id).set("status_green", true); if (ANNOUNCE_NEW_TURN) AnnounceNewTurn([next], [current], HIDE_NPC_NAMES, PULL_GM_TO_TOKEN); } if (msg.content.split(" ")[0] == "!roll-init" && playerIsGM(msg.playerid)) { if (!msg.selected) return; var turn_order = (!Campaign().get("turnorder")) ? [] : JSON.parse(Campaign().get("turnorder")); var token, mod, index; _.each(msg.selected, function (a) { token = getObj("graphic", a._id); if (token.get("name") == "Round") { turn_order.push({id: a._id, pr: 999, _pageid: token.get('pageid'), formula: "+1"}); } else { mod = (token.get("represents") != "") ? parseInt(Math.floor((getAttrByName(token.get("represents"), "dexterity") - 10) / 2)) + parseInt(getAttrByName(token.get("represents"), "initmod")) + parseInt(getAttrByName(token.get("represents"), "jack_of_all_trades")) : 0; index = turn_order.findIndex(x => x.id == a._id); if (index != -1) turn_order[index].pr = Math.floor((Math.random() * 20) + 1) + mod; else turn_order.push({id: a._id, _pageid: token.get('pageid'), pr: Math.floor((Math.random() * 20) + 1) + mod}); } }); Campaign().set("initiativepage", true); Campaign().set("turnorder", JSON.stringify(turn_order)); } if (msg.content.split(" ")[0] == "!sort-init" && playerIsGM(msg.playerid)) { if (!Campaign().get("turnorder")) return; var turn_order = JSON.parse(Campaign().get("turnorder")); if (!turn_order.length) return; var method = (msg.content.split(" ")[1] !== undefined && msg.content.split(" ")[1].toLowerCase().indexOf("a") === 0) ? "ascending" : "descending"; var current = turn_order[0]; var sorted_turn_order = (method == "descending") ? _.sortBy(turn_order, "pr").reverse() : _.sortBy(turn_order, "pr"); var next = sorted_turn_order.shift(); if (next.pr == 999) next.pr = 1; sorted_turn_order.unshift(next); Campaign().set("turnorder", JSON.stringify(sorted_turn_order)); if (current.id != -1 && SHOW_GREEN_DOT) getObj("graphic", current.id).set("status_green", false); if (next.id != -1 && SHOW_GREEN_DOT) getObj("graphic", next.id).set("status_green", true); if (ANNOUNCE_NEW_TURN) AnnounceNewTurn([next], [current], HIDE_NPC_NAMES, PULL_GM_TO_TOKEN); } if (msg.content.split(" ")[0] == "!clear-init" && playerIsGM(msg.playerid)) { var turn_order = JSON.parse(Campaign().get("turnorder")); if (!turn_order.length) return; var current = turn_order.shift(); if (current.id != -1) getObj("graphic", current.id).set("status_green", false); Campaign().set("turnorder", "[]"); Campaign().set("initiativepage", false); } }); // FUNCTIONS var HandleTurnOrderChange = function(obj, prev, HIDE_NPC_NAMES, PULL_GM_TO_TOKEN) { var current = JSON.parse(obj.get("turnorder") || []); var previous = JSON.parse(prev["turnorder"]) || []; if (obj.get("turnorder") && !obj.get("initiativepage")) Campaign().set("initiativepage", true); if (current.length == 0 && previous[0].id != -1 && SHOW_GREEN_DOT) getObj("graphic", previous[0].id).set("status_green", false); if (previous.length > 0 && previous[0].id != -1 && SHOW_GREEN_DOT) getObj("graphic", previous[0].id).set("status_green", false); if (current.length > 0 && current[0].id != -1 && SHOW_GREEN_DOT) getObj("graphic", current[0].id).set("status_green", true); if (ANNOUNCE_NEW_TURN) AnnounceNewTurn(current, previous, HIDE_NPC_NAMES, PULL_GM_TO_TOKEN); } AnnounceNewTurn = function (current, next, HIDE_NPC_NAMES, PULL_GM_TO_TOKEN) { if (_.isEmpty(next) || _.isEmpty(current) || current[0].id == next[0].id) return; if (current[0].id != -1 && getObj("graphic", current[0].id).get("layer") != "gmlayer") { var Token = getObj("graphic", current[0].id); var AlertTokenName = Token.get("name"); var Character = (Token.get("represents") != "") ? getObj("character", Token.get("represents")) : ""; var AlertColor = NPC_COLOR; if (Character != "" && getAttrByName(Character.id, "npc") != undefined && getAttrByName(Character.id, "npc") != 1) { AlertColor = PC_COLOR; AlertTokenName = (AlertTokenName == "") ? "PC" : AlertTokenName; if (PLAYER_COLOR && Character.get("controlledby") != "") AlertColor = getObj("player", Character.get("controlledby").split(",")[0]).get("color"); } else { AlertTokenName = (HIDE_NPC_NAMES == true) ? "NPC" : (Token.get("name") == "") ? "NPC" : Token.get("name"); } if (AlertTokenName == "Round") AlertTokenName = AlertTokenName + " " + current[0].pr; var AlertTextColor = (getBrightness(AlertColor) < (255 / 2)) ? "#FFF" : "#000"; var AlertShadowColor = (AlertTextColor == "#000") ? "#FFF" : "#000"; var AlertOuterStyle = "max-height: 40px; width: 100%; margin: 10px 0px 5px -7px; line-height: 40px;"; var AlertInnerStyle = "max-height: 20px; width: 100%; margin: 0px; padding: 0px 0px 2px 0px; clear: both; overflow: hidden; font-family: Candal; font-weight: lighter; font-size: 13px; line-height: 20px; color: " + AlertTextColor + "; background-color: " + AlertColor + "; background-image: linear-gradient(rgba(255, 255, 255, .4), rgba(255, 255, 255, 0)); border: 1px solid #000; border-radius: 4px; text-shadow: -1px -1px 0 " + AlertShadowColor + ", 1px -1px 0 " + AlertShadowColor + ", -1px 1px 0 " + AlertShadowColor + ", 1px 1px 0 " + AlertShadowColor + ";"; var AlertImageStyle = "height: 40px; width: 40px; float: right; margin: -32px 5px 0px 0px;"; sendChat("", "/desc <div style='" + AlertOuterStyle + "'><div style='" + AlertInnerStyle + "'>" + AlertTokenName + "</div><img src='" + Token.get("imgsrc") + "' style='" + AlertImageStyle + "'></img></div>"); sendPing(-100, -100, Campaign().get("playerpageid"), null, false); sendPing(Token.get("left"), Token.get("top"), Campaign().get("playerpageid"), null, PULL_GM_TO_TOKEN); toFront(Token); } }; function getBrightness(hex) { hex = hex.replace('#', ''); var c_r = getHex2Dec(hex.substr(0, 2)); var c_g = getHex2Dec(hex.substr(2, 2)); var c_b = getHex2Dec(hex.substr(4, 2)); return ((c_r * 299) + (c_g * 587) + (c_b * 114)) / 1000; }; function getHex2Dec(hex_string) { hex_string = (hex_string + '').replace(/[^a-f0-9]/gi, ''); return parseInt(hex_string, 16); };
This is probably a noob question, but here goes... Every time I add a turn I get the whole Help menu filling up the chat window, even though I don't have --help in the command. Is there a way to turn it off?
1645983315
The Aaron
Roll20 Production Team
API Scripter
What is the command you're running?
@TheAaron Wow, so this is weird... I logged back in and tried the same commands again: !act +1 1 --Round !act +0 99 --2ndAttacks --after And this time they worked without activating the help menu. My chat log still has the half-dozen help menus from my previous cuts-and-pastes of those same commands, so I know it wasn't my imagination! Is there any reason why it might behave one way with the API newly installed, and another after the game is refreshed?
1646059282
The Aaron
Roll20 Production Team
API Scripter
Anything is possible, though it's hard to imagine how that might have happened.  If you copy/pasted the command before, it's possible some unicode character got into confuse things, but I can't think of what that would be. I suppose if it happens again, let me know an we can try and track it down.