diff --git a/Daggerheart Fear AutoTracker/1.0.9/daggerheart-fear.js b/Daggerheart Fear AutoTracker/1.0.9/daggerheart-fear.js new file mode 100644 index 000000000..8a95bb450 --- /dev/null +++ b/Daggerheart Fear AutoTracker/1.0.9/daggerheart-fear.js @@ -0,0 +1,611 @@ +/// + +/** + * Daggerheart Fear AutoTracker + * Orbotik's Roll20 Scripts & Macros + * https://orbotik.com + * https://github.com/orbotik + * This script is © Christopher Eaton (aka @orbotik) and is licensed under CC BY-SA 4.0. + * To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/4.0/ + * + * ## API Commands: + * ### Any Player: + * !fear Shows the current fear counter value. + * !fear [on/off] Turns fear notices (from duality rolls) on or off for the commanding player. + * ### GM Only: + * !fear spend [number] Decreases the fear counter by 1 or a specific number (to a minimum of 0). + * !fear gain [number] Increases the fear counter by 1 or a specific number. + * !fear set [number] Sets the fear counter to a specific value. + * !fear text {id} Registers a text object to be updated with fear amount as it changes. The {id} is + * optional, and if omitted will set the selected text object. To stop the updating on + * a specific object, run the command again. + * !fear listen [on/off] Turn the listener for Demiplane duality rolls on or off. This is "on" by default. + * !fear text prefix [text] Specify (quoted) text to appear before the fear counter in text objects. + * !fear text suffix [text] Specify (quoted) text to appear after the fear counter in text objects. + * !fear text [number/tally/circled/bar/dots/skulls/sparkles/exes/candles/ravens] + * Switches how the fear count is displayed in the text objects. + * !fear text update Force the registered text objects to update with the current settings and fear value. + * This also lists the IDs of any registered text objects. + * !fear text monospace {id} Sets the currently selected or specified (by ID) text object to use a fixed-width + * predictable font. + * !fear text spacefill [on/off] Ensures the a uniform text length in text objects even when the fear value is low by + * filling unused character spots with a space. Paired with the monospace command this + * can help prevent the text objects from "jumping around" horizontally. + * !fear announce [on/off] Globally sets announcements to *all* players on or off when the fear amount changes. + * !fear whispers [on/off] Globally sets whispers to players on or off when the fear amount changes. + * !fear reset Resets the fear counter to 0. + * !fear reset objects Clears all fear-tracking object registrations. + * !fear reset known Clears the known player list (players will re-receive the welcome message). + */ +class DaggerheartFearScript { + + static VERSION = '1.0.9'; + + static BOT_NAME = 'The Game'; + + static MAXIMUM_FEAR = 12; //per daggerheart standard rules (pg.154 §Fear, Gaining Fear). + + static ADDITIONAL_TEXT_MODES = { + bar: '█', + dots: '•', + skulls: '💀', + sparkles: '✨', + exes: '✗', + candles: '🕯️', + stars: '✵', + ravens: '🐦‍⬛' + }; + + constructor() { + //init state + if (!state.fear || (typeof state.fear !== 'object')) { + state.fear = { + version: '1.0.0', + counter: 0, + known: [], + off: [] + }; + } + //upgrade current version to latest + let cvb = this.versionBits(state.fear.version); + if (cvb.major <= 1 && cvb.minor <= 0 && cvb.patch < 4) { + state.fear = Object.assign({ + whispers: false, + announce: true, + textMode: 'skulls', + textPrefix: '', + textSuffix: '', + textSpaceFill: true, + objects: { + text: [] + } + }, state.fear); + } + if (cvb.major <= 1 && cvb.minor <= 0 && cvb.patch < 7) { + delete state.fear.listener; + state.fear.listen = true; + } + state.fear.version = DaggerheartFearScript.VERSION; + //upgrade checks & initialization output + if (state.fear.counter > DaggerheartFearScript.MAXIMUM_FEAR) { + state.fear.counter = DaggerheartFearScript.MAXIMUM_FEAR; + } + log(`DaggerheartFearScript startup state is: ${JSON.stringify(state.fear, null, 4)}`); + //events + on('chat:message', this.chatHandler.bind(this)); + } + + /** + * Parses a semantic version string into it's major, minor, and patch components. + * @param {String} semver + * @returns {{major: Number, minor: Number, patch: Number}} Returns the parsed version components. + */ + versionBits(semver) { + if (!semver || typeof semver !== 'string') { + throw new Error('A valid semver string is required.'); + } + if (!semver.match(/^\d+\.\d+\.\d+$/)) { + throw new Error('Invalid semver format, expected "major.minor.patch" format.'); + } + let bits = semver.split('.').map(v => parseInt(v, 10)); + return { major: bits[0], minor: bits[1], patch: bits[2] }; + } + + /** + * Returns the fear message that explains the current fear level. + * @param {String} variant + * @returns {String} + */ + fearMessage(variant) { + let message = ''; + let variantVerb = ''; + if (variant && variant !== '+' && variant !== '-') { + throw new Error('Invalid fear message variant specified.'); + } else if (variant === '+') { + message = '
💀 Fear has increased!'; + variantVerb = 'now '; + } else if (variant === '-') { + message = '
🌸 Fear has been reduced!'; + variantVerb = 'still '; + } + if (!state.fear.counter || state.fear.counter <= 0) { + message += `
Have no fear. There is none to be had (0).`; + } else if (state.fear.counter === 1) { + message += `
There is ${variantVerb}a single fear (1).`; + } else if (state.fear.counter < 4) { + message += `
There is ${variantVerb}some fear (${state.fear.counter}).`; + } else if (state.fear.counter < 7) { + message += `
There is ${variantVerb}much fear (${state.fear.counter}).`; + } else if (state.fear.counter < 10) { + message += `
There is ${variantVerb}strong fear (${state.fear.counter}).`; + } else if (state.fear.counter < DaggerheartFearScript.MAXIMUM_FEAR) { + message += `
There is ${variantVerb}intense fear (${state.fear.counter}).`; + } else { + message += `
There is ${variantVerb}maximum fear (${state.fear.counter}).`; + } + return message; + } + + /** + * Send a player a private message (whisper). + * @param {String | Object} playerObjOrID + * @param {String} message + */ + pm(playerObjOrID, message) { + if (!message) { + throw new Error('A message is required to send chat.'); + } + if (typeof playerObjOrID === 'string') { + playerObjOrID = getObj('player', playerObjOrID); + } + if (typeof message !== 'string') { + throw new Error('Message must be a string.'); + } + if (!playerObjOrID) { + sendChat(DaggerheartFearScript.BOT_NAME, message); + } else { + sendChat(DaggerheartFearScript.BOT_NAME, `/w "${playerObjOrID.get('displayname')}" ${message}`, null, { noarchive: true }); + } + } + + /** + * Send a player a private message of the current fear value. + * @param {String | Object} playerObjOrID + * @param {String} variant + */ + pmFear(playerObjOrID, variant) { + let message = this.fearMessage(variant); + this.pm(playerObjOrID, message); + } + + /** + * Announce (or whisper) the current fear value to all players. + * If whispers is enabled, only registered and whisper-"on" players are pm'd. + * @param {String} variant + */ + announceFear(variant) { + if (state.fear.whispers) { + let players = findObjs({ _type: 'player' }); + let message = this.fearMessage(variant); + for (let p of players) { + if (state.fear.off.includes(p.id) === false) { + this.pm(p, message); + } + } + } else if (state.fear.announce) { + sendChat(DaggerheartFearScript.BOT_NAME, this.fearMessage(variant)); + } + } + + registerPlayer(playerObjOrID) { + if (!playerObjOrID) { + throw new Error('Player is required to register.'); + } + if (typeof playerObjOrID === 'string') { + playerObjOrID = getObj('player', playerObjOrID); + } + let knownIndex = state.fear.known.indexOf(playerObjOrID.id); + let offIndex = state.fear.off.indexOf(playerObjOrID.id); + if (knownIndex < 0 || offIndex >= 0) { + if (knownIndex < 0) { + state.fear.known.push(playerObjOrID.id); + } + if (offIndex >= 0) { + state.fear.off.splice(offIndex, 1); + } + this.pm(playerObjOrID, `Your fear notices are now on.`); + log(`Player "${playerObjOrID.get('displayname')}" registered for fear whispers.`); + } else { + log(`Player "${playerObjOrID.get('displayname')}" is already registered for fear whispers.`); + } + } + + unregisterPlayer(playerObjOrID) { + if (!playerObjOrID) { + throw new Error('Player is required to unregister.'); + } + if (typeof playerObjOrID === 'string') { + playerObjOrID = getObj('player', playerObjOrID); + } + if (state.fear.off.indexOf(playerObjOrID.id) < 0) { + state.fear.off.push(playerObjOrID.id); + this.pm(playerObjOrID, `Your fear notices are now off.`); + log(`Player "${playerObjOrID.get('displayname')}" unregistered from fear whispers.`); + } else { + log(`Player "${playerObjOrID.get('displayname')}" is already unregistered from fear whispers.`); + } + } + + registerTextObjects(...objectIDs) { + let textObjs = filterObjs(obj => objectIDs.includes(obj.id) && obj.get('type') === 'text'); + objectIDs = textObjs.map(o => o.id); + for (let oid of objectIDs) { + if (state.fear.objects.text.includes(oid) === false) { + state.fear.objects.text.push(oid); + } + } + return textObjs.map(o => o.id); + } + + unregisterTextObjects(...objectIDs) { + let found = []; + for (let oid of objectIDs) { + let index = state.fear.objects.text.indexOf(oid); + if (index >= 0) { + state.fear.objects.text.splice(index, 1); + found.push(oid); + } + } + return found; + } + + monospaceTextObjects(...objectIDs) { + let textObjs = filterObjs(obj => objectIDs.includes(obj.id) && obj.get('type') === 'text'); + for (let to of textObjs) { + to.set('font_family', 'monospace'); + } + return textObjs.map(o => o.id); + } + + updateTextObjects() { + if (state.fear.objects.text.length) { + for (let oid of state.fear.objects.text) { + let textObject = getObj('text', oid); + if (!textObject) { + this.unregisterTextObjects(oid); + log(`The text object with ID "${oid}" was not found and has been unregistered as a fear tracker.`); + } else { + let extraSpaces = 0; + let text = ''; + if (state.fear.textMode === 'tally') { + text = '𝍸'.repeat(Math.floor(state.fear.counter / 5)); + switch (state.fear.counter % 5) { + case 1: text += '𝍩'; break; + case 2: text += '𝍪'; break; + case 3: text += '𝍫'; break; + case 4: text += '𝍬'; break; + } + extraSpaces = 5 - text.length; + } else if (state.fear.textMode === 'circled') { + let parts = state.fear.counter.toString().split(''); + let glyphs = ['⓪', '⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽']; + text = parts.map(n => glyphs[parseInt(n)]).join(''); + extraSpaces = 2 - text.length; + } else if (DaggerheartFearScript.ADDITIONAL_TEXT_MODES[state.fear.textMode]) { + let charLength = DaggerheartFearScript.ADDITIONAL_TEXT_MODES[state.fear.textMode].length; + text = DaggerheartFearScript.ADDITIONAL_TEXT_MODES[state.fear.textMode].repeat(state.fear.counter); + extraSpaces = Math.min(100, DaggerheartFearScript.MAXIMUM_FEAR) - state.fear.counter; + if (charLength > 1) { + extraSpaces *= charLength; + } + } else { + text = state.fear.counter.toString(); + extraSpaces = 2 - text.length; + } + if (state.fear.textSpaceFill && extraSpaces > 0) { + text += ' '.repeat(extraSpaces); + } + if (typeof state.fear.textPrefix === 'string' && state.fear.textPrefix) { + text = state.fear.textPrefix + ' ' + text; + } + if (typeof state.fear.textSuffix === 'string' && state.fear.textSuffix) { + text += ' ' + state.fear.textSuffix; + } + //hack around roll20 bug where the game crashes due to empty text + if (text === null || text === '') { + text = ' '; + } + textObject.set('text', text); + } + } + } + } + + /** + * Parses a Roll20 Chat Message. + * @param {ChatMessage} msg + * @returns {{command:String, args:Array., player:Player, gm:Boolean }} + */ + chatCommand(msg) { + if (msg.type === 'api' && !msg.rolltemplate && msg.playerid) { + let args; + if (msg.content.indexOf('"') > -1 || msg.content.indexOf('\'') > -1) { + let matches = msg.content.substring(1).matchAll(/[^\s"']+|["']([^"']*)["']/gi); + args = []; + for (let m of matches) { + if (m[0]) { + args.push(m.length > 1 && !!m[1] ? m[1] : m[0]) + } + } + } else { + args = msg.content.substring(1).split(' '); + } + let command = args[0].toLowerCase(); + args.splice(0, 1); + args = args.map(v => v.replaceAll(/[^a-zA-Z0-9 \._=@\-()&+]/g, '')); + let player = getObj('player', msg.playerid); + let gm = playerIsGM(msg.playerid); + return { command, args, player, gm }; + } + return null; + } + + chatHandler(msg) { + //check if new player + if ((msg.type === 'general' || msg.type === 'api') && + msg.playerid && + msg.playerid !== 'API' && + state.fear.known.includes(msg.playerid) === false) { + let p = getObj('player', msg.playerid); + if (p) { + this.pm(p, `Welcome to Orbotik's Daggerheart Fear Tracker! v${DaggerheartFearScript.VERSION}
You may turn fear notices on and off using !fear on and !fear off commands.`); + this.registerPlayer(p); + } + } + //capture duality roll with fear + if (msg.type === 'advancedroll' && msg.content.match(/demiplane-dice-roll-daggerheart-character/gmi) && msg.content.match(/--roll-with-fear/gmi)) { + if (state.fear.listen) { + state.fear.counter = Math.min(DaggerheartFearScript.MAXIMUM_FEAR, state.fear.counter + 1); + this.updateTextObjects(); + this.announceFear('+'); + } + } else { + let chat = this.chatCommand(msg); + if (chat?.command === 'fear') { + if (chat.args.length === 0) { + log(`Showing fear of ${state.fear.counter ?? 0} to ${chat.player.get('displayname')}.`); + this.pmFear(chat.player); + } else { + switch (chat.args[0]) { + case 'off': + this.unregisterPlayer(chat.player); + break; + case 'on': + this.registerPlayer(chat.player); + break; + case 'whisper': + case 'whispers': + if (chat.gm) { + if (chat.args[1] === 'on') { + let message = 'Whispers are now on'; + state.fear.whispers = true; + if (state.fear.announce) { + state.fear.announce = false; + message += '
Announcements are off'; + } + this.pm(chat.player, message); + } else if (chat.args[1] === 'off') { + state.fear.whispers = false; + this.pm(chat.player, 'Whispers are now off'); + } else { + this.pm(chat.player, 'Invalid command arguments, expected "on" or "off"'); + } + } else { + this.pm(chat.player, 'Invalid command or parameters (or you may not be the GM).'); + } + break; + case 'announce': + case 'announcements': + if (chat.gm) { + if (chat.args[1] === 'on') { + let message = 'Announcements are now on'; + state.fear.announce = true; + if (state.fear.whispers) { + state.fear.whispers = false; + message += '
Whispers are off'; + } + this.pm(chat.player, message); + } else if (chat.args[1] === 'off') { + state.fear.announce = false; + this.pm(chat.player, 'Announcements are now off'); + } else { + this.pm(chat.player, 'Invalid command arguments, expected "on" or "off"'); + } + } else { + this.pm(chat.player, 'Invalid command or parameters (or you may not be the GM).'); + } + break; + case 'listen': + if (chat.gm) { + if (chat.args[1] === 'on') { + state.fear.listen = true; + this.pm(chat.player, 'Fear roll listener is now on'); + } else if (chat.args[1] === 'off') { + state.fear.listen = false; + this.pm(chat.player, 'Fear roll listener is now off'); + } else { + this.pm(chat.player, 'Invalid command arguments, expected "on" or "off"'); + } + } else { + this.pm(chat.player, 'Invalid command or parameters (or you may not be the GM).'); + } + break; + case 'spend': + case 'gain': + if (chat.gm) { + let amount = 1; + if (chat.args.length > 1 && chat.args[1] && isFinite(parseInt(chat.args[1]))) { + amount = parseInt(chat.args[1]); + } + if (chat.args[0] === 'spend' && state.fear.counter >= amount) { + state.fear.counter -= amount; + if (state.fear.counter < 0) { + state.fear.counter = 0; + } + //send notices + this.updateTextObjects(); + this.announceFear('-'); + this.pm(chat.player, `Fear has been spent. New value is ${state.fear.counter}.`); + } else if (chat.args[0] === 'gain' && state.fear.counter + amount <= DaggerheartFearScript.MAXIMUM_FEAR) { + state.fear.counter += amount; + //send notices + this.updateTextObjects(); + this.announceFear('+'); + this.pm(chat.player, `Fear has been gained. New value is ${state.fear.counter}.`); + } else { + this.pm(chat.player, `Unable to ${chat.args[0]} ${amount} fear, fear is currently: ${state.fear.counter}.`); + } + } else { + this.pm(chat.player, 'Invalid command or parameters (or you may not be the GM).'); + } + break; + case 'set': + if (chat.gm) { + let originalCounter = state.fear.counter; + let newCounter = Math.min(DaggerheartFearScript.MAXIMUM_FEAR, Math.max(0, parseInt(chat.args[1]))); + if (originalCounter != newCounter) { + state.fear.counter = newCounter; + this.updateTextObjects(); + this.announceFear(originalCounter < newCounter ? '+' : '-'); + this.pm(chat.player, `Fear has been set to ${state.fear.counter}.`); + } else { + this.pm(chat.player, `Fear is already set to ${state.fear.counter}.`); + } + } else { + this.pm(chat.player, 'Invalid command or parameters (or you may not be the GM).'); + } + break; + case 'text': + if (chat.gm) { + if (chat.args.length === 2 && ( + chat.args[1] === 'tally' || chat.args[1] === 'number' || chat.args[1] === 'circled' || + !!DaggerheartFearScript.ADDITIONAL_TEXT_MODES[chat.args[1]] + )) { + state.fear.textMode = chat.args[1]; + this.updateTextObjects(); + this.pm(chat.player, `Text mode is now set to "${chat.args[1]}".
Please note that some text modes may not work across all operating systems. If you experience a problem, please report your operating system, browser, version, and text mode you are attempting to use here:
https://github.com/orbotik/roll20-scripts/issues
`); + } else if (chat.args.length === 2 && chat.args[1] === 'update') { + this.updateTextObjects(); + let message; + let ids = state.fear.objects.text; + if (ids.length) { + message = `(${ids.length}) Text objects have been updated:`; + message += ''; + } else { + message = '(0) Text objects are registered. There are no text objects to update.'; + } + this.pm(chat.player, message); + } else if ((chat.args.length === 2 || chat.args.length === 3) && chat.args[1] === 'monospace') { + let ids = msg.selected?.filter(s => s._type === 'text').map(s => s._id); + if (chat.args.length === 3) { + ids = [chat.args[2]]; + } + if (ids && ids.length) { + this.monospaceTextObjects(...ids); + this.updateTextObjects(); + this.pm(chat.player, `The text object(s) "${ids.join('", "')}" will now use a monospace font.`); + } else { + this.pm(chat.player, `No text objects are selected. Please select a text object, or specify the object ID.`); + } + } else if (chat.args.length === 3 && chat.args[1] === 'spacefill') { + if (chat.args[2] === 'on' || chat.args[2] === 'off') { + state.fear.textSpaceFill = chat.args[2] === 'on'; + this.updateTextObjects(); + this.pm(chat.player, `Text space-fill is now ${chat.args[2]}`); + } else { + this.pm(chat.player, 'Invalid command arguments, expected "on" or "off"'); + } + } else if (chat.args.length === 3 && chat.args[1] === 'prefix') { + state.fear.textPrefix = chat.args[2]; + this.updateTextObjects(); + this.pm(chat.player, `Text objects will now show the prefix "${state.fear.textPrefix}".`); + } else if (chat.args.length === 3 && chat.args[1] === 'suffix') { + state.fear.textSuffix = chat.args[2]; + this.updateTextObjects(); + this.pm(chat.player, `Text objects will now show the suffix "${state.fear.textSuffix}".`); + } else if (chat.args.length === 1 || chat.args.length === 2) { + let ids = msg.selected?.filter(s => s._type === 'text').map(s => s._id); + if (chat.args.length === 2) { + ids = [chat.args[1]]; + } + if (ids && ids.length) { + let message; + if (ids.some(oid => state.fear.objects.text.includes(oid))) { + ids = this.unregisterTextObjects(...ids); + message = `(${ids.length}) Text objects have been unregistered as fear trackers.`; + } else { + ids = this.registerTextObjects(...ids); + this.updateTextObjects(); + message = `(${ids.length}) Text objects have been registered as fear trackers.`; + } + if (ids.length) { + message += ''; + } + this.pm(chat.player, message); + } else { + this.pm(chat.player, `No text objects are selected. Please select a text object, or specify the object ID.`); + } + } else { + this.pm(chat.player, 'Invalid arguments. Expected either a text object ID or a text mode, or no arguments and that you have selected text objects.'); + } + } else { + this.pm(chat.player, 'Invalid command or parameters (or you may not be the GM).'); + } + break; + case 'reset': + if (chat.gm) { + if (chat.args.length === 1) { + if (state.fear.counter !== 0) { + state.fear.counter = 0; + this.updateTextObjects(); + this.pm(chat.player, 'Fear has been reset to 0.'); + this.announceFear('-'); + } else { + this.pm(chat.player, 'Fear is already at 0.'); + } + } else if (chat.args[1] === 'known') { + state.fear.known = []; + this.pm(chat.player, 'The list of known players has been cleared.'); + } else if (chat.args[1] === 'objects') { + state.fear.objects = { text: [] }; + this.pm(chat.player, 'Tracking objects have been cleared.'); + } else { + this.pm(chat.player, 'Invalid command argument(s).'); + } + } else { + this.pm(chat.player, 'Invalid command or parameters (or you may not be the GM).'); + } + break; + default: + this.pm(chat.player, 'Invalid command or parameters (or you may not be the GM).'); + break; + } + } + } + } + } +} + +on('ready', () => { + log(`Daggerheart Fear script v${DaggerheartFearScript.VERSION} initializing.`); + new DaggerheartFearScript(); + log(`Daggerheart Fear script initialized.`); +}); \ No newline at end of file diff --git a/Daggerheart Fear AutoTracker/daggerheart-fear.js b/Daggerheart Fear AutoTracker/daggerheart-fear.js index a5d77f1ba..a45f2851d 100644 --- a/Daggerheart Fear AutoTracker/daggerheart-fear.js +++ b/Daggerheart Fear AutoTracker/daggerheart-fear.js @@ -1,7 +1,10 @@ +/// + /** * Daggerheart Fear AutoTracker * Orbotik's Roll20 Scripts & Macros * https://orbotik.com + * https://github.com/orbotik * This script is © Christopher Eaton (aka @orbotik) and is licensed under CC BY-SA 4.0. * To view a copy of this license, visit https://creativecommons.org/licenses/by-sa/4.0/ * @@ -36,7 +39,7 @@ */ class DaggerheartFearScript { - static VERSION = '1.0.8'; + static VERSION = '1.0.9'; static BOT_NAME = 'The Game'; @@ -69,7 +72,7 @@ class DaggerheartFearScript { state.fear = Object.assign({ whispers: false, announce: true, - textMode: 'tally', + textMode: 'skulls', textPrefix: '', textSuffix: '', textSpaceFill: true, @@ -245,15 +248,15 @@ class DaggerheartFearScript { } unregisterTextObjects(...objectIDs) { - let textObjs = filterObjs(obj => objectIDs.includes(obj.id) && obj.get('type') === 'text'); - objectIDs = textObjs.map(o => o.id); + let found = []; for (let oid of objectIDs) { let index = state.fear.objects.text.indexOf(oid); if (index >= 0) { state.fear.objects.text.splice(index, 1); + found.push(oid); } } - return textObjs.map(o => o.id); + return found; } monospaceTextObjects(...objectIDs) { @@ -268,51 +271,61 @@ class DaggerheartFearScript { if (state.fear.objects.text.length) { for (let oid of state.fear.objects.text) { let textObject = getObj('text', oid); - let extraSpaces = 0; - let text = ''; - if (state.fear.textMode === 'tally') { - text = '𝍸'.repeat(Math.floor(state.fear.counter / 5)); - switch (state.fear.counter % 5) { - case 1: text += '𝍩'; break; - case 2: text += '𝍪'; break; - case 3: text += '𝍫'; break; - case 4: text += '𝍬'; break; + if (!textObject) { + this.unregisterTextObjects(oid); + log(`The text object with ID "${oid}" was not found and has been unregistered as a fear tracker.`); + } else { + let extraSpaces = 0; + let text = ''; + if (state.fear.textMode === 'tally') { + text = '𝍸'.repeat(Math.floor(state.fear.counter / 5)); + switch (state.fear.counter % 5) { + case 1: text += '𝍩'; break; + case 2: text += '𝍪'; break; + case 3: text += '𝍫'; break; + case 4: text += '𝍬'; break; + } + extraSpaces = 5 - text.length; + } else if (state.fear.textMode === 'circled') { + let parts = state.fear.counter.toString().split(''); + let glyphs = ['⓪', '⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽']; + text = parts.map(n => glyphs[parseInt(n)]).join(''); + extraSpaces = 2 - text.length; + } else if (DaggerheartFearScript.ADDITIONAL_TEXT_MODES[state.fear.textMode]) { + let charLength = DaggerheartFearScript.ADDITIONAL_TEXT_MODES[state.fear.textMode].length; + text = DaggerheartFearScript.ADDITIONAL_TEXT_MODES[state.fear.textMode].repeat(state.fear.counter); + extraSpaces = Math.min(100, DaggerheartFearScript.MAXIMUM_FEAR) - state.fear.counter; + if (charLength > 1) { + extraSpaces *= charLength; + } + } else { + text = state.fear.counter.toString(); + extraSpaces = 2 - text.length; } - extraSpaces = 5 - text.length; - } else if (state.fear.textMode === 'circled') { - let parts = state.fear.counter.toString().split(''); - let glyphs = ['⓪', '⓵', '⓶', '⓷', '⓸', '⓹', '⓺', '⓻', '⓼', '⓽']; - text = parts.map(n => glyphs[parseInt(n)]).join(''); - extraSpaces = 2 - text.length; - } else if (DaggerheartFearScript.ADDITIONAL_TEXT_MODES[state.fear.textMode]) { - let charLength = DaggerheartFearScript.ADDITIONAL_TEXT_MODES[state.fear.textMode].length; - text = DaggerheartFearScript.ADDITIONAL_TEXT_MODES[state.fear.textMode].repeat(state.fear.counter); - extraSpaces = Math.min(100, DaggerheartFearScript.MAXIMUM_FEAR) - state.fear.counter; - if (charLength > 1) { - extraSpaces *= charLength; + if (state.fear.textSpaceFill && extraSpaces > 0) { + text += ' '.repeat(extraSpaces); } - } else { - text = state.fear.counter.toString(); - extraSpaces = 2 - text.length; - } - if (state.fear.textSpaceFill && extraSpaces > 0) { - text += ' '.repeat(extraSpaces); - } - if (typeof state.fear.textPrefix === 'string' && state.fear.textPrefix) { - text = state.fear.textPrefix + ' ' + text; - } - if (typeof state.fear.textSuffix === 'string' && state.fear.textSuffix) { - text += ' ' + state.fear.textSuffix; - } - //hack around roll20 bug where the game crashes due to empty text - if (text === null || text === '') { - text = ' '; + if (typeof state.fear.textPrefix === 'string' && state.fear.textPrefix) { + text = state.fear.textPrefix + ' ' + text; + } + if (typeof state.fear.textSuffix === 'string' && state.fear.textSuffix) { + text += ' ' + state.fear.textSuffix; + } + //hack around roll20 bug where the game crashes due to empty text + if (text === null || text === '') { + text = ' '; + } + textObject.set('text', text); } - textObject.set('text', text); } } } + /** + * Parses a Roll20 Chat Message. + * @param {ChatMessage} msg + * @returns {{command:String, args:Array., player:Player, gm:Boolean }} + */ chatCommand(msg) { if (msg.type === 'api' && !msg.rolltemplate && msg.playerid) { let args; @@ -480,7 +493,7 @@ class DaggerheartFearScript { )) { state.fear.textMode = chat.args[1]; this.updateTextObjects(); - this.pm(chat.player, `Text mode is now set to "${chat.args[1]}".`); + this.pm(chat.player, `Text mode is now set to "${chat.args[1]}".
Please note that some text modes may not work across all operating systems. If you experience a problem, please report your operating system, browser, version, and text mode you are attempting to use here:
https://github.com/orbotik/roll20-scripts/issues
`); } else if (chat.args.length === 2 && chat.args[1] === 'update') { this.updateTextObjects(); let message; @@ -595,4 +608,4 @@ on('ready', () => { log(`Daggerheart Fear script v${DaggerheartFearScript.VERSION} initializing.`); new DaggerheartFearScript(); log(`Daggerheart Fear script initialized.`); -}); \ No newline at end of file +}); diff --git a/Daggerheart Fear AutoTracker/script.json b/Daggerheart Fear AutoTracker/script.json index 83172a350..534f80402 100644 --- a/Daggerheart Fear AutoTracker/script.json +++ b/Daggerheart Fear AutoTracker/script.json @@ -1,7 +1,7 @@ { "name": "Daggerheart Fear AutoTracker", "script": "daggerheart-fear.js", - "version": "1.0.8", + "version": "1.0.9", "description": "This fear tracker listens for duality rolls from Demiplane-linked character sheets and bumps up a game fear counter everytime someone rolls with fear. It sends notices to all players showing the new fear value either as player-enabled whispers or chat announcements (or none), and can even update text objects on the maps with fear values!", "authors": "orbotik", "roll20userid": "12231884", @@ -15,5 +15,5 @@ "text.text": "write" }, "conflicts": [], - "previousversions": [] + "previousversions": ["1.0.8"] } \ No newline at end of file