I have a working well version of that script though I made a few translations in French. I'll post it once at home EDIT : Actually copied it from my mobile... It's the Robin's concentration script that has been reworked and tweaked by a few other users (such as The Aaron or David M...) /*
* Version 0.1.16
* Made By Robin Kuiper
* Skype: RobinKuiper.eu
* Discord: Atheos#1095
* My Discord Server: <a href="https://discord.gg/AcC9VME" rel="nofollow">https://discord.gg/AcC9VME</a>
* Roll20: <a href="https://app.roll20.net/users/1226016/robin" rel="nofollow">https://app.roll20.net/users/1226016/robin</a>
* Roll20 Wiki: <a href="https://wiki.roll20.net/Script:Concentration" rel="nofollow">https://wiki.roll20.net/Script:Concentration</a>
* Roll20 Thread: <a href="https://app.roll20.net/forum/post/6364317/script-concentration/?pageforid=6364317#post-6364317" rel="nofollow">https://app.roll20.net/forum/post/6364317/script-concentration/?pageforid=6364317#post-6364317</a>
* Github: <a href="https://github.com/RobinKuiper/Roll20APIScripts" rel="nofollow">https://github.com/RobinKuiper/Roll20APIScripts</a>
* Reddit: <a href="https://www.reddit.com/user/robinkuiper/" rel="nofollow">https://www.reddit.com/user/robinkuiper/</a>
* Patreon: <a href="https://patreon.com/robinkuiper" rel="nofollow">https://patreon.com/robinkuiper</a>
* Paypal.me: <a href="https://www.paypal.me/robinkuiper" rel="nofollow">https://www.paypal.me/robinkuiper</a>
*/
var Concentration = Concentration || (function() {
'use strict';
let checked = [];
// Styling for the chat responses.
const styles = {
reset: 'padding: 0; margin: 0;',
menu: 'background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px;',
button: 'background-color: #000; border: 1px solid #292929; border-radius: 3px; padding: 5px; color: #fff; text-align: center;',
textButton: 'background-color: transparent; border: none; padding: 0; color: #000; text-decoration: underline',
list: 'list-style: none;',
float: {
right: 'float: right;',
left: 'float: left;'
},
overflow: 'overflow: hidden;',
fullWidth: 'width: 100%;'
},
script_name = 'Concentration',
state_name = 'CONCENTRATION',
markers = ['concentré::4012467', 'blue', 'brown', 'green', 'pink', 'purple', 'red', 'yellow', '-', 'all-for-one', 'angel-outfit', 'archery-target', 'arrowed', 'aura', 'back-pain', 'black-flag', 'bleeding-eye', 'bolt-shield', 'broken-heart', 'broken-shield', 'broken-skull', 'chained-heart', 'chemical-bolt', 'cobweb', 'dead', 'death-zone', 'drink-me', 'edge-crack', 'fishing-net', 'fist', 'fluffy-wing', 'flying-flag', 'frozen-orb', 'grab', 'grenade', 'half-haze', 'half-heart', 'interdiction', 'lightning-helix', 'ninja-mask', 'overdrive', 'padlock', 'pummeled', 'radioactive', 'rolling-tomb', 'screaming', 'sentry-gun', 'skull', 'sleepy', 'snail', 'spanner', 'stopwatch','strong', 'three-leaves', 'tread', 'trophy', 'white-tower'],
handleInput = (msg) => {
if(state[state_name].config.auto_add_concentration_marker &&
msg && msg.rolltemplate &&
((msg.rolltemplate === 'spell' && msg.content.includes("{{concentration=1}}")) ||
(msg.rolltemplate === '5e-shaped' && msg.content.includes("{{duration=^{CONCENTRATION")) ||
((msg.rolltemplate === 'dmg' || msg.rolltemplate === 'atk') && msg.content.includes('!concentration')))){
handleConcentrationSpellCast(msg);
}
if (msg.type != 'api') return;
// Split the message into command and argument(s)
let args = msg.content.split(' ');
let command = args.shift().substring(1);
let extracommand = args.shift();
let message;
if (command == state[state_name].config.command) {
if(playerIsGM(msg.playerid)){
switch(extracommand){
case 'reset':
state[state_name] = {};
setDefaults(true);
sendConfigMenu(false, '<span style="color: red">The API Library needs to be restarted for this to take effect.</span>');
break;
case 'config':
if(args.length > 0){
let setting = args.shift().split('|');
let key = setting.shift();
let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0];
state[state_name].config[key] = value;
if(key === 'bar'){
//registerEventHandlers();
message = '<span style="color: red">The API Library needs to be restarted for this to take effect.</span>';
}
}
sendConfigMenu(false, message);
break;
case 'advantage-menu':
sendAdvantageMenu();
break;
case 'toggle-advantage':
let id = args[0];
if(state[state_name].advantages[id]){
state[state_name].advantages[id] = !state[state_name].advantages[id];
}else{
state[state_name].advantages[id] = true;
}
sendAdvantageMenu();
break;
case 'roll':
let represents = args[0],
DC = parseInt(args[1], 10),
con_save_mod = parseInt(args[2], 10),
name = args[3],
target = args[4];
roll(represents, DC, con_save_mod, name, target, true);
break;
case 'advantage':
let represents_a = args[0],
DC_a = parseInt(args[1], 10),
con_save_mod_a = parseInt(args[2], 10),
name_a = args[3],
target_a = args[4];
roll(represents_a, DC_a, con_save_mod_a, name_a, target_a, false);
break;
default:
if(msg.selected && msg.selected.length){
msg.selected.forEach(s => {
let token = getObj(s._type, s._id);
addConcentration(token, msg.playerid, extracommand);
});
return;
}
sendConfigMenu();
break;
}
}else{
if(msg.selected && msg.selected.length){
msg.selected.forEach(s => {
let token = getObj(s._type, s._id);
addConcentration(token, msg.playerid, extracommand);
});
}
}
}
},
addConcentration = (token, playerid, spell) => {
const marker = state[state_name].config.statusmarker
let character = getObj('character', token.get('represents'));
if((token.get('controlledby').split(',').includes(playerid) || token.get('controlledby').split(',').includes('all')) ||
(character && (character.get('controlledby').split(',').includes(playerid) || character.get('controlledby').split(',').includes('all'))) ||
playerIsGM(playerid)){
if(!token.get('status_'+marker)){
let target = state[state_name].config.send_reminder_to;
if(target === 'character'){
target = createWhisperName((character || { get: () => { return ''; } }).get('character_name'));
}else if(target === 'everyone'){
target = ''
}
let message;
if(spell){
message = '<b>'+token.get('name')+'</b> se concentre sur <b>'+spell+'</b>.';
}else{
message = '<b>'+token.get('name')+'</b> se concentre.';
}
makeAndSendMenu(message, '', target);
}
token.set('status_'+marker, !token.get('status_'+marker));
}
},
handleConcentrationSpellCast = (msg) => {
const marker = state[state_name].config.statusmarker
let character_name = msg.content.match(/charname=([^\n{}]*[^"\n{}])/) /* 5eOGL */ || msg.content.match(/character_name=([^\n{}]*[^"\n{}])/) /* Shaped */;
character_name = RegExp.$1;
let spell_name = msg.content.match(/title=([^\n{}]*[^"\n{}])/) /* Shaped */ || msg.content.match(/name=([^\n{}]*[^"\n{}])/) /* 5eOGL */;
spell_name = RegExp.$1;
let player = getObj('player', msg.playerid),
characterid = findObjs({ name: character_name, _type: 'character' }).shift().get('id'),
represented_tokens = findObjs({ represents: characterid, _type: 'graphic' }),
message,
target = state[state_name].config.send_reminder_to;
if(!character_name || !spell_name || !player || !characterid) return;
let search_attributes = {
represents: characterid,
_type: 'graphic',
_pageid: player.get('lastpage')
}
search_attributes['status_'+marker] = true;
let is_concentrating = (findObjs(search_attributes).length > 0);
represented_tokens.forEach(token => {
let attributes = {};
attributes['status_'+marker] = true;
token.set(attributes);
message = (is_concentrating) ? '<p style="font-size: 9pt; color: red;">Concentration précédente annulée.</p>' : '';
message += '<b>'+character_name+'</b> se concentre sur <b>'+spell_name+'</b>.';
});
if(target === 'character'){
target = createWhisperName(character_name);
}else if(target === 'everyone'){
target = ''
}
makeAndSendMenu(message, '', target);
},
handleStatusMarkerChange = (obj, prev) => {
const marker = state[state_name].config.statusmarker
if(!obj.get('status_'+marker)){
removeMarker(obj.get('represents'));
}
},
handleGraphicChange = (obj, prev) => {
if(checked.includes(obj.get('represents'))){ return false; }
let bar = 'bar'+state[state_name].config.bar+'_value',
target = state[state_name].config.send_reminder_to,
marker = state[state_name].config.statusmarker;
if(prev && obj.get('status_'+marker) && obj.get(bar) < prev[bar]){
let calc_DC = Math.floor((prev[bar] - obj.get(bar))/2),
DC = (calc_DC > 10) ? calc_DC : 10,
con_save_mod = parseInt(getAttrByName(obj.get('represents'), state[state_name].config.bonus_attribute, 'current')) || 0,
chat_text;
if(target === 'character'){
chat_text = "Faites un JP constitution - <b>DD " + DC + "</b>.";
target = createWhisperName(obj.get('name'));
}else if(target === 'everyone'){
chat_text = '<b>'+obj.get('name')+'</b> Doit faire un JP constitution - <b>DD ' + DC + '</b>.';
target = '';
}else{
chat_text = '<b>'+obj.get('name')+'</b> Doit faire un JP constitution - <b>DD ' + DC + '</b>.';
target = 'gm';
}
if(state[state_name].config.show_roll_button){
chat_text += '<hr>' + makeButton('Avantage', '!' + state[state_name].config.command + ' advantage ' + obj.get('represents') + ' ' + DC + ' ' + con_save_mod + ' ' + obj.get('name') + ' ' + target, styles.button + styles.float.right);
chat_text += '&nbsp;' + makeButton('Roll', '!' + state[state_name].config.command + ' roll ' + obj.get('represents') + ' ' + DC + ' ' + con_save_mod + ' ' + obj.get('name') + ' ' + target, styles.button + styles.float.left);
}
if(state[state_name].config.auto_roll_save){
//&{template:default} {{name='+obj.get('name')+' - Concentration Save}} {{Modifier='+con_save_mod+'}} {{Roll=[[1d20cf<'+(DC-con_save_mod-1)+'cs>'+(DC-con_save_mod-1)+'+'+con_save_mod+']]}} {{DC='+DC+'}}
roll(obj.get('represents'), DC, con_save_mod, obj.get('name'), target, state[state_name].advantages[obj.get('represents')]);
}else{
makeAndSendMenu(chat_text, '', target);
}
let length = checked.push(obj.get('represents'));
setTimeout(() => {
checked.splice(length-1, 1);
}, 1000);
}
},
roll = (represents, DC, con_save_mod, name, target, advantage) => {
sendChat(script_name, '[[1d20cf<'+(DC-con_save_mod-1)+'cs>'+(DC-con_save_mod-1)+'+'+con_save_mod+']]', results => {
let title = 'JP Constitution <br> <b style="font-size: 10pt; color: gray;">'+name+'</b>',
advantageRollResult;
let rollresult = results[0].inlinerolls[0].results.rolls[0].results[0].v;
let result = rollresult;
if(advantage){
advantageRollResult = randomInteger(20);
result = (rollresult <= advantageRollResult) ? advantageRollResult : rollresult;
}
let total = result + con_save_mod;
let success = total >= DC;
let result_text = (success) ? 'Réussite' : 'Echec',
result_color = (success) ? 'green' : 'red';
let rollResultString = (advantage) ? rollresult + ' / ' + advantageRollResult : rollresult;
let contents = ' \
<table style="width: 100%; text-align: left;"> \
<tr> \
<th>DD</th> \
<td>'+DC+'</td> \
</tr> \
<tr> \
<th>Modificateur</th> \
<td>'+con_save_mod+'</td> \
</tr> \
<tr> \
<th>Résultat dé</th> \
<td>'+rollResultString+'</td> \
</tr> \
</table> \
<div style="text-align: center"> \
<b style="font-size: 16pt;"> \
<span style="border: 1px solid '+result_color+'; padding-bottom: 2px; padding-top: 4px;">[['+result+'+'+con_save_mod+']]</span><br><br> \
'+result_text+' \
</b> \
</div>'
makeAndSendMenu(contents, title, target);
if(target !== '' && target !== 'gm'){
makeAndSendMenu(contents, title, 'gm');
}
if(!success){
removeMarker(represents);
}
});
},
removeMarker = (represents, type='graphic') => {
findObjs({ type, represents }).forEach(o => {
o.set('status_'+state[state_name].config.statusmarker, false);
});
},
createWhisperName = (name) => {
return (name||'').split(' ').shift();
},
ucFirst = (string) => {
return string.charAt(0).toUpperCase() + string.slice(1);
},
sendConfigMenu = (first, message) => {
let markerDropdown = '?{Marker';
markers.forEach((marker) => {
markerDropdown += '|'+ucFirst(marker).replace('-', ' ')+','+marker
})
markerDropdown += '}';
let markerButton = makeButton(state[state_name].config.statusmarker, '!' + state[state_name].config.command + ' config statusmarker|'+markerDropdown, styles.button + styles.float.right),
commandButton = makeButton('!'+state[state_name].config.command, '!' + state[state_name].config.command + ' config command|?{Command (without !)}', styles.button + styles.float.right),
barButton = makeButton('bar ' + state[state_name].config.bar, '!' + state[state_name].config.command + ' config bar|?{Bar|Bar 1 (green),1|Bar 2 (blue),2|Bar 3 (red),3}', styles.button + styles.float.right),
sendToButton = makeButton(state[state_name].config.send_reminder_to, '!' + state[state_name].config.command + ' config send_reminder_to|?{Send To|Everyone,everyone|Character,character|GM,gm}', styles.button + styles.float.right),
addConMarkerButton = makeButton(state[state_name].config.auto_add_concentration_marker, '!' + state[state_name].config.command + ' config auto_add_concentration_marker|'+!state[state_name].config.auto_add_concentration_marker, styles.button + styles.float.right),
autoRollButton = makeButton(state[state_name].config.auto_roll_save, '!' + state[state_name].config.command + ' config auto_roll_save|'+!state[state_name].config.auto_roll_save, styles.button + styles.float.right),
//advantageButton = makeButton(state[state_name].config.advantage, '!' + state[state_name].config.command + ' config advantage|'+!state[state_name].config.advantage, styles.button + styles.float.right),
bonusAttrButton = makeButton(state[state_name].config.bonus_attribute, '!' + state[state_name].config.command + ' config bonus_attribute|?{Attribute|'+state[state_name].config.bonus_attribute+'}', styles.button + styles.float.right),
showRollButtonButton = makeButton(state[state_name].config.show_roll_button, '!' + state[state_name].config.command + ' config show_roll_button|'+!state[state_name].config.show_roll_button, styles.button + styles.float.right),
listItems = [
'<span style="'+styles.float.left+'">Command:</span> ' + commandButton,
'<span style="'+styles.float.left+'">Statusmarker:</span> ' + markerButton,
'<span style="'+styles.float.left+'">HP Bar:</span> ' + barButton,
'<span style="'+styles.float.left+'">Send Reminder To:</span> ' + sendToButton,
'<span style="'+styles.float.left+'">Auto Add Con. Marker: <p style="font-size: 8pt;">Works only for 5e OGL and Shaped sheets.</p></span> ' + addConMarkerButton,
'<span style="'+styles.float.left+'">Auto Roll Save:</span> ' + autoRollButton,
],
resetButton = makeButton('Reset', '!' + state[state_name].config.command + ' reset', styles.button + styles.fullWidth),
title_text = (first) ? script_name + ' First Time Setup' : script_name + ' Config';
/*if(state[state_name].config.auto_roll_save){
listItems.push('<span style="'+styles.float.left+'">Advantage:</span> ' + advantageButton);
}*/
if(state[state_name].config.auto_roll_save){
listItems.push('<span style="'+styles.float.left+'">Bonus Attribute:</span> ' + bonusAttrButton)
}
if(!state[state_name].config.auto_roll_save){
listItems.push('<span style="'+styles.float.left+'">Roll Button:</span> ' + showRollButtonButton);
}
let advantageMenuButton = (state[state_name].config.auto_roll_save) ? makeButton('Advantage Menu', '!' + state[state_name].config.command + ' advantage-menu', styles.button + styles.fullWidth) : '';
message = (message) ? '<p>'+message+'</p>' : '';
let contents = message+makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow)+'<br>'+advantageMenuButton+'<hr><p style="font-size: 80%">You can always come back to this config by typing `!'+state[state_name].config.command+' config`.</p><hr>'+resetButton;
makeAndSendMenu(contents, title_text, 'gm');
},
sendAdvantageMenu = () => {
let menu_text = "";
let characters = findObjs({ type: 'character' }).sort((a, b) => {
let nameA = a.get('name').toUpperCase();
let nameB = b.get('name').toUpperCase();
if(nameA < nameB) return -1;
if(nameA > nameB) return 1;
return 0;
});
characters.forEach(character => {
let name = (state[state_name].advantages && state[state_name].advantages[character.get('id')]) ? '<b>'+character.get('name')+'</b>' : character.get('name');
menu_text += makeButton(name, '!' + state[state_name].config.command + ' toggle-advantage ' + character.get('id'), styles.textButton) + '<br>';
});
makeAndSendMenu(menu_text, 'Advantage Menu', 'gm');
},
makeAndSendMenu = (contents, title, whisper, callback) => {
title = (title && title != '') ? makeTitle(title) : '';
whisper = (whisper && whisper !== '') ? '/w ' + whisper + ' ' : '';
sendChat(script_name, whisper + '<div style="'+styles.menu+styles.overflow+'">'+title+contents+'</div>', null, {noarchive:true});
},
makeTitle = (title) => {
return '<h3 style="margin-bottom: 10px;">'+title+'</h3>';
},
makeButton = (title, href, style) => {
return '<a style="'+style+'" href="'+href+'">'+title+'</a>';
},
makeList = (items, listStyle, itemStyle) => {
let list = '<ul style="'+listStyle+'">';
items.forEach((item) => {
list += '<li style="'+itemStyle+'">'+item+'</li>';
});
list += '</ul>';
return list;
},
pre_log = (message) => {
log('---------------------------------------------------------------------------------------------');
if(!message){ return; }
log(message);
log('---------------------------------------------------------------------------------------------');
},
checkInstall = () => {
if(!_.has(state, state_name)){
state[state_name] = state[state_name] || {};
}
setDefaults();
log(script_name + ' Ready! Command: !'+state[state_name].config.command);
if(state[state_name].config.debug){ makeAndSendMenu(script_name + ' Ready! Debug On.', '', 'gm') }
},
registerEventHandlers = () => {
on('chat:message', handleInput);
on('change:graphic:bar'+state[state_name].config.bar+'_value', handleGraphicChange);
on('change:graphic:statusmarkers', handleStatusMarkerChange);
//register this script to SmartAoE to handle linked bar hp changes
if('undefined' !== typeof SmartAoE && SmartAoE.ObserveTokenChange){
SmartAoE.ObserveTokenChange(function(obj,prev){
handleGraphicChange(obj, prev);
});
};
//register this script to TokenMod to handle linked bar hp changes
if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){
TokenMod.ObserveTokenChange(function(obj,prev){
handleGraphicChange(obj, prev);
});
};
},
setDefaults = (reset) => {
const defaults = {
config: {
command: 'concentration',
statusmarker: 'stopwatch',
bar: 1,
send_reminder_to: 'everyone', // character,gm,
auto_add_concentration_marker: true,
auto_roll_save: true,
advantage: false,
bonus_attribute: 'constitution_save_bonus',
show_roll_button: true
},
advantages: {}
};
if(!state[state_name].config){
state[state_name].config = defaults.config;
}else{
if(!state[state_name].config.hasOwnProperty('command')){
state[state_name].config.command = defaults.config.command;
}
if(!state[state_name].config.hasOwnProperty('statusmarker')){
state[state_name].config.statusmarker = defaults.config.statusmarker;
}
if(!state[state_name].config.hasOwnProperty('bar')){
state[state_name].config.bar = defaults.config.bar;
}
if(!state[state_name].config.hasOwnProperty('send_reminder_to')){
state[state_name].config.send_reminder_to = defaults.config.send_reminder_to;
}
if(!state[state_name].config.hasOwnProperty('auto_add_concentration_marker')){
state[state_name].config.auto_add_concentration_marker = defaults.config.auto_add_concentration_marker;
}
if(!state[state_name].config.hasOwnProperty('auto_roll_save')){
state[state_name].config.auto_roll_save = defaults.config.auto_roll_save;
}
if(!state[state_name].config.hasOwnProperty('advantage')){
state[state_name].config.advantage = defaults.config.advantage;
}
if(!state[state_name].config.hasOwnProperty('bonus_attribute')){
state[state_name].config.bonus_attribute = defaults.config.bonus_attribute;
}
if(!state[state_name].config.hasOwnProperty('show_roll_button')){
state[state_name].config.show_roll_button = defaults.config.show_roll_button;
}
}
if(!state[state_name].advantages){
state[state_name].advantages = defaults.advantages;
}
if(!state[state_name].config.hasOwnProperty('firsttime') && !reset){
sendConfigMenu(true);
state[state_name].config.firsttime = false;
}
};
return {
CheckInstall: checkInstall,
RegisterEventHandlers: registerEventHandlers
}
})();
on('ready',function() {
'use strict';
Concentration.CheckInstall();
Concentration.RegisterEventHandlers();
}); Just add the marker you want to use in the list of markers at the beginning of the script and switch back the translations if you need to. It works well with token mod. Smart AoE too as Smart AoE is registering the use of token mod for the damages... Hope that helps.