Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f873b09
Add Collision Detection script & package
Lithl Jan 7, 2015
dc170e0
Begin collision detection help file
Lithl Jan 7, 2015
c483e83
Add dynamic lighting animation script
Lithl Jan 7, 2015
4dc7e53
Create dynamic lighting animation package
Lithl Jan 7, 2015
99e0ccf
Completed Dynamic Lighting Animation help file
Lithl Jan 8, 2015
bdc0e92
Completed Dynamic Lighting Animation package file
Lithl Jan 8, 2015
ffaae6f
Added Exalted Successes
Lithl Jan 8, 2015
7bfe623
Added Flight
Lithl Jan 8, 2015
a648ddb
Added Flip Tokens
Lithl Jan 8, 2015
f56fd0d
Added No Token Rotation
Lithl Jan 8, 2015
2b3b74f
Added Raise Count
Lithl Jan 8, 2015
2acbcb8
Added Store Commands
Lithl Jan 8, 2015
26d6609
Rename sendChat script, upgrade to v2
Lithl Jan 8, 2015
a49a17e
Add levenshteinDistance function
Lithl Jan 8, 2015
0180e7e
Add splitArgs function
Lithl Jan 8, 2015
67dd13e
Update collision detection to v2
Lithl Jan 8, 2015
ca23736
Update DL animation to v2
Lithl Jan 8, 2015
5181c8b
Update Exalted successes to v2
Lithl Jan 8, 2015
67f9176
Update flight to v3
Lithl Jan 8, 2015
31f548d
Update flip token to v2
Lithl Jan 8, 2015
3357205
Update no rotation to v1.1
Lithl Jan 8, 2015
8eb6f4e
Fixed dependency version
Lithl Jan 8, 2015
6cf9523
Update raise count to v2
Lithl Jan 8, 2015
6ea3cb6
Add a little bit of info to comment at top of file
Lithl Jan 8, 2015
46cdb96
Update store commands to v2
Lithl Jan 8, 2015
e737bac
Add string utility functions to String.prototype
Lithl Jan 8, 2015
17df6e9
Use prototyped versions of splitArgs and levenshteinDistance instead …
Lithl Jan 8, 2015
7886a8c
Fix json
Lithl Jan 8, 2015
b2397a7
DL Animation and Store Commands conflict
Lithl Jan 8, 2015
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
184 changes: 184 additions & 0 deletions Collision Detection/Collision Detection.js
Original file line number Diff line number Diff line change
@@ -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();
}
7 changes: 7 additions & 0 deletions Collision Detection/Help.txt
Original file line number Diff line number Diff line change
@@ -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.
15 changes: 15 additions & 0 deletions Collision Detection/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
145 changes: 145 additions & 0 deletions Dynamic Lighting Animation/Dynamic Lighting Animation.js
Original file line number Diff line number Diff line change
@@ -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();
});
21 changes: 21 additions & 0 deletions Dynamic Lighting Animation/Help.txt
Original file line number Diff line number Diff line change
@@ -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.
18 changes: 18 additions & 0 deletions Dynamic Lighting Animation/package.json
Original file line number Diff line number Diff line change
@@ -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"
]
}
Loading