diff --git a/Collision Detection/Collision Detection.js b/Collision Detection/Collision Detection.js new file mode 100644 index 0000000000..ab0f28dc8c --- /dev/null +++ b/Collision Detection/Collision Detection.js @@ -0,0 +1,184 @@ +/** + * Executes configured behavior when a player-controlled token moves and + * intersects a path of the configured color on the appropriate layer(s). + * + * behaviors: + * DONT_MOVE: token is returned to its starting location + * WARN_PLAYER: player is sent a message warning against the movement + * STOP_AT_WALL: token is moved to the edge of the path + * + * Set the pathColor as a hexadecimal color; only paths of the given color will + * be considered for collision events. + * + * Set the layer to the layer used for collision events. Valid values are "map", + * "objects", "gmlayer", and "walls". You may also use "all" to use paths on + * all layers. + * + * Set the behavior to the combination of behaviors you want to use. Combine + * behaviors with bitwise OR (|) like so: + * behaviors.STOP_AT_WALL | behaviors.WARN_PLAYER + * + * The STOP_AT_WALL and DONT_MOVE behaviors are incompatible with one another. + */ +var bshields = bshields || {}; +bshields.Collision = (function() { + 'use strict'; + + var version = 2.0, + polygonPaths = [], + behaviors = { + DONT_MOVE: 1, + WARN_PLAYER: 2, + STOP_AT_WALL: 4 + }, + config = { + pathColor: '#ff00ff', + layer: 'walls', + behavior: behaviors.STOP_AT_WALL | behaviors.WARN_PLAYER + }; + + function addPath(obj) { + var path; + + if (obj.get('pageid') !== Campaign().get('playerpageid') || + obj.get('stroke').toLowerCase() !== config.pathColor || + (config.layer !== 'all' && obj.get('layer') !== config.layer)) { return; } + + path = JSON.parse(obj.get('path')); + if (path.length > 1 && path[1][0] !== 'L') { return; } + polygonPaths.push(obj); + } + + function destroyPath(obj) { + polygonPaths = _.reject(polygonPaths, function(path) { return path.id === obj.id; }); + } + + function changePath(obj, prev) { + var path; + + if (config.layer === 'all') { return; } + + if (obj.get('layer') === config.layer && prev.layer !== config.layer) { + if (obj.get('pageid') !== Campaign().get('playerpageid') || + obj.get('stroke').toLowerCase() !== config.pathColor) { return; } + + path = JSON.parse(obj.get('path')); + if (path.length > 1 && path[1][0] !== 'L') { return; } + polygonPaths.push(obj); + } + + if (obj.get('layer') !== config.layer && prev.layer === config.layer) { + polygonPaths = _.reject(polygonPaths, function(path) { return path.id === obj.id; }); + } + } + + function changeGraphic(obj, prev) { + var character, l1 = L(P(prev.left, prev.top), P(obj.get('left'), obj.get('top'))); + + if (obj.get('subtype') !== 'token' || + (obj.get('top') === prev.top && obj.get('left') === prev.left)) { return; } + + if (obj.get('represents') !== '') { + character = getObj('character', obj.get('represents')); + if (character.get('controlledby') === '') { return; } // GM-only character + } else if (obj.get('controlledby') === '') { return; } // GM-only token + + _.each(polygonPaths, function(path) { + var x = path.get('left') - path.get('width') / 2, + y = path.get('top') - path.get('height') / 2, + parts = JSON.parse(path.get('path')), + pointA = P(parts[0][1] + x, parts[0][2] + y); + parts.shift(); + _.each(parts, function(pt) { + var pointB = P(pt[1] + x, pt[2] + y), + l2 = L(pointA, pointB), + denom = (l1.p1.x - l1.p2.x) * (l2.p1.y - l2.p2.y) - (l1.p1.y - l1.p2.y) * (l2.p1.x - l2.p2.x), + intersect, who, player, vec, norm; + + if (denom !== 0) { + intersect = P( + (l1.p1.x * l1.p2.y - l1.p1.y * l1.p2.x) * (l2.p1.x - l2.p2.x) - (l1.p1.x - l1.p2.x) * (l2.p1.x * l2.p2.y - l2.p1.y * l2.p2.x), + (l1.p1.x * l1.p2.y - l1.p1.y * l1.p2.x) * (l2.p1.y - l2.p2.y) - (l1.p1.y - l1.p2.y) * (l2.p1.x * l2.p2.y - l2.p1.y * l2.p2.x) + ); + intersect.x /= denom; + intersect.y /= denom; + + if (isBetween(pointA, pointB, intersect) && + isBetween(l1.p1, l1.p2, intersect)) { + // Collision event! + if ((config.behavior & behaviors.DONT_MOVE) === behaviors.DONT_MOVE) { + obj.set({ + left: Math.round(l1.p1.x), + top: Math.round(l1.p1.y) + }); + } + + if ((config.behavior & behaviors.WARN_PLAYER) === behaviors.WARN_PLAYER) { + if (obj.get('represents')) { + character = getObj('character', obj.get('represents')); + who = character.get('name'); + } else if (obj.get('controlledby') === 'all') { + who = 'all'; + } else { + player = getObj('player', obj.get('controlledby')); + who = player.get('displayname'); + } + + if (who !== 'all') { + who = who.indexOf(' ') > 0 ? who.substring(0, who.indexOf(' ')) : who; + sendChat('System', '/w ' + who + ' You are not permitted to move that token into that area.'); + } else { + sendChat('System', 'Token ' + obj.get('name') + ' is not permitted in that area.'); + } + } + + if ((config.behavior & behaviors.STOP_AT_WALL) === behaviors.STOP_AT_WALL) { + vec = P(l1.p2.x - l1.p1.x, l1.p2.y - l1.p1.y); + norm = Math.sqrt(vec.x * vec.x + vec.y * vec.y); + vec.x /= norm; + vec.y /= norm; + + obj.set({ + left: intersect.x - vec.x, + top: intersect.y - vec.y + }); + } + } + } + + pointA = P(pointB.x, pointB.y); + }); + }); + } + + function P(x, y) { return { x: x, y: y}; } + function L(p1, p2) { return { p1: p1, p2: p2 }; } + + function isBetween(a, b, c) { + var withinX = (a.x <= c.x && c.x <= b.x) || (b.x <= c.x && c.x <= a.x), + withinY = (a.y <= c.y && c.y <= b.y) || (b.y <= c.y && c.y <= a.y); + return withinX && withinY; + } + + function registerEventHandlersBeforeReady() { + on('add:path', addPath); + } + + function registerEventHandlers() { + on('destroy:path', destroyPath); + on('change:path', changePath); + on('change:graphic', changeGraphic); + } + + return { + registerPreloadEventHandlers: registerEventHandlersBeforeReady, + registerPostloadEventHandlers: registerEventHandlers + }; +}()); + +bshields.Collision.registerPreloadEventHandlers(); +on('ready', function() { + 'use strict'; + + bshields.Collision.registerPostloadEventHandlers(); +} \ No newline at end of file diff --git a/Collision Detection/Help.txt b/Collision Detection/Help.txt new file mode 100644 index 0000000000..2e5b71f1fa --- /dev/null +++ b/Collision Detection/Help.txt @@ -0,0 +1,7 @@ +## Collision Detection + +There are three configuration options available to you: + +* `config.pathColor`: The script only considers paths of a specific color, allowing you to also use paths of other colors which your players will not collide with. By default, this is fuchsia (#ff00ff); the color is specified as a hexadecimal web color, which you can see when selecting a color from the drawing interface. A path's fill color is ignored. +* `config.layer`: The script will only look at paths on the specified layer (valid values are "map", "objects", "gmlayer", or "walls"). You can also set this value to "all" and paths on every layer will be considered. +* `config.behavior`: You can customize the script's behavior when a collision event is detected. \ No newline at end of file diff --git a/Collision Detection/package.json b/Collision Detection/package.json new file mode 100644 index 0000000000..3ed6326469 --- /dev/null +++ b/Collision Detection/package.json @@ -0,0 +1,15 @@ +{ + "name": "Collision Detection", + "version": "2.0", + "description": "Watches for collisions between player-controlled tokens and a specified subset of path objects. When a collision is detected, the script may return the token to its position prior to the move, send a warning message to the token's controller, and/or halt the token at the path acting as a barrier to movement.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": {}, + "modifies": { + "token": "write", + "path": "read" + }, + "conflicts": [ + "none" + ] +} diff --git a/Dynamic Lighting Animation/Dynamic Lighting Animation.js b/Dynamic Lighting Animation/Dynamic Lighting Animation.js new file mode 100644 index 0000000000..5d5d62135f --- /dev/null +++ b/Dynamic Lighting Animation/Dynamic Lighting Animation.js @@ -0,0 +1,145 @@ +/** + * Create snapshot of the Dynamic Lighting layer and transition between them on + * a timer, creating an animation. + * + * `!snapshot frames` will capture the current state of the Dynamic Lighting layer and be ready to animate it for frames/20 seconds. + * `!reset` will clear the animation buffer + * `!run` and `!stop` will predictably start and stop the animation sequence. + */ +var bshields = bshields || {}; +bshields.animation = (function() { + 'use strict'; + + var version = 2.0, + running = false, + commands = { + snapshot: function(args, msg) { + var player = getObj('player', msg.playerid), + pageid = Campaign().get('playerpageid'), + dlPaths = findObjs({ type: 'path', pageid: pageid, layer: 'walls' }), + frames = parseInt(args[0], 10), + pathdata = []; + + if (running) { + sendChat('System', '/w ' + player.get('displayname') + ' You cannot add a frame while the animation is running.'); + return; + } + + _.each(dlPaths, function(path) { + var obj = { + id: path.id, + top: path.get('top'), + left: path.get('left'), + rotation: path.get('rotation'), + width: path.get('width'), + height: path.get('height'), + scaleX: path.get('scaleX'), + scaleY: path.get('scaleY') + }; + pathdata.push(obj); + }); + + state.bshields.animation.frames.push({ data: pathdata, frames: frames }); + }, + reset: function(args, msg) { + state.bshields.animation.currentFrame = 0; + state.bshields.animation.frames = []; + }, + run: function(args, msg) { running = true; }, + stop: function(args, msg) { running = false; } + }; + + function handleInput(msg) { + var isApi = msg.type === 'api', + args = msg.content.trim().splitArgs(), + command, args0, isHelp; + + if (!isGM(msg.playerid)) { return; } + + if (isApi) { + command = args.shift().substring(1).toLowerCase(); + arg0 = args.shift(); + isHelp = arg0.toLowerCase() === 'help' || arg0.toLowerCase() === 'h'; + + if (!isHelp) { + if (arg0 && arg0.length > 0) { + args.unshift(arg0); + } + + if (_.isFunction(commands[command])) { + commands[command](args, msg); + } + } else if (_.isFunction(commands.help)) { + commands.help(command, args, msg); + } + } else if (_.isFunction(commands['msg_' + msg.type])) { + commands['msg_' + msg.type](args, msg); + } + } + + function runAnimationCycle() { + var frame = state.bshields.animation.currentFrame, + frameCount = 0; + + setInterval(function() { + if (!running || !state.bshields.animation.frames[frame]) { return; } + + frameCount++; + if (state.bshields.animation.frames[frame].frames <= frameCount) { + setupFrame(state.bshields.animation.frames[frame].data); + frameCount -= state.bshields.animation.frames[frame].frames; + frame++; + + if (frame === state.bshields.animation.frames.length) frame = 0; + state.bshields.animation.currentFrame = frame; + } + }, 50); + } + + function setupFrame(pathdata) { + _.each(pathdata, function(obj) { + var path = getObj('path', obj.id); + path.set({ + top: obj.top, + left: obj.left, + rotation: obj.rotation, + width: obj.width, + height: obj.height, + scaleX: obj.scaleX, + scaleY: obj.scaleY + }); + }); + } + + function checkInstall() { + if (!state.bshields || + !state.bshields.animation || + !state.bshields.animation.version || + state.bshields.animation.version !== version) { + state.bshields = state.bshields || {}; + state.bshields.animation = { + version: version, + frames: [], + currentFrame: 0 + } + } + } + + function registerEventHandlers() { + on('chat:message', handleInput); + } + + return { + checkInstall: checkInstall, + registerEventHandlers: registerEventHandlers, + run: runAnimationCycle + }; +}()); + +on('ready', function() { + 'use strict'; + + bshields.animation.checkInstall(); + bshields.animation.registerEventHandlers(); + bshields.animation.run(); +}); \ No newline at end of file diff --git a/Dynamic Lighting Animation/Help.txt b/Dynamic Lighting Animation/Help.txt new file mode 100644 index 0000000000..1eb35250f0 --- /dev/null +++ b/Dynamic Lighting Animation/Help.txt @@ -0,0 +1,21 @@ +## Dynamic Lighting Animation + +There is one configuration option available to you: + +* `animation.gmIDs`: Set this to a list of d20userid values; only the specified users will be able to use the commands generated by the script. + +You can find a user's d20userid from either the URL of their profile page, or visit their userpage on the wiki: + +![Profile URL](https://wiki.roll20.net/images/thumb/0/03/Brian_Profile.jpg/120px-Brian_Profile.jpg) ![Userpage](https://wiki.roll20.net/images/thumb/e/e8/Brian_Userpage.jpg/119px-Brian_Userpage.jpg) + +### Commands +* !snapshot _frames_ +* !reset +* !run +* !stop + +You can record animation frames with `!snapshot`: set up the Dynamic Lighting as you like it, then use `!snapshot` along with the number of frames to hold that position. The animation runs as 20fps, so `!snapshot 20` will hold the position for 1s. + +`!reset` clears the animation buffer, and the commands `!run` and `!stop` predictably play or halt the animation. You cannot snapshot new positions while the animation is running. This script only stores a single animation, so you need to clear it before creating another one. + +Each snapshot only looks at the Dynamic Lighting paths on the page that currently has the player bookmark ribbon. \ No newline at end of file diff --git a/Dynamic Lighting Animation/package.json b/Dynamic Lighting Animation/package.json new file mode 100644 index 0000000000..52f5be3f44 --- /dev/null +++ b/Dynamic Lighting Animation/package.json @@ -0,0 +1,18 @@ +{ + "name": "Dynamic Lighting Animation", + "version": "2.0", + "description": "Animates paths on the Dynamic Lighting layer.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": { + "IsGM Auth Module": "1.1", + "splitArgs": "1.0" + }, + "modifies": { + "path": "write", + "message": "write" + }, + "conflicts": [ + "Store Commands" + ] +} diff --git a/Exalted Successes/Exalted Successes.js b/Exalted Successes/Exalted Successes.js new file mode 100644 index 0000000000..ca0ee8684b --- /dev/null +++ b/Exalted Successes/Exalted Successes.js @@ -0,0 +1,66 @@ +/** + * Report number of successes on a roll for Exalted. Simply roll d10s, and the + * successes will be reported automatically. + * + * Does not account for damage rolls (10s don't count double) or Sidereal + * Astrology (change target number) + */ +var bshields = bshields || {}; +bshields.exalted = (function() { + 'use strict'; + + var version = 2.0; + + function handleInput(msg) { + var json = msg.type === 'rollresult' ? JSON.parse(msg.content) : null, + inline = !!msg.inlinerolls, + results = [], + successes = 0, + botches = 0; + + if (json) { + _.each(json.rolls, function(roll) { + if (roll.sides !== 10) { return; } + results.push(roll.results); + }); + } else if (inline) { + _.each(msg.inlinerolls, function(rolldata) { + _.each(rolldata.results.rolls, function(roll) { + if (roll.sides !== 10) { return; } + results.push(roll.results); + }); + }); + } + + _.each(results, function(roll) { + _.each(roll, function(die) { + var value = die['v']; + successes += value >= 7 ? 1 : 0; + successes += value === 10 ? 1 : 0; + botches += value === 1 ? 1 : 0; + }); + }); + + if (successes === 0 && botches > 0) { + bshields.sendChat(msg, botches + ' botch' + (botches > 1 ? 'es' : '')) + } else if (successes === 0) { + bshields.sendChat(msg, 'Failure'); + } else { + bshields.sendChat(msg, successes + ' success' + (successes > 1 ? 'es' : '')); + } + } + + function registerEventHandlers() { + on('chat:message', handleInput); + } + + return { + registerEventHandlers: registerEventHandlers + }; +}()); + +on('ready', function() { + 'use strict'; + + bshields.exalted.registerEventHandlers(); +}); \ No newline at end of file diff --git a/Exalted Successes/Help.txt b/Exalted Successes/Help.txt new file mode 100644 index 0000000000..056cbee518 --- /dev/null +++ b/Exalted Successes/Help.txt @@ -0,0 +1,5 @@ +## Exalted Successes + +Whenever a message is posted to the chat which includes rolling d10s, the script will foolow up by posting the number of successes for a roll in the _Exalted_ game system by White Wolf Publishing. Restuls of 7, 8, and 9 are each 1 success, 10s are 2 successes*, and 1s are botches _if and only if_ there are zero successes. + +\* There are corner cases in the game system where dice with values less than 7 count as successes (or 7, 8, and/or 9 _don't_ count as successes), and damage rolls do not normally count 10s as double. This script does not account for those cases. \ No newline at end of file diff --git a/Exalted Successes/package.json b/Exalted Successes/package.json new file mode 100644 index 0000000000..0868374e01 --- /dev/null +++ b/Exalted Successes/package.json @@ -0,0 +1,16 @@ +{ + "name": "Exalted Successes", + "version": "2.0", + "description": "Reports successes on d10 rolls for use with the Exalted game system.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": { + "Interpreted sendChat": "2.0" + }, + "modifies": { + "message": "write" + }, + "conflicts": [ + "none" + ] +} diff --git a/Flight/Flight.js b/Flight/Flight.js new file mode 100644 index 0000000000..93eac00b1b --- /dev/null +++ b/Flight/Flight.js @@ -0,0 +1,85 @@ +/** + * Set selected tokens flying with !fly height. Clear the flight status markers with !fly. + */ +var bshields = bshields || {}; +bshields.flight = (function() { + 'use strict'; + + var version = 3.0, + commands = { + fly: function(args, msg) { + var selected = msg.selected, + height = parseInt(args[0], 10) || 0; + + if (!selected) { return; } + _.each(selected, function(obj) { + var token = getObj('graphic', obj._id), + wings = '', + digit, markers; + + if (obj._type !== 'graphic' || !token || token.get('subtype') !== 'token') { return; } + token.set('status_fluffy-wing', false); + while (height > 0) { + // Iterate over digits, from ones on up + digit = height / 10; + digit -= Math.floor(digit); + digit = Math.round(digit * 10); + + // Shift height + height = Math.floor(height / 10); + + wings += 'fluffy-wing@' + digit + ','; + } + + if (wings.length > 0) { + wings = wings.substring(0, wings.length - 1); + } + + markers = token.get('statusmarkers'); + if (markers !== '') markers += ','; + markers += wings; + token.set('statusmarkers', markers); + }); + } + }; + + function handleInput(msg) { + var isApi = msg.type === 'api', + args = msg.content.trim().splitArgs(), + command, args0, isHelp; + + if (isApi) { + command = args.shift().substring(1).toLowerCase(); + arg0 = args.shift(); + isHelp = arg0.toLowerCase() === 'help' || arg0.toLowerCase() === 'h'; + + if (!isHelp) { + if (arg0 && arg0.length > 0) { + args.unshift(arg0); + } + + if (_.isFunction(commands[command])) { + commands[command](args, msg); + } + } else if (_.isFunction(commands.help)) { + commands.help(command, args, msg); + } + } else if (_.isFunction(commands['msg_' + msg.type])) { + commands['msg_' + msg.type](args, msg); + } + } + + function registerEventHandlers() { + on('chat:message', handleInput); + } + + return { + registerEventHandlers: registerEventHandlers + }; +}()); + +on('ready', function() { + 'use strict'; + + bshields.flight.registerEventHandlers(); +}); \ No newline at end of file diff --git a/Flight/Help.txt b/Flight/Help.txt new file mode 100644 index 0000000000..46cad3c096 --- /dev/null +++ b/Flight/Help.txt @@ -0,0 +1,7 @@ +## Flight + +Adds copies of the `fluffy-wing` status icon to the selected tokens with numbers indicating how high the token is flying. + +Use `!fly height` (where _height_ is a number) while selecting one or more tokens. If _height_ is 0 or is omitted, the wings will be removed. Any integer number can be used, although any digits of the number which are 0 will show up as wings without a number. + +![Flying High](https://wiki.roll20.net/images/3/3e/Flight_Example.jpg) \ No newline at end of file diff --git a/Flight/package.json b/Flight/package.json new file mode 100644 index 0000000000..ceea8eda00 --- /dev/null +++ b/Flight/package.json @@ -0,0 +1,17 @@ +{ + "name": "Flight", + "version": "3.0", + "description": "Adds 'fluffy-wings' status icon to selected token to represent some hieght.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": { + "splitArgs": "1.0" + }, + "modifies": { + "message": "read", + "token": "write" + }, + "conflicts": [ + "none" + ] +} diff --git a/Flip Tokens/Flip Tokens.js b/Flip Tokens/Flip Tokens.js new file mode 100644 index 0000000000..7c28ebb85d --- /dev/null +++ b/Flip Tokens/Flip Tokens.js @@ -0,0 +1,80 @@ +/** + * If a player or GM uses the `!flip' command, all graphics they have selected + * will flip horizontally. Try creating a macro button for this and making it + * visible to all players! + * + * You can also add any number of parameters which are each either "vertical" or + * "horizontal" in order to flip the selected graphic in the specified direction + * in sequence. + */ +var bshields = bshields || {}; +bshields.flip = (function() { + 'use strict'; + + var version = 2.0, + commands = { + flip: function(args, msg) { + var selected = msg.selected; + + if (!selected) { return; } + + _.each(selected, function(obj) { + var token = getObj('graphic', obj._id); + + if (token) { + if (args.length === 0) { + token.set('fliph', !token.get('fliph')); + } else { + _.each(args, function(arg) { + if (arg.toLowerCase() === 'horizontal') { + token.set('fliph', !token.get('fliph')); + } else if (arg.toLowerCase() === 'vertical') { + token.set('flipv', !token.get('flipv')); + } + }); + } + } + }); + } + }; + + function handleInput(msg) { + var isApi = msg.type === 'api', + args = msg.content.trim().splitArgs(), + command, args0, isHelp; + + if (isApi) { + command = args.shift().substring(1).toLowerCase(); + arg0 = args.shift(); + isHelp = arg0.toLowerCase() === 'help' || arg0.toLowerCase() === 'h'; + + if (!isHelp) { + if (arg0 && arg0.length > 0) { + args.unshift(arg0); + } + + if (_.isFunction(commands[command])) { + commands[command](args, msg); + } + } else if (_.isFunction(commands.help)) { + commands.help(command, args, msg); + } + } else if (_.isFunction(commands['msg_' + msg.type])) { + commands['msg_' + msg.type](args, msg); + } + } + + function registerEventHandlers() { + on('chat:message', handleInput); + } + + return { + registerEventHandlers: registerEventHandlers + }; +}()); + +on('ready', function() { + 'use strict'; + + bshields.flip.registerEventHandlers(); +}); \ No newline at end of file diff --git a/Flip Tokens/Help.txt b/Flip Tokens/Help.txt new file mode 100644 index 0000000000..7452e7a08c --- /dev/null +++ b/Flip Tokens/Help.txt @@ -0,0 +1,5 @@ +## Flip Tokens + +Call `!flip` while selecting one or more graphic objects (tokens, cards), and they will flip horizontally. + +You can add any number of "horizontal" and "vertical" parameters to `!flip` in order to flip in the specified direction multiple times in sequence. \ No newline at end of file diff --git a/Flip Tokens/package.json b/Flip Tokens/package.json new file mode 100644 index 0000000000..3caddce4c3 --- /dev/null +++ b/Flip Tokens/package.json @@ -0,0 +1,17 @@ +{ + "name": "Flip Tokens", + "version": "2.0", + "description": "Flips selected graphics horizontally and/or vertically. Especially useful for games with side-view tokens, and for players who do not have access to the same context menu as GMs.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": { + "splitArgs": "1.0" + }, + "modifies": { + "message": "read", + "token": "write" + }, + "conflicts": [ + "none" + ] +} diff --git a/Interpreted sendChat/Help.txt b/Interpreted sendChat/Help.txt new file mode 100644 index 0000000000..33eaf9fa43 --- /dev/null +++ b/Interpreted sendChat/Help.txt @@ -0,0 +1,3 @@ +## Interpreted sendChat + +Provides a function for other scripts to use to assist in sending messages to the chat. This script is not intended to stand alone. \ No newline at end of file diff --git a/Interpreted sendChat/Interpreted sendChat.js b/Interpreted sendChat/Interpreted sendChat.js new file mode 100644 index 0000000000..97354bcdfe --- /dev/null +++ b/Interpreted sendChat/Interpreted sendChat.js @@ -0,0 +1,34 @@ +/** + * Sends a message to the chat as the same person who triggered a chat:message + * event. In other words, if you're speaking out of character, the message will + * be sent as you, and if you're speaking in-character, the message will be sent + * as the character you have selected. + * + * Useful for sending messages on behalf of the player/character in response to + * an API command. + * + * Example: + +on('chat:message', function(msg) { + if (msg.type === 'api') { + bshields.sendChat(msg, 'Hello World'); + } +}); + + */ +var bshields = bshields || {}; +bshields.sendChat = (function() { + 'use strict'; + + var version = 2.0; + + function interpretedSendChat(chatMsg, message) { + var who = chatMsg.who, + speaking = _.sortBy(filterObjs(function(obj) { return obj.get('type') === 'character' && obj.get('name').indexOf(who) >= 0; }), + function(chr) { return Math.abs(chr.get('name').toLowerCase().levenshteinDistance(who.toLowerCase()); })[0]; + + sendChat(speaking ? 'character|' + speaking.id : 'player|' + chatMsg.playerid, message); + } + + return interpretedSendChat; +}()); \ No newline at end of file diff --git a/Interpreted sendChat/package.json b/Interpreted sendChat/package.json new file mode 100644 index 0000000000..202c7e4817 --- /dev/null +++ b/Interpreted sendChat/package.json @@ -0,0 +1,16 @@ +{ + "name": "Interpreted sendChat", + "version": "2.0", + "description": "Provides a function for other scripts to use to assist in sending messages to the chat. This script is not intended to stand alone.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": { + "levenshteinDistance": "1.0" + }, + "modifies": { + "message": "write" + }, + "conflicts": [ + "none" + ] +} diff --git a/No Token Rotation/Help.txt b/No Token Rotation/Help.txt new file mode 100644 index 0000000000..8f89b48954 --- /dev/null +++ b/No Token Rotation/Help.txt @@ -0,0 +1,3 @@ +## No Token Rotation + +Prevents tokens from being rotated by anyone (including the GM). Any tokens which were already rotated will remain so. \ No newline at end of file diff --git a/No Token Rotation/No Token Rotation.js b/No Token Rotation/No Token Rotation.js new file mode 100644 index 0000000000..3233ea2641 --- /dev/null +++ b/No Token Rotation/No Token Rotation.js @@ -0,0 +1 @@ +on('change:graphic:rotation', function(obj, prev) { obj.set('rotation', prev.rotation); }); \ No newline at end of file diff --git a/No Token Rotation/package.json b/No Token Rotation/package.json new file mode 100644 index 0000000000..6a81f133d3 --- /dev/null +++ b/No Token Rotation/package.json @@ -0,0 +1,14 @@ +{ + "name": "No Token Rotation", + "version": "1.1", + "description": "Prevents tokens from being rotated by anyone.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": {}, + "modifies": { + "token": "write" + }, + "conflicts": [ + "none" + ] +} diff --git a/Raise Count/Help.txt b/Raise Count/Help.txt new file mode 100644 index 0000000000..15819318b7 --- /dev/null +++ b/Raise Count/Help.txt @@ -0,0 +1,13 @@ +## Raise Count + +Counts the "raises" of a die roll for the _Savage Worlds_ game system. + +Use `!rc roll target` to roll. _roll_ should be a dice expression (do **not** include `/r`, `/roll`, or inline roll brackets `[[]]`), white _target_ should be the target number of the roll. + +### Output Format + +You can change the formatting of the script's output message by altering the `config.outputFormat` string. In the string, `{0}` will end up as an inline roll, `{1}` will be _target_, and `{2}` will be the number of raises that resulted from the roll. + +### Raise Size + +You can chage the size of raises by modifying `config.raiseSize`. \ No newline at end of file diff --git a/Raise Count/Raise Count.js b/Raise Count/Raise Count.js new file mode 100644 index 0000000000..ea6e627e60 --- /dev/null +++ b/Raise Count/Raise Count.js @@ -0,0 +1,99 @@ +/** + * Counts raises for Savage Worlds. Use !rc roll-expression target-number + * + * Customize output with config.outputFormat, and customize raise size with config.raiseSize + */ +var bshields = bshields || {}; +bshields.raiseCount = (function() { + 'use strict'; + + var version = 2.0, + config = { + raiseSize: 4, + outputFormat: 'Roll: {0}, Target: {1}, Raises: {2}' + }, + commands = { + rc: function(args, msg) { + var target = parseInt(args.pop(), 10), + roll = args.join(' '); + + sendChat('', '[[' + roll + ']]', function(ops) { + var expression = ops[0].inlinerolls[1].expression, + total = ops[0].inlinerolls[1].results.total, + raises = Math.floor((total - target) / config.raiseSize), + rollOut = ''; + rollOut += value + '+'; + }); + rollOut = rollOut.substring(0, rollOut.length - 1) + ')+'; + }); + + rollOut = rollOut.substr(0, rollOut.length - 1); + rollOut += '" class="a inlinerollresult showtip tipsy-n'; + rollOut += (crit && fail ? ' importantroll' : (crit ? ' fullcrit' : (fail ? ' fullfail' : ''))) + '">' + total + ''; + + bshields.sendChat(msg, '/direct ' + format(config.outputFormat, rollOut, target, raises)); + }); + } + }; + + function format(formatString) { + var args = arguments.slice(1); + _.each(args, function(arg, index) { + formatString = formatString.replace('{' + index + '}', arg); + }); + return formatString; + } + + function handleInput(msg) { + var isApi = msg.type === 'api', + args = msg.content.trim().splitArgs(), + command, args0, isHelp; + + if (isApi) { + command = args.shift().substring(1).toLowerCase(); + arg0 = args.shift(); + isHelp = arg0.toLowerCase() === 'help' || arg0.toLowerCase() === 'h'; + + if (!isHelp) { + if (arg0 && arg0.length > 0) { + args.unshift(arg0); + } + + if (_.isFunction(commands[command])) { + commands[command](args, msg); + } + } else if (_.isFunction(commands.help)) { + commands.help(command, args, msg); + } + } else if (_.isFunction(commands['msg_' + msg.type])) { + commands['msg_' + msg.type](args, msg); + } + } + + function registerEventHandlers() { + on('chat:message', handleInput); + } + + return { + registerEventHandlers: registerEventHandlers + }; +}()); + +on('ready', function() { + 'use strict'; + + bshields.raiseCount.registerEventHandlers(); +}); \ No newline at end of file diff --git a/Raise Count/package.json b/Raise Count/package.json new file mode 100644 index 0000000000..2af347f6a3 --- /dev/null +++ b/Raise Count/package.json @@ -0,0 +1,17 @@ +{ + "name": "Raise Count", + "version": "2.0", + "description": "Counts raises for the Savage Worlds system.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": { + "splitArgs": "1.0", + "Interpreted sendChat": "2.0" + }, + "modifies": { + "message": "write" + }, + "conflicts": [ + "none" + ] +} diff --git a/Store Commands/Help.txt b/Store Commands/Help.txt new file mode 100644 index 0000000000..742b8e4d17 --- /dev/null +++ b/Store Commands/Help.txt @@ -0,0 +1,11 @@ +## Store Commands + +Use `!delay time` (where _time_ is some number of milliseconds) to set the default delay between commands. If `!delay` is not used, nor is the _time_ parameter passed to `!store`, the time delay will be 500ms (0.5s). + +Use `!store -time command` or `!store command` to store a command. The command stored can be anything, including commands that are normally GM-only such as /direct or /emas. This will not give you access to API commands whose scripts restrict their use to GMs. + +Use `!clearstore` to clear the series of stored commands. This is the only way to remove commands from the sequence, meaning you can also use this script to store a series of commands you want to use over and over. Note that command sequences do not persist between game sessions, but they are unique per-player. + +Use `!echostore` to see a list of the commands in your serquence. + +Use `!run` to run the commands. \ No newline at end of file diff --git a/Store Commands/Store Commands.js b/Store Commands/Store Commands.js new file mode 100644 index 0000000000..96e35a6324 --- /dev/null +++ b/Store Commands/Store Commands.js @@ -0,0 +1,95 @@ +/** + * Stores a series of commands to be used later, or repeated later. + * + * !delay time :: sets the default delay in milliseconds between commands + * !store [-time] command :: stores a command, with an optional time overriding the default + * !clearstore :: clears the stored commands + * !echostore :: echoes the stored commands to you + * !run :: runs the series of stored commands + */ +var bshields = bshields || {}; +bshields.storeCommands = (function() { + 'use strict'; + + var version = 2.0, + list = {}, + commands = { + delay: function(args, msg) { + list[msg.playerid].delay = parseInt(args[0], 10); + }, + clearstore: function(args, msg) { + list[msg.playerid].cmds = []; + }, + store: function(args, msg) { + var delay = list[msg.playerid].delay || 500, + obj; + + if (args[0] && args[0].indexOf('-') === 0) { + delay = parseInt(args.shift().substring(1), 10); + } + + obj = { text: args.join(' '), delay: delay }; + list[msg.playerid].cmds.push(obj); + }, + echostore: function(args, msg) { + _.each(list[msg.playerid].cmds, function(cmd) { + sendChat('System', '/w ' + msg.who + ' {' + cmd.delay + 'ms, ' + cmd.text + '}'); + }); + }, + run: function(args, msg) { + var count = 0; + _.each(list[msg.playerid].cmds, function(cmd) { + echo(msg.playerid, cmd.text, count + cmd.delay); + count += cmd.delay; + }) + } + }; + + function echo(id, text, delay) { + setTimeout(function(){ sendChat('player|'+id, text); }, delay); + } + + function handleInput(msg) { + var isApi = msg.type === 'api', + args = msg.content.trim().splitArgs(), + command, args0, isHelp; + + if (!list[msg.playerid]) { + list[msg.playerid] = { cmds: [], delay: 500 }; + } + + if (isApi) { + command = args.shift().substring(1).toLowerCase(); + arg0 = args.shift(); + isHelp = arg0.toLowerCase() === 'help' || arg0.toLowerCase() === 'h'; + + if (!isHelp) { + if (arg0 && arg0.length > 0) { + args.unshift(arg0); + } + + if (_.isFunction(commands[command])) { + commands[command](args, msg); + } + } else if (_.isFunction(commands.help)) { + commands.help(command, args, msg); + } + } else if (_.isFunction(commands['msg_' + msg.type])) { + commands['msg_' + msg.type](args, msg); + } + } + + function registerEventHandlers() { + on('chat:message', handleInput); + } + + return { + registerEventHandlers: registerEventHandlers + } +}()); + +on('ready', function() { + 'use strict'; + + bshields.storeCommands.registerEventHandlers(); +}); \ No newline at end of file diff --git a/Store Commands/package.json b/Store Commands/package.json new file mode 100644 index 0000000000..ad7d86c3b2 --- /dev/null +++ b/Store Commands/package.json @@ -0,0 +1,16 @@ +{ + "name": "Store Commands", + "version": "2.0", + "description": "Stores a series of commands (potentially with delays between them) to be executed in sequence later. Opens up GM-only commands such as /direct and /emas to players.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": { + "splitArgs": "1.0" + }, + "modifies": { + "message": "write" + }, + "conflicts": [ + "Dynamic Lighting Animation" + ] +} diff --git a/levenshteinDistance/Help.txt b/levenshteinDistance/Help.txt new file mode 100644 index 0000000000..2fb394ac0e --- /dev/null +++ b/levenshteinDistance/Help.txt @@ -0,0 +1,3 @@ +## levenshteinDistance + +Provides a levenshteinDistance function for comparing strings. This script is not intended to stand alone. \ No newline at end of file diff --git a/levenshteinDistance/levenshteinDistance.js b/levenshteinDistance/levenshteinDistance.js new file mode 100644 index 0000000000..4804d3e1c1 --- /dev/null +++ b/levenshteinDistance/levenshteinDistance.js @@ -0,0 +1,56 @@ +/** + * Compares two strings and returns the number of changes (substitutions, + * insertions, and deletions) required to move from the first string to the + * second. + * + * As a convenience, this function has been added to the String prototype + */ +var bshields = bshields || {}; +bshields.levenshteinDistance = (function() { + 'use strict'; + + var version = 1.0; + + function levenshteinDistance(a, b) { + var i, j, + matrix = []; + + if (a.length === 0) { + return b.length; + } + if (b.length === 0) { + return a.length; + } + + // Increment along the first column of each row + for (i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + + // Increment each column in the first row + for (j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + + // Fill in the rest of the matrix + for (i = 1; i <= b.length; i++) { + for (j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min(matrix[i - 1][j - 1] + 1, // Substitution + Math.min(matrix[i][j - 1] + 1, // Insertion + matrix[i - 1][j] + 1)); // Deletion + } + } + } + + return matrix[b.length][a.length]; + } + + return levenshteinDistance; +}()); + +String.prototype.levenshteinDistance = String.prototype.levenshteinDistance || function(b) { + return bshields.levenshteinDistance(this, b); +}; \ No newline at end of file diff --git a/levenshteinDistance/package.json b/levenshteinDistance/package.json new file mode 100644 index 0000000000..70fc0698c0 --- /dev/null +++ b/levenshteinDistance/package.json @@ -0,0 +1,12 @@ +{ + "name": "levenshteinDistance", + "version": "1.0", + "description": "Provides a levenshteinDistance function for comparing strings. This script is not intended to stand alone.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": {}, + "modifies": {}, + "conflicts": [ + "none" + ] +} diff --git a/sendChat as a Player or Character/Help.txt b/sendChat as a Player or Character/Help.txt deleted file mode 100644 index d8887a7345..0000000000 --- a/sendChat as a Player or Character/Help.txt +++ /dev/null @@ -1,2 +0,0 @@ -sendChat as a Player or Character (Contributed by Brian Shields) -This is a few lines you can use in a chat:message event script to ensure you're sending a message with sendChat accurately as either a Player or a Character, depending on who triggered the event. \ No newline at end of file diff --git a/sendChat as a Player or Character/package.json b/sendChat as a Player or Character/package.json deleted file mode 100644 index f4350c75db..0000000000 --- a/sendChat as a Player or Character/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "sendChat as a Player or Character", - "version": "1.1", - "description": "This is a few lines you can use in a chat:message event script to ensure you're sending a message with sendChat accurately as either a Player or a Character, depending on who triggered the event.", - "authors": "Brian Shields", - "roll20userid": "235259", - "dependencies": {}, - "modifies": { - "message": "read" - }, - "conflicts": [ - "none" - ] -} diff --git a/sendChat as a Player or Character/sendChat as a Player or Character.js b/sendChat as a Player or Character/sendChat as a Player or Character.js deleted file mode 100644 index 827847028e..0000000000 --- a/sendChat as a Player or Character/sendChat as a Player or Character.js +++ /dev/null @@ -1,11 +0,0 @@ -on("chat:message", function(msg) { - var message = ''; - // Determine the contents of `message' - - var characters = findObjs({_type: 'character'}); - var speaking; - characters.forEach(function(chr) { if(chr.get('name') == msg.who) speaking = chr; }); - - if(speaking) sendChat('character|'+speaking.id, message); - else sendChat('player|'+msg.playerid, message); -}); \ No newline at end of file diff --git a/splitArgs/Help.txt b/splitArgs/Help.txt new file mode 100644 index 0000000000..2401506a39 --- /dev/null +++ b/splitArgs/Help.txt @@ -0,0 +1,3 @@ +## splitArgs + +Provides a function for splitting arguments of an API command. This script is not intended to stand alone. \ No newline at end of file diff --git a/splitArgs/package.json b/splitArgs/package.json new file mode 100644 index 0000000000..1444e89988 --- /dev/null +++ b/splitArgs/package.json @@ -0,0 +1,12 @@ +{ + "name": "splitArgs", + "version": "1.0", + "description": "Provides a function for splitting arguments of an API command. This script is not intended to stand alone.", + "authors": "Brian Shields", + "roll20userid": "235259", + "dependencies": {}, + "modifies": {}, + "conflicts": [ + "none" + ] +} diff --git a/splitArgs/splitArgs.js b/splitArgs/splitArgs.js new file mode 100644 index 0000000000..a3b75fb94c --- /dev/null +++ b/splitArgs/splitArgs.js @@ -0,0 +1,78 @@ +/** + * Splits a string into arguments using some separator. If no separator is + * given, whitespace will be used. Most importantly, quotes in the original + * string will allow you to group delimited tokens. Single and double quotes + * can be nested one level. + * + * As a convenience, this function has been added to the String prototype, + * letting you treat it like a function of the string object. + * + * Example: + +on('chat:message', function(msg) { + var command, params; + + params = msg.content.splitArgs(); + command = params.shift().substring(1); + + // msg.content: !command with parameters, "including 'with quotes'" + // command: command + // params: ["with", "parameters,", "including 'with quotes'"] +}); + */ +var bshields = bshields || {}; +bshields.splitArgs = (function() { + 'use strict'; + + var version = 1.0; + + function splitArgs(input, separator) { + var singleQuoteOpen = false, + doubleQuoteOpen = false, + tokenBuffer = [], + ret = [], + arr = input.split(''), + element, i, matches; + separator = separator || /\s/g; + + for (i = 0; i < arr.length; i++) { + element = arr[i]; + matches = element.match(separator); + if (element === '\'') { + if (!doubleQuoteOpen) { + singleQuoteOpen = !singleQuoteOpen; + continue; + } + } else if (element === '"') { + if (!singleQuoteOpen) { + doubleQuoteOpen = !doubleQuoteOpen; + continue; + } + } + + if (!singleQuoteOpen && !doubleQuoteOpen) { + if (matches) { + if (tokenBuffer && tokenBuffer.length > 0) { + ret.push(tokenBuffer.join('')); + tokenBuffer = []; + } + } else { + tokenBuffer.push(element); + } + } else if (singleQuoteOpen || doubleQuoteOpen) { + tokenBuffer.push(element); + } + } + if (tokenBuffer && tokenBuffer.length > 0) { + ret.push(tokenBuffer.join('')); + } + + return ret; + } + + return splitArgs; +}()); + +String.prototype.splitArgs = String.prototype.splitArgs || function(separator) { + return bshields.splitArgs(this, separator); +}; \ No newline at end of file