diff --git a/CombatTracker/0.3.0/CombatTracker.js b/CombatTracker/0.3.0/CombatTracker.js new file mode 100644 index 0000000000..3b4b8f18e9 --- /dev/null +++ b/CombatTracker/0.3.0/CombatTracker.js @@ -0,0 +1,1552 @@ +/* + * Version 0.3.0 + * Made By Robin Kuiper + * Changes in Version 0.2.1, 0.2.6 by The Aaron + * Skype: RobinKuiper.eu + * Discord: Atheos#1095 + * My Discord Server: https://discord.gg/AcC9VME + * Roll20: https://app.roll20.net/users/1226016/robin + * Roll20 Thread: https://app.roll20.net/forum/post/6349145/script-combattracker + * Github: https://github.com/RobinKuiper/Roll20APIScripts + * Reddit: https://www.reddit.com/user/robinkuiper/ + * Patreon: https://patreon.com/robinkuiper + * Paypal.me: https://www.paypal.me/robinkuiper +*/ + +/* TODO + * + * Styling + * More chat message options + * Show menu with B shows always + * Add icon if not StatusInfo (?) (IF YES, remove conditions on statusmarker remove) + * Edit Conditions +*/ +/* globals StatusInfo, TokenMod */ + +var CombatTracker = CombatTracker || (function() { + 'use strict'; + + let round = 1, + timerObj, + intervalHandle, + rotationInterval, + paused = false, + observers = { + tokenChange: [] + }, + extensions = { + StatusInfo: false // This will be set to true automatically if you have StatusInfo + }; + + // 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%;', + underline: 'text-decoration: underline;', + strikethrough: 'text-decoration: strikethrough' + }, + script_name = 'CombatTracker', + state_name = 'COMBATTRACKER', + + handleInput = (msg) => { + if (msg.type != 'api') return; + + let args = msg.content.split(' '); + let command = args.shift().substring(1); + let extracommand = args.shift(); + + if(command !== state[state_name].config.command) return; + + if(extracommand === 'next'){ + if(!getTurnorder().length) return; + + if(playerIsGM(msg.playerid) || msg.playerid === 'api'){ + NextTurn(); + return; + } + + let turn = getCurrentTurn(), + token = getObj('graphic', turn.id); + if(token){ + let character = getObj('character', token.get('represents')); + if((token.get('controlledby').split(',').includes(msg.playerid) || token.get('controlledby').split(',').includes('all')) || + (character && (character.get('controlledby').split(',').includes(msg.playerid) || character.get('controlledby').split(',').includes(msg.playerid)))){ + NextTurn(); + // SHOW MENU + } + } + } + + // Below commands are only for GM's + if(!playerIsGM(msg.playerid)) return; + + let name, duration, direction, message, condition; + + switch(extracommand){ + case 'help': + sendHelpMenu(); + break; + + case 'reset': + switch(args.shift()){ + case 'conditions': + state[state_name].conditions = {}; + break; + + default: + state[state_name] = {}; + setDefaults(true); + sendConfigMenu(); + break; + } + break; + + case 'config': + if(args[0] === 'timer'){ + if(args[1]){ + let setting = args[1].split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; + + state[state_name].config.timer[key] = value; + } + + sendConfigTimerMenu(); + }else if (args[0] === 'turnorder'){ + if(args[1]){ + let setting = args[1].split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; + + state[state_name].config.turnorder[key] = value; + } + + sendConfigTurnorderMenu(); + }else if (args[0] === 'announcements'){ + if(args[1]){ + let setting = args[1].split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; + + state[state_name].config.announcements[key] = value; + } + + sendConfigAnnounceMenu(); + }else if (args[0] === 'macro'){ + if(args[1]){ + let setting = args[1].split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; + + state[state_name].config.macro[key] = value; + } + + sendConfigMacroMenu(); + }else{ + if(args[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; + } + + sendConfigMenu(); + } + break; + + case 'prev': + PrevTurn(); + break; + + case 'start': + startCombat(msg.selected); + + if(args.shift() === 'b') sendMenu(); + break; + + case 'stop': + stopCombat(); + + if(args.shift() === 'b') sendMenu(); + break; + + case 'st': + stopTimer(); + + if(args.shift() === 'b') sendMenu(); + break; + + case 'pt': + pauseTimer(); + + if(args.shift() === 'b') sendMenu(); + break; + + case 'conditions': + sendConditionsMenu(); + break; + + case 'show': { + if(!msg.selected || !msg.selected.length){ + makeAndSendMenu('No tokens are selected.', '', 'gm'); + return; + } + + let tokens = msg.selected.map(s => getObj('graphic', s._id)); + + sendTokenConditionMenu(tokens); + } + break; + + case 'add': + name = args.shift(); + if(!name){ + makeAndSendMenu('No condition name was given.', '', 'gm'); + return; + } + duration = args.shift(); + duration = (!duration || duration === 0) ? 'none' : duration; + direction = args.shift() || -1; + message = args.join(' '); + condition = { name, duration, direction, message }; + + if(!msg.selected || !msg.selected.length){ + let tokenid = args.shift(); + let token = getObj('graphic', tokenid); + if(!tokenid || !token){ + makeAndSendMenu('No tokens were selected.', '', 'gm'); + return; + } + + addCondition(token, condition, true); + + return; + } + + msg.selected.forEach(s => { + let token = getObj(s._type, s._id); + if(!token) return; + + addCondition(token, condition, true); + }); + break; + + case 'addfav': + name= args.shift(); + duration = args.shift(); + direction = args.shift() || -1; + message = args.join(' '); + condition = { name, duration, direction, message }; + + addOrEditFavoriteCondition(condition); + + sendFavoritesMenu(); + break; + + case 'editfav': + name = args.shift(); + + if(!name){ + makeAndSendMenu('No condition name was given.', '', 'gm'); + return; + } + + name = strip(name).toLowerCase(); + condition = state[state_name].favorites[name]; + + if(!condition){ + makeAndSendMenu('Condition does not exists.', '', 'gm'); + return; + } + + if(args[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].favorites[name][key] = value; + + if(key === 'name'){ + state[state_name].favorites[strip(value).toLowerCase()] = state[state_name].favorites[name]; + delete state[state_name].favorites[name]; + } + } + + sendEditFavoriteConditionMenu(condition); + break; + + case 'removefav': + removeFavoriteCondition(args.shift()); + + sendFavoritesMenu(); + break; + + case 'favorites': + sendFavoritesMenu(); + break; + + case 'remove': { + let cname = args.shift(); + let tokenid = args.shift(); + let token; + + if(!cname){ + makeAndSendMenu('No condition was given.', '', 'gm'); + return; + } + + if(tokenid){ + token = getObj('graphic', tokenid); + if(token){ + removeCondition(token, cname); + sendTokenConditionMenu([token]); + return; + } + } + + if(!msg.selected || !msg.selected.length){ + makeAndSendMenu('No tokens were selected.', '', 'gm'); + return; + } + + msg.selected.forEach(s => { + token = getObj(s._type, s._id); + if(!token) return; + + removeCondition(token, cname); + }); + } + break; + + case 'showcondition': { + let cname = args.shift(); + let tokenid = args.shift(); + let token; + + if(!cname){ + makeAndSendMenu('No condition was given.', '', 'gm'); + return; + } + + if(tokenid){ + token = getObj('graphic', tokenid); + if(token){ + showCondition(token, cname); + } + } + } + break; + + default: + sendMenu(); + break; + } + }, + + addOrEditFavoriteCondition = (condition) => { + if(condition.duration === 0 || condition.duration === '') condition.duration = undefined; + + let strippedName = strip(condition.name).toLowerCase(); + + state[state_name].favorites[strippedName] = condition; + }, + + removeFavoriteCondition = (name) => { + name = strip(name).toLowerCase(); + + delete state[state_name].favorites[name]; + }, + + addCondition = (token, condition, announce=false) => { + if(extensions.StatusInfo){ + /*const duration = condition.duration; + const direction = condition.direction; + const message = condition.message;*/ + let si_condition = StatusInfo.getConditionByName(condition.name) || condition; + condition.name = si_condition.name; + condition.icon = si_condition.icon; + } + + if(!condition.duration || condition.duration === 0 || condition.duration === '0' || condition.duration === '' || condition.duration === 'none') condition.duration = undefined; + + if(state[state_name].conditions[strip(token.get('id')).toLowerCase()]){ + let hasCondition = false; + state[state_name].conditions[strip(token.get('id')).toLowerCase()].forEach(c => { + if(c.name.toLowerCase() === condition.name.toLowerCase()) hasCondition = true; + }); + if(hasCondition) return; + + state[state_name].conditions[strip(token.get('id')).toLowerCase()].push(condition); + }else{ + state[state_name].conditions[strip(token.get('id')).toLowerCase()] = [condition]; + } + + if(condition.icon){ +// let prevSM = token.get('statusmarkers'); + token.set('status_'+condition.icon, true); + if(announce && extensions.StatusInfo){ + StatusInfo.sendConditionToChat(condition); + } + }else makeAndSendMenu('Condition ' + condition.name + ' added to ' + token.get('name')); + }, + + removeCondition = (token, condition_name, auto=false) => { + if(!state[state_name].conditions[strip(token.get('id')).toLowerCase()]) return; + + let si_condition = false; + if(extensions.StatusInfo){ + si_condition = StatusInfo.getConditionByName(condition_name) || false; + } + + state[state_name].conditions[strip(token.get('id')).toLowerCase()].forEach((condition, i) => { + if(condition.name.toLowerCase() !== condition_name.toLowerCase()) return; + + state[state_name].conditions[strip(token.get('id')).toLowerCase()].splice(i, 1); + + if(si_condition){ + token.set('status_'+condition.icon, false); + }else if(!auto){ + makeAndSendMenu('Condition ' + condition.name + ' removed from ' + token.get('name')); + } + }); + }, + + showCondition = (token, condition_name) => { + if(!state[state_name].conditions[strip(token.get('id')).toLowerCase()]) return; + + let si_condition = false; + if(extensions.StatusInfo){ + si_condition = StatusInfo.getConditionByName(condition_name) || false; + } + + state[state_name].conditions[strip(token.get('id')).toLowerCase()].forEach((condition, i) => { + if(condition.name.toLowerCase() !== condition_name.toLowerCase()) return; + + if(si_condition){ + StatusInfo.sendConditionToChat(si_condition); + } + if(condition.message) makeAndSendMenu(condition.message, condition.name, ''); + }); + }, + + strip = (str) => { + return str.replace(/[^a-zA-Z0-9]+/g, '_'); + }, + + handleTurnorderChange = (obj, prev) => { + if(obj.get('turnorder') === prev.turnorder) return; + + let turnorder = (obj.get('turnorder') === "") ? [] : JSON.parse(obj.get('turnorder')); + let prevTurnorder = (prev.turnorder === "") ? [] : JSON.parse(prev.turnorder); + + if(obj.get('turnorder') === "[]"){ + resetMarker(); + stopTimer(); + return; + } + + if(turnorder.length && prevTurnorder.length && turnorder[0].id !== prevTurnorder[0].id){ + doTurnorderChange(); + } + }, + + handleStatusMarkerChange = (obj, prev) => { + if(extensions.StatusInfo){ + prev.statusmarkers = (typeof prev.get === 'function') ? prev.get('statusmarkers') : prev.statusmarkers; + + if(obj.get('statusmarkers') !== prev.statusmarkers){ + let nS = obj.get('statusmarkers').split(','), + oS = prev.statusmarkers.split(','); + + // Marker added? + array_diff(oS, nS).forEach(icon => { + if(icon === '') return; + + getObjects(StatusInfo.getConditions(), 'icon', icon).forEach(condition => { + addCondition(obj, { name: condition.name }); + }); + }); + + // Marker Removed? + array_diff(nS, oS).forEach(icon => { + if(icon === '') return; + + getObjects(StatusInfo.getConditions(), 'icon', icon).forEach(condition => { + removeCondition(obj, condition.name); + }); + }); + } + } + }, + + handleGraphicMovement = (obj /*, prev */) => { + if(!inFight()) return; + + if(getCurrentTurn().id === obj.get('id')){ + changeMarker(obj); + } + }, + + array_diff = (a, b) => { + return b.filter(function(i) {return a.indexOf(i) < 0;}); + }, + + //return an array of objects according to key, value, or key and value matching + getObjects = (obj, key, val) => { + var objects = []; + for (var i in obj) { + if (!obj.hasOwnProperty(i)) continue; + if (typeof obj[i] == 'object') { + objects = objects.concat(getObjects(obj[i], key, val)); + } else + //if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not) + if (i == key && obj[i] == val || i == key && val == '') { // + objects.push(obj); + } else if (obj[i] == val && key == ''){ + //only add if the object is not already in the array + if (objects.lastIndexOf(obj) == -1){ + objects.push(obj); + } + } + } + return objects; + }, + + startCombat = async (selected) => { + paused = false; + resetMarker(); + Campaign().set('initiativepage', Campaign().get('playerpageid')); + + if(selected && state[state_name].config.turnorder.throw_initiative){ + await rollInitiative(selected, state[state_name].config.turnorder.auto_sort); + } + + doTurnorderChange(); + }, + + inFight = () => { + return (Campaign().get('initiativepage') !== false); + }, + + rollInitiative = async (selected, sort) => { + for (const s of selected) { + if (s._type !== 'graphic') continue; + + let token = getObj('graphic', s._id); + if (!token) continue; + + let whisper = (token.get('layer') === 'gmlayer') ? 'gm ' : ''; + + let bonus = parseFloat(await getSheetItem( + token.get('represents'), + state[state_name].config.initiative_attribute_name + )) || 0; + + let roll = randomInteger(20); + + if (state[state_name].config.turnorder.show_initiative_roll) { + let contents = ` + + + + + +
Modifier${bonus}
+
+ + + [[${roll}+${bonus}]] +

+
+
`; + makeAndSendMenu(contents, `${token.get('name')} Initiative`, whisper); + } + + addToTurnorder({ + id: token.get('id'), + pr: roll + bonus, + custom: '', + _pageid: token.get('pageid') + }); + } + + if (sort) { + sortTurnorder(); + } + }, + + stopCombat = () => { + if(timerObj) timerObj.remove(); + removeMarker(); + stopTimer(); + paused = false; + Campaign().set({ + initiativepage: false, + turnorder: '' + }); + state[state_name].turnorder = {}; + round = 1; + }, + + clearTurnorder = () => { + Campaign().set({ turnorder: '' }); + state[state_name].turnorder = {}; + }, + + removeMarker = () => { + stopRotate(); + getOrCreateMarker().remove(); + }, + + resetMarker = () => { + let marker = getOrCreateMarker(); + marker.set({ + name: 'Round ' + round, + imgsrc: state[state_name].config.marker_img, + pageid: Campaign().get('playerpageid'), + layer: 'gmlayer', + left: 35, top: 35, + width: 70, height: 70 + }); + + return marker; + }, + + doTurnorderChange = (prev=false) => { + if(!Campaign().get('initiativepage') || getTurnorder().length <= 1) return; + + let turn = getCurrentTurn(); + + if(turn.id === '-1'){ + if(!state[state_name].config.turnorder.skip_custom) resetMarker(); + else NextTurn(); + return; + } + if(turn.id === getOrCreateMarker().get('id')){ + if(prev) PrevRound(); + else NextRound(); + return; + } + + let token = getObj('graphic', turn.id); + + if(!token){ + resetMarker(); + return; + } + + toFront(token); + + if(state[state_name].config.timer.use_timer){ + startTimer(token); + } + + changeMarker(token || false); + + if(state[state_name].config.macro.run_macro){ + let ability = findObjs({ _characterid: token.get('represents'), _type: 'ability', name: state[state_name].config.macro.macro_name }) + if(ability && ability.length){ + sendChat(token.get('name'), ability[0].get('action'), null, {noarchive:true} ); + } + } + + if(state[state_name].config.announcements.announce_turn){ + announceTurn(token || turn.custom, (token.get('layer') === 'objects') ? '' : 'gm'); + }else if(state[state_name].config.announcements.announce_conditions){ + let name = token.get('name') || turn.custom; + let conditions = getConditionString(token); + if(conditions && conditions !== '') makeAndSendMenu(conditions, 'Conditions - ' + name, (token.get('layer') === 'objects') ? '' : 'gm'); + } + + Pull(token); + doFX(token); + }, + + doFX = (token) => { + if(!state[state_name].config.announcements.use_fx || token.get('layer') === 'gmlayer') return; + + let pos = {x: token.get('left'), y: token.get('top')}; + spawnFxBetweenPoints(pos, pos, state[state_name].config.announcements.fx_type, token.get('pageid')); + }, + + Pull = (token) => { + if(!state[state_name].config.pull) return; + + sendPing(token.get('left'), token.get('top'), token.get('pageid'), null, true); + }, + + startTimer = (token) => { + paused = false; + clearInterval(intervalHandle); + if(timerObj) timerObj.remove(); + + let config_time = parseInt(state[state_name].config.timer.time); + let time = config_time; + + if(token && state[state_name].config.timer.token_timer){ + timerObj = createObj('text', { + text: 'Timer: ' + time, + font_size: state[state_name].config.timer.token_font_size, + font_family: state[state_name].config.timer.token_font, + color: state[state_name].config.timer.token_font_color, + pageid: token.get('pageid'), + layer: 'gmlayer' + }); + } + + intervalHandle = setInterval(() => { + if(paused) return; + + if(timerObj) timerObj.set({ + top: token.get('top')+token.get('width')/2+40, + left: token.get('left'), + text: 'Timer: ' + time, + layer: token.get('layer') + }); + + if(state[state_name].config.timer.chat_timer && (time === config_time || config_time/2 === time || config_time/4 === time || time === 10 || time === 5)){ + makeAndSendMenu('', 'Time Remaining: ' + time); + } + + if(time <= 0){ + if(timerObj) timerObj.remove(); + clearInterval(intervalHandle); + if(state[state_name].config.timer.auto_skip) NextTurn(); + else makeAndSendMenu(token.get('name') + "'s time ran out!", ''); + } + + time--; + }, 1000); + }, + + stopTimer = () => { + clearInterval(intervalHandle); + if(timerObj) timerObj.remove(); + }, + + pauseTimer = () => { + paused = !paused; + }, + + announceTurn = (token, target) => { + target = (state[state_name].config.announcements.whisper_turn_gm) ? 'gm' : target; + + let name, imgurl; + if(typeof token === 'object'){ + name = token.get('name'); + imgurl = token.get('imgsrc'); + }else{ + name = token; + } + + let conditions = getConditionString(token); + + let image = (imgurl) ? '' : ''; + name = (state[state_name].config.announcements.handleLongName) ? handleLongString(name) : name; + + let contents = '\ + \ + \ + \ + \ + \ +
'+image+''+name+'\'s Turn
\ +
\ +
'+conditions+'
\ + ' + makeButton('Done', '!'+state[state_name].config.command+' next', styles.button + styles.float.right) +' \ +
'; + makeAndSendMenu(contents, '', target); + }, + + getConditionString = (token) => { + let name = strip(token.get('id')).toLowerCase(); + let conditionsSTR = ''; + + if(state[state_name].conditions[name] && state[state_name].conditions[name].length){ + for(let i = 0; i < state[state_name].conditions[name].length; i++){ + let condition = state[state_name].conditions[name][i]; + if(typeof condition.duration === 'undefined' || condition.duration === false){ + conditionsSTR += ''+condition.name+'
'; + }else if(condition.duration <= 1){ + conditionsSTR += ''+condition.name+' removed.
'; + removeCondition(token, condition.name, true); + i--; + } else { + state[state_name].conditions[name][i].duration = parseInt(state[state_name].conditions[name][i].duration)+parseInt(condition.direction); + conditionsSTR += ''+condition.name+': ' + condition.duration + '
'; + } + conditionsSTR += (condition.message) ? ''+condition.message+'
' : ''; + } + } + return conditionsSTR; + }, + + handleLongString = (str, max=8) => { + str = str.split(' ')[0]; + return (str.length > max) ? str.slice(0, max) + '...' : str; + }, + + NextTurn = () => { + let turnorder = getTurnorder(), + current_turn = turnorder.shift(); + + turnorder.push(current_turn); + + setTurnorder(turnorder); + doTurnorderChange(); + }, + + PrevTurn = () => { + let turnorder = getTurnorder(), + last_turn = turnorder.pop(); + turnorder.unshift(last_turn); + + setTurnorder(turnorder); + doTurnorderChange(true); + }, + + NextRound = () => { + let marker = getOrCreateMarker(); + round++; + marker.set({ name: 'Round ' + round}); + + if(state[state_name].config.announcements.announce_round){ + let text = ''+marker.get('name')+''; + makeAndSendMenu(text); + } + + if(state[state_name].config.turnorder.reroll_ini_round){ + let turnorder = getTurnorder(); + clearTurnorder(); + rollInitiative(turnorder.map(t => { return (t.id !== -1 && t.id !== marker.get('id')) ? { _type: 'graphic', _id: t.id } : false }), state[state_name].config.turnorder.auto_sort); + sortTurnorder(); + }else{ + NextTurn(); + } + }, + + PrevRound = () => { + let marker = getOrCreateMarker(); + round--; + marker.set({ name: 'Round ' + round}); + + if(state[state_name].config.announcements.announce_round){ + let text = ''+marker.get('name')+''; + makeAndSendMenu(text); + } + + PrevTurn(); + }, + + changeMarker = (token) => { + let marker = getOrCreateMarker(); + + if(!token){ + resetMarker(); + return; + } + + let settings = { + layer: token.get('layer'), + top: token.get('top'), + left: token.get('left'), + width: token.get('width')+(token.get('width')*0.35), + height: token.get('height')+(token.get('height')*0.35) + }; + + marker.set(settings); + toBack(marker); + }, + + getOrCreateMarker = () => { + let marker, + img = state[state_name].config.marker_img, + playerpageid = Campaign().get('playerpageid'), + markers = findObjs({ + pageid: playerpageid, + imgsrc: img + }); + + markers.forEach((marker, i) => { + if(i > 0) marker.remove(); + }); + + marker = markers.shift(); + if(!marker) { + marker = createObj('graphic', { + name: 'Round 0', + imgsrc: img, + pageid: playerpageid, + layer: 'gmlayer', + showplayers_name: true, + left: 35, top: 35, + width: 70, height: 70 + }); + } + checkMarkerturn(marker); + toBack(marker); + + //startRotate(marker); + + return marker; + }, + +/* + startRotate = (token) => { + clearInterval(rotationInterval); + + let i = 0; + rotationInterval = setInterval(() => { + i += 2; + + log(i); + + if(i >= 360) i = 0; + + token.set('rotation', i); + }, 50); + }, +*/ + + stopRotate = () => { + clearInterval(rotationInterval); + }, + + checkMarkerturn = (marker) => { + let turnorder = getTurnorder(), + hasTurn = false; + turnorder.forEach(turn => { + if(turn.id === marker.get('id')) hasTurn = true; + }); + + if(!hasTurn){ + turnorder.push({ id: marker.get('id'), pr: -1, custom: '', _pageid: marker.get('pageid') }); + Campaign().set('turnorder', JSON.stringify(turnorder)); + } + }, + + sortTurnorder = (order='DESC') => { + let turnorder = getTurnorder(); + + turnorder.sort((a,b) => { + return (order === 'ASC') ? a.pr - b.pr : b.pr - a.pr; + }); + + setTurnorder(turnorder); + doTurnorderChange(); + }, + + getTurnorder = () => { + return (Campaign().get('turnorder') === '') ? [] : Array.from(JSON.parse(Campaign().get('turnorder'))); + }, + + getCurrentTurn = () => { + return getTurnorder().shift(); + }, + + addToTurnorder = (turn) => { + if(!turn){ + return; + } + + let turnorder = getTurnorder(), + justDoIt = true; + turnorder.forEach(t => { + if(t.id === turn.id) justDoIt = false; + }); + + if(justDoIt){ + turnorder.push(turn); + setTurnorder(turnorder); + } + }, + + setTurnorder = (turnorder) => { + Campaign().set('turnorder', JSON.stringify(turnorder)); + }, + + randomBetween = (min, max) => { + return Math.floor(Math.random()*(max-min+1)+min); + }, + + sendTokenConditionMenu = (tokens) => { + let contents = ''; + + let i = 0; + tokens.forEach(token => { + if(!token) return; + + let conditions = state[state_name].conditions[strip(token.get('id')).toLowerCase()]; + + if(i) contents += ''; + i++; + + contents += ' \ + \ + \ + '; + + if(!conditions || !conditions.length){ + contents += ''; + }else{ + conditions.forEach(condition => { + let si_condition = false; + if(extensions.StatusInfo){ + si_condition = StatusInfo.getConditionByName(condition.name) || false; + } + + let removeButton = makeButton('', '!'+state[state_name].config.command + ' remove ' + condition.name + ' ' + token.get('id'), styles.button + styles.float.right + 'width: 16px; height: 16px;'); + let showButton = (condition.message || si_condition) ? makeButton('', '!'+state[state_name].config.command + ' showcondition ' + condition.name + ' ' + token.get('id'), styles.button + styles.float.right + 'width: 16px; height: 16px;') : ''; + let name = condition.name; + name += (condition.duration) ? ' (' + condition.duration + ')' : ''; + contents += ' \ + \ + \ + \ + '; + }); + } + }); + + contents += '

\ + \ + '+token.get('name')+' \ +
None
'+name+''+removeButton+showButton+'
'; + + makeAndSendMenu(contents, '', 'gm'); + }, + + sendConditionsMenu = () => { + let addButton; + + let SI_listItems = []; + if(extensions.StatusInfo){ + Object.keys(StatusInfo.getConditions()).map(key => StatusInfo.getConditions()[key]).forEach(condition => { + let conditionSTR = condition.name + ' ?{Duration} ?{Direction|-1} ?{Message}'; + addButton = makeButton(StatusInfo.getIcon(condition.icon, 'margin-right: 5px; margin-top: 5px; display: inline-block;') + condition.name, '!'+state[state_name].config.command + ' add ' + conditionSTR, styles.textButton); + SI_listItems.push(''+addButton+''); + }); + } + + let F_listItems = []; + Object.keys(state[state_name].favorites).map(key => state[state_name].favorites[key]).forEach(condition => { + let conditionSTR = (!condition.duration) ? condition.name : condition.name + ' ' + condition.duration + ' ' + condition.direction + ' ' + condition.message; + addButton = makeButton(condition.name, '!'+state[state_name].config.command + ' add ' + conditionSTR, styles.textButton); + F_listItems.push(''+addButton+' - '+condition.duration+':'+condition.direction+':'+condition.message+''); + }); + + let contents = ''; + + contents += '

StatusInfo Conditions

'; + if(SI_listItems.length){ + contents += makeList(SI_listItems, styles.reset + styles.list + styles.overflow, styles.overflow); + }else{ + contents += (extensions.StatusInfo) ? 'Your StatusInfo doesn\'t have any conditions.' : makeButton('StatusInfo', 'https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo', styles.textButton) + ' is not installed.'; + } + + contents += '
'; + + contents += '

Favorite Conditions

'; + if(F_listItems.length){ + contents += makeList(F_listItems, styles.reset + styles.list + styles.overflow, styles.overflow); + }else{ + contents += 'You don\'t have any favorite conditions yet.'; + } + + contents += '

' + makeButton('Edit Favorites', '!'+state[state_name].config.command + ' favorites', styles.button + styles.fullWidth); + + makeAndSendMenu(contents, 'Conditions', 'gm'); + }, + + sendFavoritesMenu = () => { + let addButton, editButton, list; + + let listItems = []; + Object.keys(state[state_name].favorites).map(key => state[state_name].favorites[key]).forEach(condition => { + let conditionSTR = (!condition.duration) ? condition.name : condition.name + ' ' + condition.duration + ' ' + condition.direction + ' ' + condition.message; + addButton = makeButton(condition.name, '!'+state[state_name].config.command + ' add ' + conditionSTR, styles.textButton); + editButton = makeButton('Edit', '!'+state[state_name].config.command + ' editfav ' + condition.name, styles.button + styles.float.right); + listItems.push(''+addButton+' '+editButton); + }); + + let newButton = makeButton('Add New', '!'+state[state_name].config.command + ' addfav ?{Name} ?{Duration} ?{Direction} ?{Message}', styles.button + styles.fullWidth); + + list = (listItems.length) ? makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow) : 'No favorites yet.'; + + makeAndSendMenu(list + '
' + newButton, 'Favorite Conditions', 'gm'); + }, + + sendEditFavoriteConditionMenu = (condition) => { + if(!state[state_name].favorites[strip(condition.name).toLowerCase()]){ + makeAndSendMenu('Condition does not exist.', '', 'gm'); + return; + } + + let nameButton = makeButton(condition.name, '!'+state[state_name].config.command + ' editfav ' + condition.name + ' name|?{Name|'+condition.name+'}', styles.button + styles.float.right); + let durationButton = makeButton(condition.duration, '!'+state[state_name].config.command + ' editfav ' + condition.name + ' duration|?{Duration|'+condition.duration+'}', styles.button + styles.float.right); + let directionButton = makeButton(condition.direction, '!'+state[state_name].config.command + ' editfav ' + condition.name + ' direction|?{Direction|'+condition.direction+'}', styles.button + styles.float.right); + + let listItems = [ + 'Name '+nameButton, + 'Duration '+durationButton + ]; + + if(condition.duration && condition.duration !== 0 && condition.duration !== '0'){ + listItems.push('Direction '+directionButton); + } + + let removeButton = makeButton('Remove', '!'+state[state_name].config.command + ' removefav ' + condition.name, styles.button + styles.fullWidth); + let backButton = makeButton('Back', '!'+state[state_name].config.command + ' favorites', styles.button + styles.fullWidth); + let messageButton = makeButton((condition.message) ? 'Change Message' : 'Set Message', '!'+state[state_name].config.command + ' editfav ' + condition.name + ' message|?{Message|'+condition.message+'}', styles.button); + + let message = (condition.message) ? condition.message : 'None'; + + makeAndSendMenu(makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow) + '
Message

' + message + '

' + messageButton + '
' + removeButton + '
' + backButton, 'Edit - ' + condition.name, 'gm'); + }, + + sendConfigMenu = (first, message) => { + let commandButton = makeButton('!'+state[state_name].config.command, '!' + state[state_name].config.command + ' config command|?{Command (without !)}', styles.button + styles.float.right), + markerImgButton = makeButton('', '!' + state[state_name].config.command + ' config marker_img|?{Image Url}', styles.button + styles.float.right), + iniAttrButton = makeButton(state[state_name].config.initiative_attribute_name, '!' + state[state_name].config.command + ' config initiative_attribute_name|?{Attribute|'+state[state_name].config.initiative_attribute_name+'}', styles.button + styles.float.right), + closeStopButton = makeButton(state[state_name].config.close_stop, '!' + state[state_name].config.command + ' config close_stop|'+!state[state_name].config.close_stop, styles.button + styles.float.right), + pullButton = makeButton(state[state_name].config.pull, '!' + state[state_name].config.command + ' config pull|'+!state[state_name].config.pull, styles.button + styles.float.right), + + listItems = [ + 'Command: ' + commandButton, + 'Ini. Attribute: ' + iniAttrButton, + 'Marker Img: ' + markerImgButton, + 'Stop on close: ' + closeStopButton, + 'Auto Pull Map: ' + pullButton, + ], + + configTurnorderButton = makeButton('Turnorder Config', '!'+state[state_name].config.command + ' config turnorder', styles.button), + configTimerButton = makeButton('Timer Config', '!'+state[state_name].config.command + ' config timer', styles.button), + configAnnouncementsButton = makeButton('Announcement Config', '!'+state[state_name].config.command + ' config announcements', styles.button), + configMacroButton = makeButton('Macro Config', '!'+state[state_name].config.command + ' config macro', styles.button), + resetButton = makeButton('Reset', '!' + state[state_name].config.command + ' reset', styles.button + styles.fullWidth), + + title_text = (first) ? script_name + ' First Time Setup' : script_name + ' Config'; + + message = (message) ? '

'+message+'

' : ''; + let contents = message+makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow)+configTurnorderButton+'
'+configTimerButton+'
'+configAnnouncementsButton+'
'+configMacroButton+'

You can always come back to this config by typing `!'+state[state_name].config.command+' config`.


'+resetButton; + makeAndSendMenu(contents, title_text, 'gm'); + }, + + sendConfigTurnorderMenu = () => { + let throwIniButton = makeButton(state[state_name].config.turnorder.throw_initiative, '!' + state[state_name].config.command + ' config turnorder throw_initiative|'+!state[state_name].config.turnorder.throw_initiative, styles.button + styles.float.right), + showRollButton = makeButton(state[state_name].config.turnorder.show_initiative_roll, '!' + state[state_name].config.command + ' config turnorder show_initiative_roll|'+!state[state_name].config.turnorder.show_initiative_roll, styles.button + styles.float.right), + autoSortButton = makeButton(state[state_name].config.turnorder.auto_sort, '!' + state[state_name].config.command + ' config turnorder auto_sort|'+!state[state_name].config.turnorder.auto_sort, styles.button + styles.float.right), + rerollIniButton = makeButton(state[state_name].config.turnorder.reroll_ini_round, '!' + state[state_name].config.command + ' config turnorder reroll_ini_round|'+!state[state_name].config.turnorder.reroll_ini_round, styles.button + styles.float.right), + skipCustomButton = makeButton(state[state_name].config.turnorder.skip_custom, '!' + state[state_name].config.command + ' config turnorder skip_custom|'+!state[state_name].config.turnorder.skip_custom, styles.button + styles.float.right), + + backButton = makeButton('< Back', '!'+state[state_name].config.command + ' config', styles.button + styles.fullWidth), + + listItems = [ + 'Auto Roll Ini.: ' + throwIniButton, + 'Reroll Ini. p. Round: ' + rerollIniButton, + 'Auto Sort: ' + autoSortButton, + 'Skip Custom Item: ' + skipCustomButton + ]; + + if(state[state_name].config.turnorder.throw_initiative){ + listItems.push('Show Initiative: ' + showRollButton) + } + + let contents = makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow)+'
'+backButton; + makeAndSendMenu(contents, script_name + ' Turnorder Config', 'gm'); + }, + + sendConfigAnnounceMenu = () =>{ + let announceTurnButton = makeButton(state[state_name].config.announcements.announce_turn, '!' + state[state_name].config.command + ' config announcements announce_turn|'+!state[state_name].config.announcements.announce_turn, styles.button + styles.float.right), + announceRoundButton = makeButton(state[state_name].config.announcements.announce_round, '!' + state[state_name].config.command + ' config announcements announce_round|'+!state[state_name].config.announcements.announce_round, styles.button + styles.float.right), + announceConditionsButton = makeButton(state[state_name].config.announcements.announce_conditions, '!' + state[state_name].config.command + ' config announcements announce_conditions|'+!state[state_name].config.announcements.announce_conditions, styles.button + styles.float.right), + handleLongNameButton = makeButton(state[state_name].config.announcements.handleLongName, '!' + state[state_name].config.command + ' config announcements handleLongName|'+!state[state_name].config.announcements.handleLongName, styles.button + styles.float.right), + useFXButton = makeButton(state[state_name].config.announcements.use_fx, '!' + state[state_name].config.command + ' config announcements use_fx|'+!state[state_name].config.announcements.use_fx, styles.button + styles.float.right), + FXTypeButton = makeButton(state[state_name].config.announcements.fx_type, '!' + state[state_name].config.command + ' config announcements fx_type|?{Type|'+state[state_name].config.announcements.fx_type+'}', styles.button + styles.float.right), + whisperTurnGMButton = makeButton(state[state_name].config.announcements.whisper_turn_gm, '!' + state[state_name].config.command + ' config announcements whisper_turn_gm|'+!state[state_name].config.announcements.whisper_turn_gm, styles.button + styles.float.right), + + backButton = makeButton('< Back', '!'+state[state_name].config.command + ' config', styles.button + styles.fullWidth), + + listItems = []; + + listItems.push('Announce Round: ' + announceRoundButton); + listItems.push('Announce Turn: ' + announceTurnButton); + + if(!state[state_name].config.announcements.announce_turn){ + listItems.push('Announce Conditions: ' + announceConditionsButton); + } + if(state[state_name].config.announcements.announce_turn){ + listItems.push('Whisper GM Only: ' + whisperTurnGMButton); + listItems.push('Shorten Long Name: ' + handleLongNameButton); + } + + listItems.push('Use FX: ' + useFXButton); + + if(state[state_name].config.announcements.use_fx){ + listItems.push('FX Type: ' + FXTypeButton); + } + + let contents = makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow)+'
'+backButton; + makeAndSendMenu(contents, script_name + ' Announcements Config', 'gm'); + }, + + sendConfigTimerMenu = () => { + let turnTimerButton = makeButton(state[state_name].config.timer.use_timer, '!' + state[state_name].config.command + ' config timer use_timer|'+!state[state_name].config.timer.use_timer, styles.button + styles.float.right), + timeButton = makeButton(state[state_name].config.timer.time, '!' + state[state_name].config.command + ' config timer time|?{Time|'+state[state_name].config.timer.time+'}', styles.button + styles.float.right), + autoSkipButton = makeButton(state[state_name].config.timer.auto_skip, '!' + state[state_name].config.command + ' config timer auto_skip|'+!state[state_name].config.timer.auto_skip, styles.button + styles.float.right), + chatTimerButton = makeButton(state[state_name].config.timer.chat_timer, '!' + state[state_name].config.command + ' config timer chat_timer|'+!state[state_name].config.timer.chat_timer, styles.button + styles.float.right), + tokenTimerButton = makeButton(state[state_name].config.timer.token_timer, '!' + state[state_name].config.command + ' config timer token_timer|'+!state[state_name].config.timer.token_timer, styles.button + styles.float.right), + tokenFontButton = makeButton(state[state_name].config.timer.token_font, '!' + state[state_name].config.command + ' config timer token_font|?{Font|Arial|Patrick Hand|Contrail|Light|Candal}', styles.button + styles.float.right), + tokenFontSizeButton = makeButton(state[state_name].config.timer.token_font_size, '!' + state[state_name].config.command + ' config timer token_font_size|?{Font Size|'+state[state_name].config.timer.token_font_size+'}', styles.button + styles.float.right), + + backButton = makeButton('< Back', '!'+state[state_name].config.command + ' config', styles.button + styles.fullWidth), + + listItems = [ + 'Turn Timer: ' + turnTimerButton, + 'Time: ' + timeButton, + 'Auto Skip: ' + autoSkipButton, + 'Show in Chat: ' + chatTimerButton, + 'Show on Token: ' + tokenTimerButton, + 'Token Font: ' + tokenFontButton, + 'Token Font Size: ' + tokenFontSizeButton + ]; + + let contents = makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow)+'
'+backButton; + makeAndSendMenu(contents, script_name + ' Timer Config', 'gm'); + }, + + sendConfigMacroMenu = () => { + let runMacroButton = makeButton(state[state_name].config.macro.run_macro, '!' + state[state_name].config.command + ' config macro run_macro|'+!state[state_name].config.macro.run_macro, styles.button + styles.float.right), + macroNameButton = makeButton(state[state_name].config.macro.macro_name, '!' + state[state_name].config.command + ' config macro macro_name|?{Macro Name|'+state[state_name].config.macro.macro_name+'}', styles.button + styles.float.right), + + backButton = makeButton('< Back', '!'+state[state_name].config.command + ' config', styles.button + styles.fullWidth), + + listItems = [ + 'Run Macro: ' + runMacroButton, + 'Macro Name: ' + macroNameButton, + ]; + + let contents = '

A macro with the right name should be in the characters ability list.

'+makeList(listItems, styles.reset + styles.list + styles.overflow, styles.overflow)+'
'+backButton; + makeAndSendMenu(contents, script_name + ' Macro Config', 'gm'); + }, + + sendMenu = () => { + let nextButton = makeButton('Next Turn', '!' + state[state_name].config.command + ' next b', styles.button), + prevButton = makeButton('Prev. Turn', '!' + state[state_name].config.command + ' prev b', styles.button), + startCombatButton = makeButton('Start Combat', '!' + state[state_name].config.command + ' start b', styles.button), + stopCombatButton = makeButton('Stop Combat', '!' + state[state_name].config.command + ' stop b', styles.button), + pauseTimerTitle = (paused) ? 'Start Timer' : 'Pause Timer', + pauseTimerButton = makeButton(pauseTimerTitle, '!' + state[state_name].config.command + ' pt b', styles.button), + stopTimerButton = makeButton('Stop Timer', '!' + state[state_name].config.command + ' st b', styles.button), + addConditionButton = makeButton('Add Condition', '!' + state[state_name].config.command + ' add ?{Condition} ?{Duration}', styles.button), + removeConditionButton = makeButton('Remove Condition', '!' + state[state_name].config.command + ' remove ?{Condition}', styles.button), + resetConditionsButton = makeButton('Reset Conditions', '!'+state[state_name].config.command + ' reset conditions', styles.button), + favoritesButton = makeButton('Favorite Conditions', '!'+state[state_name].config.command + ' favorites', styles.button), + contents; + + if(inFight()){ + contents = ' \ + '+nextButton+prevButton+'
\ + '+pauseTimerButton+stopTimerButton+' \ +
\ + With Selected:
\ + '+addConditionButton+'
\ + '+removeConditionButton+' \ +
\ + '+favoritesButton+' \ +
\ + '+stopCombatButton+'
\ + '+resetConditionsButton; + }else{ + contents = ' \ + '+startCombatButton+' \ +
\ + '+favoritesButton; + } + + makeAndSendMenu(contents, script_name + ' Menu', 'gm'); + }, + + sendHelpMenu = () => { + let configButton = makeButton('Config', '!' + state[state_name].config.command + ' config', styles.button + styles.fullWidth); + + let listItems = [ + '!'+state[state_name].config.command+' help - Shows this menu.', + '!'+state[state_name].config.command+' config - Shows the configuration menu.' + ]; + + let contents = 'Commands:'+makeList(listItems, styles.reset + styles.list)+'
'+configButton; + makeAndSendMenu(contents, script_name + ' Help', 'gm'); + }, + + makeAndSendMenu = (contents, title, whisper) => { + title = (title && title != '') ? makeTitle(title) : ''; + whisper = (whisper && whisper !== '') ? '/w ' + whisper + ' ' : ''; + sendChat(script_name, whisper + '
'+title+contents+'
', null, {noarchive:true}); + }, + + makeTitle = (title) => { + return '

'+title+'

'; + }, + + makeButton = (title, href, style) => { + return ''+title+''; + }, + + makeList = (items, listStyle, itemStyle) => { + let list = ''; + return list; + }, + + checkStatusInfo = () => { + if(typeof StatusInfo === 'undefined'){ + makeAndSendMenu('Consider installing '+makeButton('StatusInfo', 'https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo', styles.textButton)+' it works great with this script.', '', 'gm'); + return; + } + + if(!StatusInfo.version || StatusInfo.version !== "0.3.11"){ + makeAndSendMenu('Please update '+makeButton('StatusInfo', 'https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo', styles.textButton)+' to the latest version.', '', 'gm'); + return; + } + + extensions.StatusInfo = true; + }, + + checkInstall = () => { + if(!_.has(state, state_name)){ + state[state_name] = state[state_name] || {}; + } + setDefaults(); + checkStatusInfo(); + + log(script_name + ' Ready! Command: !'+state[state_name].config.command); + if(state[state_name].config.debug){ + makeAndSendMenu(script_name + ' Ready! Debug On.', '', 'gm'); + } + }, + + handeIniativePageChange = (obj,prev) => { + if(state[state_name].config.close_stop && (obj.get('initiativepage') !== prev.initiativepage && !obj.get('initiativepage'))){ + stopCombat(); + } + }, + + observeTokenChange = function(handler){ + if(handler && _.isFunction(handler)){ + observers.tokenChange.push(handler); + } + }, + + notifyObservers = function(event,obj,prev){ + _.each(observers[event],function(handler){ + handler(obj,prev); + }); + }, + + registerEventHandlers = () => { + on('chat:message', handleInput); + on('change:campaign:turnorder', handleTurnorderChange); + on('change:campaign:initiativepage', handeIniativePageChange); + on('change:graphic:top', handleGraphicMovement); + on('change:graphic:left', handleGraphicMovement); + on('change:graphic:layer', handleGraphicMovement); + on('change:graphic:statusmarkers', handleStatusMarkerChange); + + if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){ + TokenMod.ObserveTokenChange(function(obj,prev){ + handleStatusMarkerChange(obj,prev); + }); + } + }, + + setDefaults = (reset) => { + const defaults = { + config: { + command: 'ct', + marker_img: 'https://s3.amazonaws.com/files.d20.io/images/52550079/U-3U950B3wk_KRtspSPyuw/thumb.png?1524507826', + initiative_attribute_name: 'initiative_bonus', + close_stop: true, + pull: true, + turnorder: { + throw_initiative: true, + show_initiative_roll: false, + auto_sort: true, + reroll_ini_round: false, + skip_custom: true, + }, + timer: { + use_timer: true, + time: 120, + auto_skip: true, + chat_timer: true, + token_timer: true, + token_font: 'Candal', + token_font_size: 16, + token_font_color: 'rgb(255, 0, 0)' + }, + announcements: { + announce_conditions: false, + announce_turn: true, + whisper_turn_gm: false, + announce_round: true, + handleLongName: true, + use_fx: false, + fx_type: 'nova-holy' + }, + macro: { + run_macro: true, + macro_name: 'CT_TURN' + } + }, + conditions: {}, + favorites: {} + }; + + 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('marker_img')){ + state[state_name].config.marker_img = defaults.config.marker_img; + } + if(!state[state_name].config.hasOwnProperty('initiative_attribute_name')){ + state[state_name].config.initiative_attribute_name = defaults.config.initiative_attribute_name; + } + if(!state[state_name].config.hasOwnProperty('close_stop')){ + state[state_name].config.close_stop = defaults.config.close_stop; + } + if(!state[state_name].config.hasOwnProperty('pull')){ + state[state_name].config.pull = defaults.config.pull; + } + if(!state[state_name].config.hasOwnProperty('turnorder')){ + state[state_name].config.turnorder = defaults.config.turnorder; + }else{ + if(!state[state_name].config.turnorder.hasOwnProperty('skip_custom')){ + state[state_name].config.turnorder.skip_custom = defaults.config.turnorder.skip_custom; + } + if(!state[state_name].config.turnorder.hasOwnProperty('throw_initiative')){ + state[state_name].config.turnorder.throw_initiative = defaults.config.turnorder.throw_initiative; + } + if(!state[state_name].config.turnorder.hasOwnProperty('auto_sort')){ + state[state_name].config.turnorder.auto_sort = defaults.config.turnorder.auto_sort; + } + if(!state[state_name].config.hasOwnProperty('reroll_ini_round')){ + state[state_name].config.turnorder.reroll_ini_round = defaults.config.turnorder.reroll_ini_round; + } + } + if(!state[state_name].config.hasOwnProperty('timer')){ + state[state_name].config.timer = defaults.config.timer; + }else{ + if(!state[state_name].config.timer.hasOwnProperty('use_timer')){ + state[state_name].config.timer.use_timer = defaults.config.timer.use_timer; + } + if(!state[state_name].config.timer.hasOwnProperty('time')){ + state[state_name].config.timer.time = defaults.config.timer.time; + } + if(!state[state_name].config.timer.hasOwnProperty('auto_skip')){ + state[state_name].config.timer.auto_skip = defaults.config.timer.auto_skip; + } + if(!state[state_name].config.timer.hasOwnProperty('chat_timer')){ + state[state_name].config.timer.chat_timer = defaults.config.timer.chat_timer; + } + if(!state[state_name].config.timer.hasOwnProperty('token_timer')){ + state[state_name].config.timer.token_timer = defaults.config.timer.token_timer; + } + if(!state[state_name].config.timer.hasOwnProperty('token_font')){ + state[state_name].config.timer.token_font = defaults.config.timer.token_font; + } + if(!state[state_name].config.timer.hasOwnProperty('token_font_size')){ + state[state_name].config.timer.token_font_size = defaults.config.timer.token_font_size; + } + if(!state[state_name].config.timer.hasOwnProperty('token_font_color')){ + state[state_name].config.timer.token_font_color = defaults.config.timer.token_font_color; + } + } + if(!state[state_name].config.hasOwnProperty('announcements')){ + state[state_name].config.announcements = defaults.config.announcements; + }else{ + if(!state[state_name].config.announcements.hasOwnProperty('announce_turn')){ + state[state_name].config.announcements.announce_turn = defaults.config.announcements.announce_turn; + } + if(!state[state_name].config.announcements.hasOwnProperty('whisper_turn_gm')){ + state[state_name].config.announcements.whisper_turn_gm = defaults.config.announcements.whisper_turn_gm; + } + if(!state[state_name].config.announcements.hasOwnProperty('announce_round')){ + state[state_name].config.announcements.announce_round = defaults.config.announcements.announce_round; + } + if(!state[state_name].config.announcements.hasOwnProperty('announce_conditions')){ + state[state_name].config.announcements.announce_conditions = defaults.config.announcements.announce_conditions; + } + if(!state[state_name].config.announcements.hasOwnProperty('handleLongName')){ + state[state_name].config.announcements.handleLongName = defaults.config.announcements.handleLongName; + } + if(!state[state_name].config.announcements.hasOwnProperty('use_fx')){ + state[state_name].config.announcements.use_fx = defaults.config.announcements.use_fx; + } + if(!state[state_name].config.announcements.hasOwnProperty('fx_type')){ + state[state_name].config.announcements.fx_type = defaults.config.announcements.fx_type; + } + } + if(!state[state_name].config.hasOwnProperty('macro')){ + state[state_name].config.macro = defaults.config.macro; + }else{ + if(!state[state_name].config.macro.hasOwnProperty('run_macro')){ + state[state_name].config.macro.run_macro = defaults.config.macro.run_macro; + } + if(!state[state_name].config.macro.hasOwnProperty('macro_name')){ + state[state_name].config.macro.macro_name = defaults.config.macro.macro_name; + } + } + } + + if(!state[state_name].hasOwnProperty('conditions')){ + state[state_name].conditions = defaults.conditions; + } + + if(!state[state_name].hasOwnProperty('favorites')){ + state[state_name].favorites = defaults.favorites; + } + + if(!state[state_name].config.hasOwnProperty('firsttime') && !reset){ + sendConfigMenu(true); + state[state_name].config.firsttime = false; + } + }; + + return { + CheckInstall: checkInstall, + RegisterEventHandlers: registerEventHandlers, + ObserveTokenChange: observeTokenChange + }; +})(); + +on('ready',function() { + 'use strict'; + + CombatTracker.CheckInstall(); + CombatTracker.RegisterEventHandlers(); +}); + +/* +conditions = { + 54235346534564: [ + { name: 'prone', duration: '1' } + ] +} +*/ diff --git a/CombatTracker/CombatTracker.js b/CombatTracker/CombatTracker.js index f9ed6c1747..3b4b8f18e9 100644 --- a/CombatTracker/CombatTracker.js +++ b/CombatTracker/CombatTracker.js @@ -1,5 +1,5 @@ -/* - * Version 0.2.6 +/* + * Version 0.3.0 * Made By Robin Kuiper * Changes in Version 0.2.1, 0.2.6 by The Aaron * Skype: RobinKuiper.eu @@ -161,7 +161,7 @@ var CombatTracker = CombatTracker || (function() { } sendConfigMenu(); - } + } break; case 'prev': @@ -229,7 +229,7 @@ var CombatTracker = CombatTracker || (function() { } addCondition(token, condition, true); - + return; } @@ -267,7 +267,7 @@ var CombatTracker = CombatTracker || (function() { if(!condition){ makeAndSendMenu('Condition does not exists.', '', 'gm'); return; - } + } if(args[0]){ let setting = args.shift().split('|'); @@ -507,8 +507,8 @@ var CombatTracker = CombatTracker || (function() { for (var i in obj) { if (!obj.hasOwnProperty(i)) continue; if (typeof obj[i] == 'object') { - objects = objects.concat(getObjects(obj[i], key, val)); - } else + objects = objects.concat(getObjects(obj[i], key, val)); + } else //if key matches and value matches or if key matches and value is not passed (eliminating the case where key matches but passed value does not) if (i == key && obj[i] == val || i == key && val == '') { // objects.push(obj); @@ -522,15 +522,15 @@ var CombatTracker = CombatTracker || (function() { return objects; }, - startCombat = (selected) => { + startCombat = async (selected) => { paused = false; resetMarker(); Campaign().set('initiativepage', Campaign().get('playerpageid')); if(selected && state[state_name].config.turnorder.throw_initiative){ - rollInitiative(selected, state[state_name].config.turnorder.auto_sort); + await rollInitiative(selected, state[state_name].config.turnorder.auto_sort); } - + doTurnorderChange(); }, @@ -538,36 +538,49 @@ var CombatTracker = CombatTracker || (function() { return (Campaign().get('initiativepage') !== false); }, - rollInitiative = (selected, sort) => { - selected.forEach(s => { - if(s._type !== 'graphic') return; - - let token = getObj('graphic', s._id), - whisper = (token.get('layer') === 'gmlayer') ? 'gm ' : '', - bonus = parseFloat(getAttrByName(token.get('represents'), state[state_name].config.initiative_attribute_name, 'current')) || 0; - let roll = randomInteger(20); - //pr = (Math.round(pr) !== pr) ? pr.toFixed(2) : pr; - - if(state[state_name].config.turnorder.show_initiative_roll){ - let contents = ' \ - \ - \ - \ - \ - \ -
Modifier'+bonus+'
\ -
\ - \ - [['+roll+'+'+bonus+']]

\ -
\ -
' - makeAndSendMenu(contents, token.get('name') + ' Initiative', whisper); + rollInitiative = async (selected, sort) => { + for (const s of selected) { + if (s._type !== 'graphic') continue; + + let token = getObj('graphic', s._id); + if (!token) continue; + + let whisper = (token.get('layer') === 'gmlayer') ? 'gm ' : ''; + + let bonus = parseFloat(await getSheetItem( + token.get('represents'), + state[state_name].config.initiative_attribute_name + )) || 0; + + let roll = randomInteger(20); + + if (state[state_name].config.turnorder.show_initiative_roll) { + let contents = ` + + + + + +
Modifier${bonus}
+
+ + + [[${roll}+${bonus}]] +

+
+
`; + makeAndSendMenu(contents, `${token.get('name')} Initiative`, whisper); } - addToTurnorder({ id: token.get('id'), pr: roll+bonus, custom: '', _pageid: token.get('pageid') }); - }); + addToTurnorder({ + id: token.get('id'), + pr: roll + bonus, + custom: '', + _pageid: token.get('pageid') + }); + } - if(sort){ + if (sort) { sortTurnorder(); } }, @@ -677,7 +690,7 @@ var CombatTracker = CombatTracker || (function() { clearInterval(intervalHandle); if(timerObj) timerObj.remove(); - let config_time = parseInt(state[state_name].config.timer.time); + let config_time = parseInt(state[state_name].config.timer.time); let time = config_time; if(token && state[state_name].config.timer.token_timer){ @@ -795,7 +808,7 @@ var CombatTracker = CombatTracker || (function() { PrevTurn = () => { let turnorder = getTurnorder(), - last_turn = turnorder.pop(); + last_turn = turnorder.pop(); turnorder.unshift(last_turn); setTurnorder(turnorder); @@ -925,7 +938,7 @@ var CombatTracker = CombatTracker || (function() { sortTurnorder = (order='DESC') => { let turnorder = getTurnorder(); - turnorder.sort((a,b) => { + turnorder.sort((a,b) => { return (order === 'ASC') ? a.pr - b.pr : b.pr - a.pr; }); @@ -1107,7 +1120,7 @@ var CombatTracker = CombatTracker || (function() { iniAttrButton = makeButton(state[state_name].config.initiative_attribute_name, '!' + state[state_name].config.command + ' config initiative_attribute_name|?{Attribute|'+state[state_name].config.initiative_attribute_name+'}', styles.button + styles.float.right), closeStopButton = makeButton(state[state_name].config.close_stop, '!' + state[state_name].config.command + ' config close_stop|'+!state[state_name].config.close_stop, styles.button + styles.float.right), pullButton = makeButton(state[state_name].config.pull, '!' + state[state_name].config.command + ' config pull|'+!state[state_name].config.pull, styles.button + styles.float.right), - + listItems = [ 'Command: ' + commandButton, 'Ini. Attribute: ' + iniAttrButton, @@ -1168,7 +1181,7 @@ var CombatTracker = CombatTracker || (function() { listItems.push('Announce Round: ' + announceRoundButton); listItems.push('Announce Turn: ' + announceTurnButton); - + if(!state[state_name].config.announcements.announce_turn){ listItems.push('Announce Conditions: ' + announceConditionsButton); } @@ -1240,7 +1253,7 @@ var CombatTracker = CombatTracker || (function() { resetConditionsButton = makeButton('Reset Conditions', '!'+state[state_name].config.command + ' reset conditions', styles.button), favoritesButton = makeButton('Favorite Conditions', '!'+state[state_name].config.command + ' favorites', styles.button), contents; - + if(inFight()){ contents = ' \ '+nextButton+prevButton+'
\ diff --git a/CombatTracker/README.md b/CombatTracker/README.md index ef49d12a52..aba22e20f4 100644 --- a/CombatTracker/README.md +++ b/CombatTracker/README.md @@ -129,6 +129,10 @@ Roll20 Thread: https://app.roll20.net/forum/post/6349145/script-combattracker --- #### Changelog + +**v0.3.0** +* Implemented Beacon compatibility + **v0.2.5** * Fixed a "bug" where the marker wouldn't show on the first turn when initiative is not rolled with CT. * Toggle auto skip turn when timer runs out. diff --git a/CombatTracker/script.json b/CombatTracker/script.json index 26f2cbf383..f3017136a0 100644 --- a/CombatTracker/script.json +++ b/CombatTracker/script.json @@ -1,8 +1,16 @@ { "name": "CombatTracker", "script": "CombatTracker.js", - "version": "0.2.6", - "previousversions": ["0.1.10", "0.1.11", "0.2.0","0.2.3", "0.2.4", "0.2.5"], + "version": "0.3.0", + "previousversions": [ + "0.1.10", + "0.1.11", + "0.2.0", + "0.2.3", + "0.2.4", + "0.2.5", + "0.2.6" + ], "description": "## CombatTracker\n\n* Skype: RobinKuiper.eu\n* Discord: Atheos#1095\n* Roll20: https://app.roll20.net/users/1226016/robin\n* Roll20 Thread: https://app.roll20.net/forum/post/6349145/script-combattracker\n* Github: https://github.com/RobinKuiper/Roll20APIScripts\n* Reddit: https://www.reddit.com/user/robinkuiper/\n* Patreon: https://patreon.com/robinkuiper\n* Paypal.me: https://www.paypal.me/robinkuiper\n\n---\n\n![Turn](https://i.imgur.com/rqfiZZD.png \"Turn\")\n\nCombatTracker will be a great help in battles. Easily keep tracks of who's turn it is, use a turn timer, let players progress to the next turn by themselves, adding/removing conditions, etc.\nIt has a lot of customizable configuration options.\n\n### StatusInfo\nIf you use my [StatusInfo](https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo) script, it will also add the markers set there automatically to the selected token(s), and it will add a condition to this script if you manually (or through TokenMod/StatusInfo) set a marker that has a condition in StatusInfo.\n\n![Token](https://i.imgur.com/Pbca4fn.png \"Token\")\n\n### Commands\n![Menu](https://i.imgur.com/I1VJz91.png \"Menu\")\n\n* **!ct** - Shows the CombatTracker menu.\n* **!ct config** - Shows the config menu.\n* **!ct favorites** - Shows the favorites menu.\n* **!ct conditions** - Shows a list of known conditions from StatusInfo and favorites.\n* **!ct show** - Shows a list of conditions on selected tokens.\n* **!ct start** - Starts the combat, if you have tokens selected it will try to roll initiative for them and add them to the tracker.\n* **!ct stop** - Stops the combat (closes the turntracker, removes the marker, clears the turnorder list, etc.).\n* **!ct next** - Goes to the next turn (a player can also use this if it is his turn).\n* **!ct prev** - Goes to the previous turn (gm only).\n* **!ct add [condition name] [?duration] [?direction] [?message]** - Adds a condition to the selected token(s) (duration is optionally (how much rounds.)).\n * **[?duration]** - Optional duration, set `0` for no duration.\n * **[?direction]** - The direction per turn, eg. `-1`, `-3`, `+1`, `+3`.\n * **[?message]** - A message that will be visible.\n* **!ct remove [condition name]** - Removes a condition from the selected token(s).\n* **!ct reset conditions** - Resets all conditions.\n* **!ct st** - Stops the timer for this turn.\n* **!ct pt** - Pause timer toggle for this turn.\n\n### Conditions\n\n![Show](https://i.imgur.com/DaPFDhK.png \"Show\")\n\n#### Commands\n* **!ct conditions** - Shows a list of known conditions from StatusInfo and favorites.\n* **!ct show** - Shows a list of conditions on selected tokens.\n* **!ct add [condition name] [?duration] [?direction] [?message]** - Adds a condition to the selected token(s) (duration is optionally (how much rounds.)).\n * **[?duration]** - Optional duration, set `0` for no duration.\n * **[?direction]** - The direction per turn, eg. `-1`, `-3`, `+1`, `+3`.\n * **[?message]** - A message that will be visible.\n* **!ct remove [condition name]** - Removes a condition from the selected token(s).\n* **!ct reset conditions** - Resets all conditions.\n\n![Known Conditions](https://i.imgur.com/2lJxMOi.png \"Known Conditions\")\n\n### Favorites\n![Favorites](https://i.imgur.com/nQqPpNJ.png \"Favorites\")\n\nHere you can create, add and edit favorite conditions. By clicking on the name it will be added to the selected token(s).\n* **Name** - The name of the condition.\n* **Duration** - How long the condition lasts (0 for no duration).\n* **Direction** - The direction the duration will go to, eg. `-1`, `-3`, `+1`, `+3`.\n* **Message** - A small message that will be visible when it's the characters turn.\n\n![Edit Condition1](https://i.imgur.com/4qX4U3P.png \"Edit Condition1\")\n![Edit Condition2](https://i.imgur.com/u2HYbtz.png \"Edit Condition2\")\n\n### Config\n![Config](https://i.imgur.com/YCDPz24.png \"Config\")\n\n* **Command** - The command used for this script, eg. !ct.\n* **Ini. Attribute** - The initiative bonus attribute used in the character sheet that you are using, defaults to `initiative_bonus` used in the 5e OGL sheet.\n* **Marker Img.** - Image (url) you want to use as a marker.\n* **Stop on Close** - Stop the combat on turnorder close (removes the marker, clears the turnorder list, etc.).\n* **Auto Roll Ini.** - If you want to autoroll (and add) the selected tokens' initiative when you start combat.\n* **Auto Pull Map** - If you want to pull the page to the token (same as `shift + hold left click`).\n\n## Timer Config\n![Timer Config](https://i.imgur.com/QZRKy6a.png \"Timer Config\")\n\n* **Turn Timer** - If you want to use the timer.\n* **Time** - The time per turn (in seconds).\n* **Show in Chat** - Announce remaining time in chat at intervals.\n* **Show on Token** - Show a timer above the current token.\n* **Token Font** - The font used for the token timer.\n* **Font Size** - The font size used for the token timer.\n\n## Announcement Config\n![Announcement Config](https://i.imgur.com/DIL89VN.png \"Announcement Config\")\n\n* **Announce Turn** - Announces who's turn it is in chat.\n* **Announce Round** - Announces the round in chat.\n* **Announce Conditions** - If you don't announce the turn, you can choose to only announce conditions in chat.\n* **Shorten Long Name** - If you want to shorten a long name in chat.\n* **Use FX** - If you want to use some special effect with the turn change.\n * **FX Type** - The name-color of the FX you want to use, for custom FX use the id.\n\n\nRoll20 Thread: https://app.roll20.net/forum/post/6349145/script-combattracker\n\n---\n\n#### Changelog\n**v0.2.0**\n* Optionally auto Pull\n* Optionally use FX on turn change.\n* Fixed condition round counter.\n* Logical shit & bugfixes.\n\n**v0.1.13**\n* Bugfixes\n\n**v0.1.12**\n* Show a list of conditions on selected tokens, `!ct show`.\n* Show a list of known conditions (from StatusInfo and favorites), `!ct conditions`.\n\n**v0.1.11**\n* !ct menu expanded.\n* Players can see Round number now.\n* Save and use favorite conditions.\n* Possibility to add a custom message to a condition.\n* Possibility to add a direction to the duration of a condition, eg. `+1`, `+3`, `-1`, `-3`.\n* Possibility to pause the timer, `!ct pt` will toggle the pause. There also is a button in the menu, `!ct menu` or `!ct`.\n* Possibility to shorten a long name in the chat announcements.\n* Possibility to go to the previous turn.\n* Fixed initiative attribute config option.\n\n**v0.1.10**\n* Fixed a bug were conditions with StatusInfo support didn't get the duration provided (needs StatusInfo update to).\n* Fixed a bug were selecting tokens by dragging caused an error.\n* Some small fixes.\n\n**v0.1.9 - 02-05-2018**\n* Good connection with [StatusInfo](https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo)\n* Add/remove conditions.\n* Whisper hidden turns to gm.\n* Better initiative calculations.\n* Changed config menu.\n* Changed some styling.\n* Bugfixes\n\n**v0.1.4 - 27-04-2018**\n* Fixed a bug were manually going further in the turnorder list didn't work.\n* Added the option to stop combat on turnorder list close.", "authors": "Robin Kuiper", "roll20userid": "1226016", @@ -13,4 +21,4 @@ "state.COMBATTRACKER": "read,write" }, "conflicts": [] -} +} \ No newline at end of file