Skip to content

Commit 0f10fc8

Browse files
committed
Custom FX release
1 parent 31b1768 commit 0f10fc8

File tree

3 files changed

+390
-0
lines changed

3 files changed

+390
-0
lines changed

Custom FX/0.1/Custom FX.js

+330
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,330 @@
1+
var bshields = bshields || {};
2+
bshields.customfx = (function() {
3+
'use strict';
4+
5+
var version = 0.1,
6+
definitionKeyOrder = ['angle', 'angleRandom', 'duration', 'emissionRate', 'endColour', 'endColourRandom', 'gravity', 'lifeSpan',
7+
'lifeSpanRandom', 'maxParticles', 'size', 'sizeRandom', 'speed', 'speedRandom', 'startColour', 'startColourRandom'],
8+
defaultDefinition = {
9+
maxParticles: 100,
10+
emissionRate: 3,
11+
size: 35,
12+
sizeRandom: 15,
13+
lifeSpan: 10,
14+
lifeSpanRandom: 3,
15+
speed: 3,
16+
speedRandom: 1.5,
17+
gravity: {x: 0.01, y: 0.01},
18+
angle: 0,
19+
angleRandom: 180,
20+
duration: -1,
21+
startColour: [220, 35, 0, 1],
22+
startColourRandom: [62, 0, 0, 0.25],
23+
endColour: [220, 35, 0, 0],
24+
endColourRandom:[60, 60, 60, 0]
25+
},
26+
commands = {
27+
createfx: function(args, msg) {
28+
var name = (args.shift() || '').toLowerCase(),
29+
fxDefinition = argsToFxDefinition(args);
30+
31+
if (name.length === 0) {
32+
error('noname', msg);
33+
return;
34+
}
35+
36+
createObj('custfx', { name: name, definition: _.defaults(fxDefinition, defaultDefinition) });
37+
},
38+
previewfx: function(args, msg) {
39+
var fxDefinition = argsToFxDefinition(args),
40+
pos = { x: cma(), y: cma() },
41+
page;
42+
43+
if (state.bshields.customfx.previewDefinition) {
44+
state.bshields.customfx.previewDefinition = _.defaults(fxDefinition, state.bshields.customfx.previewDefinition);
45+
} else {
46+
state.bshields.customfx.previewDefinition = _.defaults(fxDefinition, defaultDefinition);
47+
}
48+
49+
if (previewIntervalId > 0) {
50+
clearInterval(previewIntervalId);
51+
}
52+
53+
if (msg.selected) {
54+
pos = _.reduce(msg.selected, function(memo, s) {
55+
var obj = getObj(s._type, s._id),
56+
x = obj.get('left'),
57+
y = obj.get('top');
58+
59+
page = page ? page : getObj('page', obj.get('pageid'));
60+
memo.x.push(x);
61+
memo.y.push(y);
62+
return memo;
63+
}, pos);
64+
} else {
65+
page = getObj('page', Campaign().get('playerpageid'));
66+
pos.x.push(page.get('width') * 70);
67+
pos.x.push(0);
68+
pos.y.push(page.get('height') * 70);
69+
pos.y.push(0);
70+
}
71+
72+
commands.endpreview();
73+
previewIntervalId = setInterval(showPreview(pos, page.id), 1000);
74+
},
75+
savepreview: function(args, msg) {
76+
var name = (args.shift() || '').toLowerCase();
77+
78+
if (name.length === 0) {
79+
error('noname', msg);
80+
return;
81+
}
82+
commands.endpreview();
83+
createObj('custfx', { name: name, definition: state.bshields.customfx.previewDefinition });
84+
delete state.bshields.customfx.previewDefinition;
85+
},
86+
endpreview: function(args, msg) {
87+
if (previewIntervalId) {
88+
clearInterval(previewIntervalId);
89+
} else {
90+
return;
91+
}
92+
if (previewIntervalId._idleStart > 0) {
93+
clearInterval(previewIntervalId._idleStart);
94+
}
95+
previewIntervalId = 0;
96+
},
97+
help: function(command, args, msg) {
98+
if (_.isFunction(commands['help_' + command])) {
99+
commands['help_' + command](args, msg);
100+
}
101+
},
102+
help_: function(args, msg) {
103+
var name = getObj('player', msg.playerid).get('displayname');
104+
105+
sendChat('Custom FX.js', '/w "' + name + '" ' + helpFormat('Help: Custom FX', '<p>The Custom FX script facilitates the creation of new FX '
106+
+ 'objects for use by GMs from the left toolbar, and by players with the /fx command. Four API commands are exposed by this script (click '
107+
+ 'on a command for more information):<ul style="list-style:none;margin-left:0"><li>' + helpAPICommand('!help createfx', '!createfx')
108+
+ ' will create new FX objects directly.</li><li>' + helpAPICommand('!help previewfx', '!previewfx') + ' will let you preview changes to '
109+
+ 'an effect in real time before saving it.</li><li>' + helpAPICommand('!help savepreview', '!savepreview') + ' will save an effect '
110+
+ 'created with <strong>!previewfx</strong> as an FX object.</li><li>' + helpAPICommand('!help endpreview', '!endpreview') + ' will halt '
111+
+ 'the looping preview started by <strong>!previewfx</strong>, but it will not erase the properties saved for the preview.</li></ul></p>',
112+
'version ' + version));
113+
},
114+
help_createfx: function(args, msg) {
115+
var name = getObj('player', msg.playerid).get('displayname');
116+
117+
sendChat('Custom FX.js', '/w "' + name + '" ' + helpFormat('Help: !createfx', '<p>Creates a new FX object. <strong>name</strong> will be '
118+
+ 'used to identify the new FX in the GM\'s FX menu, and it will be used by all players in the campaign for the /fx command. '
119+
+ '<strong>name</strong> is required.</p><p><strong>properties</strong> is a list of FX properties. Each property may be labeled or not. '
120+
+ 'Labeled properties take the form <strong>propertyName:propertyValue</strong> (property names are case-sentitive). Unlabeled properties '
121+
+ 'will be assigned in order: angle, angleRandom, duration, emissionRate, endColour, endColourRandom, gravity, lifeSpan, lifeSpanRandom, '
122+
+ 'maxParticles, size, sizeRandom, speed, speedRandom, startColour, and startColourRandom.</p><p>Properties representing colors '
123+
+ '(startColour, endColour, startColourRandom, endColourRandom) should be arrays with four elements: <strong>[red, green, blue, '
124+
+ 'alpha]</strong> (spaces optional). Red, green, and blue values should be an integer in the range 0-255. Alpha values should be a '
125+
+ 'decimal number in the range 0-1.</p><p>The gravity property must be specified as an object in the form <strong>{ x: <em>number</em>, '
126+
+ 'y: <em>number</em> }</strong> (x and y order do not matter, spaces optional).</p><p>Any property value containing spaces must be '
127+
+ 'enclosed in quotes. !createfx example gravity:"{ x: 5, y: 6 }", !createfx example "gravity:{ x: 5, y: 6 }", and !createfx example '
128+
+ 'gravity:{x:5,y:6} are all valid.</p>', '!createfx &lt;name&gt; [properties]'));
129+
},
130+
help_previewfx: function(args, msg) {
131+
var name = getObj('player', msg.playerid).get('displayname');
132+
133+
sendChat('Custom FX.js', '/w "' + name + '" ' + helpFormat('Help: !previewfx', '<p>Creates a preview of an FX object. '
134+
+ '<strong>properties</strong> is a list of FX properties; see ' + helpAPICommand('!help createfx', '!createfx') + ' for details on their '
135+
+ 'syntax.</p><p>When you call !previewfx, an effect with the specified properties (filling in any unspecified properties with default '
136+
+ 'values) will spawn at the location of your selected object on the tabletop, or the average of all selected objects if you have '
137+
+ 'multiple selected, or the center of the current map if nothing is selected. The effect will repeat on an endless loop until you call '
138+
+ helpAPICommand('!help savepreview', '!savepreview') + ' or ' + helpAPICommand('!help endpreview', '!endpreview') + ', or the API sandbox '
139+
+ 'restarts.</p><p>Each subsequent time you call !previewfx, the FX preview will update its properties, overwriting its existing ones '
140+
+ 'with the properties you supply in the newest call to the command. Using labeled properties, this makes it easy to incrementally build '
141+
+ 'the effect you desire, as you can set one property at a time, and see the changes live.</p><p>This command is extra stressful on the '
142+
+ 'network connection, so it is not recommended for use while other players are in the game. The properties you\'ve set with this command '
143+
+ 'will persist between sessions, so long as you do not call <strong>!savepreview</strong>.</p>', '!previewfx [properties]'));
144+
},
145+
help_savepreview: function(args, msg) {
146+
var name = getObj('player', msg.playerid).get('displayname');
147+
148+
sendChat('Custom FX.js', '/w "' + name + '" ' + helpFormat('Help: !savepreview', '<p>Saves the preview FX as an FX object. After settling on '
149+
+ 'an effect you like using ' + helpAPICommand('!help previewfx', '!previewfx') + ', you can save it, giving it a name, using this '
150+
+ 'command. This will make the effect available from the FX menu for GMs, and it will be available to all players via the /fx command.</p>',
151+
'!savepreview &lt;name&gt;'));
152+
},
153+
help_endpreview: function(args, msg) {
154+
var name = getObj('player', msg.playerid).get('displayname');
155+
156+
sendChat('Custom FX.js', '/w "' + name + '" ' + helpFormat('Help: !endpreview', '<p>Ends the looping animation started by '
157+
+ helpAPICommand('!help previewfx', '!previewfx') + '. This will not erase the properties set to the preview FX object, it simply stops '
158+
+ 'displaying the preview.</p>', '!endpreview'));
159+
}
160+
},
161+
errors = {
162+
noname: 'You must give a name to your FX.'
163+
},
164+
previewIntervalId;
165+
166+
function showPreview(pos, pageid) {
167+
return function() {
168+
spawnFxWithDefinition(pos.x.value, pos.y.value, state.bshields.customfx.previewDefinition, pageid)
169+
};
170+
}
171+
172+
function argsToFxDefinition(args) {
173+
var propertyTypes = _.partition(args, function(a) {
174+
return (a.indexOf(':') >= 0 && !/\{\s*x\s*:\s*[0-9]*\.?[0-9]+\s*,\s*y\s*:\s*[0-9]*\.?[0-9]+\s*\}/i.test(a)
175+
&& !/\{\s*y\s*:\s*[0-9]*\.?[0-9]+\s*,\s*x\s*:\s*[0-9]*\.?[0-9]+\s*\}/i.test(a)) ||
176+
/[a-z]+:\{.*\}/i.test(a);
177+
}),
178+
unnamedProperties = propertyTypes[1],
179+
namedProperties = propertyTypes[0],
180+
fxDefinition = _.reduce(unnamedProperties, function(memo, val, idx) { return (memo[definitionKeyOrder[idx]] = val,memo); }, {});
181+
182+
_.each(namedProperties, function(prop) {
183+
var propName = prop.substring(0, prop.indexOf(':')).trim(),
184+
propVal = prop.substring(prop.indexOf(':') + 1);
185+
186+
fxDefinition[propName] = propVal;
187+
});
188+
189+
_.each(fxDefinition, function(val, key) {
190+
var asJSON = tryParseJSON(val),
191+
isObj = /\{\s*(?:[a-z]+\s*:\s*\S+)?(\s*,\s*[a-z]+\s*:\s*\S+)*\s*\}/i.test(val),
192+
propList;
193+
194+
if (asJSON || asJSON === 0 || val === 'false' || val === 'null') {
195+
fxDefinition[key] = asJSON;
196+
return;
197+
}
198+
if (isObj) {
199+
fxDefinition[key] = {};
200+
val = val.substring(1, val.length - 1).trim();
201+
propList = val.splitArgs(',');
202+
_.each(propList, function(prop) {
203+
var propName = prop.substring(0, prop.indexOf(':')).trim(),
204+
propVal = prop.substring(prop.indexOf(':') + 1);
205+
fxDefinition[key][propName] = tryParseJSON(propVal);
206+
});
207+
}
208+
});
209+
210+
return fxDefinition;
211+
}
212+
213+
function error(errorKey, msg) {
214+
var name = getObj('player', msg.playerid).get('displayname');
215+
216+
sendChat('Custom FX.js', '/w "' + name + '" ' + errors[errorKey]);
217+
}
218+
219+
function tryParseJSON(str) {
220+
var parsed;
221+
222+
try {
223+
parsed = JSON.parse(str);
224+
} catch (e) {
225+
return false;
226+
}
227+
228+
return parsed;
229+
}
230+
231+
function helpAPICommand(command, text) {
232+
return '<strong><a href="' + command + '" style="padding:0;background:transparent;color:#ce0f69;border:none">' + text + '</a></strong>';
233+
}
234+
235+
function helpBody(text) {
236+
return '<div style="padding:5px">' + text + '</div>';
237+
}
238+
239+
function helpSyntax(text) {
240+
if (!text) return '';
241+
return '<div style="font-family:consolas;padding:5px;background-color:#f8e8dd">' + text + '</div>';
242+
}
243+
244+
function helpTitle(text) {
245+
return '<h3 style="background-color:purple;border-top-left-radius:4px;border-top-right-radius:4px;padding:5px;color:white">' + text + '</h3>';
246+
}
247+
248+
function helpFormat(title, body, syntax) {
249+
return '<div style="border-radius:4px;border:1px solid purple;background-color:white">'
250+
+ helpTitle(title) + helpSyntax(syntax) + helpBody(body) + '</div>';
251+
}
252+
253+
function handleInput(msg) {
254+
var isApi = msg.type === 'api',
255+
args = msg.content.trim().splitArgs(),
256+
command, arg0, isHelp;
257+
258+
if (!playerIsGM(msg.playerid)) return;
259+
260+
if (isApi) {
261+
command = args.shift().substring(1).toLowerCase();
262+
arg0 = args.shift() || '';
263+
isHelp = arg0.toLowerCase() === 'help' || arg0.toLowerCase() === 'h' || command === 'help';
264+
265+
if (!isHelp) {
266+
if (arg0) {
267+
args.unshift(arg0);
268+
}
269+
270+
if (_.isFunction(commands[command])) {
271+
commands[command](args, msg);
272+
}
273+
} else if (_.isFunction(commands.help)) {
274+
commands.help(command === 'help' ? arg0 : command, args, msg);
275+
}
276+
} else if (_.isFunction(commands['msg_' + msg.type])) {
277+
commands['msg_' + msg.type](args, msg);
278+
}
279+
}
280+
281+
function checkInstall() {
282+
if (!state.bshields ||
283+
!state.bshields.customfx ||
284+
!state.bshields.customfx.version ||
285+
state.bshields.customfx.version !== version) {
286+
state.bshields = state.bshields || {};
287+
state.bshields.customfx = {
288+
version: version,
289+
gcUpdated: 0,
290+
}
291+
}
292+
checkGlobalConfig();
293+
}
294+
295+
function checkGlobalConfig() {
296+
var gc = globalconfig && globalconfig.customfx,
297+
st = state.bshields.customfx;
298+
299+
if (gc && gc.lastsaved && gc.lastsaved > st.gcUpdated) {
300+
st.gcUpdated = gc.lastsaved;
301+
}
302+
}
303+
304+
function registerEventHandlers() {
305+
on('chat:message', handleInput);
306+
}
307+
308+
function cma() {
309+
var avg = 0,
310+
n = 0;
311+
312+
return {
313+
get value() { return avg; },
314+
get length() { return n; },
315+
push: function(num) { return (avg = avg + (num - avg) / ++n); }
316+
};
317+
}
318+
319+
return {
320+
checkInstall: checkInstall,
321+
registerEventHandlers: registerEventHandlers
322+
};
323+
}());
324+
325+
on('ready', function() {
326+
'use strict';
327+
328+
bshields.customfx.checkInstall();
329+
bshields.customfx.registerEventHandlers();
330+
});

Custom FX/README.md

+34
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
## Custom FX
2+
3+
### Commands
4+
* !createfx <_name_> [_properties_]
5+
* !previewfx [_properties_]
6+
* !savepreview <_name_>
7+
* !endpreview
8+
9+
`!createfx` will create an FX object directly. You must supply a name. All properties are optional (default values will be used for any not specified), and may be labeled or not. Labeled properties take the form *propertyName:propertyValue*, and the names are case-sentitive. Unlabeled properties will be consumed in order:
10+
11+
* angle
12+
* angleRandom
13+
* duration
14+
* emissionRate
15+
* endColour
16+
* endColourRandom
17+
* gravity
18+
* lifeSpan
19+
* lifeSpanRandom
20+
* maxParticles
21+
* size
22+
* sizeRandom
23+
* speed
24+
* speedRandom
25+
* startColour
26+
* startColourRandom
27+
28+
The four color properties must be specified as an arracy of four values: `[red, green, blue, alpha]`. The first three should each be an integer in the range 0-255, and the last should be a number in the range 0-1.
29+
30+
`gravity` must be specified as an object in the form `{ x: num, y: num }`. The spaces are optional, and the x/y can swap their order.
31+
32+
Any properties with spaces must be enclosed in quotes. `gravity:"{ x: 5, y: 6}"`, `"gravity:{ x: 5, y: 6 }"`, and `gravity:{x:5,y:6}` are all valid.
33+
34+
`!previewfx` takes the same property parameters as `!createfx`, but instead of immediately creating the FX object, it spawns a looping FX animation on the map, which you can change with additional calls to `!previewfx`. You can stop the animation with `!endpreview` (however, your saved properties will not be erased), and you can save the FX with `!savepreview`, which will erase your preview properties and create an FX object with the same values.

Custom FX/script.json

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"name": "Custom FX",
3+
"script": "Custom FX.js",
4+
"version": "0.1",
5+
"previousversions": [],
6+
"description": "Enables the creation and previewing of FX objects. `!createfx` will create an FX object directly, while `!previewfx` will allow you to build an FX object incrementally, seeing the changes to the effect live. Use `!savepreview` to save the FX being built, and `!endpreview` to stop displaying it. (Ending the preview will not erase the saved properties for the preview object, however.) See `!help` for more detailed information.",
7+
"authors": "Brian Shields",
8+
"roll20userid": "235259",
9+
"patreon": "https://www.patreon.com/bshields",
10+
"useroptions": [],
11+
"dependencies": ["splitArgs"],
12+
"modifies": {
13+
"Campaign.playerpageid": "read",
14+
"graphic.left": "read",
15+
"graphic.top": "read",
16+
"path.top": "read,write",
17+
"path.left": "read,write",
18+
"path.rotation": "read,write",
19+
"path.width": "read,write",
20+
"path.height": "read,write",
21+
"path.scaleX": "read,write",
22+
"path.scaleY": "read,write",
23+
"state.bshields.customfx": "read,write"
24+
},
25+
"conflicts": []
26+
}

0 commit comments

Comments
 (0)