· 3 years ago · Aug 15, 2022, 03:10 AM
1/*
2 * Version 0.1.14
3 * Made By Robin Kuiper
4 * Skype: RobinKuiper.eu
5 * Discord: Atheos#1095
6 * My Discord Server: https://discord.gg/AcC9VME
7 * Roll20: https://app.roll20.net/users/1226016/robin
8 * Roll20 Wiki: https://wiki.roll20.net/Script:Concentration
9 * Roll20 Thread: https://app.roll20.net/forum/post/6364317/script-concentration/?pageforid=6364317#post-6364317
10 * Github: https://github.com/RobinKuiper/Roll20APIScripts
11 * Reddit: https://www.reddit.com/user/robinkuiper/
12 * Patreon: https://patreon.com/robinkuiper
13 * Paypal.me: https://www.paypal.me/robinkuiper
14*/
15
16var Concentration = Concentration || (function() {
17 'use strict';
18
19 let checked = [];
20
21 // Styling for the chat responses.
22 const styles = {
23 reset: 'padding: 0; margin: 0;',
24 menu: 'background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px;',
25 button: 'background-color: #000; border: 1px solid #292929; border-radius: 3px; padding: 5px; color: #fff; text-align: center;',
26 textButton: 'background-color: transparent; border: none; padding: 0; color: #000; text-decoration: underline',
27 list: 'list-style: none;',
28 float: {
29 right: 'float: right;',
30 left: 'float: left;'
31 },
32 overflow: 'overflow: hidden;',
33 fullWidth: 'width: 100%;'
34 },
35 script_name = 'Concentration',
36 state_name = 'CONCENTRATION',
37 markers = ['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'],
38
39 handleInput = (msg) => {
40 if(state[state_name].config.auto_add_concentration_marker && msg && msg.rolltemplate && msg.rolltemplate === 'spell' && (msg.content.includes("{{concentration=1}}"))){
41 handleConcentrationSpellCast(msg);
42 }
43
44 if (msg.type != 'api') return;
45
46 // Split the message into command and argument(s)
47 let args = msg.content.split(' ');
48 let command = args.shift().substring(1);
49 let extracommand = args.shift();
50 let message;
51
52 if (command == state[state_name].config.command) {
53 if(playerIsGM(msg.playerid)){
54 switch(extracommand){
55 case 'reset':
56 state[state_name] = {};
57 setDefaults(true);
58 sendConfigMenu(false, '<span style="color: red">The API Library needs to be restarted for this to take effect.</span>');
59 break;
60
61 case 'config':
62 if(args.length > 0){
63 let setting = args.shift().split('|');
64 let key = setting.shift();
65 let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0];
66
67 state[state_name].config[key] = value;
68
69 if(key === 'bar'){
70 //registerEventHandlers();
71 message = '<span style="color: red">The API Library needs to be restarted for this to take effect.</span>';
72 }
73 }
74
75 sendConfigMenu(false, message);
76 break;
77
78 case 'advantage-menu':
79 sendAdvantageMenu();
80 break;
81
82 case 'toggle-advantage': {
83 let id = args[0];
84
85 if(state[state_name].advantages[id]){
86 state[state_name].advantages[id] = !state[state_name].advantages[id];
87 }else{
88 state[state_name].advantages[id] = true;
89 }
90
91 sendAdvantageMenu();
92 }
93 break;
94
95 case 'roll': {
96 let represents = args[0],
97 DC = parseInt(args[1], 10),
98 con_save_mod = parseInt(args[2], 10),
99 name = args[3],
100 target = args[4];
101
102 roll(represents, DC, con_save_mod, name, target, false);
103 }
104 break;
105
106 case 'advantage': {
107 let represents_a = args[0],
108 DC_a = parseInt(args[1], 10),
109 con_save_mod_a = parseInt(args[2], 10),
110 name_a = args[3],
111 target_a = args[4];
112
113 roll(represents_a, DC_a, con_save_mod_a, name_a, target_a, true);
114 }
115 break;
116
117 default:
118 if(msg.selected && msg.selected.length){
119 msg.selected.forEach(s => {
120 let token = getObj(s._type, s._id);
121 addConcentration(token, msg.playerid, extracommand);
122 });
123 return;
124 }
125
126 sendConfigMenu();
127 break;
128 }
129 }else{
130 if(msg.selected && msg.selected.length){
131 msg.selected.forEach(s => {
132 let token = getObj(s._type, s._id);
133 addConcentration(token, msg.playerid, extracommand);
134 });
135 }
136 }
137 }
138 },
139
140 addConcentration = (token, playerid, spell) => {
141 const marker = state[state_name].config.statusmarker;
142 let character = getObj('character', token.get('represents'));
143 if((token.get('controlledby').split(',').includes(playerid) || token.get('controlledby').split(',').includes('all')) ||
144 (character && (character.get('controlledby').split(',').includes(playerid) || character.get('controlledby').split(',').includes('all'))) ||
145 playerIsGM(playerid)){
146 if(!token.get('status_'+marker)){
147 let target = state[state_name].config.send_reminder_to;
148 if(target === 'character'){
149 target = character.get('name');
150 }else if(target === 'everyone'){
151 target = '';
152 }
153
154 let message;
155 if(spell){
156 message = '<b>'+token.get('name')+'</b> is now concentrating on <b>'+spell+'</b>.';
157 }else{
158 message = '<b>'+token.get('name')+'</b> is now concentrating.';
159 }
160
161 makeAndSendMenu(message, '', target);
162 }
163 token.set('status_'+marker, !token.get('status_'+marker));
164 }
165 },
166
167 handleConcentrationSpellCast = (msg) => {
168 const marker = state[state_name].config.statusmarker;
169
170 let character_name = msg.content.match(/charname=([^\n{}]*[^"\n{}])/);
171 character_name = RegExp.$1;
172 let spell_name = msg.content.match(/name=([^\n{}]*[^"\n{}])/);
173 spell_name = RegExp.$1;
174 let player = getObj('player', msg.playerid),
175 characterid = findObjs({ name: character_name, _type: 'character' }).shift().get('id'),
176 represented_tokens = findObjs({ represents: characterid, _type: 'graphic' }),
177 message,
178 target = state[state_name].config.send_reminder_to;
179
180 if(!character_name || !spell_name || !player || !characterid) return;
181
182 let search_attributes = {
183 represents: characterid,
184 _type: 'graphic',
185 _pageid: player.get('lastpage')
186 };
187 search_attributes['status_'+marker] = true;
188 let is_concentrating = (findObjs(search_attributes).length > 0);
189
190 if(is_concentrating){
191 message = '<b>'+character_name+'</b> is concentrating already.';
192 }else{
193 represented_tokens.forEach(token => {
194 let attributes = {};
195 attributes['status_'+marker] = true;
196 token.set(attributes);
197 message = '<b>'+character_name+'</b> is now concentrating on <b>'+spell_name+'</b>.';
198 });
199 }
200
201 if(target === 'character'){
202 target = character_name;
203 }else if(target === 'everyone'){
204 target = '';
205 }
206
207 makeAndSendMenu(message, '', target);
208 },
209
210 handleStatusMarkerChange = (obj /*, prev */) => {
211 const marker = state[state_name].config.statusmarker;
212
213 if(!obj.get('status_'+marker)){
214 removeMarker(obj.get('represents'));
215 }
216 },
217
218 handleGraphicChange = (obj, prev) => {
219 if(checked.includes(obj.get('represents'))){ return false; }
220
221 let bar = 'bar'+state[state_name].config.bar+'_value',
222 target = state[state_name].config.send_reminder_to,
223 marker = state[state_name].config.statusmarker;
224
225 if(prev && obj.get('status_'+marker) && obj.get(bar) < prev[bar]){
226 let calc_DC = Math.floor((prev[bar] - obj.get(bar))/2),
227 DC = (calc_DC > 10) ? calc_DC : 10,
228 con_save_mod = parseInt(getAttrByName(obj.get('represents'), state[state_name].config.bonus_attribute, 'current')) || 0,
229 chat_text;
230
231 if(target === 'character'){
232 chat_text = "Make a Concentration Check - <b>DC " + DC + "</b>.";
233 target = obj.get('name');
234 }else if(target === 'everyone'){
235 chat_text = '<b>'+obj.get('name')+'</b> must make a Concentration Check - <b>DC ' + DC + '</b>.';
236 target = '';
237 }else{
238 chat_text = '<b>'+obj.get('name')+'</b> must make a Concentration Check - <b>DC ' + DC + '</b>.';
239 target = 'gm';
240 }
241
242 if(state[state_name].config.show_roll_button){
243 chat_text += '<hr>' + makeButton('Advantage', '!' + state[state_name].config.command + ' advantage ' + obj.get('represents') + ' ' + DC + ' ' + con_save_mod + ' ' + obj.get('name') + ' ' + target, styles.button + styles.float.right);
244 chat_text += ' ' + makeButton('Roll', '!' + state[state_name].config.command + ' roll ' + obj.get('represents') + ' ' + DC + ' ' + con_save_mod + ' ' + obj.get('name') + ' ' + target, styles.button + styles.float.left);
245 }
246
247 if(state[state_name].config.auto_roll_save){
248 //&{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+'}}
249 roll(obj.get('represents'), DC, con_save_mod, obj.get('name'), target, state[state_name].advantages[obj.get('represents')]);
250 }else{
251 makeAndSendMenu(chat_text, '', target);
252 }
253
254 let length = checked.push(obj.get('represents'));
255 setTimeout(() => {
256 checked.splice(length-1, 1);
257 }, 1000);
258 }
259 },
260
261 roll = (represents, DC, con_save_mod, name, target, advantage) => {
262 sendChat(script_name, '[[1d20cf<'+(DC-con_save_mod-1)+'cs>'+(DC-con_save_mod-1)+'+'+con_save_mod+']]', results => {
263 let title = 'Concentration Save <br> <b style="font-size: 10pt; color: gray;">'+name+'</b>',
264 advantageRollResult;
265
266 let rollresult = results[0].inlinerolls[0].results.rolls[0].results[0].v;
267 let result = rollresult;
268
269 if(advantage){
270 advantageRollResult = randomInteger(20);
271 result = (rollresult <= advantageRollResult) ? advantageRollResult : rollresult;
272 }
273
274 let total = result + con_save_mod;
275
276 let success = total >= DC;
277
278 let result_text = (success) ? 'Success' : 'Failed',
279 result_color = (success) ? 'green' : 'red';
280
281 let rollResultString = (advantage) ? rollresult + ' / ' + advantageRollResult : rollresult;
282
283 let contents = ' \
284 <table style="width: 100%; text-align: left;"> \
285 <tr> \
286 <th>DC</th> \
287 <td>'+DC+'</td> \
288 </tr> \
289 <tr> \
290 <th>Modifier</th> \
291 <td>'+con_save_mod+'</td> \
292 </tr> \
293 <tr> \
294 <th>Roll Result</th> \
295 <td>'+rollResultString+'</td> \
296 </tr> \
297 </table> \
298 <div style="text-align: center"> \
299 <b style="font-size: 16pt;"> \
300 <span style="border: 1px solid '+result_color+'; padding-bottom: 2px; padding-top: 4px;">[['+result+'+'+con_save_mod+']]</span><br><br> \
301 '+result_text+' \
302 </b> \
303 </div>';
304 makeAndSendMenu(contents, title, target);
305
306 if(target !== '' && target !== 'gm'){
307 makeAndSendMenu(contents, title, 'gm');
308 }
309
310 if(!success){
311 removeMarker(represents);
312 }
313 });
314 },
315
316 removeMarker = (represents, type='graphic') => {
317 findObjs({ type, represents }).forEach(o => {
318 o.set('status_'+state[state_name].config.statusmarker, false);
319 });
320 },
321
322 ucFirst = (string) => {
323 return string.charAt(0).toUpperCase() + string.slice(1);
324 },
325
326 sendConfigMenu = (first, message) => {
327 let markerDropdown = '?{Marker';
328 markers.forEach((marker) => {
329 markerDropdown += '|'+ucFirst(marker).replace('-', ' ')+','+marker;
330 });
331 markerDropdown += '}';
332
333 let markerButton = makeButton(state[state_name].config.statusmarker, '!' + state[state_name].config.command + ' config statusmarker|'+markerDropdown, styles.button + styles.float.right),
334 commandButton = makeButton('!'+state[state_name].config.command, '!' + state[state_name].config.command + ' config command|?{Command (without !)}', styles.button + styles.float.right),
335 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),
336 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),
337 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),
338 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),
339 //advantageButton = makeButton(state[state_name].config.advantage, '!' + state[state_name].config.command + ' config advantage|'+!state[state_name].config.advantage, styles.button + styles.float.right),
340 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),
341 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),
342
343 listItems = [
344 '<span style="'+styles.float.left+'">Command:</span> ' + commandButton,
345 '<span style="'+styles.float.left+'">Statusmarker:</span> ' + markerButton,
346 '<span style="'+styles.float.left+'">HP Bar:</span> ' + barButton,
347 '<span style="'+styles.float.left+'">Send Reminder To:</span> ' + sendToButton,
348 '<span style="'+styles.float.left+'">Auto Add Con. Marker: <p style="font-size: 8pt;">Works only for 5e OGL Sheet.</p></span> ' + addConMarkerButton,
349 '<span style="'+styles.float.left+'">Auto Roll Save:</span> ' + autoRollButton
350 ],
351
352 resetButton = makeButton('Reset', '!' + state[state_name].config.command + ' reset', styles.button + styles.fullWidth),
353
354 title_text = (first) ? script_name + ' First Time Setup' : script_name + ' Config';
355
356 /*if(state[state_name].config.auto_roll_save){
357 listItems.push('<span style="'+styles.float.left+'">Advantage:</span> ' + advantageButton);
358 }*/
359
360 if(state[state_name].config.auto_roll_save){
361 listItems.push('<span style="'+styles.float.left+'">Bonus Attribute:</span> ' + bonusAttrButton);
362 }
363
364 if(!state[state_name].config.auto_roll_save){
365 listItems.push('<span style="'+styles.float.left+'">Roll Button:</span> ' + showRollButtonButton);
366 }
367
368 let advantageMenuButton = (state[state_name].config.auto_roll_save) ? makeButton('Advantage Menu', '!' + state[state_name].config.command + ' advantage-menu', styles.button + styles.fullWidth) : '';
369
370 message = (message) ? '<p>'+message+'</p>' : '';
371 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;
372 makeAndSendMenu(contents, title_text, 'gm');
373 },
374
375 sendAdvantageMenu = () => {
376 let menu_text = "";
377 let characters = findObjs({ type: 'character' }).sort((a, b) => {
378 let nameA = a.get('name').toUpperCase();
379 let nameB = b.get('name').toUpperCase();
380
381 if(nameA < nameB) return -1;
382 if(nameA > nameB) return 1;
383
384 return 0;
385 });
386
387 characters.forEach(character => {
388 let name = (state[state_name].advantages && state[state_name].advantages[character.get('id')]) ? '<b>'+character.get('name')+'</b>' : character.get('name');
389 menu_text += makeButton(name, '!' + state[state_name].config.command + ' toggle-advantage ' + character.get('id'), styles.textButton) + '<br>';
390 });
391
392 makeAndSendMenu(menu_text, 'Advantage Menu', 'gm');
393 },
394
395 makeAndSendMenu = (contents, title, whisper /*, callback */) => {
396 title = (title && title != '') ? makeTitle(title) : '';
397 whisper = (whisper && whisper !== '') ? `/w "${whisper}" ` : '';
398 sendChat(script_name, whisper + '<div style="'+styles.menu+styles.overflow+'">'+title+contents+'</div>', null, {noarchive:true});
399 },
400
401 makeTitle = (title) => {
402 return '<h3 style="margin-bottom: 10px;">'+title+'</h3>';
403 },
404
405 makeButton = (title, href, style) => {
406 return '<a style="'+style+'" href="'+href+'">'+title+'</a>';
407 },
408
409 makeList = (items, listStyle, itemStyle) => {
410 let list = '<ul style="'+listStyle+'">';
411 items.forEach((item) => {
412 list += '<li style="'+itemStyle+'">'+item+'</li>';
413 });
414 list += '</ul>';
415 return list;
416 },
417
418/*
419 pre_log = (message) => {
420 log('---------------------------------------------------------------------------------------------');
421 if(!message){ return; }
422 log(message);
423 log('---------------------------------------------------------------------------------------------');
424 },
425 */
426
427 checkInstall = () => {
428 if(!_.has(state, state_name)){
429 state[state_name] = state[state_name] || {};
430 }
431 setDefaults();
432
433 log(script_name + ' Ready! Command: !'+state[state_name].config.command);
434 if(state[state_name].config.debug){ makeAndSendMenu(script_name + ' Ready! Debug On.', '', 'gm'); }
435 },
436
437 registerEventHandlers = () => {
438 on('chat:message', handleInput);
439 on('change:graphic:bar'+state[state_name].config.bar+'_value', handleGraphicChange);
440 on('change:graphic:statusmarkers', handleStatusMarkerChange);
441 setTimeout(() => {
442 if (typeof(TokenMod) === 'object') TokenMod.ObserveTokenChange(handleGraphicChange);
443 }, 500);
444 },
445
446 setDefaults = (reset) => {
447 const defaults = {
448 config: {
449 command: 'concentration',
450 statusmarker: 'stopwatch',
451 bar: 1,
452 send_reminder_to: 'everyone', // character,gm,
453 auto_add_concentration_marker: true,
454 auto_roll_save: true,
455 advantage: false,
456 bonus_attribute: 'constitution_save_bonus',
457 show_roll_button: true
458 },
459 advantages: {}
460 };
461
462 if(!state[state_name].config){
463 state[state_name].config = defaults.config;
464 }else{
465 if(!state[state_name].config.hasOwnProperty('command')){
466 state[state_name].config.command = defaults.config.command;
467 }
468 if(!state[state_name].config.hasOwnProperty('statusmarker')){
469 state[state_name].config.statusmarker = defaults.config.statusmarker;
470 }
471 if(!state[state_name].config.hasOwnProperty('bar')){
472 state[state_name].config.bar = defaults.config.bar;
473 }
474 if(!state[state_name].config.hasOwnProperty('send_reminder_to')){
475 state[state_name].config.send_reminder_to = defaults.config.send_reminder_to;
476 }
477 if(!state[state_name].config.hasOwnProperty('auto_add_concentration_marker')){
478 state[state_name].config.auto_add_concentration_marker = defaults.config.auto_add_concentration_marker;
479 }
480 if(!state[state_name].config.hasOwnProperty('auto_roll_save')){
481 state[state_name].config.auto_roll_save = defaults.config.auto_roll_save;
482 }
483 if(!state[state_name].config.hasOwnProperty('advantage')){
484 state[state_name].config.advantage = defaults.config.advantage;
485 }
486 if(!state[state_name].config.hasOwnProperty('bonus_attribute')){
487 state[state_name].config.bonus_attribute = defaults.config.bonus_attribute;
488 }
489 if(!state[state_name].config.hasOwnProperty('show_roll_button')){
490 state[state_name].config.show_roll_button = defaults.config.show_roll_button;
491 }
492 }
493 if(!state[state_name].advantages){
494 state[state_name].advantages = defaults.advantages;
495 }
496
497 if(!state[state_name].config.hasOwnProperty('firsttime') && !reset){
498 sendConfigMenu(true);
499 state[state_name].config.firsttime = false;
500 }
501 };
502
503 return {
504 CheckInstall: checkInstall,
505 RegisterEventHandlers: registerEventHandlers
506 };
507})();
508
509on('ready',function() {
510 'use strict';
511
512 Concentration.CheckInstall();
513 Concentration.RegisterEventHandlers();
514});
515