
I noticed a little issue on the concentration script : When i Drag 2
identical monsters from journal, and i "concentrate" one of them, both
of them get the symbol on, and if one of them is losing concentration
failing its Constitution check, both of them lose their concentration...
Is there a way that this doesn't happen ? It only happens for concentration and not if you apply another status marker from another script... I know that this script isn't maintained anymore (unfoirtunately) but if anyone could find the solution ? I'm using the 0.1.16 version of the script /* * 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); }); }; }, 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(); }); I made a few translations, but otherwise teh script is the same... I guess there must be a referencing problem between the character concentrating and something like represents or character name. Maybe changing the script to identify the selected token_id instead of the "represents" or the "character name" could solve the issue but i don't know how to do that.... Thanks for anyone's help. EDIT : The issue actually occurs when casting a spell from a NPC Character's sheet. It doesn't happen if i manually call for the !concentration