diff --git a/StatusInfo/0.3.12/StatusInfo.js b/StatusInfo/0.3.12/StatusInfo.js new file mode 100644 index 0000000000..d3054aa866 --- /dev/null +++ b/StatusInfo/0.3.12/StatusInfo.js @@ -0,0 +1,793 @@ +/* + * Version: 0.3.11 + * Made By Robin Kuiper + * 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/6252784/script-statusinfo + * Roll20 Wiki: https://wiki.roll20.net/Script:StatusInfo + * 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 + * + * COMMANDS (with default command): + * !condition [CONDITION] - Shows condition. + * !condtion help - Shows help menu. + * !condition config - Shows config menu. + * + * !condition add [condtion(s)] - Add condition(s) to selected tokens, eg. !condition add prone paralyzed + * !condition remove [condtion(s)] - Remove condition(s) from selected tokens, eg. !condition remove prone paralyzed +* !condition toggle [condtion(s)] - Toggles condition(s) of selected tokens, eg. !condition toggle prone paralyzed + * + * !condition config export - Exports the config (with conditions). + * !condition config import [json] - Import the given config (with conditions). + * + * TODO: + * Icon span + * whisper system + * stylings +*/ + +var StatusInfo = StatusInfo || (function() { + 'use strict'; + + let whisper, handled = [], + observers = { + tokenChange: [] + }; + + + const version = "0.3.11", + + // Styling for the chat responses. + style = "overflow: hidden; background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px;", + buttonStyle = "background-color: #000; border: 1px solid #292929; border-radius: 3px; padding: 5px; color: #fff; text-align: center; float: right;", + conditionStyle = "background-color: #fff; border: 1px solid #000; padding: 5px; border-radius: 5px;", + conditionButtonStyle = "text-decoration: underline; background-color: #fff; color: #000; padding: 0", + listStyle = 'list-style: none; padding: 0; margin: 0;', + + icon_image_positions = {red:"#C91010",blue:"#1076C9",green:"#2FC910",brown:"#C97310",purple:"#9510C9",pink:"#EB75E1",yellow:"#E5EB75",dead:"X",skull:0,sleepy:34,"half-heart":68,"half-haze":102,interdiction:136,snail:170,"lightning-helix":204,spanner:238,"chained-heart":272,"chemical-bolt":306,"death-zone":340,"drink-me":374,"edge-crack":408,"ninja-mask":442,stopwatch:476,"fishing-net":510,overdrive:544,strong:578,fist:612,padlock:646,"three-leaves":680,"fluffy-wing":714,pummeled:748,tread:782,arrowed:816,aura:850,"back-pain":884,"black-flag":918,"bleeding-eye":952,"bolt-shield":986,"broken-heart":1020,cobweb:1054,"broken-shield":1088,"flying-flag":1122,radioactive:1156,trophy:1190,"broken-skull":1224,"frozen-orb":1258,"rolling-bomb":1292,"white-tower":1326,grab:1360,screaming:1394,grenade:1428,"sentry-gun":1462,"all-for-one":1496,"angel-outfit":1530,"archery-target":1564}, + markers = ['blue', 'brown', 'green', 'pink', 'purple', 'red', 'yellow', '-', 'all-for-one', 'angel-outfit', 'archery-target', 'arrowed', 'aura', 'back-pain', 'black-flag', 'bleeding-eye', 'bolt-shield', 'broken-heart', 'broken-shield', 'broken-skull', 'chained-heart', 'chemical-bolt', 'cobweb', 'dead', 'death-zone', 'drink-me', 'edge-crack', 'fishing-net', 'fist', 'fluffy-wing', 'flying-flag', 'frozen-orb', 'grab', 'grenade', 'half-haze', 'half-heart', 'interdiction', 'lightning-helix', 'ninja-mask', 'overdrive', 'padlock', 'pummeled', 'radioactive', 'rolling-bomb', 'screaming', 'sentry-gun', 'skull', 'sleepy', 'snail', 'spanner', 'stopwatch','strong', 'three-leaves', 'tread', 'trophy', 'white-tower'], + shaped_conditions = ['blinded', 'charmed', 'deafened', 'frightened', 'grappled', 'incapacitated', 'invisible', 'paralyzed', 'petrified', 'poisoned', 'prone', 'restrained', 'stunned', 'unconscious'], + + script_name = 'StatusInfo', + state_name = 'STATUSINFO', + + handleInput = (msg) => { + if (msg.type != 'api') return; + + // !condition BlindedBlinded + + // Split the message into command and argument(s) + let args = msg.content.split(' '); + let command = args.shift().substring(1); + let extracommand = args.shift(); + + if(command === state[state_name].config.command){ + switch(extracommand){ + case 'reset': + if(!playerIsGM(msg.playerid)) return; + + state[state_name] = {}; + setDefaults(true); + sendConfigMenu(); + break; + + case 'help': + if(!playerIsGM(msg.playerid)) return; + + sendHelpMenu(); + break; + + case 'config': + if(!playerIsGM(msg.playerid)) return; + + if(args.length > 0){ + if(args[0] === 'export' || args[0] === 'import'){ + if(args[0] === 'export'){ + makeAndSendMenu('
'+HE(JSON.stringify(state[state_name]))+'

Copy the entire content above and save it on your pc.

'); + } + if(args[0] === 'import'){ + let json; + let config = msg.content.substring(('!'+state[state_name].config.command+' config import ').length); + try{ + json = JSON.parse(config); + } catch(e) { + makeAndSendMenu('This is not a valid JSON string.'); + return; + } + state[state_name] = json; + sendConfigMenu(); + } + + return; + } + + + let setting = args.shift().split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; + + if(key === 'prefix' && value.charAt(0) !== '_'){ value = '_' + value} + + state[state_name].config[key] = value; + + whisper = (state[state_name].config.sendOnlyToGM) ? '/w gm ' : ''; + } + + sendConfigMenu(); + break; + + // !s config-conditions + // !s config-conditions add + // !s config-conditions prone + // !s config-conditions prone name|blaat + case 'config-conditions': + if(!playerIsGM(msg.playerid)) return; + + let condition = args.shift(); + if(condition === 'add'){ + condition = args.shift(); + if(!condition){ + sendConditionsConfigMenu('You didn\'t give a condition name, eg. !'+state[state_name].config.command+' config-conditions add Prone.'); + return; + } + if(state[state_name].conditions[condition.toLowerCase()]){ + sendConditionsConfigMenu('The condition `'+condition+'` already exists.'); + return; + } + + state[state_name].conditions[condition.toLowerCase()] = { + name: condition, + icon: 'red', + description: '' + } + + sendSingleConditionConfigMenu(condition.toLowerCase()); + return; + } + + if(condition === 'remove'){ + let condition = args.shift(), + justDoIt = (args.shift() === 'yes'); + + if(!justDoIt) return; + + if(!condition){ + sendConditionsConfigMenu('You didn\'t give a condition name, eg. !'+state[state_name].config.command+' config-conditions remove Prone.'); + return; + } + if(!state[state_name].conditions[condition.toLowerCase()]){ + sendConditionsConfigMenu('The condition `'+condition+'` does\'t exist.'); + return; + } + + delete state[state_name].conditions[condition.toLowerCase()]; + sendConditionsConfigMenu('The condition `'+condition+'` is removed.'); + } + + if(state[state_name].conditions[condition]){ + if(args.length > 0){ + let setting = args.shift().split('|'); + let key = setting.shift(); + let value = (setting[0] === 'true') ? true : (setting[0] === 'false') ? false : setting[0]; + + if(key === 'name' && value !== state[state_name].conditions[condition].name){ + state[state_name].conditions[value.toLowerCase()] = state[state_name].conditions[condition]; + delete state[state_name].conditions[condition]; + condition = value.toLowerCase(); + } + + // If we are editting the description, join the args all together in a string. + value = (key === 'description') ? value + ' ' + args.join(' ') : value; + + state[state_name].conditions[condition][key] = value; + } + + sendSingleConditionConfigMenu(condition); + return; + } + + sendConditionsConfigMenu(); + break; + + case 'add': case 'remove': case 'toggle': + if(!state[state_name].config.userToggle && !playerIsGM(msg.playerid)) return; + + if(!msg.selected || !msg.selected.length){ + makeAndSendMenu('No tokens are selected.'); + return; + } + if(!args.length){ + makeAndSendMenu('No condition(s) were given. Use: !'+state[state_name].config.command+' '+extracommand+' prone'); + return; + } + + let tokens = msg.selected.map(s => getObj(s._type, s._id)) + handleConditions(args, tokens, extracommand); + break; + + default: + if(!state[state_name].config.userAllowed && !playerIsGM(msg.playerid)) return; + + let condition_name = extracommand; + if(condition_name){ + let condition; + // Check if hte condition exists in the condition object. + if(condition = getConditionByName(condition_name)){ + // Send it to chat. + sendConditionToChat(condition); + }else{ + sendChat((whisper) ? script_name : '', whisper + 'Condition ' + condition_name + ' does not exist.', null, {noarchive:true}); + } + }else{ + if(!playerIsGM(msg.playerid)) return; + + sendMenu(msg.selected); + } + break; + } + } + }, + + handleConditions = (conditions, tokens, type='add', error=true) => { + conditions.forEach(condition_key => { + if(!state[state_name].conditions[condition_key.toLowerCase()]){ + if(error) makeAndSendMenu('The condition `'+condition_key+'` does not exist.'); + return; + } + + condition_key = condition_key.toLowerCase(); + + tokens.forEach(token => { + let prevSM = token.get('statusmarkers'); + let add = (type === 'add') ? true : (type === 'toggle') ? !token.get('status_'+getConditionByName(condition_key).icon) : false; + token.set('status_'+getConditionByName(condition_key).icon, add); + + let prev = token; + prev.attributes.statusmarkers = prevSM; + + notifyObservers('tokenChange', token, prev); + + if(add && !handled.includes(condition_key)){ + sendConditionToChat(getConditionByName(condition_key)); + doHandled(condition_key); + } + + handleShapedSheet(token.get('represents'), condition_key, add); + }); + }); + }, + + handleShapedSheet = (characterid, condition, add) => { + let character = getObj('character', characterid); + if(character){ + let sheet = character.get("charactersheetname"); + if(!sheet || !sheet.toLowerCase().includes('shaped')) return; + if(!shaped_conditions.includes(condition)) return; + + let attributes = {}; + attributes[condition] = (add) ? '1': '0'; + setAttrs(character.get('id'), attributes); + } + }, + + esRE = function (s) { + var escapeForRegexp = /(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g; + return s.replace(escapeForRegexp,"\\$1"); + }, + + HE = (function(){ + var entities={ + //' ' : '&'+'nbsp'+';', + '<' : '&'+'lt'+';', + '>' : '&'+'gt'+';', + "'" : '&'+'#39'+';', + '@' : '&'+'#64'+';', + '{' : '&'+'#123'+';', + '|' : '&'+'#124'+';', + '}' : '&'+'#125'+';', + '[' : '&'+'#91'+';', + ']' : '&'+'#93'+';', + '"' : '&'+'quot'+';' + }, + re=new RegExp('('+_.map(_.keys(entities),esRE).join('|')+')','g'); + return function(s){ + return s.replace(re, function(c){ return entities[c] || c; }); + }; + }()), + + handleStatusmarkerChange = (obj, prev) => { + if(handled.includes(obj.get('represents')) || !prev || !obj) return + + prev.statusmarkers = (typeof prev.get === 'function') ? prev.get('statusmarkers') : prev.statusmarkers; + + if(state[state_name].config.showDescOnStatusChange && typeof prev.statusmarkers === 'string'){ + // Check if the statusmarkers string is different from the previous statusmarkers string. + if(obj.get('statusmarkers') !== prev.statusmarkers){ + // Create arrays from the statusmarkers strings. + var prevstatusmarkers = prev.statusmarkers.split(","); + var statusmarkers = obj.get('statusmarkers').split(","); + + // Loop through the statusmarkers array. + statusmarkers.forEach(function(marker){ + let condition = getConditionByMarker(marker); + if(!condition) return; + // If it is a new statusmarkers, get the condition from the conditions object, and send it to chat. + if(marker !== "" && !prevstatusmarkers.includes(marker)){ + if(handled.includes(condition.name.toLowerCase())) return; + + //sendConditionToChat(condition); + handleConditions([condition.name], [obj], 'add', false) + doHandled(obj.get('represents')); + } + }); + + prevstatusmarkers.forEach((marker) => { + let condition = getConditionByMarker(marker); + if(!condition) return; + + if(marker !== '' && !statusmarkers.includes(marker)){ + handleConditions([condition.name], [obj], 'remove', false); + } + }) + } + } + }, + + handleAttributeChange = (obj, prev) => { + if(!shaped_conditions.includes(obj.get('name'))) return; + + let tokens = findObjs({ represents: obj.get('characterid') }); + + handleConditions([obj.get('name')], tokens, (obj.get('current') === '1') ? 'add' : 'remove') + }, + + doHandled = (what) => { + handled.push(what); + setTimeout(() => { + handled.splice(handled.indexOf(what), 1); + }, 1000); + }, + + getConditionByMarker = (marker) => { + return getObjects(state[state_name].conditions, 'icon', marker).shift() || false; + }, + + getConditionByName = (name) => { + return state[state_name].conditions[name.toLowerCase()] || false; + }, + + sendConditionToChat = (condition, w) => { + if(!condition.description || condition.description === '') return; + + let icon = (state[state_name].config.showIconInDescription) ? getIcon(condition.icon, 'margin-right: 5px; margin-top: 5px; display: inline-block;') || '' : ''; + + makeAndSendMenu(condition.description, icon+condition.name, { + title_tag: 'h2', + whisper: (state[state_name].config.sendOnlyToGM) ? 'gm' : '' + }); + }, + + getIcon = (icon, style='') => { + let X = ''; + let iconStyle = '' + + if(typeof icon_image_positions[icon] === 'undefined') return false; + //if(!icon_image_positions[icon]) return false; + + iconStyle += 'width: 24px; height: 24px;'; + + if(Number.isInteger(icon_image_positions[icon])){ + iconStyle += 'background-image: url(https://roll20.net/images/statussheet.png);' + iconStyle += 'background-repeat: no-repeat;' + iconStyle += 'background-position: -'+icon_image_positions[icon]+'px 0;' + }else if(icon_image_positions[icon] === 'X'){ + iconStyle += 'color: red; margin-right: 0px;'; + X = 'X'; + }else{ + iconStyle += 'background-color: ' + icon_image_positions[icon] + ';'; + iconStyle += 'border: 1px solid white; border-radius: 50%;' + } + + iconStyle += style; + + // TODO: Make span + return '
'+X+'
'; + }, + + ucFirst = (string) => { + return string.charAt(0).toUpperCase() + string.slice(1); + }, + + //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; + }, + + sendConditionsConfigMenu = (message) => { + if(!state[state_name].conditions || typeof state[state_name].conditions === 'object') setDefaults(); + + let listItems = [], + icons = [], + check = true; + for(let key in state[state_name].conditions){ + let configButton = makeButton('Change', '!' + state[state_name].config.command + ' config-conditions '+key, buttonStyle); + listItems.push(''+getIcon(state[state_name].conditions[key].icon, 'display: inline-block;')+state[state_name].conditions[key].name+' ' + configButton); + + if(check && icons.includes(state[state_name].conditions[key].icon)){ + message = message || '' + '
Multiple conditions use the same icon'; + check = false; + } + + icons.push(state[state_name].conditions[key].icon); + } + + let backButton = makeButton('Back', '!' + state[state_name].config.command + ' config', buttonStyle + ' width: 100%'); + let addButton = makeButton('Add Condition', '!' + state[state_name].config.command + ' config-conditions add ?{Name}', buttonStyle + 'float: none;'); + + message = (message) ? '

'+message+'

' : ''; + let contents = makeList(listItems, listStyle + ' overflow:hidden;', 'overflow: hidden')+'
'+message+addButton+'
'+backButton; + makeAndSendMenu(contents, 'Conditions'); + }, + + sendSingleConditionConfigMenu = (conditionKey, message) => { + if(!conditionKey || !state[state_name].conditions[conditionKey]){ + sendConditionsConfigMenu('Condition '+conditionKey+' does not exist.'); + return; + } + + let condition = state[state_name].conditions[conditionKey]; + + let listItems = []; + let nameButton = makeButton(condition.name, '!' + state[state_name].config.command + ' config-conditions '+conditionKey+' name|?{Name}', buttonStyle); + listItems.push('Name: ' + nameButton); + + let markerDropdown = '?{Marker'; + markers.forEach((marker) => { + markerDropdown += '|'+ucFirst(marker).replace(/-/g, ' ')+','+marker + }) + markerDropdown += '}'; + + let markerButton = makeButton(getIcon(condition.icon) || condition.icon, '!' + state[state_name].config.command + ' config-conditions '+conditionKey+' icon|'+markerDropdown, buttonStyle); + listItems.push('Statusmarker: ' + markerButton); + + let backButton = makeButton('Back', '!' + state[state_name].config.command + ' config-conditions', buttonStyle + ' width: 100%'); + let removeButton = makeButton('Remove', '!' + state[state_name].config.command + ' config-conditions remove '+conditionKey+' ?{Are you sure?|Yes,yes|No,no}', buttonStyle + ' width: 100%'); + let changeButton = makeButton('Edit Description', '!' + state[state_name].config.command + ' config-conditions '+conditionKey+' description|?{Description|'+condition.description+'}', buttonStyle); + + message = (message) ? '

'+message+'

' : ''; + let contents = message+makeList(listItems, listStyle + ' overflow:hidden;', 'overflow: hidden')+'
Description:'+condition.description+changeButton+'

'+removeButton+backButton+'

'; + makeAndSendMenu(contents, condition.name + ' - Config'); + }, + + sendMenu = (selected, show_names) => { + let contents = ''; + if(selected && selected.length){ + selected.forEach(s => { + let token = getObj(s._type, s._id); + if(token && token.get('statusmarkers') !== ''){ + let statusmarkers = token.get('statusmarkers').split(','); + let active_conditions = []; + statusmarkers.forEach(marker => { + let con; + if(con = getObjects(state[state_name].conditions, 'icon', marker)){ + if(con[0] && con[0].name) active_conditions.push(con[0].name); + } + }); + + if(active_conditions.length){ + contents += ''+token.get('name') + '\'s Conditions:
' + active_conditions.join(', ') + '
'; + } + } + }); + } + + contents += 'Toggle Condition on Selected Token(s):
' + for(let condition_key in state[state_name].conditions){ + let condition = state[state_name].conditions[condition_key]; + contents += makeButton(getIcon(condition.icon) || condition.name, '!' + state[state_name].config.command + ' toggle '+condition_key, buttonStyle + 'float: none; margin-right: 5px;', condition.name); + } + //contents += (!show_names) ? '
' + makeButton('Show Names', '!' + state[state_name].config.command + ' names', buttonStyle + 'float: none;') : '
' + makeButton('Hide Names', '!' + state[state_name].config.command, buttonStyle + 'float: none;'); + + makeAndSendMenu(contents, script_name + ' Menu'); + }, + + sendConfigMenu = (first) => { + let commandButton = makeButton('!'+state[state_name].config.command, '!' + state[state_name].config.command + ' config command|?{Command (without !)}', buttonStyle); + let userAllowedButton = makeButton(state[state_name].config.userAllowed, '!' + state[state_name].config.command + ' config userAllowed|'+!state[state_name].config.userAllowed, buttonStyle); + let userToggleButton = makeButton(state[state_name].config.userToggle, '!' + state[state_name].config.command + ' config userToggle|'+!state[state_name].config.userToggle, buttonStyle); + let toGMButton = makeButton(state[state_name].config.sendOnlyToGM, '!' + state[state_name].config.command + ' config sendOnlyToGM|'+!state[state_name].config.sendOnlyToGM, buttonStyle); + let statusChangeButton = makeButton(state[state_name].config.showDescOnStatusChange, '!' + state[state_name].config.command + ' config showDescOnStatusChange|'+!state[state_name].config.showDescOnStatusChange, buttonStyle); + let showIconButton = makeButton(state[state_name].config.showIconInDescription, '!' + state[state_name].config.command + ' config showIconInDescription|'+!state[state_name].config.showIconInDescription, buttonStyle); + + let listItems = [ + 'Command: ' + commandButton, + 'Only to GM: '+toGMButton, + 'Player Show: '+userAllowedButton, + 'Player Toggle: '+userToggleButton, + 'Show on Status Change: '+statusChangeButton, + 'Display icon in chat: '+showIconButton + ]; + + let configConditionsButton = makeButton('Conditions Config', '!' + state[state_name].config.command + ' config-conditions', buttonStyle + ' width: 100%'); + let resetButton = makeButton('Reset Config', '!' + state[state_name].config.command + ' reset', buttonStyle + ' width: 100%'); + + let exportButton = makeButton('Export Config', '!' + state[state_name].config.command + ' config export', buttonStyle + ' width: 100%'); + let importButton = makeButton('Import Config', '!' + state[state_name].config.command + ' config import ?{Config}', buttonStyle + ' width: 100%'); + + let title_text = (first) ? script_name+' First Time Setup' : script_name+' Config'; + let contents = makeList(listItems, listStyle + ' overflow:hidden;', 'overflow: hidden')+'
'+configConditionsButton+'

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


'+exportButton+importButton+resetButton; + makeAndSendMenu(contents, title_text) + }, + + sendHelpMenu = (first) => { + let configButton = makeButton('Config', '!' + state[state_name].config.command + ' config', buttonStyle + ' width: 100%;') + + let listItems = [ + '!'+state[state_name].config.command+' help - Shows this menu.', + '!'+state[state_name].config.command+' config - Shows the configuration menu.', + '!'+state[state_name].config.command+' [CONDITION] - Shows the description of the condition entered.', + ' ', + '!'+state[state_name].config.command+' add [CONDITIONS] - Add the given condition(s) to the selected token(s).', + '!'+state[state_name].config.command+' remove [CONDITIONS] - Remove the given condition(s) from the selected token(s).', + ' ', + '!'+state[state_name].config.command+' config export - Exports the config (with conditions).', + '!'+state[state_name].config.command+' config import [JSON] - Imports the given config (with conditions).' + ] + + let contents = 'Commands:'+makeList(listItems, listStyle)+'
'+configButton; + makeAndSendMenu(contents, script_name+' Help') + }, + + makeAndSendMenu = (contents, title, settings) => { + settings = settings || {}; + settings.whisper = (typeof settings.whisper === 'undefined' || settings.whisper === 'gm') ? '/w gm ' : ''; + title = (title && title != '') ? makeTitle(title, settings.title_tag || '') : ''; + sendChat(script_name, settings.whisper + '
'+title+contents+'
', null, {noarchive:true}); + }, + + makeTitle = (title, title_tag) => { + title_tag = (title_tag && title_tag !== '') ? title_tag : 'h3'; + return '<'+title_tag+' style="margin-bottom: 10px;">'+title+''; + }, + + makeButton = (title, href, style, alt) => { + return ''+title+''; + }, + + makeList = (items, listStyle, itemStyle) => { + let list = ''; + return list; + }, + + getConditions = () => { + return state[state_name].conditions; + }, + + checkInstall = () => { + if(!_.has(state, state_name)){ + state[state_name] = state[state_name] || {}; + } + setDefaults(); + + log(script_name + ' Ready! Command: !'+state[state_name].config.command); + }, + + 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:graphic:statusmarkers', handleStatusmarkerChange); + on('change:attribute', handleAttributeChange); + + // Handle condition descriptions when tokenmod changes the statusmarkers on a token. + if('undefined' !== typeof TokenMod && TokenMod.ObserveTokenChange){ + TokenMod.ObserveTokenChange((obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + + if('undefined' !== typeof ApplyDamage && ApplyDamage.registerObserver){ + ApplyDamage.registerObserver("change",(obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + + if('undefined' !== typeof DeathTracker && DeathTracker.ObserveTokenChange){ + DeathTracker.ObserveTokenChange((obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + + if('undefined' !== typeof InspirationTracker && InspirationTracker.ObserveTokenChange){ + InspirationTracker.ObserveTokenChange((obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + + if('undefined' !== typeof CombatTracker && CombatTracker.ObserveTokenChange){ + CombatTracker.ObserveTokenChange((obj,prev) => { + handleStatusmarkerChange(obj,prev); + }); + } + }, + + setDefaults = (reset) => { + + // DEVELOPER NOTE: ON CHANGE! CHECK BITCH! DENK OM OLD IMPORTS! + + const defaults = { + config: { + command: 'condition', + userAllowed: false, + userToggle: false, + sendOnlyToGM: false, + showDescOnStatusChange: true, + showIconInDescription: true + }, + conditions: { + blinded: { + name: 'Blinded', + description: '

A blinded creature can’t see and automatically fails any ability check that requires sight.

Attack rolls against the creature have advantage, and the creature’s Attack rolls have disadvantage.

', + icon: 'bleeding-eye' + }, + charmed: { + name: 'Charmed', + description: '

A charmed creature can’t Attack the charmer or target the charmer with harmful Abilities or magical effects.

The charmer has advantage on any ability check to interact socially with the creature.

', + icon: 'broken-heart' + }, + deafened: { + name: 'Deafened', + description: '

A deafened creature can’t hear and automatically fails any ability check that requires hearing.

', + icon: 'edge-crack' + }, + frightened: { + name: 'Frightened', + description: '

A frightened creature has disadvantage on Ability Checks and Attack rolls while the source of its fear is within line of sight.

The creature can’t willingly move closer to the source of its fear.

', + icon: 'screaming' + }, + grappled: { + name: 'Grappled', + description: '

A grappled creature’s speed becomes 0, and it can’t benefit from any bonus to its speed.

The condition ends if the Grappler is incapacitated.

The condition also ends if an effect removes the grappled creature from the reach of the Grappler or Grappling effect, such as when a creature is hurled away by the Thunderwave spell.

', + icon: 'grab' + }, + incapacitated: { + name: 'Incapacitated', + description: '

An incapacitated creature can’t take actions or reactions.

', + icon: 'interdiction' + }, + inspiration: { + name: 'Inspiration', + description: '

If you have inspiration, you can expend it when you make an Attack roll, saving throw, or ability check. Spending your inspiration gives you advantage on that roll.

Additionally, if you have inspiration, you can reward another player for good roleplaying, clever thinking, or simply doing something exciting in the game. When another player character does something that really contributes to the story in a fun and interesting way, you can give up your inspiration to give that character inspiration.

', + icon: 'black-flag' + }, + invisibility: { + name: 'Invisibility', + description: '

An invisible creature is impossible to see without the aid of magic or a Special sense. For the purpose of Hiding, the creature is heavily obscured. The creature’s location can be detected by any noise it makes or any tracks it leaves.

Attack rolls against the creature have disadvantage, and the creature’s Attack rolls have advantage.

', + icon: 'ninja-mask' + }, + paralyzed: { + name: 'Paralyzed', + description: '

A paralyzed creature is incapacitated and can’t move or speak.

The creature automatically fails Strength and Dexterity saving throws.

Attack rolls against the creature have advantage.

Any Attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.

', + icon: 'pummeled' + }, + petrified: { + name: 'Petrified', + description: '

A petrified creature is transformed, along with any nonmagical object it is wearing or carrying, into a solid inanimate substance (usually stone). Its weight increases by a factor of ten, and it ceases aging.

The creature is incapacitated, can’t move or speak, and is unaware of its surroundings.

Attack rolls against the creature have advantage.

The creature automatically fails Strength and Dexterity saving throws.

The creature has Resistance to all damage.

The creature is immune to poison and disease, although a poison or disease already in its system is suspended, not neutralized.

', + icon: 'frozen-orb' + }, + poisoned: { + name: 'Poisoned', + description: '

A poisoned creature has disadvantage on Attack rolls and Ability Checks.

', + icon: 'chemical-bolt' + }, + prone: { + name: 'Prone', + description: '

A prone creature’s only Movement option is to crawl, unless it stands up and thereby ends the condition.

The creature has disadvantage on Attack rolls.

An Attack roll against the creature has advantage if the attacker is within 5 feet of the creature. Otherwise, the Attack roll has disadvantage.

', + icon: 'back-pain' + }, + restrained: { + name: 'Restrained', + description: '

A restrained creature’s speed becomes 0, and it can’t benefit from any bonus to its speed.

Attack rolls against the creature have advantage, and the creature’s Attack rolls have disadvantage.

The creature has disadvantage on Dexterity saving throws.

', + icon: 'fishing-net' + }, + stunned: { + name: 'Stunned', + description: '

A stunned creature is incapacitated, can’t move, and can speak only falteringly.

The creature automatically fails Strength and Dexterity saving throws.

Attack rolls against the creature have advantage.

', + icon: 'fist' + }, + unconscious: { + name: 'Unconscious', + description: '

An unconscious creature is incapacitated, can’t move or speak, and is unaware of its surroundings.

The creature drops whatever it’s holding and falls prone.

The creature automatically fails Strength and Dexterity saving throws.

Attack rolls against the creature have advantage.

Any Attack that hits the creature is a critical hit if the attacker is within 5 feet of the creature.

', + icon: 'sleepy' + }, + }, + }; + + 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('userAllowed')){ + state[state_name].config.userAllowed = defaults.config.userAllowed; + } + if(!state[state_name].config.hasOwnProperty('userToggle')){ + state[state_name].config.userToggle = defaults.config.userToggle; + } + if(!state[state_name].config.hasOwnProperty('sendOnlyToGM')){ + state[state_name].config.sendOnlyToGM = defaults.config.sendOnlyToGM; + } + if(!state[state_name].config.hasOwnProperty('showDescOnStatusChange')){ + state[state_name].config.showDescOnStatusChange = defaults.config.showDescOnStatusChange; + } + if(!state[state_name].config.hasOwnProperty('showIconInDescription')){ + state[state_name].config.showIconInDescription = defaults.config.showIconInDescription; + } + } + + if(!state[state_name].conditions || typeof state[state_name].conditions !== 'object'){ + state[state_name].conditions = defaults.conditions; + } + + whisper = (state[state_name].config.sendOnlyToGM) ? '/w gm ' : ''; + + if(!state[state_name].config.hasOwnProperty('firsttime') && !reset){ + sendConfigMenu(true); + state[state_name].config.firsttime = false; + } + }; + + return { + checkInstall, + ObserveTokenChange: observeTokenChange, + registerEventHandlers, + getConditions, + getConditionByName, + handleConditions, + sendConditionToChat, + getIcon, + version + }; +})(); + +on('ready', () => { + 'use strict'; + + StatusInfo.checkInstall(); + StatusInfo.registerEventHandlers(); +}); diff --git a/StatusInfo/README.md b/StatusInfo/README.md index 6a36cff9b0..670ab3e04a 100644 --- a/StatusInfo/README.md +++ b/StatusInfo/README.md @@ -14,7 +14,7 @@ --- ``` -LATEST UPDATE: It now allows you to create and edit conditions, export/import the config, and add/remove/toggle condition(s) to/from token(s), see below. +LATEST UPDATE: Updated to work with the D&D 2024 sheet. ``` StatusInfo works nicely together with [Tokenmod](https://app.roll20.net/forum/post/4225825/script-update-tokenmod-an-interface-to-adjusting-properties-of-a-token-from-a-macro-or-the-chat-area/?pageforid=4225825#post-4225825) and my own [DeathTracker](https://github.com/RobinKuiper/Roll20APIScripts/tree/master/DeathTracker) and [InspirationTracker](https://github.com/RobinKuiper/Roll20APIScripts/tree/master/InspirationTracker) scripts. diff --git a/StatusInfo/StatusInfo.js b/StatusInfo/StatusInfo.js index dfcd1e31ff..d3054aa866 100644 --- a/StatusInfo/StatusInfo.js +++ b/StatusInfo/StatusInfo.js @@ -264,7 +264,7 @@ var StatusInfo = StatusInfo || (function() { handleShapedSheet = (characterid, condition, add) => { let character = getObj('character', characterid); if(character){ - let sheet = getAttrByName(character.get('id'), 'character_sheet', 'current'); + let sheet = character.get("charactersheetname"); if(!sheet || !sheet.toLowerCase().includes('shaped')) return; if(!shaped_conditions.includes(condition)) return; diff --git a/StatusInfo/script.json b/StatusInfo/script.json index 11f35ea60a..846b6f6797 100644 --- a/StatusInfo/script.json +++ b/StatusInfo/script.json @@ -1,9 +1,9 @@ { "name": "StatusInfo", "script": "StatusInfo.js", - "version": "0.3.11", - "previousversions": ["0.3.2", "0.3.4", "0.3.6", "0.3.8", "0.3.10"], - "description": "All info and latest version on \n\n https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo", + "version": "0.3.12", + "previousversions": ["0.3.2", "0.3.4", "0.3.6", "0.3.8", "0.3.10", "0.3.11"], + "description": "All info and latest version on \n\n https://github.com/RobinKuiper/Roll20APIScripts/tree/master/StatusInfo\n\nThis script is compatible with the D&D 2024 Character Sheet.", "authors": "Robin Kuiper", "roll20userid": "1226016", "patreon": "https://www.patreon.com/robinkuiper",