From e59a63bd027108f10adfbcaacd4b714723085504 Mon Sep 17 00:00:00 2001 From: timmaugh Date: Tue, 20 May 2025 08:32:22 -0400 Subject: [PATCH] Script Updates Messenger - better CSS processing ShowButtons - can use 'all' as an argument value SheetDefaults - bug fix RE: race condition --- Messenger/1.0.2/Messenger.js | 661 +++++++++++++++++++++++++++ Messenger/Messenger.js | 92 ++-- Messenger/script.json | 5 +- SheetDefaults/1.0.1/SheetDefaults.js | 554 ++++++++++++++++++++++ SheetDefaults/SheetDefaults.js | 10 +- SheetDefaults/script.json | 6 +- ShowButtons/1.0.2/ShowButtons.js | 528 +++++++++++++++++++++ ShowButtons/ShowButtons.js | 129 ++++-- ShowButtons/script.json | 4 +- 9 files changed, 1909 insertions(+), 80 deletions(-) create mode 100644 Messenger/1.0.2/Messenger.js create mode 100644 SheetDefaults/1.0.1/SheetDefaults.js create mode 100644 ShowButtons/1.0.2/ShowButtons.js diff --git a/Messenger/1.0.2/Messenger.js b/Messenger/1.0.2/Messenger.js new file mode 100644 index 0000000000..fb1574e3bd --- /dev/null +++ b/Messenger/1.0.2/Messenger.js @@ -0,0 +1,661 @@ +/* eslint no-prototype-builtins: "off" */ +/* +========================================================= +Name : Messenger +GitHub : +Roll20 Contact : timmaugh +Version : 1.0.2 +Last Update : 20 MAY 2025 +========================================================= +*/ +var API_Meta = API_Meta || {}; +API_Meta.Messenger = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; +{ try { throw new Error(''); } catch (e) { API_Meta.Messenger.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (13)); } } + +const Messenger = (() => { // eslint-disable-line no-unused-vars + const apiproject = 'Messenger'; + const apilogo = `https://i.imgur.com/DEkWTak.png`; + const version = '1.0.2'; + const schemaVersion = 0.1; + API_Meta[apiproject].version = version; + const vd = new Date(1747744062840); + const versionInfo = () => { + log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`); + }; + const logsig = () => { + // initialize shared namespace for all signed projects, if needed + state.torii = state.torii || {}; + // initialize siglogged check, if needed + state.torii.siglogged = state.torii.siglogged || false; + state.torii.sigtime = state.torii.sigtime || Date.now() - 3001; + if (!state.torii.siglogged || Date.now() - state.torii.sigtime > 3000) { + const logsig = '\n' + + ' _____________________________________________ ' + '\n' + + ' )_________________________________________( ' + '\n' + + ' )_____________________________________( ' + '\n' + + ' ___| |_______________| |___ ' + '\n' + + ' |___ _______________ ___| ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + '______________|_|_______________|_|_______________' + '\n' + + ' ' + '\n'; + log(`${logsig}`); + state.torii.siglogged = true; + state.torii.sigtime = Date.now(); + } + return; + }; + // ================================================== + // STATE MANAGEMENT + // ================================================== + const checkInstall = () => { + if (!state.hasOwnProperty(apiproject) || state[apiproject].version !== schemaVersion) { // eslint-disable-line no-prototype-builtins + log(` > Updating ${apiproject} Schema to v${schemaVersion} <`); + switch (state[apiproject] && state[apiproject].version) { + + case 0.1: + /* falls through */ + + case 'UpdateSchemaVersion': + state[apiproject].version = schemaVersion; + break; + + default: + state[apiproject] = { + settings: { + }, + defaults: { + }, + version: schemaVersion + } + break; + } + } + }; + let stateReady = false; + const assureState = () => { + if (!stateReady) { + checkInstall(); + stateReady = true; + } + }; + const manageState = { // eslint-disable-line no-unused-vars + reset: () => state[apiproject].settings = _.clone(state[apiproject].defaults), + clone: () => { return _.clone(state[apiproject].settings); }, + set: (p, v) => state[apiproject].settings[p] = v, + get: (p) => { return state[apiproject].settings[p]; } + }; + + // ============================================ + // PRESENTATION + // ============================================ + const getTextColor = (h) => { + h = `#${h.replace(/#/g, '')}`; + let hc = hexToRGBA(h); + return (((hc[0] * 299) + (hc[1] * 587) + (hc[2] * 114)) / 1000 >= 128) ? "#000000" : "#ffffff"; + }; + const hexToRGBA = (hex, alpha, reqAlpha = 'auto') => { + + const isValidHex = (hex) => /^#([A-Fa-f0-9]{3,4}){1,2}$/.test(hex); + + const getChunksFromString = (st, chunkSize) => st.match(new RegExp(`.{${chunkSize}}`, "g")); + + const convertHexUnitTo256 = (hexStr) => parseInt(hexStr.repeat(2 / hexStr.length), 16); + + const getAlphafloat = (a, alpha) => { + if (typeof a !== "undefined") { return a / 255 } + if ((typeof alpha != "number") || alpha < 0 || alpha > 1) { // eslint-disable-line eqeqeq + return 1 + } + return alpha + }; + + if (!isValidHex(hex)) { throw new Error("Invalid HEX") } + const chunkSize = Math.floor((hex.length - 1) / 3) + const hexArr = getChunksFromString(hex.slice(1), chunkSize) + const [r, g, b, a] = hexArr.map(convertHexUnitTo256) + switch (reqAlpha) { + case true: + return `rgba(${r}, ${g}, ${b}, ${getAlphafloat(a, alpha)})`; + case false: + return `rgb(${r}, ${g}, ${b})`; + default: + return `rgb${a || alpha ? 'a' : ''}(${r}, ${g}, ${b}${a || alpha ? `, ${getAlphafloat(a, alpha)}` : ''})`; + } + }; + + //const hexToRGB = (h) => { + // let r = 0, g = 0, b = 0; + + // // 3 digits + // if (h.length === 4) { + // r = "0x" + h[1] + h[1]; + // g = "0x" + h[2] + h[2]; + // b = "0x" + h[3] + h[3]; + // // 6 digits + // } else if (h.length === 7) { + // r = "0x" + h[1] + h[2]; + // g = "0x" + h[3] + h[4]; + // b = "0x" + h[5] + h[6]; + // } + // return [+r, +g, +b]; + //}; + const validCSSColors = { + AliceBlue: `#F0F8FF`, + AntiqueWhite: `#FAEBD7`, + Aqua: `#00FFFF`, + Aquamarine: `#7FFFD4`, + Azure: `#F0FFFF`, + Beige: `#F5F5DC`, + Bisque: `#FFE4C4`, + Black: `#000000`, + BlanchedAlmond: `#FFEBCD`, + Blue: `#0000FF`, + BlueViolet: `#8A2BE2`, + Brown: `#A52A2A`, + BurlyWood: `#DEB887`, + CadetBlue: `#5F9EA0`, + Chartreuse: `#7FFF00`, + Chocolate: `#D2691E`, + Coral: `#FF7F50`, + CornflowerBlue: `#6495ED`, + Cornsilk: `#FFF8DC`, + Crimson: `#DC143C`, + Cyan: `#00FFFF`, + DarkBlue: `#00008B`, + DarkCyan: `#008B8B`, + DarkGoldenrod: `#B8860B`, + DarkGray: `#A9A9A9`, + DarkGreen: `#006400`, + DarkGrey: `#A9A9A9`, + DarkKhaki: `#BDB76B`, + DarkMagenta: `#8B008B`, + DarkOliveGreen: `#556B2F`, + DarkOrange: `#FF8C00`, + DarkOrchid: `#9932CC`, + DarkRed: `#8B0000`, + DarkSalmon: `#E9967A`, + DarkSeaGreen: `#8FBC8F`, + DarkSlateBlue: `#483D8B`, + DarkSlateGray: `#2F4F4F`, + DarkSlateGrey: `#2F4F4F`, + DarkTurquoise: `#00CED1`, + DarkViolet: `#9400D3`, + DeepPink: `#FF1493`, + DeepSkyBlue: `#00BFFF`, + DimGray: `#696969`, + DimGrey: `#696969`, + DodgerBlue: `#1E90FF`, + FireBrick: `#B22222`, + FloralWhite: `#FFFAF0`, + ForestGreen: `#228B22`, + Fuchsia: `#FF00FF`, + Gainsboro: `#DCDCDC`, + GhostWhite: `#F8F8FF`, + Gold: `#FFD700`, + Goldenrod: `#DAA520`, + Gray: `#808080`, + Green: `#008000`, + GreenYellow: `#ADFF2F`, + Grey: `#808080`, + Honeydew: `#F0FFF0`, + HotPink: `#FF69B4`, + IndianRed: `#CD5C5C`, + Indigo: `#4B0082`, + Ivory: `#FFFFF0`, + Khaki: `#F0E68C`, + Lavender: `#E6E6FA`, + LavenderBlush: `#FFF0F5`, + LawnGreen: `#7CFC00`, + LemonChiffon: `#FFFACD`, + LightBlue: `#ADD8E6`, + LightCoral: `#F08080`, + LightCyan: `#E0FFFF`, + LightGoldenrodYellow: `#FAFAD2`, + LightGray: `#D3D3D3`, + LightGreen: `#90EE90`, + LightGrey: `#D3D3D3`, + LightPink: `#FFB6C1`, + LightSalmon: `#FFA07A`, + LightSeaGreen: `#20B2AA`, + LightSkyBlue: `#87CEFA`, + LightSlateGray: `#778899`, + LightSlateGrey: `#778899`, + LightSteelBlue: `#B0C4DE`, + LightYellow: `#FFFFE0`, + Lime: `#00FF00`, + LimeGreen: `#32CD32`, + Linen: `#FAF0E6`, + Magenta: `#FF00FF`, + Maroon: `#800000`, + MediumAquamarine: `#66CDAA`, + MediumBlue: `#0000CD`, + MediumOrchid: `#BA55D3`, + MediumPurple: `#9370DB`, + MediumSeaGreen: `#3CB371`, + MediumSlateBlue: `#7B68EE`, + MediumSpringGreen: `#00FA9A`, + MediumTurquoise: `#48D1CC`, + MediumVioletRed: `#C71585`, + MidnightBlue: `#191970`, + MintCream: `#F5FFFA`, + MistyRose: `#FFE4E1`, + Moccasin: `#FFE4B5`, + NavajoWhite: `#FFDEAD`, + Navy: `#000080`, + OldLace: `#FDF5E6`, + Olive: `#808000`, + OliveDrab: `#6B8E23`, + Orange: `#FFA500`, + OrangeRed: `#FF4500`, + Orchid: `#DA70D6`, + PaleGoldenrod: `#EEE8AA`, + PaleGreen: `#98FB98`, + PaleTurquoise: `#AFEEEE`, + PaleVioletRed: `#DB7093`, + PapayaWhip: `#FFEFD5`, + PeachPuff: `#FFDAB9`, + Peru: `#CD853F`, + Pink: `#FFC0CB`, + Plum: `#DDA0DD`, + PowderBlue: `#B0E0E6`, + Purple: `#800080`, + RebeccaPurple: `#663399`, + Red: `#FF0000`, + RosyBrown: `#BC8F8F`, + RoyalBlue: `#4169E1`, + SaddleBrown: `#8B4513`, + Salmon: `#FA8072`, + SandyBrown: `#F4A460`, + SeaGreen: `#2E8B57`, + Seashell: `#FFF5EE`, + Sienna: `#A0522D`, + Silver: `#C0C0C0`, + SkyBlue: `#87CEEB`, + SlateBlue: `#6A5ACD`, + SlateGray: `#708090`, + SlateGrey: `#708090`, + Snow: `#FFFAFA`, + SpringGreen: `#00FF7F`, + SteelBlue: `#4682B4`, + Tan: `#D2B48C`, + Teal: `#008080`, + Thistle: `#D8BFD8`, + Tomato: `#FF6347`, + Transparent: 'transparent', + Turquoise: `#40E0D0`, + Unset: 'unset', + Violet: `#EE82EE`, + Wheat: `#F5DEB3`, + White: `#FFFFFF`, + WhiteSmoke: `#F5F5F5`, + Yellow: `#FFFF00`, + YellowGreen: `#9ACD32` + }; + const validateHexColor = (s, d = defaultThemeColor1) => { + let colorRegX = /^#?([A-Fa-f0-9]{3,4}){1,2}$/; + let cname = Object.keys(validCSSColors).filter(c => c.toLowerCase() === s.toLowerCase())[0]; + if (cname) return validCSSColors[cname]; + return `#${colorRegX.test(s) ? s.replace('#', '') : d.replace('#', '')}`; + }; + + // CSS ======================================== + const defaultThemeColor1 = '#66806a'; + const css = { + divContainer: { + 'background-color': '#00000000', + 'overflow': `hidden`, + width: '100%', + border: 'none' + }, + div: { + 'background-color': '#00000000', + 'overflow': `hidden` + }, + rounded: { + 'border-radius': `10px`, + 'border': `2px solid #000000`, + }, + tb: { + width: '100%', + margin: '0 auto', + 'border-collapse': 'collapse', + 'font-size': '12px', + }, + p: { + 'font-family': 'inherit' + }, + a: {}, + img: {}, + h1: {}, + h2: {}, + h3: {}, + h4: {}, + h5: {}, + ol: {}, + ul: {}, + li: {}, + th: { + 'border-bottom': `1px solid #000000`, + 'font-weight': `bold`, + 'text-align': `center`, + 'line-height': `22px` + }, + tr: {}, + td: { + padding: '4px', + 'min-width': '10px' + }, + code: {}, + pre: { + 'color': 'dimgray', + 'background': 'transparent', + 'border': 'none', + 'white-space': 'pre-wrap', + 'font-family': 'Inconsolata, Consolas, monospace' + }, + span: {}, + messageHeader: { + 'border-bottom': `1px solid #000000`, + 'background-color': '#dedede', + 'display': 'block' + }, + messageHeaderContent: { + margin: '0px auto', + width: '98%', + 'line-height': `24px`, + 'padding': '2px 8px', + 'min-height': '25px' + }, + messageBody: { + 'display': 'block', + 'background-color': '#ededed', + 'padding-top': '6px', + 'padding-bottom': '8px' + }, + messageBodyContent: { + margin: '0px auto', + width: '95%', + 'font-size': '13px' + }, + messageButtons: { + 'text-align': `right`, + 'margin': `4px 0px 8px`, + 'padding': '8px' + }, + messageFooterContent: { +// margin: '0px 8px', +// width: '98%' + }, + button: { + 'background-color': defaultThemeColor1, + 'border-radius': '6px', + 'min-width': '25px', + 'padding': '6px 8px' + }, + divShadow: { + 'margin': '0px 16px 16px 0px', + 'box-shadow': '5px 8px 8px #888888' + }, + inlineEmphasis: { + 'font-weight': 'bold' + } + }; + const combineCSS = (origCSS = {}, ...assignCSS) => { + return Object.assign({}, origCSS, assignCSS.reduce((m, v) => { + return Object.assign(m, v || {}); + }, {})); + }; + const confirmReadability = (origCSS = {}) => { + let outputCSS = Object.assign({}, origCSS); + if (outputCSS['background-color']) outputCSS['background-color'] = validateHexColor(outputCSS['background-color'] || "#dedede"); + if (!outputCSS['color'] && outputCSS['background-color']) outputCSS['color'] = getTextColor(outputCSS['background-color'] || "#dedede"); + return outputCSS; + }; + const assembleCSS = (css) => { + return `"${Object.keys(css).map((key) => { return `${key}:${css[key]};` }).join('')}"`; + }; + const processCSS = (...css) => { + return assembleCSS(combineCSS(...css)); + }; + + // HTML ======================================= + const html = { + div: (content, ...CSS) => `
${content}
`, + h1: (content, ...CSS) => `

${content}

`, + h2: (content, ...CSS) => `

${content}

`, + h3: (content, ...CSS) => `

${content}

`, + h4: (content, ...CSS) => `

${content}

`, + h5: (content, ...CSS) => `
${content}
`, + p: (content, ...CSS) => `

${content}

`, + table: (content, ...CSS) => `${content}
`, + th: (content, ...CSS) => `${content}`, + tr: (content, ...CSS) => `${content}`, + td: (content, ...CSS) => `${content}`, + td2: (content, ...CSS) => `${content}`, + tdcs: (content, colspan, ...CSS) => `${content}`, + tdrs: (content, rowspan, ...CSS) => `${content}`, + tdcrs: (content, colspan, rowspan, ...CSS) => `${content}`, + code: (content, ...CSS) => `${content}`, + pre: (content, ...CSS) => `
${content}
`, + span: (content, ...CSS) => `${content}`, + a: (content, link, ...CSS) => `${content}`, + img: (content, altText, ...CSS) => `${altText}`, + tip: (content, tipText, ...CSS) => `${content}`, + tag: (tag, content, ...CSS) => `<${tag} style=${processCSS(css.div, ...CSS)}>${content}`, + ol: (content, ...CSS) => `
    ${content}
`, + ul: (content, ...CSS) => ``, + li: (content, ...CSS) => `
  • ${content}
  • `, + }; + + // HTML Escaping function + const HE = (() => { + const esRE = (s) => s.replace(/(\\|\/|\[|\]|\(|\)|\{|\}|\?|\+|\*|\||\.|\^|\$)/g, '\\$1'); + const e = (s) => `&${s};`; + const entities = { + '<': e('lt'), + '>': e('gt'), + "'": e('#39'), + '@': e('#64'), + '{': e('#123'), + '|': e('#124'), + '}': e('#125'), + '[': e('#91'), + ']': e('#93'), + '"': e('quot'), + '/': e('#47') + }; + const re = new RegExp(`(${Object.keys(entities).map(esRE).join('|')})`, 'g'); + return (s) => s.replace(re, (c) => (entities[c] || c)); + })(); + + // MESSAGING ================================== + const button = ({ elem: elem = '', label: label = '', char: char = '', type: type = '%', css: css = Messenger.Css.button } = {}) => { + const htmlTable = { + '@': '@', 'attr': '@', 'attribute': '@', + '#': '#', 'mac': '#', 'macro': '#', + '%': '%', 'abil': '%', 'ability': '%', + '!': '!', 'api': '!', 'mod': '!', 'script': '!', 'bang': '!', + 'handout': 'handout', 'ho': 'handout' + }; + type = htmlTable[type]; + if (!type) return ''; + let btnCSS = confirmReadability(Array.isArray(css) ? combineCSS(...css) : css); + let api = ''; + switch (type) { + case '#': // macro + api = `${type}${elem}`; + break; + case '%': // ability + case '@': // attribute + api = `${type}{${char}|${elem}}`; + break; + case '!': // api + api = `${type}${/^!/.test(elem) ? elem.slice(1) : elem}`; + break; + case 'handout': // button to open a handout + api = `${elem}`; + break; + } + + if (!api) return; + if (type !== 'handout') api = `! ${api}`; + return html.a(label, HE(api), btnCSS); + }; + const hobutton = ({ elem: elem = '', label: label = '', char: char = '', type: type = '%', css: css = Messenger.Css.button } = {}) => { + const htmlTable = { + '@': '@', 'attr': '@', 'attribute': '@', + '#': '#', 'mac': '#', 'macro': '#', + '%': '%', 'abil': '!', 'ability': '%', + '!': '!', 'api': '!', 'mod': '!', 'script': '!', 'bang': '!' + }; + type = htmlTable[type]; + if (!type) return ''; + let btnCSS = confirmReadability(Array.isArray(css) ? combineCSS(...css) : css); + let api = ''; + switch (type) { + case '#': // macro + api = `${type}${elem}`; + break; + case '%': // ability + case '@': // attribute + api = `${type}{${char}|${elem}}`; + break; + case '!': // api + api = `${type}${/^!/.test(elem) ? elem.slice(1) : elem}`; + break; + } + + if (!api) return; + api = `${api}`; + return html.a(label, `\`${api}`, btnCSS); + }; + const msgbox = ({ + msg: msg = 'message', + title: title = '', + footer: footer = '', + btn: btn = '', + sendas: sendas = 'API', + whisperto: whisperto = '', + containercss: containercss = {}, + boundingcss: boundingcss = {}, + headercss: headercss = {}, + bodycss: bodycss = {}, + contentcss: contentcss = {}, + footercss: footercss = {}, + noarchive: noarchive = false + } = {}) => { + let containerCSS = confirmReadability(combineCSS(css.divContainer, Array.isArray(containercss) ? combineCSS(...containercss) : containercss )); + let boundingCSS = confirmReadability(combineCSS(css.div, css.rounded, Array.isArray(boundingcss) ? combineCSS(...boundingcss) : boundingcss )); + let hdrCSS = confirmReadability(combineCSS(css.messageHeader, Array.isArray(headercss) ? combineCSS(...headercss) : headercss )); + let bodyCSS = confirmReadability(combineCSS(css.messageBody, Array.isArray(bodycss) ? combineCSS(...bodycss) : bodycss )); + let footerCSS = confirmReadability(combineCSS(css.messageFooterContent, Array.isArray(footercss) ? combineCSS(...footercss) : footercss)); + let contentCSS = confirmReadability(combineCSS(css.messageBodyContent, Array.isArray(contentcss) ? combineCSS(...contentcss) : contentcss)); + + let hdr = title !== '' ? html.div(html.div(title, css.messageHeaderContent), hdrCSS) : ''; + let body = html.div(html.div(msg, contentCSS), bodyCSS); + let buttons = btn !== '' ? html.div(btn, css.messageButtons) : ''; + if (footer) footer = html.div(footer); + if (footer || buttons) { + footer = html.div(html.div(footer + buttons), footerCSS); + } + let output = html.div(html.div(html.div(`${hdr}${body}${footer}`, {}), boundingCSS), containerCSS); + if (whisperto) output = `/w "${whisperto}" ${output}`; + sendChat(sendas, output, null, { noarchive: !!noarchive }); + }; + + const checkDependencies = (deps) => { + /* pass array of objects like + { name: 'ModName', version: '#.#.#' || '', mod: ModName || undefined, checks: [ [ExposedItem, type], [ExposedItem, type] ] } + */ + const dependencyEngine = (deps) => { + const versionCheck = (mv, rv) => { + let modv = [...mv.split('.'), ...Array(4).fill(0)].slice(0, 4); + let reqv = [...rv.split('.'), ...Array(4).fill(0)].slice(0, 4); + return reqv.reduce((m, v, i) => { + if (m.pass || m.fail) return m; + if (i < 3) { + if (parseInt(modv[i]) > parseInt(reqv[i])) m.pass = true; + else if (parseInt(modv[i]) < parseInt(reqv[i])) m.fail = true; + } else { + // all betas are considered below the release they are attached to + if (reqv[i] === 0 && modv[i] === 0) m.pass = true; + else if (modv[i] === 0) m.pass = true; + else if (reqv[i] === 0) m.fail = true; + else if (parseInt(modv[i].slice(1)) >= parseInt(reqv[i].slice(1))) m.pass = true; + } + return m; + }, { pass: false, fail: false }).pass; + }; + + let result = { passed: true, failures: {}, optfailures: {} }; + deps.forEach(d => { + let failObj = d.optional ? result.optfailures : result.failures; + if (!d.mod) { + if (!d.optional) result.passed = false; + failObj[d.name] = 'Not found'; + return; + } + if (d.version && d.version.length) { + if (!(API_Meta[d.name].version && API_Meta[d.name].version.length && versionCheck(API_Meta[d.name].version, d.version))) { + if (!d.optional) result.passed = false; + failObj[d.name] = `Incorrect version. Required v${d.version}. ${API_Meta[d.name].version && API_Meta[d.name].version.length ? `Found v${API_Meta[d.name].version}` : 'Unable to tell version of current.'}`; + return; + } + } + d.checks.reduce((m, c) => { + if (!m.passed) return m; + let [pname, ptype] = c; + if (!d.mod.hasOwnProperty(pname) || typeof d.mod[pname] !== ptype) { + if (!d.optional) m.passed = false; + failObj[d.name] = `Incorrect version.`; + } + return m; + }, result); + }); + return result; + }; + let depCheck = dependencyEngine(deps); + let failures = '', contents = '', msg = ''; + if (Object.keys(depCheck.optfailures).length) { // optional components were missing + failures = Object.keys(depCheck.optfailures).map(k => `• ${k} : ${depCheck.optfailures[k]}`).join('
    '); + contents = `${apiproject} utilizies one or more other scripts for optional features, and works best with those scripts installed. You can typically find these optional scripts in the 1-click Mod Library:
    ${failures}`; + msg = `
    MISSING MOD DETECTED
    ${contents}
    `; + sendChat(apiproject, `/w gm ${msg}`); + } + if (!depCheck.passed) { + failures = Object.keys(depCheck.failures).map(k => `• ${k} : ${depCheck.failures[k]}`).join('
    '); + contents = `${apiproject} requires other scripts to work. Please use the 1-click Mod Library to correct the listed problems:
    ${failures}`; + msg = `
    MISSING MOD DETECTED
    ${contents}
    `; + sendChat(apiproject, `/w gm ${msg}`); + return false; + } + return true; + }; + + on('ready', () => { + versionInfo(); + assureState(); + logsig(); + let reqs = [ + // { name: 'Messenger', mod: typeof Messenger !== 'undefined' ? Messenger : undefined, checks: [['Button', 'function'], ['MsgBox', 'function'], ['HE', 'function'], ['Html', 'object']] } + ]; + if (reqs.length && !checkDependencies(reqs)) return; + }); + return { + Button: button, + HOButton: hobutton, + MsgBox: msgbox, + ProcessCSS: processCSS, + Html: () => _.clone(html), + Css: () => _.clone(css), + HE: HE, + version: version, + ValidateHexColor: validateHexColor + }; +})(); + +{ try { throw new Error(''); } catch (e) { API_Meta.Messenger.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.Messenger.offset); } } diff --git a/Messenger/Messenger.js b/Messenger/Messenger.js index be041f9dc7..fb1574e3bd 100644 --- a/Messenger/Messenger.js +++ b/Messenger/Messenger.js @@ -4,8 +4,8 @@ Name : Messenger GitHub : Roll20 Contact : timmaugh -Version : 1.0.1 -Last Update : 5/4/2023 +Version : 1.0.2 +Last Update : 20 MAY 2025 ========================================================= */ var API_Meta = API_Meta || {}; @@ -15,10 +15,10 @@ API_Meta.Messenger = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; const Messenger = (() => { // eslint-disable-line no-unused-vars const apiproject = 'Messenger'; const apilogo = `https://i.imgur.com/DEkWTak.png`; - const version = '1.0.1'; + const version = '1.0.2'; const schemaVersion = 0.1; API_Meta[apiproject].version = version; - const vd = new Date(1683234644321); + const vd = new Date(1747744062840); const versionInfo = () => { log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`); }; @@ -285,7 +285,9 @@ const Messenger = (() => { // eslint-disable-line no-unused-vars Teal: `#008080`, Thistle: `#D8BFD8`, Tomato: `#FF6347`, + Transparent: 'transparent', Turquoise: `#40E0D0`, + Unset: 'unset', Violet: `#EE82EE`, Wheat: `#F5DEB3`, White: `#FFFFFF`, @@ -323,7 +325,9 @@ const Messenger = (() => { // eslint-disable-line no-unused-vars 'border-collapse': 'collapse', 'font-size': '12px', }, - p: {}, + p: { + 'font-family': 'inherit' + }, a: {}, img: {}, h1: {}, @@ -331,6 +335,9 @@ const Messenger = (() => { // eslint-disable-line no-unused-vars h3: {}, h4: {}, h5: {}, + ol: {}, + ul: {}, + li: {}, th: { 'border-bottom': `1px solid #000000`, 'font-weight': `bold`, @@ -376,12 +383,12 @@ const Messenger = (() => { // eslint-disable-line no-unused-vars }, messageButtons: { 'text-align': `right`, - 'margin': `4px 4px 8px`, + 'margin': `4px 0px 8px`, 'padding': '8px' }, messageFooterContent: { - margin: '0px 8px', - width: '98%' +// margin: '0px 8px', +// width: '98%' }, button: { 'background-color': defaultThemeColor1, @@ -411,28 +418,37 @@ const Messenger = (() => { // eslint-disable-line no-unused-vars const assembleCSS = (css) => { return `"${Object.keys(css).map((key) => { return `${key}:${css[key]};` }).join('')}"`; }; + const processCSS = (...css) => { + return assembleCSS(combineCSS(...css)); + }; // HTML ======================================= const html = { - div: (content, ...CSS) => `
    ${content}
    `, - h1: (content, ...CSS) => `

    ${content}

    `, - h2: (content, ...CSS) => `

    ${content}

    `, - h3: (content, ...CSS) => `

    ${content}

    `, - h4: (content, ...CSS) => `

    ${content}

    `, - h5: (content, ...CSS) => `
    ${content}
    `, - p: (content, ...CSS) => `

    ${content}

    `, - table: (content, ...CSS) => `${content}
    `, - th: (content, ...CSS) => `${content}`, - tr: (content, ...CSS) => `${content}`, - td: (content, ...CSS) => `${content}`, - td2: (content, ...CSS) => `${content}`, - tdcs: (content, colspan, ...CSS) => `${content}`, - code: (content, ...CSS) => `${content}`, - pre: (content, ...CSS) => `
    ${content}
    `, - span: (content, ...CSS) => `${content}`, - a: (content, link, ...CSS) => `${content}`, - img: (content, altText, ...CSS) => `${altText}`, - tip: (content, tipText, ...CSS) => `${content}` + div: (content, ...CSS) => `
    ${content}
    `, + h1: (content, ...CSS) => `

    ${content}

    `, + h2: (content, ...CSS) => `

    ${content}

    `, + h3: (content, ...CSS) => `

    ${content}

    `, + h4: (content, ...CSS) => `

    ${content}

    `, + h5: (content, ...CSS) => `
    ${content}
    `, + p: (content, ...CSS) => `

    ${content}

    `, + table: (content, ...CSS) => `${content}
    `, + th: (content, ...CSS) => `${content}`, + tr: (content, ...CSS) => `${content}`, + td: (content, ...CSS) => `${content}`, + td2: (content, ...CSS) => `${content}`, + tdcs: (content, colspan, ...CSS) => `${content}`, + tdrs: (content, rowspan, ...CSS) => `${content}`, + tdcrs: (content, colspan, rowspan, ...CSS) => `${content}`, + code: (content, ...CSS) => `${content}`, + pre: (content, ...CSS) => `
    ${content}
    `, + span: (content, ...CSS) => `${content}`, + a: (content, link, ...CSS) => `${content}`, + img: (content, altText, ...CSS) => `${altText}`, + tip: (content, tipText, ...CSS) => `${content}`, + tag: (tag, content, ...CSS) => `<${tag} style=${processCSS(css.div, ...CSS)}>${content}`, + ol: (content, ...CSS) => `
      ${content}
    `, + ul: (content, ...CSS) => ``, + li: (content, ...CSS) => `
  • ${content}
  • `, }; // HTML Escaping function @@ -467,7 +483,7 @@ const Messenger = (() => { // eslint-disable-line no-unused-vars }; type = htmlTable[type]; if (!type) return ''; - let btnCSS = confirmReadability(css); + let btnCSS = confirmReadability(Array.isArray(css) ? combineCSS(...css) : css); let api = ''; switch (type) { case '#': // macro @@ -498,7 +514,7 @@ const Messenger = (() => { // eslint-disable-line no-unused-vars }; type = htmlTable[type]; if (!type) return ''; - let btnCSS = confirmReadability(css); + let btnCSS = confirmReadability(Array.isArray(css) ? combineCSS(...css) : css); let api = ''; switch (type) { case '#': // macro @@ -528,17 +544,19 @@ const Messenger = (() => { // eslint-disable-line no-unused-vars boundingcss: boundingcss = {}, headercss: headercss = {}, bodycss: bodycss = {}, + contentcss: contentcss = {}, footercss: footercss = {}, noarchive: noarchive = false } = {}) => { - let containerCSS = confirmReadability(combineCSS(css.divContainer, containercss)); - let boundingCSS = confirmReadability(combineCSS(css.div, css.rounded, boundingcss)); - let hdrCSS = confirmReadability(combineCSS(css.messageHeader, headercss)); - let bodyCSS = confirmReadability(combineCSS(css.messageBody, bodycss)); - let footerCSS = confirmReadability(combineCSS(css.messageFooterContent, footercss)); + let containerCSS = confirmReadability(combineCSS(css.divContainer, Array.isArray(containercss) ? combineCSS(...containercss) : containercss )); + let boundingCSS = confirmReadability(combineCSS(css.div, css.rounded, Array.isArray(boundingcss) ? combineCSS(...boundingcss) : boundingcss )); + let hdrCSS = confirmReadability(combineCSS(css.messageHeader, Array.isArray(headercss) ? combineCSS(...headercss) : headercss )); + let bodyCSS = confirmReadability(combineCSS(css.messageBody, Array.isArray(bodycss) ? combineCSS(...bodycss) : bodycss )); + let footerCSS = confirmReadability(combineCSS(css.messageFooterContent, Array.isArray(footercss) ? combineCSS(...footercss) : footercss)); + let contentCSS = confirmReadability(combineCSS(css.messageBodyContent, Array.isArray(contentcss) ? combineCSS(...contentcss) : contentcss)); let hdr = title !== '' ? html.div(html.div(title, css.messageHeaderContent), hdrCSS) : ''; - let body = html.div(html.div(msg, css.messageBodyContent), bodyCSS); + let body = html.div(html.div(msg, contentCSS), bodyCSS); let buttons = btn !== '' ? html.div(btn, css.messageButtons) : ''; if (footer) footer = html.div(footer); if (footer || buttons) { @@ -631,10 +649,12 @@ const Messenger = (() => { // eslint-disable-line no-unused-vars Button: button, HOButton: hobutton, MsgBox: msgbox, + ProcessCSS: processCSS, Html: () => _.clone(html), Css: () => _.clone(css), HE: HE, - version: version + version: version, + ValidateHexColor: validateHexColor }; })(); diff --git a/Messenger/script.json b/Messenger/script.json index ea3daed9c0..3497ea6084 100644 --- a/Messenger/script.json +++ b/Messenger/script.json @@ -1,7 +1,7 @@ { "name": "Messenger", "script": "Messenger.js", - "version": "1.0.1", + "version": "1.0.2", "description": "Messenger is a utility script for some of timmaugh's scripts that require a formatted chat output.", "authors": "timmaugh", "roll20userid": "5962076", @@ -9,6 +9,7 @@ "modifies": { }, "conflicts": [], "previousversions": [ - "1.0.0" + "1.0.0", + "1.0.1" ] } \ No newline at end of file diff --git a/SheetDefaults/1.0.1/SheetDefaults.js b/SheetDefaults/1.0.1/SheetDefaults.js new file mode 100644 index 0000000000..580a1143f9 --- /dev/null +++ b/SheetDefaults/1.0.1/SheetDefaults.js @@ -0,0 +1,554 @@ +/* +========================================================= +Name : SheetDefaults +GitHub : +Roll20 Contact : timmaugh +Version : 1.0.1 +Last Update : 10 MAR 2025 +========================================================= +*/ +var API_Meta = API_Meta || {}; +API_Meta.SheetDefaults = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; +{ try { throw new Error(''); } catch (e) { API_Meta.SheetDefaults.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (12)); } } + +const SheetDefaults = (() => { // eslint-disable-line no-unused-vars + const apiproject = 'SheetDefaults'; + const version = '1.0.0'; + const apilogo = "https://i.imgur.com/VDdtqpt.png"; + const apilogoalt = "https://i.imgur.com/Pq6mmmB.png"; + const apilogosmall = "https://i.imgur.com/3mafbf8.png"; + const schemaVersion = 0.1; + API_Meta[apiproject].version = version; + const vd = new Date(1741657143142); + const versionInfo = () => { + log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`); + }; + const logsig = () => { + // initialize shared namespace for all signed projects, if needed + state.torii = state.torii || {}; + // initialize siglogged check, if needed + state.torii.siglogged = state.torii.siglogged || false; + state.torii.sigtime = state.torii.sigtime || Date.now() - 3001; + if (!state.torii.siglogged || Date.now() - state.torii.sigtime > 3000) { + const logsig = '\n' + + ' _____________________________________________ ' + '\n' + + ' )_________________________________________( ' + '\n' + + ' )_____________________________________( ' + '\n' + + ' ___| |_______________| |___ ' + '\n' + + ' |___ _______________ ___| ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + '______________|_|_______________|_|_______________' + '\n' + + ' ' + '\n'; + log(`${logsig}`); + state.torii.siglogged = true; + state.torii.sigtime = Date.now(); + } + return; + }; + const checkInstall = () => { + if (!state.hasOwnProperty(apiproject) || state[apiproject].version !== schemaVersion) { + log(` > Updating ${apiproject} Schema to v${schemaVersion} <`); + switch (state[apiproject] && state[apiproject].version) { + + case 0.1: + /* falls through */ + + case 'UpdateSchemaVersion': + state[apiproject].version = schemaVersion; + break; + + default: + state[apiproject] = { + settings: {}, + defaults: {}, + version: schemaVersion + } + break; + } + } + }; + let stateReady = false; + const assureState = () => { + if (!stateReady) { + checkInstall(); + stateReady = true; + } + }; + const manageState = { // eslint-disable-line no-unused-vars + reset: () => state[apiproject].settings = _.clone(state[apiproject].defaults), + clone: () => { return _.clone(state[apiproject].settings); }, + set: (p, v) => state[apiproject].settings[p] = v, + get: (p) => { return state[apiproject].settings[p]; } + }; + // ================================================== + // MESSAGING + // ================================================== + let html = {}; + let css = {}; // eslint-disable-line no-unused-vars + let HE = () => { }; // eslint-disable-line no-unused-vars + const theme = { + primaryColor: '#226381', + primaryTextColor: '#232323', + primaryTextBackground: '#ededed' + } + const localCSS = { + msgheader: { + 'background-color': theme.primaryColor, + 'color': 'white', + 'font-size': '1.2em', + 'padding-left': '4px' + }, + msgbody: { + 'color': theme.primaryTextColor, + 'background-color': theme.primaryTextBackground + }, + msgfooter: { + 'color': theme.primaryTextColor, + 'background-color': theme.primaryTextBackground + }, + msgheadercontent: { + 'display': 'table-cell', + 'vertical-align': 'middle', + 'padding': '4px 8px 4px 6px' + }, + msgheaderlogodiv: { + 'display': 'table-cell', + 'max-height': '30px', + 'margin-right': '8px', + 'margin-top': '4px', + 'vertical-align': 'middle' + }, + logoimg: { + 'background-color': 'transparent', + 'float': 'left', + 'border': 'none', + 'max-height': '30px' + }, + boundingcss: { + 'background-color': theme.primaryTextBackground + }, + inlineEmphasis: { + 'font-weight': 'bold' + }, + tableheader: { + 'color': `${theme.primaryTextColor} !important` + }, + button: { + 'background-color': theme.primaryColor, + 'color': 'white', + 'border-radius': '6px' + }, + leftalign: { + 'text-align': 'left' + }, + rightalign: { + 'text-align': 'right' + }, + tipContainer: { + 'overflow': 'hidden', + 'width': '100%', + 'border': 'none', + 'max-width': '250px', + 'display': 'block' + }, + tipBounding: { + 'border-radius': '10px', + 'border': '2px solid #000000', + 'display': 'table-cell', + 'width': '100%', + 'overflow': 'hidden', + 'font-size': '12px' + }, + tipHeaderLine: { + 'overflow': 'hidden', + 'display': 'table', + 'background-color': theme.primaryColor, + 'width': '100%' + }, + tipLogoSpan: { + 'display': 'table-cell', + 'overflow': 'hidden', + 'vertical-align': 'middle', + 'width': '40px' + }, + tipLogoImg: { + 'min-height': '37px', + 'margin-left': '3px', + 'margin-top': '5px', + 'background-image': `url('${apilogosmall}')`, + 'background-repeat': 'no-repeat', + 'backgound-size': 'contain', + 'width': '37px', + 'display': 'inline-block' + }, + tipContentLine: { + 'overflow': 'hidden', + 'display': 'table', + 'background-color': theme.primaryLightColor, + 'width': '100%' + }, + tipContent: { + 'display': 'table-cell', + 'overflow': 'hidden', + 'padding': '5px 8px', + 'text-align': 'left', + 'color': '#232323', + 'background-color': theme.primaryLightColor + }, + tipHeaderTitle: { + 'display': 'table-cell', + 'overflow': 'hidden', + 'padding': '5px 8px', + 'text-align': 'left', + 'color': theme.primaryLightColor, + 'font-size': '1.2em', + 'vertical-align': 'middle', + 'font-weight': 'bold' + } + + } + const msgbox = ({ + msg: msg = '', + title: title = '', + headercss: headercss = localCSS.msgheader, + bodycss: bodycss = localCSS.msgbody, + footercss: footercss = localCSS.msgfooter, + sendas: sendas = apiproject, + whisperto: whisperto = 'gm', + footer: footer = '', + btn: btn = '', + } = {}) => { + if (title) title = html.div(html.div(html.img(apilogo, `${apiproject} Logo`, localCSS.logoimg), localCSS.msgheaderlogodiv) + html.div(title, localCSS.msgheadercontent), {}); + Messenger.MsgBox({ msg: msg, title: title, bodycss: bodycss, sendas: sendas, whisperto: whisperto, footer: footer, btn: btn, headercss: headercss, footercss: footercss, boundingcss: localCSS.boundingcss, noarchive: true }); + }; + const button = ({ + elem: elem = '', + label: label = '', + char: char = '', + type: type = '!', + css: css = localCSS.button + } = {}) => { + return Messenger.Button({ elem: elem, label: label, char: char, type: type, css: css }); + }; + const getTip = (contents, label, header = '', contentcss = {}) => { + let contentCSS = Object.assign(_.clone(localCSS.tipContent), contentcss); + return html.tip( + label, + html.span( // container + html.span( // bounding + html.span( // header line + html.span( // left (logo) + html.span('', localCSS.tipLogoImg), + localCSS.tipLogoSpan) + + html.span( // right (content) + header, + localCSS.tipHeaderTitle), + localCSS.tipHeaderLine) /*+ + html.span( // content line + html.span( // content cell + contents, + contentCSS), + localCSS.tipContentLine)*/, + localCSS.tipBounding), + localCSS.tipContainer), + { 'display': 'inline-block' } + ); + }; + + // ================================================== + // LOGGING + // ================================================== + let oLog = {}; + let oData = []; + const resetLog = () => { + oLog = { + actions: [], + args: [], + origLength: 0, + startTime: Date.now() + }; + }; + class ActionToken { + constructor({ action: action = '', cid: cid = '', attr: attr, name: name = '', current: current = '' } = {}) { + this.action = action; + this.cid = cid; + this.attr = attr; + this.name = name; + this.current = current; + } + + } + + // ================================================== + // HANDLE CHAT + // ================================================== + // PROPERTY OBJECTS + const propObj = { + w: 'w', + wtype: 'w', + whisper: 'w', + + r: 'r', + rtype: 'r', + roll: 'r', + + d: 'd', + dtype: 'd', + damage: 'd' + }; + + const attrOptions = { + w: { + always: '/w gm ', + never: '', + toggle: '@{whispertoggle}', + query: '?{Whisper?|Public Roll,|Whisper Roll,/w gm }' + }, + r: { + always: '{{always=1}} {{r2=[[1d20', + never: '{{normal=1}} {{r2=[[0d20', + toggle: '@{advantagetoggle}', + query: '@{queryadvantage}' + }, + d: { + auto: 'full', + yes: 'full', + no: 'pick' + } + }; + let statusButton; + + const handleInquiry = (msg) => { + if (msg.type !== 'api' || !/^!sheetdefaults-status/i.test(msg.content)) return; + if (!playerIsGM(msg.playerid)) { + msgbox({ msg: 'GM access required to run that command.', title: 'GM Required' }); + return; + } + if (oData.length) { + msgbox({ msg: `SheetDefaults has processed ${oLog.actions.length} of ${oLog.origLength} attributes.`, title: `Processing...`, btn: statusButton }); + } else { + msgbox({ msg: `There is no currently processing SheetDefault operation.`, title: `No Current Process` }); + } + }; + const handleInput = (msg) => { + if (msg.type !== 'api' || !/^!sheetdefaults\s/i.test(msg.content)) return; + if (!playerIsGM(msg.playerid)) { + msgbox({ msg: 'GM access required to run that command.', title: 'GM Required' }); + return; + } + let argObj = Object.fromEntries( + msg.content + .split(/\s+--/) + .slice(1) + .map(a => a.toLowerCase().split(/#|\|/)) + .filter(a => a.length > 1) + .map(a => a.slice(0, 2)) + .filter(a => propObj.hasOwnProperty(a[0]) && attrOptions[propObj[a[0]]].hasOwnProperty(a[1])) + .map(a => [propObj[a[0]], attrOptions[propObj[a[0]]][a[1]]]) + ); + if (!Object.keys(argObj).length) { + msgbox({ msg: `You must supply one or more of w, d, or r arguments with valid settings. Please consult the script's documentation. Exiting process.`, title: `Invalid Command` }); + return; + } + resetLog(); + oLog.args = argObj; + oData = findObjs({ type: 'character' }) + .reduce((m, c) => { + Object.keys(argObj).forEach(k => { + let attrs = findObjs({ type: 'attribute', characterid: c.id, name: `${k}type` }); + if (!attrs.length) { + m = [ + ...m, + new ActionToken({ action: 'create', cid: c.id, name: `${k}type`, current: argObj[k] }) + ]; + } else { + m = [ + ...m, + ...attrs.slice(1).map(a => { + return new ActionToken({ action: 'delete', cid: c.id, attr: a, name: `${k}type`, current: argObj[k] }); + }), + new ActionToken({ action: 'change', cid: c.id, attr: attrs[0], name: `${k}type`, current: argObj[k] }) + ]; + } + }); + return m; + }, []); + + if (oData.length) { + oLog.origLength = oData.length; + oLog.startTime = Date.now(); + let formattedTime = new Date(oLog.startTime).toLocaleTimeString(); + + let rptObj = Object.assign({ w: 'Not Provided', r: 'Not Provided', d: 'Not Provided' }, oLog.args); + let tbl = html.table( + html.tr( + html.th(getTip('', html.span('ATTR', localCSS.tableheader), 'Attribute'), localCSS.leftalign, localCSS.tableheader) + // attribute name heading + html.th(getTip('', html.span('VALUE', localCSS.tableheader), 'Value'), localCSS.leftalign, localCSS.tableheader) // value heading + ) + + Object.keys(rptObj).map(k => { + if (rptObj[k] === 'Not Provided') { + return ''; + } + return html.tr( + html.td(`${k}type`, localCSS.leftalign) + + html.td(HE(rptObj[k]), localCSS.leftalign) + ); + }).join('')); + msgbox({ + msg: `SheetDefaults began working at ${formattedTime} (Roll20 Server Time). It might take a while before the process completes. Please be patient. Commands in use:${tbl}`, + title: 'Process Started', + btn: statusButton + }); + burndown(); + } + }; + const outputLog = () => { + let msg = `Process completed in ${Math.round((Date.now() - oLog.startTime) / 10) / 100} seconds.`; + let tbl = html.table( + html.tr( + html.th(getTip('', html.span('ATTR', localCSS.tableheader),'Attribute'), localCSS.leftalign, localCSS.tableheader) + // attribute name heading + html.th(getTip('', html.span('VALUE', localCSS.tableheader), 'Value'), localCSS.leftalign, localCSS.tableheader) + // value heading + html.th(getTip('', '\u{2705}', 'Attributes Changed') + ' (' + getTip('', '\u{2A}\u{FE0F}\u{20E3}', 'Attributes Created') + ')', localCSS.rightalign, localCSS.tableheader) + // set(new) heading + html.th(getTip('', '\u{274C}', 'Attributes Deleted'), localCSS.rightalign, localCSS.tableheader) // deleted heading + ) + + Object.keys(oLog.args).map(k => { + let newCount = oLog.actions.filter(a => a.action === 'create' && a.name === `${k}type`).length; + let setCount = oLog.actions.filter(a => ['create', 'change'].includes(a.action) && a.name === `${k}type`).length; + let delCount = oLog.actions.filter(a => a.action === 'delete' && a.name === `${k}type`).length; + return html.tr( + html.td(`${k}type`, localCSS.leftalign) + + html.td(HE(oLog.args[k]), localCSS.leftalign) + + html.td(`${setCount} (${newCount})`, localCSS.rightalign) + + html.td(delCount, localCSS.rightalign) + ); + }).join('') + ); + msgbox({ msg: msg + tbl, title: 'SheetDefaults Log' }); + }; + const burndown = () => { + if (!oData.length) { + outputLog(); + return; + } + let data = oData.shift(); + let attr; + switch (data.action) { + case 'create': + createObj('attribute', { characterid: data.cid, name: data.name, current: '' }) + .setWithWorker({ current: data.current }); + break; + case 'delete': + data.attr.remove(); + break; + default: + data.attr.setWithWorker({ current: data.current }); + } + oLog.actions.push(data); + setTimeout(burndown, 0); + }; + + const registerEventHandlers = () => { + on('chat:message', handleInput); + on('chat:message', handleInquiry); + }; + + const checkDependencies = (deps) => { + /* pass array of objects like + { name: 'ModName', version: '#.#.#' || '', mod: ModName || undefined, checks: [ [ExposedItem, type], [ExposedItem, type] ] } + */ + const dependencyEngine = (deps) => { + const versionCheck = (mv, rv) => { + let modv = [...mv.split('.'), ...Array(4).fill(0)].slice(0, 4); + let reqv = [...rv.split('.'), ...Array(4).fill(0)].slice(0, 4); + return reqv.reduce((m, v, i) => { + if (m.pass || m.fail) return m; + if (i < 3) { + if (parseInt(modv[i]) > parseInt(reqv[i])) m.pass = true; + else if (parseInt(modv[i]) < parseInt(reqv[i])) m.fail = true; + } else { + // all betas are considered below the release they are attached to + if (reqv[i] === 0 && modv[i] === 0) m.pass = true; + else if (modv[i] === 0) m.pass = true; + else if (reqv[i] === 0) m.fail = true; + else if (parseInt(modv[i].slice(1)) >= parseInt(reqv[i].slice(1))) m.pass = true; + } + return m; + }, { pass: false, fail: false }).pass; + }; + + let result = { passed: true, failures: {}, optfailures: {} }; + deps.forEach(d => { + let failObj = d.optional ? result.optfailures : result.failures; + if (!d.mod) { + if (!d.optional) result.passed = false; + failObj[d.name] = 'Not found'; + return; + } + if (d.version && d.version.length) { + if (!(API_Meta[d.name].version && API_Meta[d.name].version.length && versionCheck(API_Meta[d.name].version, d.version))) { + if (!d.optional) result.passed = false; + failObj[d.name] = `Incorrect version. Required v${d.version}. ${API_Meta[d.name].version && API_Meta[d.name].version.length ? `Found v${API_Meta[d.name].version}` : 'Unable to tell version of current.'}`; + return; + } + } + d.checks.reduce((m, c) => { + if (!m.passed) return m; + let [pname, ptype] = c; + if (!d.mod.hasOwnProperty(pname) || typeof d.mod[pname] !== ptype) { + if (!d.optional) m.passed = false; + failObj[d.name] = `Incorrect version.`; + } + return m; + }, result); + }); + return result; + }; + let depCheck = dependencyEngine(deps); + let failures = '', contents = '', msg = ''; + if (Object.keys(depCheck.optfailures).length) { // optional components were missing + failures = Object.keys(depCheck.optfailures).map(k => `• ${k} : ${depCheck.optfailures[k]}`).join('
    '); + contents = `${apiproject} utilizies one or more other scripts for optional features, and works best with those scripts installed. You can typically find these optional scripts in the 1-click Mod Library:
    ${failures}`; + msg = `
    MISSING MOD DETECTED
    ${contents}
    `; + sendChat(apiproject, `/w gm ${msg}`); + } + if (!depCheck.passed) { + failures = Object.keys(depCheck.failures).map(k => `• ${k} : ${depCheck.failures[k]}`).join('
    '); + contents = `${apiproject} requires other scripts to work. Please use the 1-click Mod Library to correct the listed problems:
    ${failures}`; + msg = `
    MISSING MOD DETECTED
    ${contents}
    `; + sendChat(apiproject, `/w gm ${msg}`); + return false; + } + return true; + }; + + on('ready', () => { + versionInfo(); + assureState(); + logsig(); + + let reqs = [ + { + name: 'Messenger', + version: `1.0.0`, + mod: typeof Messenger !== 'undefined' ? Messenger : undefined, + checks: [['Button', 'function'], ['MsgBox', 'function'], ['HE', 'function'], ['Html', 'function'], ['Css', 'function']] + } + ]; + if (!checkDependencies(reqs)) return; + html = Messenger.Html(); + css = Messenger.Css(); + HE = Messenger.HE; + statusButton = button({ elem: '!sheetdefaults-status', label: 'Status', type: '!' }); + + registerEventHandlers(); + resetLog(); + }); + return {}; +})(); + +{ try { throw new Error(''); } catch (e) { API_Meta.SheetDefaults.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.SheetDefaults.offset); } } +/* */ \ No newline at end of file diff --git a/SheetDefaults/SheetDefaults.js b/SheetDefaults/SheetDefaults.js index a93d390721..580a1143f9 100644 --- a/SheetDefaults/SheetDefaults.js +++ b/SheetDefaults/SheetDefaults.js @@ -3,8 +3,8 @@ Name : SheetDefaults GitHub : Roll20 Contact : timmaugh -Version : 1.0.0 -Last Update : 8/10/2024 +Version : 1.0.1 +Last Update : 10 MAR 2025 ========================================================= */ var API_Meta = API_Meta || {}; @@ -19,7 +19,7 @@ const SheetDefaults = (() => { // eslint-disable-line no-unused-vars const apilogosmall = "https://i.imgur.com/3mafbf8.png"; const schemaVersion = 0.1; API_Meta[apiproject].version = version; - const vd = new Date(1723316966114); + const vd = new Date(1741657143142); const versionInfo = () => { log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`); }; @@ -368,10 +368,10 @@ const SheetDefaults = (() => { // eslint-disable-line no-unused-vars } else { m = [ ...m, - new ActionToken({ action: 'change', cid: c.id, attr: attrs[0], name: `${k}type`, current: argObj[k] }), ...attrs.slice(1).map(a => { return new ActionToken({ action: 'delete', cid: c.id, attr: a, name: `${k}type`, current: argObj[k] }); - }) + }), + new ActionToken({ action: 'change', cid: c.id, attr: attrs[0], name: `${k}type`, current: argObj[k] }) ]; } }); diff --git a/SheetDefaults/script.json b/SheetDefaults/script.json index a5f0fbb2b1..13bad96a8e 100644 --- a/SheetDefaults/script.json +++ b/SheetDefaults/script.json @@ -1,7 +1,7 @@ { "name": "SheetDefaults", "script": "SheetDefaults.js", - "version": "1.0.0", + "version": "1.0.1", "description": "SheetDefaults helps correct situations where Applying Default values doesn't work in D&D 5E 2014 games (formerly the D&D 5E by Roll20 sheet). If you notice that Apply Defaults doesn't work in your game, run this script to clean up corrupted attributes. This script accepts values for the whisper type (wtype), damage type (dtype), and roll type (rtype)./r/rFor more information and for syntax, see the original forum thread: [SheetDefaults Forum Thread](https://app.roll20.net/forum/permalink/12030906/)", "authors": "timmaugh", "roll20userid": "5962076", @@ -11,5 +11,7 @@ "state.SheetDefaults": "read, write" }, "conflicts": [], - "previousversions": [] + "previousversions": [ + "1.0.1" + ] } \ No newline at end of file diff --git a/ShowButtons/1.0.2/ShowButtons.js b/ShowButtons/1.0.2/ShowButtons.js new file mode 100644 index 0000000000..859edbca33 --- /dev/null +++ b/ShowButtons/1.0.2/ShowButtons.js @@ -0,0 +1,528 @@ +/* +========================================================= +Name : ShowButtons +GitHub : +Roll20 Contact : timmaugh +Version : 1.0.2 +Last Update : 08 MAY 2025 +========================================================= +*/ +var API_Meta = API_Meta || {}; +API_Meta.ShowButtons = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; +{ try { throw new Error(''); } catch (e) { API_Meta.ShowButtons.offset = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - (13)); } } + +const ShowButtons = (() => { // eslint-disable-line no-unused-vars + const apiproject = 'ShowButtons'; + const version = '1.0.2'; + const apilogo = 'https://i.imgur.com/JLcfnek.png'; + const apilogoalt = 'https://i.imgur.com/IbS0DA7.png'; + const schemaVersion = 0.3; + API_Meta[apiproject].version = version; + const vd = new Date(1746714262551); + const versionInfo = () => { + log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`); + }; + const logsig = () => { + // initialize shared namespace for all signed projects, if needed + state.torii = state.torii || {}; + // initialize siglogged check, if needed + state.torii.siglogged = state.torii.siglogged || false; + state.torii.sigtime = state.torii.sigtime || Date.now() - 3001; + if (!state.torii.siglogged || Date.now() - state.torii.sigtime > 3000) { + const logsig = '\n' + + ' _____________________________________________ ' + '\n' + + ' )_________________________________________( ' + '\n' + + ' )_____________________________________( ' + '\n' + + ' ___| |_______________| |___ ' + '\n' + + ' |___ _______________ ___| ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + ' | | | | ' + '\n' + + '______________|_|_______________|_|_______________' + '\n' + + ' ' + '\n'; + log(`${logsig}`); + state.torii.siglogged = true; + state.torii.sigtime = Date.now(); + } + return; + }; + // ================================================== + // STATE MANAGEMENT + // ================================================== + const checkInstall = () => { + if (!state.hasOwnProperty(apiproject) || state[apiproject].version !== schemaVersion) { + log(` > Updating ${apiproject} Schema to v${schemaVersion} <`); + switch (state[apiproject] && state[apiproject].version) { + + case 0.1: + state[apiproject].settings.playerscanids = false; + /* falls through */ + + case 0.2: + state[apiproject].settings.verbose = false; + state[apiproject].defaults.verbose = false; + /* falls through */ + + case 'UpdateSchemaVersion': + state[apiproject].version = schemaVersion; + break; + + default: + state[apiproject] = { + settings: { + report: true, + playerscanids: false, + verbose: false + }, + defaults: { + report: true, + playerscanids: false, + verbose: false + }, + version: schemaVersion + } + break; + } + } + }; + let stateReady = false; + const assureState = () => { + if (!stateReady) { + checkInstall(); + stateReady = true; + } + }; + const manageState = { // eslint-disable-line no-unused-vars + reset: () => state[apiproject].settings = _.clone(state[apiproject].defaults), + clone: () => { return _.clone(state[apiproject].settings); }, + set: (p, v) => state[apiproject].settings[p] = v, + get: (p) => { return state[apiproject].settings[p]; } + }; + + // ================================================== + // UTILITIES + // ================================================== + const escapeRegExp = (string) => { return string.replace(/[.*+\-?^${}()|[\]\\]/g, '\\$&'); }; + + // ================================================== + // PRESENTATION + // ================================================== + let html = {}; + let css = {}; // eslint-disable-line no-unused-vars + let HE = () => { }; // eslint-disable-line no-unused-vars + const theme = { + primaryColor: '#007999', + primaryTextColor: '#232323', + primaryTextBackground: '#ededed', + secondaryColor: '#b6d2d9' + } + const localCSS = { + msgheader: { + 'background-color': theme.primaryColor, + 'color': 'white', + 'font-size': '1.2em', + 'padding-left': '4px' + }, + msgbody: { + 'color': theme.primaryTextColor, + 'background-color': theme.primaryTextBackground + }, + msgfooter: { + 'color': theme.primaryTextColor, + 'background-color': theme.primaryTextBackground + }, + msgheadercontent: { + 'display': 'table-cell', + 'vertical-align': 'middle', + 'padding': '4px 8px 4px 6px' + }, + msgheaderlogodiv: { + 'display': 'table-cell', + 'max-height': '30px', + 'margin-right': '8px', + 'margin-top': '4px', + 'vertical-align': 'middle' + }, + logoimg: { + 'background-color': 'transparent', + 'float': 'left', + 'border': 'none', + 'max-height': '30px', + 'max-width': '30px' + }, + boundingcss: { + 'background-color': theme.primaryTextBackground + }, + tablesectionheader: { + 'font-size': '1.2em', + 'background-color': theme.secondaryColor, + 'border-radius': '6px' + }, + tablesubheading: { + 'border-bottom': `1px solid ${theme.secondaryColor}`, + 'font-size': '1.2em', + }, + actionindicator: { + 'width': '6px', + 'font-family': 'pictos', + 'font-size': '1.1em' + }, + inlineEmphasis: { + 'font-weight': 'bold' + } + + } + const msgbox = ({ + msg: msg = '', + title: title = '', + headercss: headercss = localCSS.msgheader, + bodycss: bodycss = localCSS.msgbody, + footercss: footercss = localCSS.msgfooter, + sendas: sendas = 'ShowButtons', + whisperto: whisperto = '', + footer: footer = '', + btn: btn = '' + } = {}) => { + if (title) title = html.div(html.div(html.img(apilogoalt, 'ShowButtons Logo', localCSS.logoimg), localCSS.msgheaderlogodiv) + html.div(title, localCSS.msgheadercontent), {}); + Messenger.MsgBox({ msg: msg, title: title, bodycss: bodycss, sendas: sendas, whisperto: whisperto, footer: footer, btn: btn, headercss: headercss, footercss: footercss, boundingcss: localCSS.boundingcss, noarchive: true }); + }; + + const getWhisperTo = (who) => who.toLowerCase() === 'api' ? 'gm' : who.replace(/\s\(gm\)$/i, ''); + + // ================================================== + // ROLL20 DATA + // ================================================== + const hasAccess = pid => obj => { + someCheck = id => id.toLowerCase() === 'all' || id === pid; + let prop = (o = { + macro: () => [...(obj.get('visibleto') || '').split(/\s*,\s*/), obj.get('playerid')].some(someCheck), + ability: () => (getObj('character', obj.get('characterid'))?.get('controlledby') || '').split(/\s*,\s*/).some(someCheck), + character: () => (obj.get('controlledby') || '').split(/\s*,\s*/).some(someCheck), + default: () => undefined + })[obj && obj.get && typeof obj.get === 'function' && Object.keys(o).includes(obj.get('type')) ? obj.get('type') : 'default'](); + + return typeof prop === 'undefined' ? false : playerIsGM(pid) || prop; + }; + const getChar = (query, pid) => { + let character; + if (typeof query !== 'string' || !query.length) return character; + let qrx = new RegExp(escapeRegExp(query), 'i'); + let charsIControl = findObjs({ type: 'character' }).filter(hasAccess(pid)); + //charsIControl = playerIsGM(pid) || manageState.get('playerscanids') ? charsIControl : charsIControl.filter(c => { + // return c.get('controlledby').split(',').reduce((m, p) => { + // return m || p === 'all' || p === pid; + // }, false) + //}); + character = charsIControl.filter(c => c.id === query)[0] || + charsIControl.filter(c => c.id === (getObj('graphic', query) || { get: () => { return '' } }).get('represents'))[0] || + charsIControl.filter(c => c.get('name') === query)[0] || + charsIControl.filter(c => { + qrx.lastIndex = 0; + return qrx.test(c.get('name')); + })[0]; + return character; + }; + + // ================================================== + // TOKENIZING + // ================================================== + const tokenize = (cmd) => { + let pos = 0; + let tokens = []; + class ObjectToken { + constructor({ type: type = '', char: char = '', query: query = '' } = {}) { + this.type = type; + this.char = char; + this.query = query; + } + } + + const getNext = () => { + const ticksrx = /^(`[^`#|]+`|'[^'#|]+'|"[^"#|]+")/; + let ret; + if (ticksrx.test(cmd.slice(pos))) { + ret = ticksrx.exec(cmd.slice(pos)); + pos += ret[0].length; + return ret[0].slice(1, ret[0].length - 1); + } else { + ret = /^[^\s#|]*?(?=\s|#|\||$)/.exec(cmd.slice(pos)); + pos += ret[0].length; + return ret[0]; + } + }; + + const getToken = () => { + let tok = new ObjectToken(); + let first = getNext(); + if (['#', '|'].includes(cmd.charAt(pos))) { + tok.type = 'ability'; + tok.char = first; + pos++; + tok.query = getNext(); + pos++; + } else if ([' '].includes(cmd.charAt(pos)) || pos >= cmd.length) { + tok.type = 'macro'; + tok.query = first; + pos++; + } else { + tok.type = 'error'; + tok.query = `Unexpected character at position ${pos} of: ${cmd}`; + pos++; + } + return tok; + }; + + while (pos <= cmd.length) { + tokens.push(getToken()); + } + return tokens; + }; + + // ================================================== + // HANDLE INPUT + // ================================================== + const testHandles = (cmd) => { + if (/^!showbuttons?\b/i.test(cmd)) return true; + }; + const finders = { + macro: (props) => findObjs({ type: 'macro', ...props }), + ability: (props) => findObjs({ type: 'ability', ...props }) + }; + const handleInput = (msg) => { + if (msg.type !== 'api' || !testHandles(msg.content)) return; + let report = manageState.get('report'); + let argObj = msg.content.split(/\s+--/).slice(1) + .filter(a => { + if (['report','silent'].includes(a.toLowerCase())) report = a.toLowerCase() === 'report'; + return !['report', 'silent'].includes(a.toLowerCase()); + }) + .map(a => { + if (a.indexOf(' ') > 0) return [a.slice(0, a.indexOf(' ')).toLowerCase(), a.slice(a.indexOf(' ') + 1).trim()]; + }) + .filter(a => a) + .reduce((m, a) => { + m[a[0]] = [...(m[a[0]] || []), ...tokenize(a[1])]; + return m; + }, {}); + let switchObj = { + 'show': () => true, + 'hide': () => false, + 'toggle': (c) => !c + }; + let reportList = {}; + + Object.keys(argObj).filter(k => Object.keys(switchObj).includes(k.toLowerCase())).forEach(k => { + argObj[k].forEach(a => { + let character = getChar(a.char, msg.playerid) || { id: undefined }; + let fArgs = { + macro: {}, + ability: { characterid: character.id } + } + let found = a.query.toLowerCase() === 'all' + ? (finders[a.type] || (() => []))(fArgs[a.type]).filter(hasAccess(msg.playerid)) + : [(finders[a.type] || (() => []))({ name: a.query, ...fArgs[a.type] }).filter(hasAccess(msg.playerid))[0]] + ; + + //found.forEach(o => { + // if (o) { + // reportList[k] = [ + // ...(reportList[k] || []), + // { + // ...a, + // id: o.id, + // name: o.get('name'), + // action: switchObj[k](o.get('istokenaction')), + // change: (switchObj[k] || (() => { }))(o.get('istokenaction')) === o.get('istokenaction') + // } + // ]; + // o.set({ istokenaction: (switchObj[k] || (() => { }))(o.get('istokenaction')) }); + // } else { + // reportList.notfound = [...(reportList.notfound || []), a]; + // } + //}) + reportList = found.reduce((m, o) => { + if (o) { + m.hasOwnProperty(o.id) + ? m[o.id].action = switchObj[k.toLowerCase()](m[o.id].action) + : m[o.id] = { + name: o.get('name'), + type: o.get('type'), + char: character.id ? character.get('name') : '', + action: switchObj[k.toLowerCase()](o.get('istokenaction')), + initial: o.get('istokenaction'), + obj: o + } + ; + } else { + m.notfound = [...(m.notfound || []), a]; + } + + return m; + }, reportList); + }); + }); + Object.entries(reportList) + .filter(([key,val]) => key !== 'notfound' && val.action !== val.initial) + .forEach(([_key, val]) => val.obj.set({ istokenaction: val.action })); + let verbose = manageState.get('verbose'); + if (report) { + let recipient = getWhisperTo(msg.who); + let message = html.table( + html.tr(html.td(' ', { 'width': '6px' }) + html.td(` `, localCSS.inlineEmphasis, localCSS.tablesubheading) + html.td(`NAME`, localCSS.inlineEmphasis, localCSS.tablesubheading) + html.td(`CHARACTER`, localCSS.inlineEmphasis, localCSS.tablesubheading)) + + Object.entries(reportList) + .filter(([key, val]) => key !== 'notfound' && (verbose || val.action !== val.initial)) + .map(([_key,val]) => { + return html.tr( + html.td('L', localCSS.actionindicator, { 'color': val.action ? '#0bcd0b' : '#DD2222' }) + + html.td(val.type === 'ability' ? '%' : '#', { 'text-align': 'right' }) + + html.td(val.name) + html.td(val.char) + ) + }).join('') + ); + msgbox({ title: `ShowButtons Report`, whisperto: recipient, msg: message }); + } + }; + + // ================================================== + // HANDLE CONFIG + // ================================================== + const handleConfig = msg => { + if (msg.type !== 'api' || !/^!showbuttons?config/.test(msg.content)) return; + let recipient = getWhisperTo(msg.who); + if (!playerIsGM(msg.playerid)) { + msgbox({ title: 'GM Rights Required', msg: 'You must be a GM to perform that operation', whisperto: recipient }); + return; + } + let cfgrx = /^(\+|-)(report|playerscanids|verbose)$/i; + let res; + let cfgTrack = {}; + let message; + if (/^!showbuttons?config\s+[^\s]/.test(msg.content)) { + msg.content.split(/\s+/).slice(1).forEach(a => { + res = cfgrx.exec(a); + if (!res) return; + if (res[2].toLowerCase() === 'report') { + manageState.set('report', (res[1] === '+')); + cfgTrack[res[2]] = res[1]; + } else if (res[2].toLowerCase() === 'playerscanids') { + manageState.set('playerscanids', (res[1] === '+')); + cfgTrack[res[2]] = res[1]; + } else if (res[2].toLowerCase() === 'verbose') { + manageState.set('verbose', (res[1] === '+')); + cfgTrack[res[2]] = res[1]; + } + }); + let changes = Object.keys(cfgTrack).map(k => `${html.span(k, localCSS.inlineEmphasis)}: ${cfgTrack[k] === '+' ? 'ON' : 'OFF'}`).join('
    '); + msgbox({ title: `ShowButtons Config Changed`, msg: `You have made the following changes to the ShowButtons configuration:
    ${changes}`, whisperto: recipient }); + } else { // naked call to config, show panel of current config + cfgTrack.report = `${html.span('report', localCSS.inlineEmphasis)}: ${manageState.get('report') ? 'ON' : 'OFF'}`; + cfgTrack.playerscanids = `${html.span('playerscanids', localCSS.inlineEmphasis)}: ${manageState.get('playerscanids') ? 'ON' : 'OFF'}`; + cfgTrack.verbose = `${html.span('verbose', localCSS.inlineEmphasis)}: ${manageState.get('verbose') ? 'ON' : 'OFF'}`; + message = `ShowButtons is currently configured as follows:
    ${cfgTrack.report}
    ${cfgTrack.playerscanids}`; + msgbox({ title: 'ShowButtons Configuration', msg: message, whisperto: recipient }); + } + }; + + // ================================================== + // DEPENDENCIES + // ================================================== + const checkDependencies = (deps) => { + /* pass array of objects like + { name: 'ModName', version: '#.#.#' || '', mod: ModName || undefined, checks: [ [ExposedItem, type], [ExposedItem, type] ] } + */ + const dependencyEngine = (deps) => { + const versionCheck = (mv, rv) => { + let modv = [...mv.split('.'), ...Array(4).fill(0)].slice(0, 4); + let reqv = [...rv.split('.'), ...Array(4).fill(0)].slice(0, 4); + return reqv.reduce((m, v, i) => { + if (m.pass || m.fail) return m; + if (i < 3) { + if (parseInt(modv[i]) > parseInt(reqv[i])) m.pass = true; + else if (parseInt(modv[i]) < parseInt(reqv[i])) m.fail = true; + } else { + // all betas are considered below the release they are attached to + if (reqv[i] === 0 && modv[i] === 0) m.pass = true; + else if (modv[i] === 0) m.pass = true; + else if (reqv[i] === 0) m.fail = true; + else if (parseInt(modv[i].slice(1)) >= parseInt(reqv[i].slice(1))) m.pass = true; + } + return m; + }, { pass: false, fail: false }).pass; + }; + + let result = { passed: true, failures: {}, optfailures: {} }; + deps.forEach(d => { + let failObj = d.optional ? result.optfailures : result.failures; + if (!d.mod) { + if (!d.optional) result.passed = false; + failObj[d.name] = 'Not found'; + return; + } + if (d.version && d.version.length) { + if (!(API_Meta[d.name].version && API_Meta[d.name].version.length && versionCheck(API_Meta[d.name].version, d.version))) { + if (!d.optional) result.passed = false; + failObj[d.name] = `Incorrect version. Required v${d.version}. ${API_Meta[d.name].version && API_Meta[d.name].version.length ? `Found v${API_Meta[d.name].version}` : 'Unable to tell version of current.'}`; + return; + } + } + d.checks.reduce((m, c) => { + if (!m.passed) return m; + let [pname, ptype] = c; + if (!d.mod.hasOwnProperty(pname) || typeof d.mod[pname] !== ptype) { + if (!d.optional) m.passed = false; + failObj[d.name] = `Incorrect version.`; + } + return m; + }, result); + }); + return result; + }; + let depCheck = dependencyEngine(deps); + let failures = '', contents = '', msg = ''; + if (Object.keys(depCheck.optfailures).length) { // optional components were missing + failures = Object.keys(depCheck.optfailures).map(k => `• ${k} : ${depCheck.optfailures[k]}`).join('
    '); + contents = `${apiproject} utilizies one or more other scripts for optional features, and works best with those scripts installed. You can typically find these optional scripts in the 1-click Mod Library:
    ${failures}`; + msg = `
    MISSING MOD DETECTED
    ${contents}
    `; + sendChat(apiproject, `/w gm ${msg}`); + } + if (!depCheck.passed) { + failures = Object.keys(depCheck.failures).map(k => `• ${k} : ${depCheck.failures[k]}`).join('
    '); + contents = `${apiproject} requires other scripts to work. Please use the 1-click Mod Library to correct the listed problems:
    ${failures}`; + msg = `
    MISSING MOD DETECTED
    ${contents}
    `; + sendChat(apiproject, `/w gm ${msg}`); + return false; + } + return true; + }; + + const registerEventHandlers = () => { + on('chat:message', handleInput); + on('chat:message', handleConfig); + }; + + on('ready', () => { + versionInfo(); + assureState(); + logsig(); + let reqs = [ + { + name: 'Messenger', + version: `1.0.0`, + mod: typeof Messenger !== 'undefined' ? Messenger : undefined, + checks: [['Button', 'function'], ['MsgBox', 'function'], ['HE', 'function'], ['Html', 'function'], ['Css', 'function']] + } + ]; + if (!checkDependencies(reqs)) return; + html = Messenger.Html(); + css = Messenger.Css(); + HE = Messenger.HE; + + registerEventHandlers(); + }); + return {}; +})(); + +{ try { throw new Error(''); } catch (e) { API_Meta.ShowButtons.lineCount = (parseInt(e.stack.split(/\n/)[1].replace(/^.*:(\d+):.*$/, '$1'), 10) - API_Meta.ShowButtons.offset); } } +/* */ \ No newline at end of file diff --git a/ShowButtons/ShowButtons.js b/ShowButtons/ShowButtons.js index e1147aade8..859edbca33 100644 --- a/ShowButtons/ShowButtons.js +++ b/ShowButtons/ShowButtons.js @@ -3,8 +3,8 @@ Name : ShowButtons GitHub : Roll20 Contact : timmaugh -Version : 1.0.1 -Last Update : 01 APR 2024 +Version : 1.0.2 +Last Update : 08 MAY 2025 ========================================================= */ var API_Meta = API_Meta || {}; @@ -13,12 +13,12 @@ API_Meta.ShowButtons = { offset: Number.MAX_SAFE_INTEGER, lineCount: -1 }; const ShowButtons = (() => { // eslint-disable-line no-unused-vars const apiproject = 'ShowButtons'; - const version = '1.0.1'; + const version = '1.0.2'; const apilogo = 'https://i.imgur.com/JLcfnek.png'; const apilogoalt = 'https://i.imgur.com/IbS0DA7.png'; - const schemaVersion = 0.2; + const schemaVersion = 0.3; API_Meta[apiproject].version = version; - const vd = new Date(1711998033639); + const vd = new Date(1746714262551); const versionInfo = () => { log(`\u0166\u0166 ${apiproject} v${API_Meta[apiproject].version}, ${vd.getFullYear()}/${vd.getMonth() + 1}/${vd.getDate()} \u0166\u0166 -- offset ${API_Meta[apiproject].offset}`); }; @@ -61,6 +61,8 @@ const ShowButtons = (() => { // eslint-disable-line no-unused-vars /* falls through */ case 0.2: + state[apiproject].settings.verbose = false; + state[apiproject].defaults.verbose = false; /* falls through */ case 'UpdateSchemaVersion': @@ -71,11 +73,13 @@ const ShowButtons = (() => { // eslint-disable-line no-unused-vars state[apiproject] = { settings: { report: true, - playerscanids: false + playerscanids: false, + verbose: false }, defaults: { report: true, - playerscanids: false + playerscanids: false, + verbose: false }, version: schemaVersion } @@ -190,16 +194,27 @@ const ShowButtons = (() => { // eslint-disable-line no-unused-vars // ================================================== // ROLL20 DATA // ================================================== + const hasAccess = pid => obj => { + someCheck = id => id.toLowerCase() === 'all' || id === pid; + let prop = (o = { + macro: () => [...(obj.get('visibleto') || '').split(/\s*,\s*/), obj.get('playerid')].some(someCheck), + ability: () => (getObj('character', obj.get('characterid'))?.get('controlledby') || '').split(/\s*,\s*/).some(someCheck), + character: () => (obj.get('controlledby') || '').split(/\s*,\s*/).some(someCheck), + default: () => undefined + })[obj && obj.get && typeof obj.get === 'function' && Object.keys(o).includes(obj.get('type')) ? obj.get('type') : 'default'](); + + return typeof prop === 'undefined' ? false : playerIsGM(pid) || prop; + }; const getChar = (query, pid) => { let character; - if (typeof query !== 'string') return character; + if (typeof query !== 'string' || !query.length) return character; let qrx = new RegExp(escapeRegExp(query), 'i'); - let charsIControl = findObjs({ type: 'character' }); - charsIControl = playerIsGM(pid) || manageState.get('playerscanids') ? charsIControl : charsIControl.filter(c => { - return c.get('controlledby').split(',').reduce((m, p) => { - return m || p === 'all' || p === pid; - }, false) - }); + let charsIControl = findObjs({ type: 'character' }).filter(hasAccess(pid)); + //charsIControl = playerIsGM(pid) || manageState.get('playerscanids') ? charsIControl : charsIControl.filter(c => { + // return c.get('controlledby').split(',').reduce((m, p) => { + // return m || p === 'all' || p === pid; + // }, false) + //}); character = charsIControl.filter(c => c.id === query)[0] || charsIControl.filter(c => c.id === (getObj('graphic', query) || { get: () => { return '' } }).get('represents'))[0] || charsIControl.filter(c => c.get('name') === query)[0] || @@ -271,6 +286,10 @@ const ShowButtons = (() => { // eslint-disable-line no-unused-vars const testHandles = (cmd) => { if (/^!showbuttons?\b/i.test(cmd)) return true; }; + const finders = { + macro: (props) => findObjs({ type: 'macro', ...props }), + ability: (props) => findObjs({ type: 'ability', ...props }) + }; const handleInput = (msg) => { if (msg.type !== 'api' || !testHandles(msg.content)) return; let report = manageState.get('report'); @@ -293,33 +312,73 @@ const ShowButtons = (() => { // eslint-disable-line no-unused-vars 'toggle': (c) => !c }; let reportList = {}; - const finders = { - macro: ({ name }) => findObjs({ type: 'macro', name })[0], - ability: ({ name, characterid }) => findObjs({ type: 'ability', name, characterid })[0] - }; - Object.keys(argObj).forEach(k => { + Object.keys(argObj).filter(k => Object.keys(switchObj).includes(k.toLowerCase())).forEach(k => { argObj[k].forEach(a => { let character = getChar(a.char, msg.playerid) || { id: undefined }; - let o = (finders[a.type] || (() => { }))({ name: a.query, characterid: character.id }); - if (o) { - reportList[k] = [...(reportList[k] || []), { ...a, action: switchObj[k](o.get('istokenaction')) }]; - o.set({ istokenaction: (switchObj[k] || (() => { }))(o.get('istokenaction')) }); - } else { - reportList.notfound = [...(reportList.notfound || []), a]; + let fArgs = { + macro: {}, + ability: { characterid: character.id } } + let found = a.query.toLowerCase() === 'all' + ? (finders[a.type] || (() => []))(fArgs[a.type]).filter(hasAccess(msg.playerid)) + : [(finders[a.type] || (() => []))({ name: a.query, ...fArgs[a.type] }).filter(hasAccess(msg.playerid))[0]] + ; + + //found.forEach(o => { + // if (o) { + // reportList[k] = [ + // ...(reportList[k] || []), + // { + // ...a, + // id: o.id, + // name: o.get('name'), + // action: switchObj[k](o.get('istokenaction')), + // change: (switchObj[k] || (() => { }))(o.get('istokenaction')) === o.get('istokenaction') + // } + // ]; + // o.set({ istokenaction: (switchObj[k] || (() => { }))(o.get('istokenaction')) }); + // } else { + // reportList.notfound = [...(reportList.notfound || []), a]; + // } + //}) + reportList = found.reduce((m, o) => { + if (o) { + m.hasOwnProperty(o.id) + ? m[o.id].action = switchObj[k.toLowerCase()](m[o.id].action) + : m[o.id] = { + name: o.get('name'), + type: o.get('type'), + char: character.id ? character.get('name') : '', + action: switchObj[k.toLowerCase()](o.get('istokenaction')), + initial: o.get('istokenaction'), + obj: o + } + ; + } else { + m.notfound = [...(m.notfound || []), a]; + } + + return m; + }, reportList); }); }); + Object.entries(reportList) + .filter(([key,val]) => key !== 'notfound' && val.action !== val.initial) + .forEach(([_key, val]) => val.obj.set({ istokenaction: val.action })); + let verbose = manageState.get('verbose'); if (report) { let recipient = getWhisperTo(msg.who); let message = html.table( html.tr(html.td(' ', { 'width': '6px' }) + html.td(` `, localCSS.inlineEmphasis, localCSS.tablesubheading) + html.td(`NAME`, localCSS.inlineEmphasis, localCSS.tablesubheading) + html.td(`CHARACTER`, localCSS.inlineEmphasis, localCSS.tablesubheading)) + - Object.keys(reportList, {}) - .filter(k => k !== 'notfound') - .map(k => { - return reportList[k].map(o => { - return html.tr(html.td('L', localCSS.actionindicator, { 'color': o.action ? '#0bcd0b' : '#DD2222' }) + html.td(o.type === 'ability' ? '%' : '#', {'text-align': 'right'}) + html.td(o.query) + html.td(o.char)) - }).join('') + Object.entries(reportList) + .filter(([key, val]) => key !== 'notfound' && (verbose || val.action !== val.initial)) + .map(([_key,val]) => { + return html.tr( + html.td('L', localCSS.actionindicator, { 'color': val.action ? '#0bcd0b' : '#DD2222' }) + + html.td(val.type === 'ability' ? '%' : '#', { 'text-align': 'right' }) + + html.td(val.name) + html.td(val.char) + ) }).join('') ); msgbox({ title: `ShowButtons Report`, whisperto: recipient, msg: message }); @@ -336,7 +395,7 @@ const ShowButtons = (() => { // eslint-disable-line no-unused-vars msgbox({ title: 'GM Rights Required', msg: 'You must be a GM to perform that operation', whisperto: recipient }); return; } - let cfgrx = /^(\+|-)(report|playerscanids)$/i; + let cfgrx = /^(\+|-)(report|playerscanids|verbose)$/i; let res; let cfgTrack = {}; let message; @@ -350,13 +409,17 @@ const ShowButtons = (() => { // eslint-disable-line no-unused-vars } else if (res[2].toLowerCase() === 'playerscanids') { manageState.set('playerscanids', (res[1] === '+')); cfgTrack[res[2]] = res[1]; + } else if (res[2].toLowerCase() === 'verbose') { + manageState.set('verbose', (res[1] === '+')); + cfgTrack[res[2]] = res[1]; } }); let changes = Object.keys(cfgTrack).map(k => `${html.span(k, localCSS.inlineEmphasis)}: ${cfgTrack[k] === '+' ? 'ON' : 'OFF'}`).join('
    '); msgbox({ title: `ShowButtons Config Changed`, msg: `You have made the following changes to the ShowButtons configuration:
    ${changes}`, whisperto: recipient }); - } else { + } else { // naked call to config, show panel of current config cfgTrack.report = `${html.span('report', localCSS.inlineEmphasis)}: ${manageState.get('report') ? 'ON' : 'OFF'}`; cfgTrack.playerscanids = `${html.span('playerscanids', localCSS.inlineEmphasis)}: ${manageState.get('playerscanids') ? 'ON' : 'OFF'}`; + cfgTrack.verbose = `${html.span('verbose', localCSS.inlineEmphasis)}: ${manageState.get('verbose') ? 'ON' : 'OFF'}`; message = `ShowButtons is currently configured as follows:
    ${cfgTrack.report}
    ${cfgTrack.playerscanids}`; msgbox({ title: 'ShowButtons Configuration', msg: message, whisperto: recipient }); } diff --git a/ShowButtons/script.json b/ShowButtons/script.json index 3b0220d96e..187dd92b31 100644 --- a/ShowButtons/script.json +++ b/ShowButtons/script.json @@ -1,7 +1,7 @@ { "name": "ShowButtons", "script": "ShowButtons.js", - "version": "1.0.1", + "version": "1.0.2", "description": "ShowButtons allows you to programmatically control what buttons show as 'token actions' when you've selected a token. This is helpful, for instance, if you have a shapeshifting character who has certain actions available to them in one form, and certain other actions available in another form./r/rHistorically, control over the character (or being a GM) has been required for this script to function, however with the v1.0.1 update, players may be granted the ability to act on characters they don't control. This is helpful for situations where you might have one character casting a spell on another character in the party, granting the target certain abilities./r/rFor more information and for syntax, see the original forum thread: [ShowButtons Forum Thread](https://app.roll20.net/forum/post/11429784/script-showbuttons-show-slash-hide-token-action-buttons-without-opening-character-sheet-or-collections-tab/)", "authors": "timmaugh", "roll20userid": "5962076", @@ -11,5 +11,5 @@ "state.ShowButtons": "read, write" }, "conflicts": [], - "previousversions": [] + "previousversions": [ "1.0.1"] } \ No newline at end of file