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 += ' \
+ \
+ \
+ \
+ '+token.get('name')+' \
+ | \
+
';
+
+ if(!conditions || !conditions.length){
+ contents += 'None |
';
+ }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 += ' \
+ \
+ '+name+' | \
+ '+removeButton+showButton+' | \
+
';
+ });
+ }
+ });
+
+ contents += '
';
+
+ 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 = '';
+ items.forEach((item) => {
+ list += '- '+item+'
';
+ });
+ 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\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\n\n### Commands\n\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\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\n\n### Favorites\n\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\n\n\n### Config\n\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\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\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