· 7 years ago · Dec 16, 2018, 02:58 AM
1/**
2 * trackerjacker.js
3 *
4 * * Copyright 2015: Ken L.
5 * Licensed under the GPL Version 3 license.
6 * http://www.gnu.org/licenses/gpl.html
7 *
8 * This script is free software: you can redistribute it and/or modify
9 * it under the terms of the GNU General Public License as published by
10 * the Free Software Foundation, either version 3 of the License, or
11 * (at your option) any later version.
12 *
13 * This script is distributed in the hope that it will be useful,
14 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16 * GNU General Public License for more details.
17 *
18 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
19 * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
20 *
21 * The goal of this script is to be an iniative tracker, that manages statuses,
22 * effects, and durations.
23 *
24 * 1. It should advance the turn order and display a notification in chat with
25 * optional toggles.
26 *
27 * 1.1 It should have the ability to announce rounds
28 *
29 * 2. It should allow some kind of underlay graphic with or without some kind of
30 * underlay graphic like TurnMarker.js
31 *
32 * 3. It should have the ability to tie status conditions to tokens with concise
33 * visual cues to relay to chat (IE fog cloud has X turns remaining on it or has lasted N turns).
34 *
35 * 4. It should be extensible to other scripts by exposing a call structure for
36 * a speedier access of innate functions without cluttering up the message queue. TODO
37 *
38 * 5. It should be verbose in terms of error reporting where all are recoverable.
39 *
40 * 6. It should save turn information within the state object to ensure recovery
41 * of all effects in the event of API connection failure.
42 *
43 * 7. It should be lightweight with a minimal amount of passed messages.
44 *
45 *
46 *
47 *
48 */
49
50var TrackerJacker = (function() {
51 'use strict';
52 var version = 1.081,
53 author = 'Ken L.',
54 pending = null;
55
56 var TJ_StateEnum = Object.freeze({
57 ACTIVE: 0,
58 PAUSED: 1,
59 STOPPED: 2,
60 FROZEN: 3
61 });
62
63 var PR_Enum = Object.freeze({
64 YESNO: 'YESNO',
65 CUSTOM: 'CUSTOM',
66 });
67
68 var fields = {
69 feedbackName: 'TrackerJacker',
70 feedbackImg: 'https://s3.amazonaws.com/files.d20.io/images/11514664/jfQMTRqrT75QfmaD98BQMQ/thumb.png?1439491849',
71
72 trackerId: '',
73 trackerName: 'trackerjacker_tracker',
74
75 trackerImg: 'https://s3.amazonaws.com/files.d20.io/images/11920268/i0nMbVlxQLNMiO12gW9h3g/thumb.png?1440939062',
76 //trackerImg: 'https://s3.amazonaws.com/files.d20.io/images/6623517/8xw1KOSSOO1WocN3KQYmzw/thumb.png?1417994946',
77 trackerImgRatio: 2.25,
78 rotation_degree: 10,
79 };
80
81 var flags = {
82 tj_state: TJ_StateEnum.STOPPED, image: true,
83 rotation: true,
84 animating: false,
85 archive: false,
86 clearonclose: true
87 };
88
89 var design = {
90 turncolor: '#D8F9FF',
91 roundcolor: '#363574',
92 statuscolor: '#F0D6FF',
93 statusbgcolor: '#897A87',
94 statusbordercolor: '#430D3D',
95 edit_icon: 'https://s3.amazonaws.com/files.d20.io/images/11380920/W_Gy4BYGgzb7jGfclk0zVA/thumb.png?1439049597',
96 delete_icon: 'https://s3.amazonaws.com/files.d20.io/images/11381509/YcG-o2Q1-CrwKD_nXh5yAA/thumb.png?1439051579',
97 settings_icon: 'https://s3.amazonaws.com/files.d20.io/images/11920672/7a2wOvU1xjO-gK5kq5whgQ/thumb.png?1440940765',
98 apply_icon: 'https://s3.amazonaws.com/files.d20.io/images/11407460/cmCi3B1N0s9jU6ul079JeA/thumb.png?1439137300'
99 };
100
101 var statusMarkers = Object.freeze([
102 {name:"red",img:'https://s3.amazonaws.com/files.d20.io/images/8123890/TkC_M8_6X-UHy8euEymakQ/thumb.png?1425804412'},
103 {name:"blue",img:'https://s3.amazonaws.com/files.d20.io/images/8123884/pV7HJJVqORAhrOftpmVHUw/thumb.png?1425804373'},
104 {name:"green",img:'https://s3.amazonaws.com/files.d20.io/images/8123885/sbim5jTRF3XsuSs01ycKrg/thumb.png?1425804385'},
105 {name:"brown",img:'https://s3.amazonaws.com/files.d20.io/images/8123886/q0axCUI6vBsvDGOwFbsBXw/thumb.png?1425804393'},
106 {name:"purple",img:'https://s3.amazonaws.com/files.d20.io/images/8123889/xEOFbIKegEaFgN0vLnzG0g/thumb.png?1425804406'},
107 {name:"pink",img:'https://s3.amazonaws.com/files.d20.io/images/8123887/iyJDiq2Ngwuh6Si3-FLztQ/thumb.png?1425804400'},
108 {name:"yellow",img:'https://s3.amazonaws.com/files.d20.io/images/8123892/oL21nVVRUpDjGLaHXftstQ/thumb.png?1425804422'},
109 {name:"dead",img:'https://s3.amazonaws.com/files.d20.io/images/8093499/ca_OFvFT0w_MtJKY6c83Ew/thumb.png?1425688175'},
110 {name:"skull",img:'https://s3.amazonaws.com/files.d20.io/images/8074161/wpqmZJQlkzmyee0_lsNv4A/thumb.png?1425598594'},
111 {name:"sleepy",img:'https://s3.amazonaws.com/files.d20.io/images/8074159/PaeQH3jsdmPbUOiODPx5fg/thumb.png?1425598590'},
112 {name:"half-heart",img:'https://s3.amazonaws.com/files.d20.io/images/8074186/k5X_UUMwcuq1LZjEL58mpA/thumb.png?1425598650'},
113 {name:"half-haze",img:'https://s3.amazonaws.com/files.d20.io/images/8074190/YvdObVqX0hT711vcbML7OA/thumb.png?1425598654'},
114 {name:"interdiction",img:'https://s3.amazonaws.com/files.d20.io/images/8074185/cyt6rWIaUiMvq-4CnpskZQ/thumb.png?1425598647'},
115 {name:"snail",img:'https://s3.amazonaws.com/files.d20.io/images/8074158/YDHHfsu8T8wcqbby33fweA/thumb.png?1425598587'},
116 {name:"lightning-helix",img:'https://s3.amazonaws.com/files.d20.io/images/8074184/iUPFB-lXP9ySnktTut-3uA/thumb.png?1425598643'},
117 {name:"spanner",img:'https://s3.amazonaws.com/files.d20.io/images/8074154/2qufcEnyNJqjSN_f9XrgiQ/thumb.png?1425598583'},
118 {name:"chained-heart",img:'https://s3.amazonaws.com/files.d20.io/images/8074213/f6jmFoQWX-7KRsux_HaIqg/thumb.png?1425598699'},
119 {name:"chemical-bolt",img:'https://s3.amazonaws.com/files.d20.io/images/8074212/B-U3tyYf06An3NonHrh1xA/thumb.png?1425598696'},
120 {name:"death-zone",img:'https://s3.amazonaws.com/files.d20.io/images/8074210/CPzQbQ8h-vZnNinShD1L_Q/thumb.png?1425598689'},
121 {name:"drink-me",img:'https://s3.amazonaws.com/files.d20.io/images/8074207/bElenkvmnfe15u6e23_XxQ/thumb.png?1425598686'},
122 {name:"edge-crack",img:'https://s3.amazonaws.com/files.d20.io/images/8074206/7N52ErC13lHDxRwrt-igyQ/thumb.png?1425598682'},
123 {name:"ninja-mask",img:'https://s3.amazonaws.com/files.d20.io/images/8074181/XDbfFm8Ul3Iy7zkiDB321w/thumb.png?1425598638'},
124 {name:"stopwatch",img:'https://s3.amazonaws.com/files.d20.io/images/8074152/UW9235lWLTTryx6zCP2MQA/thumb.png?1425598581'},
125 {name:"fishing-net",img:'https://s3.amazonaws.com/files.d20.io/images/8074205/v83unarpA-nUZqp2HKOr0w/thumb.png?1425598678'},
126 {name:"overdrive",img:'https://s3.amazonaws.com/files.d20.io/images/8074178/CYZFHZzMBdssRjoxWvP7MQ/thumb.png?1425598630'},
127 {name:"strong",img:'https://s3.amazonaws.com/files.d20.io/images/8074151/DHoYUsnyz2AOaTVGR5mV7A/thumb.png?1425598577'},
128 {name:"fist",img:'https://s3.amazonaws.com/files.d20.io/images/8074201/GZ0py5UxO7pFUOfobTKGVw/thumb.png?1425598674'},
129 {name:"padlock",img:'https://s3.amazonaws.com/files.d20.io/images/8074174/euydq4AuqYk_7y0GqObChw/thumb.png?1425598626'},
130 {name:"three-leaves",img:'https://s3.amazonaws.com/files.d20.io/images/8074149/3GodR7irhqJXoQcfm7tkng/thumb.png?1425598573'},
131 {name:"fluffy-wing",img:'https://s3.amazonaws.com/files.d20.io/images/8093436/nozRPKmjhulSuQZO-NV7xw/thumb.png?1425687966'},
132 {name:"pummeled",img:'https://s3.amazonaws.com/files.d20.io/images/8074171/pPhgEmVHP6bHMbcj-wn98g/thumb.png?1425598619'},
133 {name:"tread",img:'https://s3.amazonaws.com/files.d20.io/images/8074145/-hBmfcug0Bhr7nWxXMNd1A/thumb.png?1425598570'},
134 {name:"arrowed",img:'https://s3.amazonaws.com/files.d20.io/images/8074234/Z48uPYYNGR5iD4DEy3RYbA/thumb.png?1425598735'},
135 {name:"aura",img:'https://s3.amazonaws.com/files.d20.io/images/8074231/g6ogG9gDMBsIG_fdx-Hl5w/thumb.png?1425598731'},
136 {name:"back-pain",img:'https://s3.amazonaws.com/files.d20.io/images/8074229/xdGkbAHaELU5HK9rpMUZkg/thumb.png?1425598727'},
137 {name:"black-flag",img:'https://s3.amazonaws.com/files.d20.io/images/8074226/mJgQqm9Hl3ek75xoXcecVg/thumb.png?1425598724'},
138 {name:"bleeding-eye",img:'https://s3.amazonaws.com/files.d20.io/images/8074224/IdGVnqxciFoDI6dXLyoSgA/thumb.png?1425598720'},
139 {name:"bolt-shield",img:'https://s3.amazonaws.com/files.d20.io/images/8074221/8E3S_XJF1rpkYmkQc7iwcw/thumb.png?1425598713'},
140 {name:"broken-heart",img:'https://s3.amazonaws.com/files.d20.io/images/8074218/ylXLOkQFHyAaj6kumKEaOw/thumb.png?1425598709'},
141 {name:"cobweb",img:'https://s3.amazonaws.com/files.d20.io/images/8074211/KNY0AO4fj2md_M2n6Uf4IQ/thumb.png?1425598692'},
142 {name:"broken-shield",img:'https://s3.amazonaws.com/files.d20.io/images/8074217/wV6Cx457yk_jTwjKzWRVXw/thumb.png?1425598706'},
143 {name:"flying-flag",img:'https://s3.amazonaws.com/files.d20.io/images/8074198/n2hH7I_YrEXNYb1jh0Oo5Q/thumb.png?1425598670'},
144 {name:"radioactive",img:'https://s3.amazonaws.com/files.d20.io/images/8074167/4zCBr9YKxZvRuhDo2VWQnQ/thumb.png?1425598611'},
145 {name:"trophy",img:'https://s3.amazonaws.com/files.d20.io/images/8074143/QVNHRiiQ56k6Mn2rro3_bg/thumb.png?1425598567'},
146 {name:"broken-skull",img:'https://s3.amazonaws.com/files.d20.io/images/8074215/rTI3ahu2dE3VKO-W7i3jcw/thumb.png?1425598702'},
147 {name:"frozen-orb",img:'https://s3.amazonaws.com/files.d20.io/images/8074197/K7xZkKvW0GeMvwkm8VfxTg/thumb.png?1425598666'},
148 {name:"rolling-bomb",img:'https://s3.amazonaws.com/files.d20.io/images/8074165/fd9kK4Peiprwr8wyI_pcEQ/thumb.png?1425598604'},
149 {name:"white-tower",img:'https://s3.amazonaws.com/files.d20.io/images/8074141/M5p2-7dryUVxCJjhUcJe5Q/thumb.png?1425598564'},
150 {name:"grab",img:'https://s3.amazonaws.com/files.d20.io/images/8074194/tfeQLEm-AmBi_IMF-h8vEg/thumb.png?1425598663'},
151 {name:"screaming",img:'https://s3.amazonaws.com/files.d20.io/images/8074163/CwKqOWu7ZprFzkkcafs8cQ/thumb.png?1425598601'},
152 {name:"grenade",img:'https://s3.amazonaws.com/files.d20.io/images/8074191/dd_UjtADigCKYzcP4RBCVg/thumb.png?1425598657'},
153 {name:"sentry-gun",img:'https://s3.amazonaws.com/files.d20.io/images/8074162/rlpAA3Eg04Ct8csKCjbcdQ/thumb.png?1425598597'},
154 {name:"all-for-one",img:'https://s3.amazonaws.com/files.d20.io/images/8074239/2VxQwqrsz5BXvXIkraKE1g/thumb.png?1425598746'},
155 {name:"angel-outfit",img:'https://s3.amazonaws.com/files.d20.io/images/8074238/dKSnapoJ7JyGcINc8PIA1Q/thumb.png?1425598742'},
156 {name:"archery-target",img:'https://s3.amazonaws.com/files.d20.io/images/8074237/ei4JHB51P6az3slwgZmTEw/thumb.png?1425598739'}
157 ]);
158
159 var TrackerJacker_tmp = (function() {
160 var templates = {
161 button: _.template('<a style="display: inline-block; font-size: 100%; color: black; padding: 3px 3px 3px 3px; margin: 2px 2px 2px 2px; border: 1px solid black; border-radius: 0.5em; font-weight: bold; text-shadow: -1px -1px 1px #FFF, 1px -1px 1px #FFF, -1px 1px 1px #FFF, 1px 1px 1px #FFF; background-color: #C7D0D2;" href="<%= command %>"><%= text %></a>'),
162 confirm_box: _.template('<div style="font-weight: bold; background-color: #FFF; text-align: center; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 1em; border: 1px solid black; margin: 5px 5px 5px 5px; padding: 2px 2px 2px 2px;">'
163 + '<div style="border-bottom: 1px solid black;">'
164 + '<%= message %>'
165 + '</div>'
166 + '<table style="text-align: center; width: 100%">'
167 + '<tr>'
168 + '<td>'
169 + '<%= confirm_button %>'
170 + '</td>'
171 + '<td>'
172 + '<%= reject_button %>'
173 + '</td>'
174 + '</tr>'
175 + '</table>'
176 + '</div>')
177 };
178
179 return {
180 getTemplate: function(tmpArgs, type) {
181 var retval;
182
183 retval = _.find(templates, function(e,i) {
184 if (type === i) {
185 {return true;}
186 }
187 })(tmpArgs);
188
189 return retval;
190 },
191
192 hasTemplate: function(type) {
193 if (!type)
194 {return false;}
195 return !!_.find(_.keys(templates), function(elem) {
196 {return (elem === type);}
197 });
198
199 }
200 };
201 }());
202
203 /**
204 * PendingResponse constructor
205 */
206 var PendingResponse = function(type,func,args) {
207 if (!type || !args)
208 {return undefined;}
209
210 this.type = type;
211 this.func = func;
212 this.args = args;
213 };
214
215 /**
216 * PendingResponse prototypes
217 */
218 PendingResponse.prototype = {
219 getType: function() { return this.type; },
220 getArgs: function() { return this.args; },
221 doOps: function(carry) {
222 if (!this.func)
223 {return null;}
224 return this.func(this.args,carry);
225 },
226 doCustomOps: function(args) { return this.func(args); },
227 };
228
229 /**
230 * Add a pending response to the stack, return the associated hash
231 * TODO make the search O(1) rather than O(n)
232 */
233 var addPending = function(pr,hash) {
234 if (!pr)
235 {return null;}
236 if (!hash)
237 {hash = genHash(pr.type+pr.args,pending);}
238 var retval = hash;
239 if (pending) {
240 if (pending[hash]) {
241 throw 'hash already in pending queue';
242 }
243 pending[hash] = {};
244 pending[hash].pr = pr;
245 } else {
246 pending = {};
247 pending[hash] = {};
248 pending[hash].pr = pr;
249 }
250 return retval;
251 };
252
253 /**
254 * find a pending response
255 */
256 var findPending = function(hash) {
257 var retval = null;
258 if (!pending)
259 {return retval;}
260 retval = pending[hash];
261 if (retval)
262 {retval = retval.pr;}
263 return retval;
264 };
265
266 /**
267 * Clear pending responses
268 */
269 var clearPending = function(hash) {
270 if (pending[hash])
271 {delete pending[hash]; }
272 };
273
274 /**
275 * @author lordvlad @stackoverflow
276 * @contributor Ken L.
277 */
278 var genHash = function(seed,hashset) {
279 if (!seed)
280 {return null;}
281 seed = seed.toString();
282 var hash = seed.split("").reduce(function(a,b) {a=((a<<5)-a)+b.charCodeAt(0);return a&a;},0);
283 if (hashset && hashset[hash]) {
284 var d = new Date();
285 return genHash((hash+d.getTime()*Math.random()).toString(),hashset);
286 }
287 return hash;
288 };
289
290 /**
291 * Init
292 */
293 var init = function() {
294 if (!state.trackerjacker)
295 {state.trackerjacker = {};}
296 if (!state.trackerjacker.effects)
297 {state.trackerjacker.effects = {};}
298 if (!state.trackerjacker.statuses)
299 {state.trackerjacker.statuses = [];}
300 if (!state.trackerjacker.favs)
301 {state.trackerjacker.favs = {};}
302 };
303
304 /**
305 * check if the character object exists, return first match
306 */
307 var characterObjExists = function(name, type, charId) {
308 var retval = null;
309 var obj = findObjs({
310 _type: type,
311 name: name,
312 _characterid: charId
313 });
314 if (obj.length > 0)
315 {retval = obj[0];}
316 return retval;
317 };
318
319 /**
320 * Return the string with the roll formatted, this is accomplished by simply
321 * surrounding roll equations with [[ ]] TODO, should be replaced with a
322 * single regex
323 *
324 */
325 var getFormattedRoll = function(str) {
326 if (!str) {return "";}
327 var retval = str,
328 re = /\d+d\d+/,
329 idx,
330 expr,
331 roll,
332 pre,
333 post;
334
335 if ((roll=re.exec(str))) {
336 expr = getExpandedExpr(roll[0],str,roll.index);
337 idx = str.indexOf(expr);
338 pre = str.substring(0,idx);
339 post = str.substring(idx+expr.length);
340 } else { return retval;}
341
342 return pre+"[["+expr+"]]"+getFormattedRoll(post);
343 };
344
345 /**
346 * Return the target expression expanded as far as it logically can span
347 * within the provided line.
348 *
349 * ie: target = 1d20
350 * locHint = 4
351 * line = "2+1d20+5+2d4 bla (bla 1d20+8 bla) bla (4d8...) bla bla"
352 *
353 * result = 2+1d20+5+2d4
354 */
355 var getExpandedExpr = function(target, line, locHint) {
356 if (!target || !line)
357 {return;}
358 if (!locHint)
359 {locHint = 0;}
360 var retval = target,
361 re = /\d|[\+\-]|d/,
362 loc = -1,
363 start = 0,
364 end = 0;
365
366 if((loc=line.indexOf(target,locHint)) !== -1) {
367 start = loc;
368 while (start > 0) {
369 if (line[start].match(re))
370 {start--;}
371 else
372 {start++;break;}
373 }
374 end = loc;
375 while (end < line.length) {
376 if (line[end].match(re))
377 {end++;}
378 else
379 {break;}
380 }
381 retval = line.substring(start,end);
382 retval = getLegalRollExpr(retval);
383 }
384
385 return retval;
386 };
387
388 /**
389 * Gets a legal roll expression.
390 */
391 var getLegalRollExpr = function(expr) {
392 if (!expr) {return;}
393 var retval = expr,
394 stray = expr.match(/d/g),
395 valid = expr.match(/\d+d\d+/g),
396 errMsg = "Illegal expression " + expr;
397
398 try {
399 if (expr.match(/[^\s\d\+-d]/g) ||
400 !stray ||
401 !valid ||
402 (stray.length =! valid.length))
403 {throw errMsg;}
404
405 stray = expr.match(/\+/g);
406 valid = expr.match(/\d+\+\d+/g);
407 if ((stray !== null) && (valid !== null) &&
408 (stray.length !== valid.length))
409 {throw errMsg;}
410 stray = expr.match(/-/g);
411 valid = expr.match(/\d+-\d+/g);
412 if ((stray !== null) && (valid !== null) &&
413 (stray.length !== valid.length))
414 {throw errMsg;}
415 } catch (e) {
416 throw e;
417 }
418
419 //check for leading, trailing, operands
420 if (retval[0].match(/\+|-/))
421 {retval = retval.substring(1);}
422 if (retval[retval.length-1].match(/\+|-/))
423 {retval = retval.substring(0,retval.length-1);}
424
425 return retval;
426 };
427
428 /**
429 * Prepare the turn order by checking if the tracker is present,
430 * if so, then we're resuming a previous turnorder (perhaps a restart).
431 * Fetch information from the state and double check that all refereces
432 * line up. If any references don't line up anymore, inform the GM of
433 * this, then remove them from the tracker. In the case of items existing
434 * on the tracker, perform normal impomtu add behavior.
435 */
436 var prepareTurnorder = function(turnorder) {
437 if (!turnorder)
438 {turnorder = Campaign().get('turnorder');}
439 if (!turnorder)
440 {turnorder = [];}
441 else if (typeof(turnorder) === 'string')
442 {turnorder = JSON.parse(turnorder);}
443 var tracker;
444
445 if (tracker = _.find(turnorder, function(e,i) {if (parseInt(e.id) === -1 && parseInt(e.pr) === -100 && e.custom.match(/Round\s*\d+/)){return true;}})) {
446 // resume logic
447 } else {
448 turnorder.push({
449 id: '-1',
450 pr: '-100',
451 custom: 'Round 1',
452 });
453 //TODO only clear statuses that have a duration
454 updateTurnorderMarker(turnorder);
455 }
456 if (!state.trackerjacker)
457 {state.trackerjacker = {};}
458 if (!state.trackerjacker.effects)
459 {state.trackerjacker.effects = {};}
460 if (!state.trackerjacker.statuses)
461 {state.trackerjacker.statuses = [];}
462 if (!state.trackerjacker.favs)
463 {state.trackerjacker.favs = {};}
464 };
465
466
467 /**
468 * update the status display the appears beneath the turn order
469 */
470 var updateStatusDisplay = function(curToken) {
471 if (!curToken) {return;}
472 var effects = getStatusEffects(curToken),
473 gstatus,
474 statusArgs,
475 toRemove = [],
476 content = '',
477 hcontent = '';
478
479 _.each(effects, function(e) {
480 if (!e) {return;}
481 statusArgs = e;
482 gstatus = statusExists(e.name);
483 statusArgs.duration = parseInt(statusArgs.duration) +
484 parseInt(statusArgs.direction);
485 if (gstatus.marker)
486 {content += makeStatusDisplay(e);}
487 else
488 {hcontent += makeStatusDisplay(e)}
489 });
490 effects = _.reject(effects,function(e) {
491 if (e.duration <= 0) {
492 // remove from status args
493 var removedStatus = updateGlobalStatus(e.name,undefined,-1);
494 toRemove.push(removedStatus);
495 return true;
496 }
497 });
498 setStatusEffects(curToken,effects);
499 updateAllTokenMarkers(toRemove);
500 return {public: content, hidden: hcontent};
501 };
502
503 /**
504 * Update the global status array, if a status is removed, return the
505 * removed status (for final cleanup)
506 */
507 var updateGlobalStatus = function(statusName, marker, inc) {
508 if (!statusName || !inc || isNaN(inc)) {return;}
509 var retval;
510 statusName = statusName.toLowerCase();
511 var found = _.find(state.trackerjacker.statuses, function(e) {
512 if (e.name === statusName) {
513 retval = e;
514 e.refc += inc;
515 if (e.refc <= 0) {
516 state.trackerjacker.statuses = _.reject(state.trackerjacker.statuses, function(e) {
517 if (e.name === statusName)
518 {return true;}
519 });
520 }
521 return true;
522 }
523 else if (e.marker && e.marker === marker) {
524 return true;
525 }
526 return false;
527 });
528
529 if (!found) {
530 state.trackerjacker.statuses.push({
531 name: statusName.toLowerCase(),
532 marker: marker,
533 refc: inc
534 });
535 }
536 return retval;
537 };
538
539 /**
540 * Updates every token marker related to a status
541 */
542 var updateAllTokenMarkers = function(toRemove) {
543 var token,
544 effects,
545 tokenStatusString,
546 statusName,
547 status,
548 hasRemovedEffect;
549
550 _.each(_.keys(state.trackerjacker.effects), function(e) {
551 token = getObj('graphic',e);
552 if (!token) {
553 return;
554 }
555 effects = getStatusEffects(token);
556 tokenStatusString = token.get('statusmarkers');
557 if (_.isUndefined(tokenStatusString) || tokenStatusString === 'undefined') {
558 log('Unable to get status string for ' + e + ' status string is ' + tokenStatusString);
559 return;
560 }
561 tokenStatusString = tokenStatusString.split(',');
562 _.each(effects, function(elem) {
563 statusName = elem.name.toLowerCase();
564 status = _.findWhere(state.trackerjacker.statuses,{name: statusName});
565 if (status) {
566 tokenStatusString = _.reject(tokenStatusString, function(j) {
567 return j.match(new RegExp(status.marker+'@?[1-9]?$'));
568 });
569 tokenStatusString.push(status.marker + ((elem.duration > 0 && elem.duration <= 9 && elem.direction !== 0) ? ('@'+elem.duration):''));
570 }
571 });
572
573 if (!!toRemove) {
574 _.each(toRemove,function(e) {
575 if (!e) {return;}
576 hasRemovedEffect = _.findWhere(effects,{name:e.name});
577 if (!hasRemovedEffect) {
578 tokenStatusString = _.reject(tokenStatusString, function(rre) {
579 if (rre.match(new RegExp(e.marker+'@?[1-9]?$')) ||
580 rre === 'undefined')
581 {return true;}
582 });
583 }
584 });
585 }
586
587 if (tokenStatusString.length > 0) {
588 tokenStatusString = _.reduce(tokenStatusString,function(memo,str) {
589 if (memo === 'undefined')
590 {return str;}
591 if (str === 'undefined')
592 {return memo;}
593 return ((memo ? (memo+','):'')+str);
594 });
595 }
596
597 token.set('statusmarkers',(tokenStatusString||''));
598 });
599 };
600
601 /**
602 * Update the tracker's marker in the turn order
603 */
604 var updateTurnorderMarker = function(turnorder) {
605 if (!turnorder)
606 {turnorder = Campaign().get('turnorder');}
607 if (!turnorder)
608 {return;}
609 if (typeof(turnorder) === 'string')
610 {turnorder = JSON.parse(turnorder);}
611 var tracker,
612 trackerpos;
613
614 if (!!(tracker = _.find(turnorder, function(e,i) {if (parseInt(e.id) === -1 && parseInt(e.pr) === -100 && e.custom.match(/Round\s*\d+/)){trackerpos = i;return true;}}))) {
615
616 var indicator,
617 graphic = findTrackerGraphic(),
618 rounds = tracker.custom.substring(tracker.custom.indexOf('Round')).match(/\d+/);
619
620 if (rounds)
621 {rounds = parseInt(rounds[0]);}
622
623 switch(flags.tj_state) {
624 case TJ_StateEnum.ACTIVE:
625 graphic.set('tint_color','transparent');
626 indicator = 'â–¶ ';
627 break;
628 case TJ_StateEnum.PAUSED:
629 graphic = findTrackerGraphic();
630 graphic.set('tint_color','#FFFFFF');
631 indicator ='â–â–';
632 break;
633 case TJ_StateEnum.STOPPED:
634 graphic.set('tint_color','transparent');
635 indicator = 'â—¼ ';
636 break;
637 default:
638 indicator = tracker.custom.substring(0,tracker.custom.indexOf('Round')).trim();
639 break;
640 }
641 tracker.custom = indicator + 'Round ' + rounds;
642
643 }
644
645 turnorder = JSON.stringify(turnorder);
646 Campaign().set('turnorder',turnorder);
647
648 };
649
650 /**
651 * Status exists
652 */
653 var statusExists = function(statusName) {
654 return _.findWhere(state.trackerjacker.statuses,{name: statusName});
655 };
656
657 /**
658 * get status effects for a token
659 */
660 var getStatusEffects = function(curToken) {
661 if (!curToken)
662 {return;}
663
664 var effects = state.trackerjacker.effects[curToken.get('_id')];
665 if (effects && effects.length > 0)
666 {return effects;}
667 return undefined;
668 };
669
670 /**
671 * set status effects for a token
672 */
673 var setStatusEffects = function(curToken,effects) {
674 if (!curToken)
675 {return;}
676
677 if(Array.isArray(effects))
678 {state.trackerjacker.effects[curToken.get('_id')] = effects;}
679 };
680
681 /**
682 * Make the display for editing a status for multiple tokens.
683 * This differs from the single edit case in that it performs
684 * across several tokens.
685 */
686 var makeMultiStatusConfig = function(action, statusName, idString) {
687 if (!action || !statusName || !idString)
688 {return;}
689
690 var content = '',
691 globalStatus = statusExists(statusName),
692 mImg;
693
694 if (!statusName)
695 {return '<span style="color: red; font-weight: bold;">Invalid syntax</span>'; }
696 if (!globalStatus)
697 {return '<span style="color: red; font-weight: bold;">Status no longer exists</span>'; }
698
699 mImg = _.findWhere(statusMarkers,{name: globalStatus.marker});
700 if (mImg)
701 {mImg = '<img src="' + mImg.img + '"></img>'; }
702 else
703 {mImg = 'none';}
704
705 content += '<div style="background-color: '+design.statuscolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; text-align: center;">'
706 + '<div style="border-bottom: 2px solid black;">'
707 + '<table width="100%"><tr><td width="100%"><span style="font-weight: bold; font-size: 125%">Edit Group Status "'+statusName+'"</span></td></tr></table>'
708 + '</div>'
709 + '<table width="100%">'
710 + '<tr style="background-color: #FFF; border-bottom: 1px solid '+design.statusbordercolor+';" >'
711 + '<td>'
712 + '<div><span style="font-weight: bold;">Name</span><br>'+'<span style="font-style: italic;">'+statusName+'</span></div>'
713 + '</td>'
714 + '<td width="32px" height="32px">'
715 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Name" href="!tj -edit_multi_status '
716 + statusName + ' @ name @ ?{name|'+statusName+'} @ ' + idString
717 + '"><img src="'+design.edit_icon+'"></img></a>'
718 + '</td>'
719 + '</tr>'
720 + '<tr style="background-color: #FFF; border-bottom: 1px solid '+design.statusbordercolor+';" >'
721 + '<td>'
722 + '<div><span style="font-weight: bold;">Marker</span><br>'+'<span style="font-style: italic;">'+mImg+'</span></div>'
723 + '</td>'
724 + '<td width="32px" height="32px">'
725 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Marker" href="!tj -edit_multi_status '
726 + statusName + ' @ marker @ 1 @ ' + idString
727 + '"><img src="'+design.edit_icon+'"></img></a>'
728 + '</td>'
729 + '</tr>'
730 + '<tr style="border-bottom: 1px solid '+design.statusbordercolor+';" >'
731 + '<td>'
732 + '<div><span style="font-weight: bold;">Duration</span><br>'+'<span style="font-style: italic;">Varies</span></div>'
733 + '</td>'
734 + '<td width="32px" height="32px">'
735 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Duration" href="!tj -edit_multi_status '
736 + statusName + ' @ duration @ ?{duration|1} @ ' + idString
737 + '"><img src="'+design.edit_icon+'"></img></a>'
738 + '</td>'
739 + '</tr>'
740 + '<tr style="border-bottom: 1px solid '+design.statusbordercolor+';" >'
741 + '<td>'
742 + '<div><span style="font-weight: bold;">Direction</span><br>'+'<span style="font-style: italic;">Varies</span></div>'
743 + '</td>'
744 + '<td width="32px" height="32px">'
745 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Direction" href="!tj -edit_multi_status '
746 + statusName + ' @ direction @ ?{direction|-1} @ ' + idString
747 + '"><img src="'+design.edit_icon+'"></img></a>'
748 + '</td>'
749 + '</tr>'
750 + '<tr style="border-bottom: 1px solid '+design.statusbordercolor+';" >'
751 + '<td>'
752 + '<div><span style="font-weight: bold;">Message</span><br>'+'<span style="font-style: italic;">Varies</span></div>'
753 + '</td>'
754 + '<td width="32px" height="32px">'
755 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Message" href="!tj -edit_multi_status '
756 + statusName + ' @ message @ ?{message} @ ' + idString
757 + '"><img src="'+design.edit_icon+'"></img></a>'
758 + '</td>'
759 + '</tr>'
760 + '</table>'
761 + '</div>';
762
763 return content;
764
765 };
766
767 /**
768 * Make the display for multi-token configuration in selecting
769 * which status to edit for the group of tokens selected.
770 */
771 var makeMultiTokenConfig = function(tuple) {
772 if (!tuple)
773 {return;}
774
775 var content = '',
776 midcontent = '',
777 gstatus,
778 markerdef;
779
780 _.each(tuple, function(e) {
781 gstatus = statusExists(e.statusName);
782 if (!gstatus)
783 {return;}
784 markerdef = _.findWhere(statusMarkers,{name: gstatus.marker});
785 midcontent +=
786 '<tr style="border-bottom: 1px solid '+design.statusbordercolor+';" >'
787 + (markerdef ? ('<td width="21px" height="21px">'
788 + '<div style="width: 21px; height: 21px;"><img src="'+markerdef.img+'"></img></div>'
789 + '</td>'):'<td width="0px" height="0px"></td>')
790 + '<td>'
791 + e.statusName
792 + '</td>'
793 + '<td width="32px" height="32px">'
794 + '<a style="height: 16px; width: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit '+e.statusName+' status" '
795 + 'href="!tj -dispmultistatusconfig change @ ' + e.statusName + ' @ ' + e.id
796 + '"><img src="'+design.edit_icon+'"></img></a>'
797 + '</td>'
798 + '<td width="32px" height="32px">'
799 + '<a style="height: 16px; width: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Remove '+e.statusName+' status" '
800 + 'href="!tj -dispmultistatusconfig remove @ ' + e.statusName + ' @ ' + e.id
801 + '"><img src="'+design.delete_icon+'"></img></a>'
802 + '</td>'
803 + '</tr>';
804 });
805
806 if ('' === midcontent) {
807 midcontent = '<span style="font-style: italic;">No Status Effects Present</span>';
808 }
809
810 content += '<div style="background-color: '+design.statuscolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; text-align: center;">'
811 + '<div style="border-bottom: 2px solid black; font-size: 125%; font-weight: bold; ">'
812 + 'Edit Status Group'
813 + '</div>'
814 + '<div style="border-bottom: 2px solid black; font-size: 75%; ">'
815 + '<span style="color: red; font-weight: bold;">Warning: </span> Changing a status across multiple tokens will change the status for <b><u><i>all selected</i></u></b> tokens.'
816 + '</div>'
817 + '<table width="100%">';
818 content += midcontent;
819 content += '</table></div>';
820 return content;
821 };
822
823 /**
824 * Build marker selection display
825 */
826 var makeMarkerDisplay = function(statusName,favored,custcommand) {
827 var markerList = '',
828 takenList = '',
829 command,
830 taken,
831 content;
832
833 _.each(statusMarkers,function(e) {
834 if (!favored)
835 {command = (!custcommand ? ('!tj -marker ' + e.name + ' %% ' + statusName) : (custcommand+e.name));}
836 else
837 {command = (!custcommand ? ('!tj -marker ' + e.name + ' %% ' + statusName + ' %% ' + 'fav') : (custcommand+e.name));}
838 //n*m is evil
839 if (!favored && (taken = _.findWhere(state.trackerjacker.statuses,{marker: e.name}))) {
840 takenList += '<div style="float: left; padding: 1px 1px 1px 1px; width: 25px; height: 25px;">'
841 + '<span class="showtip tipsy" title="'+taken.name+'" style="width: 21px; height: 21px"><img style="text-align: center;" src="'+e.img+'"></img></span>'
842 +'</div>';
843 } else {
844 markerList += '<div style="float: left; padding: 1px 1px 1px 1px; width: 25px; height: 25px;">'
845 + '<a style="font-size: 0px; background: url('+e.img+') center center no-repeat; width: 21px; height: 21px" href="'+command+'"><img style="text-align: center;" src="'+e.img+'"></img></a>'
846 +'</div>';
847 }
848 });
849 content = '<div style="font-weight: bold; background-color: #FFF; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; margin-left: 2px; margin-right: 2px; padding-top: 5px; padding-bottom: 5px;">'
850 + '<div style="text-align: center; border-bottom: 2px solid black;">'
851 + '<span style="font-weight: bold; font-size: 125%">Available Markers</span>'
852 + '</div>'
853 + '<div style="padding-left: 1px; padding-right: 1px; overflow: hidden;">'
854 + markerList
855 +'<div style="clear:both;"></div>'
856 + '</div>'
857 + (takenList ? ('<br>'
858 + '<div style="border-top: 2px solid black; border-bottom: 2px solid black;">'
859 + '<span style="font-weight: bold; font-size: 125%">Taken Markers</span>'
860 + '</div>'
861 + '<div style="padding-left: 1px; padding-right: 1px; overflow: hidden;">'
862 + takenList
863 +'<div style="clear:both;"></div>'
864 + '</div>'):'')
865 + '</div>';
866
867 return content;
868 };
869
870 /**
871 * Build status display
872 */
873 var makeStatusDisplay = function(statusArgs) {
874 var content = '',
875 gstatus = statusExists(statusArgs.name),
876 markerdef;
877
878 if (gstatus && gstatus.marker)
879 {markerdef = _.findWhere(statusMarkers,{name: gstatus.marker});}
880
881 content += '<div style="font-weight: bold; font-style: italic; color: '+design.statuscolor+'; background-color: '+design.statusbgcolor+'; border: 2px solid '+design.statusbordercolor
882 +'; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 1em; text-align: center;">'
883 + '<table width="100%">'
884 + '<tr>'
885 + (markerdef ? ('<td><div style="width: 21px; height: 21px;"><img src="'+markerdef.img+'"></img></div></td>'):'')
886 + '<td width="100%">'+statusArgs.name + ' ' + (parseInt(statusArgs.direction) === 0 ? '': (parseInt(statusArgs.duration) <= 0 ? '<span style="color: red;">Expiring</span>':statusArgs.duration))
887 + (parseInt(statusArgs.direction)===0 ? '<span style="color: blue;">∞</span>' : (parseInt(statusArgs.direction) > 0 ? '<span style="color: green;">▲(+'+statusArgs.direction+')</span>':'<span style="color: red;">▼('+statusArgs.direction+')</span>'))
888 + ((statusArgs.msg) ? ('<br><span style="color: #000">' + getFormattedRoll(statusArgs.msg) + '</span>'):'')+'</td>'
889 + '</tr>'
890 + '</table>'
891 + '</div>';
892 return content;
893 };
894
895 /**
896 * Build round display
897 */
898 var makeRoundDisplay = function(round) {
899 if (!round)
900 {return;}
901 var content = '';
902
903 content += '<div style="padding: 10px 10px 10px 10px; text-shadow: 1px 1px 2px #000, 0px 0px 1em #FFF, 0px 0px 0.2em #FFF, 1px 1px 1px #FFF; font-style: normal; font-size: 150%; font-weight: bold; color: #FFF; background-color: '+design.roundcolor+'; border: 3px solid #FFF; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 2em; text-align: center;">'
904 + 'Round ' + round
905 +'</div>';
906 return content;
907 };
908
909 /**
910 * Build turn display
911 */
912 var makeTurnDisplay = function(curToken) {
913 if (!curToken)
914 {return;}
915
916 var content = '',
917 journal,
918 name,
919 player,
920 controllers = getTokenControllers(curToken);
921
922 if ((journal = getObj('character',curToken.get('represents')))) {
923 name = characterObjExists('name','attribute',journal.get('_id'));
924 if (name)
925 {name = name.get('current');}
926 else if (curToken.get('showplayers_name'))
927 {name = curToken.get('name');}
928 else
929 {name = journal.get('name');}
930 } else if (curToken.get('showplayers_name')) {
931 name = curToken.get('name');
932 }
933
934 content += '<div style="background-color: '+design.turncolor+'; font-weight: bold; font-style: italic; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; text-align: center; min-height: 50px;">'
935 + '<table width="100%">'
936 + '<tr>'
937 + '<td width="50px" height="50px"><div style="margin-right 2px; padding-top: 2px; padding-bottom: 2px; padding-left: 2px; padding-right: 2px; text-align: center; width: 50px">'
938 + '<img width="50px" height="50px" src="' + curToken.get('imgsrc') + '"></img></div></td>'
939 + '<td width="100%">'
940 + (name ? ('It is ' + name + '\'s turn') : 'Turn')
941 + '</td>'
942 + '<td width="32px" height="32px">'
943 + '<a style="width: 20px; height: 18px; background: none; border: none;" href="!tj -disptokenconfig '+curToken.get('_id')+'"><img src="'+design.settings_icon+'"></img></a>'
944 + '</td>'
945 + '</tr>';
946
947 if (_.find(controllers,function(e){return (e === 'all');})) {
948 content += '<tr>'
949 + '<td colspan="3"><div style="margin-left: -2px; font-style: normal; font-weight: bold; font-size: 125%; text-shadow: -1px -1px 1px #FFF, 1px -1px 1px #FFF, -1px 1px 1px #FFF, 1px 1px 1px #FFF; color #FFF; border: 2px solid #000; width: 100%; background-color: #FFF;">All Players</div></td>'
950 + '</tr>';
951 } else {
952 _.each(controllers,function(e) {
953 player = getObj('player',e);
954 if (player) {
955 content += '<tr>'
956 + '<td colspan="3"><div style="margin-left: -2px; font-style: normal; font-weight: bold; font-size: 125%; text-shadow: -1px -1px 1px #000, 1px -1px 1px #000, -1px 1px 1px #000, 1px 1px 1px #000; color: #FFF; border:2px solid #000; width: 100%; background-color: ' + player.get('color') + ';">' + player.get('displayname') + '</div></td>'
957 + '</tr>';
958 }
959 });
960 }
961 content += '</table>'
962 + "</div>";
963
964 return content;
965 };
966
967 /**
968 * Build a listing of favorites with buttons that allow them
969 * to be applied to a selection.
970 */
971 var makeFavoriteConfig = function() {
972 var midcontent = '',
973 content = '',
974 markerdef;
975
976 _.each(state.trackerjacker.favs,function(e) {
977 markerdef = _.findWhere(statusMarkers,{name: e.marker});
978 midcontent +=
979 '<tr style="border-bottom: 1px solid '+design.statusbordercolor+';" >'
980 + (markerdef ? ('<td width="21px" height="21px">'
981 + '<div style="width: 21px; height: 21px;"><img src="'+markerdef.img+'"></img></div>'
982 + '</td>'):'<td width="0px" height="0px"></td>')
983 + '<td>'
984 + e.name
985 + '</td>'
986 + '<td width="32px" height="32px">'
987 + '<a style="height: 16px; width: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Apply '+e.name+' status" href="!tj -applyfav '
988 + e.name
989 + '"><img src="'+design.apply_icon+'"></img></a>'
990 + '</td>'
991 + '<td width="32px" height="32px">'
992 + '<a style="height: 16px; width: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit '+e.name+' status" href="!tj -dispstatusconfig '
993 + ' %% changefav %% '+e.name
994 + '"><img src="'+design.edit_icon+'"></img></a>'
995 + '</td>'
996 + '<td width="32px" height="32px">'
997 + '<a style="height: 16px; width: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Remove '+e.name+' status" href="!tj -dispstatusconfig '
998 + ' %% removefav %% '+e.name
999 + '"><img src="'+design.delete_icon+'"></img></a>'
1000 + '</td>'
1001 + '</tr>';
1002 });
1003
1004 if ('' === midcontent)
1005 {midcontent = 'No Favorites Available';}
1006
1007 content = '<div style="background-color: '+design.statuscolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; text-align: center;">'
1008 + '<div style="font-weight: bold; font-size: 125%; border-bottom: 2px solid black;">'
1009 + 'Favorites'
1010 + '</div>'
1011 + '<table width="100%">';
1012 content += midcontent;
1013 content += '</table></div>';
1014
1015 return content;
1016 };
1017
1018 /**
1019 * Build a settings dialog given a token that has effects upon it.
1020 */
1021 var makeStatusConfig = function(curToken, statusName, favored) {
1022 if (!statusName || (!curToken && !favored)) {
1023 return '<span style="color: red; font-weight: bold;">Invalid syntax</span>';
1024 }
1025 var globalStatus = statusExists(statusName),
1026 effects = getStatusEffects(curToken),
1027 status = _.findWhere(effects,{name:statusName}),
1028 mImg,
1029 content = '';
1030
1031 if (!favored && (!status || !globalStatus)) {
1032 return '<span style="color: red; font-weight: bold;">Invalid syntax</span>';
1033 }
1034
1035 if (favored) {
1036 status=favored;
1037 globalStatus=favored;
1038 }
1039
1040 if (!globalStatus || !status) {
1041 return '<span style="color: red; font-weight: bold;">Status does not exist internally</span>';
1042 }
1043
1044 mImg = _.findWhere(statusMarkers,{name: globalStatus.marker});
1045 if (mImg)
1046 {mImg = '<img src="' + mImg.img + '"></img>';}
1047 else
1048 {mImg = 'none';}
1049
1050 content += '<div style="background-color: '+design.statuscolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; text-align: center;">'
1051 + '<div style="border-bottom: 2px solid black;">'
1052 + '<table width="100%"><tr><td width="100%"><span style="font-weight: bold; font-size: 125%">'+ (favored ? 'Edit Favorite' :('Edit "'+statusName+'" for'))+'</span></td>'+(favored ? ('<td width="100%">'+statusName+'</td>') : ('<td width="32px" height="32px"><div style="width: 32px; height: 32px"><img src="'+curToken.get('imgsrc')+'"></img></div></td>')) + '</tr></table>'
1053 + '</div>'
1054 + '<table width="100%">'
1055 + '<tr style="background-color: #FFF; border-bottom: 1px solid '+design.statusbordercolor+';" >'
1056 + '<td>'
1057 + '<div><span style="font-weight: bold;">Name</span><br>'+'<span style="font-style: italic;">'+statusName+'</span></div>'
1058 + '</td>'
1059 + '<td width="32px" height="32px">'
1060 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Name" href="!tj -edit_status '
1061 + (favored ? 'changefav':'change')+' %% ' + (favored ? (''):(curToken.get('_id'))) +' %% '+statusName+' %% name %% ?{name|'+statusName+'}'
1062 + '"><img src="'+design.edit_icon+'"></img></a>'
1063 + '</td>'
1064 + '</tr>'
1065 + '<tr style="background-color: #FFF; border-bottom: 1px solid '+design.statusbordercolor+';" >'
1066 + '<td>'
1067 + '<div><span style="font-weight: bold;">Marker</span><br>'+'<span style="font-style: italic;">'+mImg+'</span></div>'
1068 + '</td>'
1069 + '<td width="32px" height="32px">'
1070 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Marker" href="!tj -edit_status '
1071 + (favored ? 'changefav':'change')+' %% ' + (favored ? (''):(curToken.get('_id'))) +' %% '+statusName+' %% marker %% mark'
1072 + '"><img src="'+design.edit_icon+'"></img></a>'
1073 + '</td>'
1074 + '</tr>'
1075 + '<tr style="border-bottom: 1px solid '+design.statusbordercolor+';" >'
1076 + '<td>'
1077 + '<div><span style="font-weight: bold;">Duration</span><br>'+'<span style="font-style: italic;">'+status.duration+'</span></div>'
1078 + '</td>'
1079 + '<td width="32px" height="32px">'
1080 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Duration" href="!tj -edit_status '
1081 + (favored ? 'changefav':'change')+' %% ' + (favored ? (''):(curToken.get('_id'))) +' %% '+statusName+' %% duration %% ?{duration|'+status.duration+'}'
1082 + '"><img src="'+design.edit_icon+'"></img></a>'
1083 + '</td>'
1084 + '</tr>'
1085 + '<tr style="border-bottom: 1px solid '+design.statusbordercolor+';" >'
1086 + '<td>'
1087 + '<div><span style="font-weight: bold;">Direction</span><br>'+'<span style="font-style: italic;">'+status.direction+'</span></div>'
1088 + '</td>'
1089 + '<td width="32px" height="32px">'
1090 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Direction" href="!tj -edit_status '
1091 + (favored ? 'changefav':'change')+' %% ' + (favored ? (''):(curToken.get('_id'))) +' %% '+statusName+' %% direction %% ?{direction|'+status.direction+'}'
1092 + '"><img src="'+design.edit_icon+'"></img></a>'
1093 + '</td>'
1094 + '</tr>'
1095 + '<tr style="border-bottom: 1px solid '+design.statusbordercolor+';" >'
1096 + '<td>'
1097 + '<div><span style="font-weight: bold;">Message</span><br>'+'<span style="font-style: italic;">'+status.msg+'</span></div>'
1098 + '</td>'
1099 + '<td width="32px" height="32px">'
1100 + '<a style= "width: 16px; height: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit Message" href="!tj -edit_status '
1101 + (favored ? 'changefav':'change')+' %% ' + (favored ? (''):(curToken.get('_id'))) +' %% '+statusName+' %% message %% ?{message|'+status.msg+'}'
1102 + '"><img src="'+design.edit_icon+'"></img></a>'
1103 + '</td>'
1104 + '</tr>'
1105 + (favored ? '':('<tr>'
1106 + '<td colspan="2">'
1107 //+ '<a href="!CreatureGen -help">cookies</a>'
1108 //+ '<a style="font-weight: bold" href="!tj -addfav '+statusName+' %% '+status.duration+' %% '+status.direction+' %% '+status.msg+' %% '+globalStatus.marker+'"> Add to Favorites</a>'
1109 + TrackerJacker_tmp.getTemplate({command: '!tj -addfav '+statusName+' %% '+status.duration+' %% '+status.direction+' %% '+status.msg+' %% '+globalStatus.marker, text: 'Add to Favorites'},'button')
1110
1111 + '</td>'
1112 + '</tr>'))
1113 + '</table>'
1114 + '</div>';
1115
1116 return content;
1117
1118 };
1119
1120 /**
1121 * Build the token dialog to display statuses effecting it
1122 */
1123 var makeTokenConfig = function(curToken) {
1124 if (!curToken)
1125 {return;}
1126
1127 var content = '',
1128 midcontent = '',
1129 gstatus,
1130 markerdef,
1131 effects = getStatusEffects(curToken);
1132
1133 _.each(effects, function(e) {
1134 gstatus = statusExists(e.name);
1135 if (!gstatus)
1136 {return;}
1137 markerdef = _.findWhere(statusMarkers,{name: gstatus.marker});
1138 midcontent +=
1139 '<tr style="border-bottom: 1px solid '+design.statusbordercolor+';" >'
1140 + (markerdef ? ('<td width="21px" height="21px">'
1141 + '<div style="width: 21px; height: 21px;"><img src="'+markerdef.img+'"></img></div>'
1142 + '</td>'):'<td width="0px" height="0px"></td>')
1143 + '<td>'
1144 + e.name
1145 + '</td>'
1146 + '<td width="32px" height="32px">'
1147 + '<a style="height: 16px; width: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Edit '+e.name+' status" href="!tj -dispstatusconfig '
1148 + curToken.get('_id')+' %% change %% '+e.name
1149 + '"><img src="'+design.edit_icon+'"></img></a>'
1150 + '</td>'
1151 + '<td width="32px" height="32px">'
1152 + '<a style="height: 16px; width: 16px; border: 1px solid '+design.statusbordercolor+'; border-radius: 0.2em; background: none" title="Remove '+e.name+' status" href="!tj -dispstatusconfig '
1153 + curToken.get('_id')+' %% remove %% '+e.name
1154 + '"><img src="'+design.delete_icon+'"></img></a>'
1155 + '</td>'
1156 + '</tr>';
1157 });
1158
1159 if ('' === midcontent) {
1160 midcontent += '<tr><td><div style="text-align: center; font-style: italic;">No Status Effects Present</div></td></tr>';
1161 }
1162
1163 content += '<div style="background-color: '+design.statuscolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; text-align: center;">'
1164 + '<div style="border-bottom: 2px solid black;">'
1165 + '<table width="100%"><tr><td width="100%"><span style="font-weight: bold; font-size: 125%">Statuses for</span></td><td width="32px" height="32px"><div style="width: 32px; height: 32px"><img src="'+curToken.get('imgsrc')+'"></img></div></td></tr></table>'
1166 + '</div>'
1167 + '<table width="100%">';
1168 content += midcontent;
1169 content += '</table>';
1170 content += /*'<div style="border-top: 1px solid black;">'
1171 + '<a style="font-weight: bold" href="!tj -addstatus ?{name}:?{duration}:?{direction}:?{message}"> Add Status</a>'
1172 + '<br><a style="font-weight: bold" href="!tj -listfavs"> Apply Favorite</a>'
1173 + '</div>'+*/'</div>';
1174 return content;
1175 };
1176
1177 /**
1178 * Show a listing of markers
1179 */
1180 var doShowMarkers = function() {
1181 var disp = makeMarkerDisplay();
1182 sendFeedback(disp);
1183 };
1184
1185 /**
1186 * Is a tracker
1187 */
1188 var isTracker = function(turn) {
1189 if (parseInt(turn.id) === -1
1190 && parseInt(turn.pr) === -100
1191 && turn.custom.match(/Round\s*\d+/))
1192 {return true;}
1193 return false;
1194 };
1195
1196 /**
1197 * Get the graphic object for the tracker (if any) for the current page.
1198 * If it does not exist, create it. Avoid creating a duplicate where possible
1199 */
1200 var findTrackerGraphic = function(pageid) {
1201 var graphic = getObj('graphic',fields.trackerId),
1202 curToken = findCurrentTurnToken();
1203
1204 pageid = (pageid ? pageid : (curToken ? curToken.get('_pageid') : Campaign().get('playerpageid')));
1205
1206 if (graphic && graphic.get('_pageid') === pageid) {
1207 return graphic;
1208 } else {
1209 // we find the graphic
1210 var cannidates = findObjs({
1211 _pageid: pageid,
1212 _type: 'graphic',
1213 name: fields.trackerName,
1214 });
1215 if (cannidates && cannidates[0]) {
1216 graphic = cannidates[0];
1217 fields.trackerId = graphic.get('_id');
1218 return graphic;
1219 } else {
1220 // we make the graphic
1221 graphic = createObj('graphic', {
1222 _type: 'graphic',
1223 _subtype: 'token',
1224 _pageid: pageid,
1225 name: fields.trackerName,
1226 imgsrc: fields.trackerImg,
1227 layer: 'gmlayer',
1228 width: 70,
1229 height: 70,
1230 });
1231 fields.trackerId = graphic.get('_id');
1232 return graphic;
1233 }
1234 }
1235
1236 };
1237
1238 /**
1239 * Find the current token at the top of the tracker if any
1240 */
1241 var findCurrentTurnToken = function(turnorder) {
1242 if (!turnorder)
1243 {turnorder = Campaign().get('turnorder');}
1244 if (!turnorder)
1245 {return undefined;}
1246 if (typeof(turnorder) === 'string')
1247 {turnorder = JSON.parse(turnorder);}
1248 if (turnorder && turnorder.length > 0 && turnorder[0].id !== -1)
1249 {return getObj('graphic',turnorder[0].id);}
1250 return;
1251 };
1252
1253 /**
1254 * Announce the round
1255 */
1256 var announceRound = function(round) {
1257 if (!round)
1258 {return;}
1259 var disp = makeRoundDisplay(round);
1260 sendPublic(disp);
1261 };
1262
1263 /**
1264 * Announce the turn with an optional rider display
1265 */
1266 var announceTurn = function(curToken,statusRiders) {
1267 if (!curToken)
1268 {return;}
1269 var disp = makeTurnDisplay(curToken);
1270 disp += statusRiders.public;
1271 if (curToken.get('layer') !== 'objects') {
1272 disp += statusRiders.hidden;
1273 sendFeedback(disp);
1274 } else {
1275 sendPublic(disp);
1276 if (statusRiders.hidden)
1277 {sendFeedback(statusRiders.hidden);}
1278 }
1279 };
1280
1281 /**
1282 * Handle the turn order advancement given the current and prior ordering
1283 */
1284 var handleAdvanceTurn = function(turnorder,priororder) {
1285 if (flags.tj_state === TJ_StateEnum.STOPPED || flags.tj_state === TJ_StateEnum.PAUSED || !turnorder || !priororder)
1286 {return;}
1287 if (typeof(turnorder) === 'string')
1288 {turnorder = JSON.parse(turnorder);}
1289 if (typeof(priororder) === 'string')
1290 {priororder = JSON.parse(priororder);}
1291 var currentTurn = turnorder[0];
1292
1293 if (currentTurn) {
1294 if (turnorder.length > 1
1295 && isTracker(currentTurn)) {
1296 // ensure that last turn we weren't also atop the order
1297 if (!priororder || isTracker(priororder[0]))
1298 {return;}
1299 var rounds = parseInt(currentTurn.custom.match(/\d+/)[0]);
1300 rounds++;
1301 currentTurn.custom = currentTurn.custom.substring(0,currentTurn.custom.indexOf('Round'))
1302 + 'Round ' + rounds;
1303 announceRound(rounds);
1304 turnorder.shift();
1305 turnorder.push(currentTurn);
1306 currentTurn = turnorder[0];
1307 updateTurnorderMarker(turnorder);
1308 }
1309 if (currentTurn.id !== -1
1310 && priororder
1311 && priororder[0].id !== currentTurn.id) {
1312 var graphic,
1313 curToken = getObj('graphic',currentTurn.id),
1314 priorToken = getObj('graphic',priororder[0].id),
1315 maxsize = 0;
1316 if (!curToken)
1317 {return;}
1318
1319 if (priorToken && priorToken.get('_pageid') !== curToken.get('_pageid')) {
1320 graphic = findTrackerGraphic(priorToken.get('_pageid'));
1321 graphic.set('layer','gmlayer');
1322 }
1323 graphic = findTrackerGraphic();
1324
1325 if (flags.tj_state === TJ_StateEnum.ACTIVE)
1326 {flags.tj_state = TJ_StateEnum.FROZEN;}
1327 maxsize = Math.max(parseInt(curToken.get('width')),parseInt(curToken.get('height')));
1328 graphic.set('layer','gmlayer');
1329 graphic.set('left',curToken.get('left'));
1330 graphic.set('top',curToken.get('top'));
1331 graphic.set('width',parseFloat(maxsize*fields.trackerImgRatio));
1332 graphic.set('height',parseFloat(maxsize*fields.trackerImgRatio));
1333 toFront(curToken);
1334 setTimeout(function() {
1335 if (graphic) {
1336 if (curToken.get('layer') === 'gmlayer') {
1337 graphic.set('layer','gmlayer');
1338 toBack(graphic);
1339 } else {
1340 graphic.set('layer','map');
1341 toFront(graphic);
1342 }
1343 if (flags.tj_state === TJ_StateEnum.FROZEN)
1344 {flags.tj_state = TJ_StateEnum.ACTIVE;}
1345 }
1346 },500);
1347 // Manage status
1348 // Announce Turn
1349 announceTurn(curToken,updateStatusDisplay(curToken));
1350 }
1351 }
1352
1353 turnorder = JSON.stringify(turnorder);
1354 Campaign().set('turnorder',turnorder);
1355 };
1356
1357 /**
1358 * Check if a favorite status exists
1359 */
1360 var favoriteExists = function(statusName) {
1361 statusName = statusName.toLowerCase();
1362 var found = _.find(_.keys(state.trackerjacker.favs), function(e) {
1363 return e === statusName;
1364 });
1365 if (found)
1366 {found = state.trackerjacker.favs[found]; }
1367 return found;
1368 };
1369
1370 /**
1371 * Produce a listing of favorites
1372 */
1373 var doApplyFavorite = function(statusName,selection) {
1374 if (!statusName)
1375 {return;}
1376 statusName = statusName.toLowerCase();
1377
1378 var fav = favoriteExists(statusName),
1379 markerdef,
1380 curToken,
1381 effectId,
1382 effectList,
1383 status,
1384 content = '',
1385 midcontent = '';
1386
1387 if (!fav) {
1388 sendError('<b>"'+statusName+'"</b> is not a known favorite status');
1389 return;
1390 }
1391
1392 var markerUsed = _.find(state.trackerjacker.statuses, function(e) {
1393 if (typeof(e.marker) !== 'undefined'
1394 && e.marker === fav.marker
1395 && e.name !== fav.name)
1396 {return true;}
1397 });
1398
1399 if (markerUsed) {
1400 markerdef = _.findWhere(statusMarkers,{name: markerUsed.marker});
1401 sendError('Status <i>"'+markerUsed.name+'"</i> already uses marker <img src="'+markerdef.img+'"></img>. You can either change the marker for favorite <i>"'+statusName+'"</i> or the marker for <i>"'+markerUsed.name+'"</i>');
1402 return;
1403 }
1404
1405 markerdef = _.findWhere(statusMarkers,{name: fav.marker});
1406
1407 _.each(selection,function(e) {
1408 curToken = getObj('graphic', e._id);
1409 if (!curToken || curToken.get('_subtype') !== 'token' || curToken.get('isdrawing'))
1410 {return;}
1411 effectId = e._id;
1412 effectList = state.trackerjacker.effects[effectId];
1413
1414 if ((status = _.find(effectList,function(elem) {return elem.name.toLowerCase() === fav.name.toLowerCase();}))) {
1415 return;
1416 } else if (effectList && Array.isArray(effectList)) {
1417 effectList.push({
1418 name: fav.name,
1419 duration: fav.duration,
1420 direction: fav.direction,
1421 msg: fav.msg,
1422 });
1423 updateGlobalStatus(fav.name,undefined,1);
1424 } else {
1425 state.trackerjacker.effects[effectId] = effectList = new Array({
1426 name: fav.name,
1427 duration: fav.duration,
1428 direction: fav.direction,
1429 msg: fav.msg,
1430 });
1431 updateGlobalStatus(fav.name,undefined,1);
1432 }
1433 midcontent += '<div style="width: 40px; height 40px; display: inline-block;"><img src="'+curToken.get('imgsrc')+'"></div>';
1434 });
1435
1436 if ('' === midcontent)
1437 {midcontent = '<div style="font-style: italic; text-align: center; font-size: 125%; ">None</div>';}
1438
1439 content += '<div style="font-weight: bold; background-color: '+design.statusbgcolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em;">'
1440 + '<div style="text-align: center; color: '+design.statuscolor+'; border-bottom: 2px solid black;">'
1441 + '<span style="font-weight: bold; font-size: 120%">Apply Favorite</span>'
1442 + '</div>'
1443 + 'Name: ' + '<span style="color:'+design.statuscolor+';">'+fav.name+'</span>'
1444 + '<br>Marker: ' + (markerdef ? ('<img src="'+markerdef.img+'"></img>'):'none')
1445 + '<br>Duration: ' + fav.duration
1446 + '<br>Direction: ' + fav.direction + (fav.msg ? ('<br>Message: ' + fav.msg):'')
1447 + '<br><br><span style="font-style: normal;">Status placed on the following:</span><br>' ;
1448
1449 content += midcontent;
1450
1451 status = statusExists(fav.name.toLowerCase());
1452 if (status && !status.marker && fav.marker)
1453 {doDirectMarkerApply(markerdef.name+' %% '+fav.name); }
1454 else if (status && !status.marker)
1455 {content += '<br><div style="text-align: center;">'+TrackerJacker_tmp.getTemplate({command: '!tj -dispmarker '+fav.name, text: 'Choose Marker'},'button')+'</div>';}
1456
1457 updateAllTokenMarkers();
1458 content += '</div>';
1459 sendFeedback(content);
1460 };
1461
1462 /**
1463 * Add a favorite status to the list of statuses
1464 */
1465 var doAddFavorite = function(args) {
1466 if (!args)
1467 {return;}
1468
1469 args = args.split(/:| %% /);
1470
1471 if (args.length < 3 || args.length > 5) {
1472 sendError('Invalid favorite status syntax');
1473 return;
1474 }
1475
1476 var name = args[0],
1477 duration = parseInt(args[1]),
1478 direction = parseInt(args[2]),
1479 msg = args[3],
1480 marker = args[4],
1481 markerdef;
1482
1483 if (typeof(name) === 'string')
1484 {name = name.toLowerCase();}
1485
1486 if (isNaN(duration) || isNaN(direction)) {
1487 sendError('Invalid favorite status syntax');
1488 return;
1489 }
1490
1491 if (marker && !_.findWhere(statusMarkers,{name: marker})) {
1492 marker = undefined;
1493 } else {
1494 markerdef = _.findWhere(statusMarkers,{name: marker});
1495 }
1496
1497 if (favoriteExists(name)) {
1498 sendError('Favorite with the name "'+name+'" already exists');
1499 return;
1500 }
1501
1502 var newFav = {
1503 name: name,
1504 duration: duration,
1505 direction: direction,
1506 msg: msg,
1507 marker: marker
1508 };
1509
1510 state.trackerjacker.favs[name] = newFav;
1511
1512 var content = '<div style="font-weight: bold; background-color: '+design.statusbgcolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em;">'
1513 + '<div style="text-align: center; color: '+design.statuscolor+'; border-bottom: 2px solid black;">'
1514 + '<span style="font-weight: bold; font-size: 120%">Add Favorite</span>'
1515 + '</div>'
1516 + 'Name: ' + '<span style="color:'+design.statuscolor+';">'+name+'</span>'
1517 + '<br>Marker: ' + (markerdef ? ('<img src="'+markerdef.img+'"></img>'):'none')
1518 + '<br>Duration: ' + duration
1519 + '<br>Direction: ' + direction
1520 + (msg ? ('<br>Message: ' + msg):'')
1521 + (marker ? '':('<br><div style="text-align: center;">'+TrackerJacker_tmp.getTemplate({command: '!tj -dispmarker '+name+ ' %% fav', text: 'Choose Marker'},'button')+'</div>'));
1522 content += '</div>';
1523
1524 sendFeedback(content);
1525
1526 };
1527
1528 /**
1529 * Remove a favorite from the tracker
1530 */
1531 var doRemoveFavorite = function(statusName) {
1532 if (!statusName)
1533 {return;}
1534 statusName = statusName.toLowerCase();
1535
1536 if (!favoriteExists(statusName)) {
1537 sendFeedback('Status "' + statusName + '" is not on the favorite list');
1538 return;
1539 }
1540
1541 var content = '<div style="font-weight: bold; background-color: '+design.statusbgcolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em;">'
1542 + '<div style="text-align: center; color: '+design.statuscolor+'; border-bottom: 2px solid black;">'
1543 + '<span style="font-weight: bold; font-size: 120%">Remove Favorite</span>'
1544 + '</div>'
1545 + 'Favorite ' + '<span style="color:'+design.statuscolor+';">'+statusName+'</span> removed.'
1546 + '</div>';
1547
1548 delete state.trackerjacker.favs[statusName];
1549 sendFeedback(content);
1550
1551
1552 };
1553
1554 /**
1555 * Add turn item
1556 */
1557 var doAddStatus = function(args,selection) {
1558 if (!args)
1559 {return;}
1560 if (!selection) {
1561 sendError('Invalid selection');
1562 return;
1563 }
1564
1565 args = args.split(':');
1566
1567 if (args.length <3 || args.length > 5) {
1568 sendError('Invalid status item syntax');
1569 return;
1570 }
1571 var name = args[0],
1572 duration = parseInt(args[1]),
1573 direction = parseInt(args[2]),
1574 msg = args[3],
1575 marker = args[4];
1576
1577 if (marker === 'undefined')
1578 {marker = false;}
1579
1580 if (typeof(name) === 'string')
1581 {name = name.toLowerCase();}
1582
1583 if (isNaN(duration) || isNaN(direction) || !name) {
1584 sendError('Invalid status item syntax');
1585 return;
1586 }
1587
1588 if (marker && (!_.find(statusMarkers, function(e) { return e.name === marker; })
1589 || !!_.find(state.trackerjacker.statuses, function(e) {return e.name === e.marker;}))) {
1590 sendError('Marker invalid or already in use');
1591 return;
1592 }
1593
1594 var curToken,
1595 effectId,
1596 effectList,
1597 status,
1598 content = '',
1599 midcontent = '';
1600
1601 _.each(selection,function(e) {
1602 curToken = getObj('graphic', e._id);
1603 if (!curToken || curToken.get('_subtype') !== 'token' || curToken.get('isdrawing'))
1604 {return;}
1605 effectId = e._id;
1606 effectList = state.trackerjacker.effects[effectId];
1607
1608 if ((status = _.find(effectList,function(elem) {return elem.name.toLowerCase() === name.toLowerCase();}))) {
1609 return;
1610 } else if (effectList && Array.isArray(effectList)) {
1611 effectList.push({
1612 name: name,
1613 duration: duration,
1614 direction: direction,
1615 msg: msg
1616 });
1617 updateGlobalStatus(name,undefined,1);
1618 } else {
1619 state.trackerjacker.effects[effectId] = effectList = new Array({
1620 name: name,
1621 duration: duration,
1622 direction: direction,
1623 msg: msg
1624 });
1625 updateGlobalStatus(name,undefined,1);
1626 }
1627 midcontent += '<div style="width: 40px; height 40px; display: inline-block;"><img src="'+curToken.get('imgsrc')+'"></div>';
1628 });
1629
1630 if ('' === midcontent)
1631 {midcontent = '<div style="font-style: italic; text-align: center; font-size: 125%; ">None</div>';}
1632
1633
1634 content += '<div style="font-weight: bold; background-color: '+design.statusbgcolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em;">'
1635 + '<div style="text-align: center; color: '+design.statuscolor+'; border-bottom: 2px solid black;">'
1636 + '<span style="font-weight: bold; font-size: 120%">Add Status</span>'
1637 + '</div>'
1638 + 'Name: ' + '<span style="color:'+design.statuscolor+';">'+name+'</span>'
1639 + '<br>Duration: ' + duration
1640 + '<br>Direction: ' + direction + (msg ? ('<br>Message: ' + msg):'')
1641 + '<br><br><span style="font-style: normal;">Status placed on the following:</span><br>' ;
1642 content += midcontent;
1643
1644 status = statusExists(name.toLowerCase());
1645 if (status && !status.marker) {
1646 if (marker)
1647 {status.marker = marker;}
1648 else
1649 {content += '<br><div style="text-align: center;">'+TrackerJacker_tmp.getTemplate({command: '!tj -dispmarker '+name, text: 'Choose Marker'},'button')+'</div>';}
1650 }
1651
1652 content += '</div>';
1653 updateAllTokenMarkers();
1654 sendFeedback(content);
1655 };
1656
1657 /**
1658 * Remove a status from the selected tokens
1659 */
1660 var doRemoveStatus = function(args,selection) {
1661 if (!args || !selection) {
1662 sendError('Invalid selection');
1663 return;
1664 }
1665 var effects,
1666 found = false,
1667 toRemove = [],
1668 curToken,
1669 effectId,
1670 removedStatus,
1671 content = '',
1672 midcontent = '';
1673
1674 args = args.toLowerCase();
1675
1676 _.each(selection, function(e) {
1677 effectId = e._id;
1678 curToken = getObj('graphic', e._id);
1679 if (!curToken || curToken.get('_subtype') !== 'token' || curToken.get('isdrawing'))
1680 {return;}
1681 effects = state.trackerjacker.effects[effectId];
1682 effects = _.reject(effects,function(elem) {
1683 if (elem.name.toLowerCase() === args) {
1684 found = true;
1685 midcontent += '<div style="width: 40px; height 40px; display: inline-block;"><img src="'+curToken.get('imgsrc')+'"></div>';
1686 removedStatus = updateGlobalStatus(elem.name,undefined,-1);
1687 return true;
1688 }
1689 return false;
1690 });
1691 setStatusEffects(curToken,effects);
1692 toRemove.push(removedStatus);
1693 // Remove markers
1694 });
1695
1696 if ('' === midcontent)
1697 {midcontent = '<div style="font-style: italic; text-align: center; font-size: 125%; ">None</div>';}
1698
1699
1700 content += '<div style="font-weight: bold; background-color: '+design.statusbgcolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em;">'
1701 + '<div style="text-align: center; color: '+design.statuscolor+'; border-bottom: 2px solid black;">'
1702 + '<span style="font-weight: bold; font-size: 120%">Remove Status</span>'
1703 + '</div>'
1704 + '<span style="font-style: normal;">Status "<span style="color: '+design.statuscolor+';">' +args+'</span>" removed from the following:</span><br>';
1705 content += midcontent;
1706 content += '</div>';
1707 if (!found)
1708 {content = '<span style="color: red; font-weight:bold;">No status "' + args + '" exists on any in the selection</span>'; }
1709 updateAllTokenMarkers(toRemove);
1710 sendFeedback(content);
1711 };
1712
1713 /**
1714 * Display marker list (internally used)
1715 */
1716 var doDisplayMarkers = function(args) {
1717 if (!args)
1718 {return;}
1719 args = args.toLowerCase();
1720 args = args.split(' %% ');
1721 var statusName = args[0],
1722 isfav = args[1],
1723 content = '';
1724
1725 if (!isfav && !statusExists(statusName))
1726 {return;}
1727
1728 content = makeMarkerDisplay(statusName,(isfav === 'fav'));
1729 sendFeedback(content);
1730 };
1731
1732 /**
1733 * Display token configuration (internally used)
1734 */
1735 var doDisplayTokenConfig = function(args) {
1736 if (!args)
1737 {return;}
1738
1739 var curToken = getObj('graphic',args);
1740 if (!curToken || curToken.get('_subtype') !== 'token') {
1741 sendError('Invalid target');
1742 }
1743
1744 var content = makeTokenConfig(curToken);
1745 sendFeedback(content);
1746 };
1747
1748 /**
1749 * Display status configuration (internally used)
1750 */
1751 var doDisplayStatusConfig = function(args) {
1752 if (!args)
1753 {return;}
1754
1755 args = args.split(/ %% /);
1756 var tokenId = args[0],
1757 action = args[1],
1758 statusName = args[2];
1759
1760 // dirty fix for lack of trim()
1761 if (tokenId)
1762 {tokenId = tokenId.trim();}
1763
1764 var curToken = getObj('graphic',tokenId);
1765 if ((tokenId && (!curToken || curToken.get('_subtype') !== 'token'))
1766 || !action
1767 || !statusName) {
1768 sendError('Invalid syntax');
1769 return;
1770 }
1771
1772 var content;
1773 switch (action) {
1774 case 'remove':
1775 doRemoveStatus(statusName,[{_id: tokenId}]);
1776 break;
1777 case 'change':
1778 content = makeStatusConfig(curToken,statusName);
1779 sendFeedback(content);
1780 break;
1781 case 'removefav':
1782 doRemoveFavorite(statusName);
1783 break;
1784 case 'changefav':
1785 content = makeStatusConfig('',statusName,favoriteExists(statusName));
1786 sendFeedback(content);
1787 break;
1788 default:
1789 sendError('Invalid syntax');
1790 return;
1791 }
1792 };
1793
1794 /**
1795 * Display favorite configuration
1796 */
1797 var doDisplayFavConfig = function() {
1798 var content = makeFavoriteConfig();
1799 sendFeedback(content);
1800 };
1801
1802 /**
1803 * Perform a single edit operation
1804 */
1805 var doEditTokenStatus = function(selection) {
1806 var graphic;
1807 if (!selection
1808 || selection.length !== 1
1809 || !(graphic = getObj('graphic',selection[0]._id)
1810 || graphic.get('_subtype') !== 'token' )
1811 || graphic.get('isdrawing')) {
1812 sendError('Invalid selection');
1813 return;
1814 }
1815 var curToken = getObj('graphic',selection[0]._id);
1816 var content = makeTokenConfig(curToken);
1817 sendFeedback(content);
1818 };
1819
1820 /**
1821 * Display the status edit dialog for a multi edit
1822 */
1823 var doDisplayMultiStatusConfig = function(args) {
1824 if (!args)
1825 {return;}
1826
1827 args = args.split(' @ ');
1828
1829 var action = args[0],
1830 statusName = args[1],
1831 idString = args[2],
1832 content = '';
1833
1834 if (action === 'remove') {
1835 idString = idString.split(' %% ');
1836 var selection = [];
1837 _.each(idString, function(e) {
1838 selection.push({_id: e, _type: 'graphic'});
1839 });
1840 doRemoveStatus(statusName,selection);
1841 return;
1842 } else if (action !== 'change') {
1843 return;
1844 }
1845
1846 content = makeMultiStatusConfig(action,statusName,idString);
1847
1848 sendFeedback(content);
1849
1850 };
1851
1852 /**
1853 * Display the multi edit token dialog
1854 */
1855 var doMultiEditTokenStatus = function(selection) {
1856 if (!selection)
1857 {return;}
1858 if (selection.length === 1)
1859 {return doEditTokenStatus(selection);}
1860
1861 var tuple = [],
1862 subTuple,
1863 curToken,
1864 effects,
1865 content;
1866
1867 _.each(selection,function(e) {
1868 curToken = getObj('graphic',e._id);
1869 if(curToken && curToken.get('_subtype') === 'token' && !curToken.get('isdrawing')) {
1870 effects = getStatusEffects(curToken);
1871 if (effects) {
1872 _.each(effects,function(f) {
1873 if (!(subTuple=_.find(tuple,function(g){return g.statusName === f.name;})))
1874 {tuple.push({id: e._id, statusName: f.name});}
1875 else
1876 {subTuple.id = subTuple.id + ' %% ' + e._id;}
1877 });
1878 }
1879 }
1880 });
1881
1882 content = makeMultiTokenConfig(tuple);
1883 sendFeedback(content);
1884 };
1885
1886 /**
1887 * Perform the edit operation on multiple tokens whose ids
1888 * are supplied.
1889 */
1890 var doEditMultiStatus = function(args) {
1891 if (!args)
1892 {return;}
1893
1894 args = args.split(' @ ');
1895
1896 var statusName = args[0],
1897 attrName = args[1],
1898 newValue = args[2],
1899 idString = args[3],
1900 gstatus = statusExists(statusName),
1901 effectList,
1902 content = '',
1903 midcontent,
1904 errMsg;
1905
1906 // input sanitation
1907 if (!newValue)
1908 {newValue = '';}
1909 if (!statusName || !attrName) {
1910 sendError('Error on multi-selection');
1911 return;
1912 }
1913
1914 // dirty fix for lack of trim()
1915 statusName = statusName.toLowerCase().trim();
1916 idString = idString.trim();
1917 idString = idString.split(' %% ');
1918
1919
1920 if (attrName === 'name') {
1921 if (statusExists(newValue)) {
1922 sendError('Status name already exists');
1923 return;
1924 }
1925 gstatus = statusExists(statusName);
1926 newValue = newValue.toLowerCase();
1927 effectList = state.trackerjacker.effects;
1928 _.each(effectList,function(effects) {
1929 _.each(effects,function(e) {
1930 if (e.name === statusName)
1931 {e.name = newValue;}
1932 });
1933 });
1934 gstatus.name = newValue;
1935 midcontent = 'New status name is "' + newValue + '"';
1936 } else if (attrName === 'marker') {
1937 content = makeMarkerDisplay(statusName);
1938 sendFeedback(content);
1939 return;
1940 } else {
1941 idString = _.chain(_.keys(state.trackerjacker.effects))
1942 .reject(function(n) {
1943 return !_.contains(idString,n);
1944 })
1945 .value();
1946 _.each(idString, function(e) {
1947 effectList = getStatusEffects(getObj('graphic',e));
1948 _.find(effectList,function(f) {
1949 if (f.name === statusName) {
1950 switch (attrName) {
1951 case 'duration':
1952 if (!isNaN(newValue)) {
1953 f.duration = parseInt(newValue);
1954 if (!midcontent)
1955 {midcontent = 'New duration is ' + newValue;}
1956 } else if (!errMsg) {
1957 errMsg = 'Invalid Value';
1958 }
1959 // change duration for selected statuses
1960 break;
1961 case 'direction':
1962 if (!isNaN(newValue)) {
1963 f.direction = parseInt(newValue);
1964 if (!midcontent)
1965 {midcontent = 'New direction is ' + newValue;}
1966 } else if (!errMsg) {
1967 errMsg = 'Invalid Value';
1968 }
1969 // change direction for selected statuses
1970 break;
1971 case 'message':
1972 f.msg = newValue;
1973 if (!midcontent)
1974 {midcontent = 'New message is ' + newValue;}
1975 // change message for selected statuses
1976 break;
1977 default:
1978 sendError('Bad syntax/selection');
1979 return;
1980 }
1981 }
1982 });
1983 });
1984 if (errMsg)
1985 {sendError(errMsg);}
1986 else
1987 {updateAllTokenMarkers();}
1988 }
1989
1990 content += '<div style="background-color: '+design.statusbgcolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; text-align: center; font-weight: bold;">'
1991 + '<div style="color: ' + design.statuscolor + '; font-weight: bold; border-bottom: 2px solid black;">'
1992 + '<table width="100%"><tr><td width="100%"><span style="font-weight: bold; font-size: 125%">Edit Group Status "'+statusName+'"</span></td></tr></table>'
1993 + '</div>';
1994 content += midcontent;
1995 content += '</div>';
1996
1997 if (midcontent)
1998 {sendFeedback(content);}
1999 };
2000
2001 /**
2002 * Add player statuses
2003 */
2004 var doPlayerAddStatus = function(args, selection, senderId) {
2005 if (!args)
2006 {return;}
2007 if (!selection) {
2008 sendResponseError('Invalid selection');
2009 return;
2010 }
2011
2012 args = args.split(':');
2013
2014 if (args.length <3 || args.length > 4) {
2015 sendResponseError('Invalid status item syntax');
2016 return;
2017 }
2018 var name = args[0],
2019 duration = parseInt(args[1]),
2020 direction = parseInt(args[2]),
2021 msg = args[3],
2022 statusArgs = {},
2023 statusArgsString = '',
2024 status,
2025 markerdef,
2026 hashes = [],
2027 curToken,
2028 pr_choosemarker,
2029 pr_nomarker,
2030 choosemarker_args = {},
2031 nomarker_args = {},
2032 content = '',
2033 midcontent = '',
2034 d = new Date();
2035
2036 if (typeof(name) === 'string')
2037 {name = name.toLowerCase();}
2038
2039 if (isNaN(duration) || isNaN(direction)) {
2040 sendResponseError('Invalid status item syntax');
2041 return;
2042 }
2043
2044 if (!!(status=statusExists(name))) {
2045 markerdef = _.findWhere(statusMarkers,{name: status.marker});
2046 }
2047
2048 statusArgs.name = name;
2049 statusArgs.duration = duration;
2050 statusArgs.direction = direction;
2051 statusArgs.msg = msg;
2052 statusArgs.marker = (markerdef ? markerdef.name:undefined);
2053 statusArgsString = name + ' @ ' + duration + ' @ ' + direction + ' @ ' + msg;
2054
2055 hashes.push(genHash(d.getTime()*Math.random(),pending));
2056 hashes.push(genHash(d.getTime()*Math.random(),pending));
2057 choosemarker_args.hlist = hashes;
2058 choosemarker_args.statusArgs = statusArgs;
2059 choosemarker_args.statusArgsString = statusArgsString;
2060 choosemarker_args.senderId = senderId;
2061 choosemarker_args.selection = selection;
2062 nomarker_args.hlist = hashes;
2063 nomarker_args.statusArgs = statusArgs;
2064 nomarker_args.senderId = senderId;
2065 nomarker_args.selection = selection;
2066
2067 pr_choosemarker = new PendingResponse(PR_Enum.CUSTOM,function(args) {
2068 var hashes = [],
2069 pr_marker,
2070 content;
2071
2072 hashes.push(genHash(d.getTime()*Math.random(),pending));
2073
2074 pr_marker = new PendingResponse(PR_Enum.CUSTOM,function(args, carry) {
2075 args.statusArgs.marker = carry;
2076 doDispPlayerStatusAllow(args.statusArgs,args.selection,args.senderId);
2077
2078 },args);
2079 addPending(pr_marker,hashes[0]);
2080
2081 content = makeMarkerDisplay(undefined,false,'!tj -relay hc% '
2082 + hashes[0]
2083 + ' %% ');
2084
2085 sendResponse(args.senderId,content);
2086 _.each(args.hlist,function(e) {
2087 clearPending(e) ;
2088 });
2089 },choosemarker_args);
2090
2091 pr_nomarker = new PendingResponse(PR_Enum.CUSTOM,function(args) {
2092 sendResponse('<span style="color: orange; font-weight: bold;">Request sent for \''+statusArgs.name+'\'</span>');
2093 doDispPlayerStatusAllow(args.statusArgs,args.selection,args.senderId);
2094 _.each(args.hlist,function(e) {
2095 clearPending(e) ;
2096 });
2097 },nomarker_args);
2098
2099 addPending(pr_choosemarker,hashes[0]);
2100 addPending(pr_nomarker,hashes[1]);
2101
2102
2103 _.each(selection,function(e) {
2104 curToken = getObj('graphic', e._id);
2105 if (!curToken || curToken.get('_subtype') !== 'token' || curToken.get('isdrawing'))
2106 {return;}
2107 midcontent += '<div style="width: 40px; height 40px; display: inline-block;"><img src="'+curToken.get('imgsrc')+'"></div>';
2108 });
2109
2110 content += '<div style="font-weight: bold; background-color: '+design.statusbgcolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em;">'
2111 + '<div style="text-align: center; color: '+design.statuscolor+'; border-bottom: 2px solid black;">'
2112 + '<span style="font-weight: bold; font-size: 120%">Request Add Status</span>'
2113 + '</div>'
2114 + 'Name: ' + '<span style="color:'+design.statuscolor+';">'+name+'</span>'
2115 + '<br>Marker: ' + (markerdef ? ('<img src="'+markerdef.img+'"></img>'):'none')
2116 + '<br>Duration: ' + duration
2117 + '<br>Direction: ' + direction + (msg ? ('<br>Message: ' + msg):'')
2118 + '<br><br><span style="font-style: normal;">Status requested to be placed on the following:</span><br>';
2119 content += midcontent;
2120 content += (markerdef ? '': (
2121 '<div style="text-align: center;">'
2122 + TrackerJacker_tmp.getTemplate({command: '!tj -relay hc% ' + hashes[0], text: 'Choose Marker'},'button')
2123 + TrackerJacker_tmp.getTemplate({command: '!tj -relay hc% ' + hashes[1], text: 'Request Without Marker'},'button')
2124 + '</div>'
2125 ));
2126 content += '</div>';
2127 sendResponse(senderId,content);
2128
2129 if (markerdef)
2130 {doDispPlayerStatusAllow(statusArgs,selection,senderId);}
2131 };
2132
2133 /**
2134 * make dialog to allow/disallow a player status add
2135 */
2136 var doDispPlayerStatusAllow = function(statusArgs,selection,senderId) {
2137 var hashes = [],
2138 confirmArgs = {},
2139 rejectArgs = {},
2140 pr_confirm,
2141 pr_reject,
2142 content = '',
2143 midcontent = '',
2144 player,
2145 markerdef,
2146 curToken,
2147 d = new Date();
2148
2149 player = getObj('player',senderId);
2150 if (!player) {
2151 sendError('Non-existant player requested to add a status?');
2152 return;
2153 }
2154
2155 _.each(selection,function(e) {
2156 curToken = getObj('graphic', e._id);
2157 if (!curToken || curToken.get('_subtype') !== 'token' || curToken.get('isdrawing'))
2158 {return;}
2159 midcontent += '<div style="width: 40px; height 40px; display: inline-block;"><img src="'+curToken.get('imgsrc')+'"></div>';
2160 });
2161
2162 hashes.push(genHash(d.getTime()*Math.random(),pending));
2163 hashes.push(genHash(d.getTime()*Math.random(),pending));
2164 confirmArgs.hlist = hashes;
2165 confirmArgs.statusArgs = statusArgs;
2166 confirmArgs.selection = selection;
2167 confirmArgs.senderId = senderId;
2168 rejectArgs.hlist = hashes;
2169 rejectArgs.statusArgs = statusArgs;
2170 rejectArgs.selection = selection;
2171 rejectArgs.senderId = senderId;
2172
2173 pr_confirm = new PendingResponse(PR_Enum.YESNO,function(args) {
2174 var argStr = args.statusArgs.name
2175 + ':' + args.statusArgs.duration
2176 + ':' + args.statusArgs.direction
2177 + ':' + args.statusArgs.msg
2178 + ':' + args.statusArgs.marker,
2179 markerdef;
2180 markerdef = _.findWhere(statusMarkers,{name: statusArgs.marker});
2181
2182 if (statusExists(args.statusArgs.name)) {
2183 doAddStatus(argStr,selection);
2184 } else if(!!!_.find(state.trackerjacker.statuses,function(e){if (e.marker === args.statusArgs.marker){return true;}})) {
2185 doAddStatus(argStr,selection);
2186 } else {
2187 sendError('Marker <img src="'+markerdef.img+'"></img> is already in use, cannot use it for \'' + args.statusArgs.name + '\' ');
2188 sendResponseError(args.senderId,'Status application \''+statusArgs.name+'\' rejected, marker <img src="'+markerdef.img+'"></img> already in use');
2189 return;
2190 }
2191 sendResponse(args.senderId,'<span style="color: green; font-weight: bold;">Status application for \''+statusArgs.name+'\' accepted</span>');
2192
2193 _.each(args.hlist,function(e) {
2194 clearPending(e) ;
2195 });
2196 },confirmArgs);
2197
2198 pr_reject = new PendingResponse(PR_Enum.YESNO,function(args) {
2199 var player = getObj('player',args.senderId);
2200 if (!player)
2201 {sendError('Non-existant player requested to add a status?');}
2202 sendResponseError(args.senderId,'Status application for \''+statusArgs.name+'\' rejected');
2203 sendError('Rejected status application for \''+statusArgs.name+'\' from ' + player.get('_displayname'));
2204
2205 _.each(args.hlist,function(e) {
2206 clearPending(e) ;
2207 });
2208 },rejectArgs);
2209
2210 addPending(pr_confirm,hashes[0]);
2211 addPending(pr_reject,hashes[1]);
2212
2213
2214 markerdef = _.findWhere(statusMarkers,{name: statusArgs.marker});
2215
2216 content += '<div style="font-weight: bold; background-color: '+design.statusbgcolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em;">'
2217 + '<div style="text-align: center; color: '+design.statuscolor+'; border-bottom: 2px solid black;">'
2218 + '<span style="font-weight: bold; font-size: 120%">Request Add Status</span>'
2219 + '</div>'
2220 + '<span style="color:'+design.statuscolor+';">'+ player.get('_displayname') + '</span> requested to add the following status...<br>'
2221 + '<br>Name: ' + '<span style="color:'+design.statuscolor+';">'+statusArgs.name+'</span>'
2222 + '<br>Marker: ' + (markerdef ? ('<img src="'+markerdef.img+'"></img>'):'none')
2223 + '<br>Duration: ' + statusArgs.duration
2224 + '<br>Direction: ' + statusArgs.direction + (statusArgs.msg ? ('<br>Message: ' + statusArgs.msg):'')
2225 + '<br><br><span style="font-style: normal;">Status requested to be placed on the following:</span><br>';
2226 content += midcontent;
2227
2228 content += '<table style="text-align: center; width: 100%">'
2229 + '<tr>'
2230 + '<td>'
2231 + TrackerJacker_tmp.getTemplate({command: '!tj -relay hc% ' + hashes[0], text: 'Confirm'},'button')
2232 + '</td>'
2233 + '<td>'
2234 + TrackerJacker_tmp.getTemplate({command: '!tj -relay hc% ' + hashes[1], text: 'Reject'},'button')
2235 + '</td>'
2236 + '</tr>'
2237 + '</table>';
2238 // GM feedback
2239 sendFeedback(content);
2240 // Player feedback
2241 sendResponse(senderId,'<span style="color: orange; font-weight: bold;">Request sent for \''+statusArgs.name+'\'</span>');
2242 };
2243
2244 /**
2245 * Performs a direct marker application to a status name.
2246 * An internal command that is still sanitized to prevent
2247 * awful things.
2248 */
2249 var doDirectMarkerApply = function(args) {
2250 // directly apply a marker to a token id
2251 if (!args)
2252 {return;}
2253 args = args.split(' %% ');
2254 if (!args)
2255 {return;}
2256
2257 var markerName = args[0],
2258 statusName = args[1],
2259 isFav = args[2];
2260
2261 isFav = isFav === 'fav';
2262
2263 if (typeof(markerName) === 'string')
2264 {markerName = markerName.toLowerCase();}
2265 if (typeof(statusName) === 'string')
2266 {statusName = statusName.toLowerCase();}
2267
2268 var status,
2269 found,
2270 markerdef,
2271 oldMarker;
2272
2273 // if we're a favorite we don't bother with the status and active effects.
2274 if (isFav) {
2275 var fav = favoriteExists(statusName);
2276 if (fav) {
2277 fav.marker = markerName;
2278 markerdef = _.findWhere(statusMarkers,{name: markerName});
2279 sendFeedback('<div style="color: green; font-weight: bold;">Marker for <i><b>Favorite</i> "'+statusName+'"</b> set as <div style="width: 21px; height 21px; display: inline-block;"><img src="'+markerdef.img+'"></img></div></div>' );
2280 } else {
2281 sendError('Favorite <u>"'+statusName+'"</u> does not exist.');
2282 }
2283 return;
2284 }
2285
2286 _.each(state.trackerjacker.statuses, function(e) {
2287 if (e.marker === markerName)
2288 {found = e;}
2289 if (e.name === statusName)
2290 {status = e;}
2291 });
2292 if (status) {
2293 if (found) {
2294 markerdef = _.findWhere(statusMarkers,{name: markerName});
2295 if (!markerdef)
2296 {return;}
2297 sendError('Marker <div style="width: 21px; height 21px; display: inline-block;"><img src="'+markerdef.img+'"></img></div> already taken by "' + found.name + '"');
2298 // marker taken
2299 } else {
2300 if (status.marker) {
2301 oldMarker = status.marker;
2302 }
2303 markerdef = _.findWhere(statusMarkers,{name: markerName});
2304 status.marker = markerName;
2305 if (!markerdef)
2306 {return;}
2307 sendFeedback('<div style="color: green; font-weight: bold;">Marker for <b>"'+statusName+'"</b> set as <div style="width: 21px; height 21px; display: inline-block;"><img src="'+markerdef.img+'"></img></div></div>' );
2308 updateAllTokenMarkers([{name: '', marker: oldMarker}]);
2309 }
2310 }
2311 };
2312
2313 /**
2314 * Perform a status edit on a single token, internal command, but
2315 * still performs sanitation of input to prevent awful things.
2316 */
2317 var doEditStatus = function(args) {
2318 if (!args) {
2319 sendError('Bad syntax/selection');
2320 return;
2321 }
2322
2323 args = args.split(' %% ');
2324 var action = args[0],
2325 tokenId = args[1],
2326 statusName = args[2],
2327 attrName = args[3],
2328 newValue = args[4],
2329 effects,
2330 effectList,
2331 curToken,
2332 localEffect,
2333 fav,
2334 content = '',
2335 midcontent = '';
2336
2337 if (!newValue) {
2338 newValue = '';
2339 attrName = attrName.replace('%%','').trim();
2340 }
2341 if (!action
2342 || !statusName
2343 || !attrName) {
2344 sendError('Bad syntax/selection values');
2345 return;
2346 }
2347
2348 // if no token is available
2349 curToken = getObj('graphic',tokenId);
2350 if (tokenId
2351 && curToken
2352 && (curToken.get('_subtype') !== 'token' || curToken.get('isdrawing'))) {
2353 sendError('Bad syntax/selection');
2354 return;
2355 }
2356 if (action === 'change') {
2357 switch(attrName) {
2358 case 'name':
2359 var gstatus = statusExists(statusName);
2360 if (!gstatus) {
2361 sendError('Status "'+statusName+'" does not exist');
2362 return;
2363 }
2364 if (statusExists(newValue)) {
2365 sendError('Status name already exists');
2366 return;
2367 }
2368 gstatus = statusExists(statusName);
2369 newValue = newValue.toLowerCase();
2370 effectList = state.trackerjacker.effects;
2371 _.each(effectList,function(effects) {
2372 _.each(effects,function(e) {
2373 if (e.name === statusName) {
2374 e.name = newValue;
2375 }
2376 });
2377 });
2378
2379 gstatus.name = newValue;
2380 midcontent += 'Status name now: ' + newValue;
2381 break;
2382 case 'marker':
2383 content = makeMarkerDisplay(statusName);
2384 sendFeedback(content);
2385 return;
2386 case 'duration':
2387 effects = getStatusEffects(curToken);
2388 localEffect = _.findWhere(effects,{name: statusName});
2389 if (!localEffect || isNaN(newValue)) {
2390 sendError('Bad syntax/selection');
2391 return;
2392 }
2393 localEffect.duration = parseInt(newValue);
2394 midcontent += 'New "'+statusName+'" duration ' + newValue;
2395 updateAllTokenMarkers();
2396 break;
2397 case 'direction':
2398 effects = getStatusEffects(curToken);
2399 localEffect = _.findWhere(effects,{name: statusName});
2400 if (!localEffect || isNaN(newValue)) {
2401 sendError('Bad syntax/selection');
2402 return;
2403 }
2404 localEffect.direction = parseInt(newValue);
2405 midcontent += 'New "'+statusName+'" direction ' + newValue;
2406 updateAllTokenMarkers();
2407 break;
2408 case 'message':
2409 effects = getStatusEffects(curToken);
2410 localEffect = _.findWhere(effects,{name: statusName});
2411 if (!localEffect) {
2412 sendError('Bad syntax/selection');
2413 return;
2414 }
2415 localEffect.msg = newValue;
2416 midcontent += 'New "'+statusName+'" message ' + newValue;
2417 break;
2418 default:
2419 sendError('Bad syntax/selection');
2420 return;
2421 }
2422 } else if (action === 'changefav') {
2423 switch(attrName) {
2424 case 'name':
2425 fav = favoriteExists(statusName);
2426 if (favoriteExists(newValue)) {
2427 sendError('Favorite name already exists');
2428 return;
2429 }
2430 fav.name = newValue.toLowerCase();
2431 //manually remove from state
2432 delete state.trackerjacker.favs[statusName];
2433 state.trackerjacker.favs[newValue] = fav;
2434 midcontent += 'Status name now: ' + newValue;
2435 break;
2436 case 'marker':
2437 fav = favoriteExists(statusName);
2438 content = makeMarkerDisplay(statusName,fav);
2439 sendFeedback(content);
2440 return;
2441 case 'duration':
2442 fav = favoriteExists(statusName);
2443 if (!fav || isNaN(newValue)) {
2444 sendError('Bad syntax/selection');
2445 }
2446 fav.duration = parseInt(newValue);
2447 midcontent += 'New "'+statusName+'" duration ' + newValue;
2448 break;
2449 case 'direction':
2450 fav = favoriteExists(statusName);
2451 if (!fav || isNaN(newValue)) {
2452 sendError('Bad syntax/selection');
2453 }
2454 fav.direction = parseInt(newValue);
2455 midcontent += 'New "'+statusName+'" direction ' + newValue;
2456 break;
2457 case 'message':
2458 fav = favoriteExists(statusName);
2459 if (!fav) {
2460 sendError('Bad syntax/selection');
2461 }
2462 fav.msg = newValue;
2463 midcontent += 'New "'+statusName+'" message ' + newValue;
2464 break;
2465 default:
2466 sendError('Bad syntax/selection');
2467 return;
2468 }
2469 }
2470
2471 content += '<div style="font-weight: bold; background-color: '+design.statusbgcolor+'; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; text-align: center;">'
2472 + '<div style="color: '+design.statuscolor+'; border-bottom: 2px solid black;">'
2473 + '<table width="100%"><tr><td width="100%"><span style="font-weight: bold; font-size: 125%">'+(curToken ? ('Editing "'+statusName+'" for'):('Editing Favorite ' + statusName))+'</span></td>'+ (tokenId ? ('<td width="32px" height="32px"><div style="width: 32px; height: 32px"><img src="'+curToken.get('imgsrc')+'"></img></div></td>'):'') +'</tr></table>'
2474 + '</div>';
2475 content += midcontent;
2476 content += '</div>';
2477 sendFeedback(content);
2478 return;
2479 };
2480
2481 /**
2482 * Resets the turn order the the provided round number
2483 * or in its absense, configures it to 1. Does no other
2484 * operation other than change the round counter.
2485 */
2486 var doResetTurnorder = function(args) {
2487 var initial = (typeof(args) === 'string' ? args.match(/\d+/) : 1);
2488 if (!initial)
2489 {initial = 1;}
2490 var turnorder = Campaign().get('turnorder');
2491 if (turnorder && typeof(turnorder) === 'string')
2492 {turnorder = JSON.parse(turnorder);}
2493
2494 if (!turnorder) {
2495 prepareTurnorder();
2496 } else {
2497 if(!_.find(turnorder, function(e) {
2498 if (parseInt(e.id) === -1 && parseInt(e.pr) === -100 && e.custom.match(/Round\s*\d+/)) {
2499 e.custom = 'Round ' + initial;
2500 return true;
2501 }
2502 })) {
2503 prepareTurnorder();
2504 } else {
2505 updateTurnorderMarker(turnorder);
2506 }
2507 }
2508
2509 };
2510
2511 /**
2512 * Get an array of controllers for the current token either
2513 * from the direct token control, or linked journal control
2514 */
2515 var getTokenControllers = function(token) {
2516 if (!token)
2517 {return;}
2518 var controllers;
2519 if (token.get('represents')) {
2520 var journal = getObj('character',token.get('represents'));
2521 if (journal)
2522 {controllers = journal.get('controlledby').split(',');}
2523 } else {
2524 controllers = token.get('controlledby').split(',');
2525 }
2526 return controllers;
2527 };
2528
2529 /**
2530 * determine if the sender controls the token either by
2531 * linked journal, or by direct token control.
2532 */
2533 var isTokenController = function(token,senderId) {
2534 if (!token) {
2535 return false;
2536 } else if (playerIsGM(senderId)) {
2537 return true;
2538 } else if (_.find(token.get('controlledby').split(','),function(e){return e===senderId;})) {
2539 return true;
2540 } else if (token.get('represents')) {
2541 var journal = getObj('character',token.get('represents'));
2542 if (journal && _.find(journal.get('controlledby').split(','),function(e){return e===senderId;})) {
2543 return true;
2544 }
2545 }
2546 return false;
2547 };
2548
2549 /**
2550 * Animate the tracker
2551 *
2552 * TODO make the rotation rate a field variable
2553 */
2554 var animateTracker = function() {
2555 if (!flags.animating)
2556 {return;}
2557
2558 if (flags.tj_state === TJ_StateEnum.ACTIVE) {
2559 if (flags.rotation) {
2560 var graphic = findTrackerGraphic();
2561 graphic.set('rotation',parseInt(graphic.get('rotation'))+fields.rotation_degree);
2562 }
2563 setTimeout(function() {animateTracker();},500);
2564 } else if (flags.tj_state === TJ_StateEnum.PAUSED
2565 || flags.tj_state === TJ_StateEnum.FROZEN) {
2566 setTimeout(function() {animateTracker();},500);
2567 } else {
2568 flags.animating = false;
2569 }
2570 };
2571
2572 /**
2573 * Start/Pause the tracker, does not annouce the starting turn
2574 * as if you're moving around while paused, to reposition, you
2575 * don't want it to tick down on status effects.
2576 */
2577 var doStartTracker = function() {
2578 if (flags.tj_state === TJ_StateEnum.ACTIVE) {
2579 doPauseTracker();
2580 return;
2581 }
2582 flags.tj_state = TJ_StateEnum.ACTIVE;
2583 prepareTurnorder();
2584 var curToken = findCurrentTurnToken();
2585 if (curToken) {
2586 var graphic = findTrackerGraphic();
2587 var maxsize = Math.max(parseInt(curToken.get('width')),parseInt(curToken.get('height')));
2588 graphic.set('layer','gmlayer');
2589 graphic.set('left',curToken.get('left'));
2590 graphic.set('top',curToken.get('top'));
2591 graphic.set('width',maxsize*fields.trackerImgRatio);
2592 graphic.set('height',maxsize*fields.trackerImgRatio);
2593 setTimeout(function() {
2594 if (!!(curToken = getObj('graphic',curToken.get('_id')))) {
2595 if (curToken.get('layer') === 'gmlayer') {
2596 graphic.set('layer','gmlayer');
2597 toBack(graphic);
2598 } else {
2599 graphic.set('layer','map');
2600 toFront(graphic);
2601 }
2602 }
2603 },500);
2604 }
2605
2606 updateTurnorderMarker();
2607 if (!flags.animating) {
2608 flags.animating = true;
2609 animateTracker();
2610 }
2611 };
2612
2613 /**
2614 * Stops the tracker, removing all trackerjacker controlled
2615 * statuses.
2616 */
2617 var doStopTracker = function() {
2618 flags.tj_state = TJ_StateEnum.STOPPED;
2619 // Remove Graphic
2620 var trackergraphics = findObjs({
2621 _type: 'graphic',
2622 name: fields.trackerName,
2623 });
2624 _.each(trackergraphics, function(elem) {
2625 if (elem)
2626 {elem.remove();}
2627 });
2628 // Update turnorder
2629 updateTurnorderMarker();
2630 // Clean markers
2631 var toRemove = [];
2632 _.each(state.trackerjacker.statuses,function(e) {
2633 toRemove.push({name: '', marker: e.marker});
2634 });
2635 updateAllTokenMarkers(toRemove);
2636 // Clean state
2637 state.trackerjacker.effects = {};
2638 state.trackerjacker.statuses = [];
2639 };
2640
2641 /**
2642 * Pause the tracker
2643 *
2644 * DEPRECATED due to toggle of !tj -start
2645 */
2646 var doPauseTracker = function() {
2647 if(flags.tj_state === TJ_StateEnum.PAUSED) {
2648 doStartTracker();
2649 } else {
2650 flags.tj_state = TJ_StateEnum.PAUSED;
2651 updateTurnorderMarker();
2652 }
2653 };
2654
2655 /**
2656 * Perform player controled turn advancement (!eot)
2657 */
2658 var doPlayerAdvanceTurn = function(senderId) {
2659 if (!senderId || flags.tj_state !== TJ_StateEnum.ACTIVE)
2660 {return;}
2661 var turnorder = Campaign().get('turnorder');
2662 if (!turnorder)
2663 {return;}
2664 if (typeof(turnorder) === 'string')
2665 {turnorder = JSON.parse(turnorder);}
2666
2667 var token = getObj('graphic',turnorder[0].id);
2668 if (token && isTokenController(token,senderId)) {
2669 var priorOrder = JSON.stringify(turnorder);
2670 turnorder.push(turnorder.shift());
2671 turnorder = JSON.stringify(turnorder);
2672 handleAdvanceTurn(turnorder,priorOrder);
2673 }
2674 };
2675
2676 /**
2677 * Clear the turn order
2678 */
2679 var doClearTurnorder = function() {
2680 Campaign().set('turnorder','');
2681 doStopTracker();
2682 };
2683
2684 /**
2685 * Handle Pending Requests
2686 */
2687 var doRelay = function(args,senderId) {
2688 if (!args)
2689 {return;}
2690 var carry,
2691 hash;
2692 args = args.split(' %% ');
2693 if (!args) { log(args); return; }
2694 hash = args[0];
2695 if (hash) {
2696 hash = hash.match(/hc% .+/);
2697 if (!hash) { log(hash); return; }
2698 hash = hash[0].replace('hc% ','');
2699 carry = args[1];
2700 if (carry)
2701 {carry = carry.trim();}
2702 var pr = findPending(hash);
2703 if (pr) {
2704 pr.doOps(carry);
2705 clearPending(hash);
2706 } else {
2707 sendResponseError(senderId,'Selection Invalidated');
2708 }
2709 }
2710 };
2711
2712 /**
2713 * Show help message
2714 */
2715 var showHelp = function() {
2716 var content =
2717 '<div style="background-color: #FFF; border: 2px solid #000; box-shadow: rgba(0,0,0,0.4) 3px 3px; border-radius: 0.5em; margin-left: 2px; margin-right: 2px; padding-top: 5px; padding-bottom: 5px;">'
2718 + '<div style="font-weight: bold; text-align: center; border-bottom: 2px solid black;">'
2719 + '<span style="font-weight: bold; font-size: 125%">TrackerJacker v'+version+'</span>'
2720 + '</div>'
2721 + '<div style="padding-left: 5px; padding-right: 5px; overflow: hidden;">'
2722 + '<div style="font-weight: bold;">'
2723 + '!tj -help'
2724 + '</div>'
2725 + '<li style="padding-left: 10px;">'
2726 + 'Display this message'
2727 + '</li>'
2728 + '<br>'
2729 + '<div style="font-weight: bold;">'
2730 + '!tj -start'
2731 + '</div>'
2732 + '<li style="padding-left: 10px;">'
2733 + 'Start/Pause the tracker. If not started starts; if active pauses; if paused, resumes. Behaves as a toggle.'
2734 + '</li>'
2735 + '<br>'
2736 + '<div style="font-weight: bold;">'
2737 + '!tj -stop'
2738 + '</div>'
2739 + '<li style="padding-left: 10px;">'
2740 + 'Stops the tracker and clears all status effects.'
2741 + '</li>'
2742 + '<br>'
2743 + '<div style="font-weight: bold;">'
2744 + '!tj -clear'
2745 + '</div>'
2746 + '<li style="padding-left: 10px;">'
2747 + 'Stops the tracker as the -stop command, but in addition clears the turnorder'
2748 + '</li>'
2749 + '<br>'
2750 + '<div style="font-weight: bold;">'
2751 + '!tj -pause'
2752 + '</div>'
2753 + '<li style="padding-left: 10px;">'
2754 + 'Pauses the tracker.'
2755 + '</li>'
2756 + '<br>'
2757 + '<div style="font-weight: bold;">'
2758 + '!tj -reset [round#]'
2759 + '</div>'
2760 + '<li style="padding-left: 10px;">'
2761 + 'Reset the tracker\'s round counter to the given round, if none is supplied, it is set to round 1.'
2762 + '</li>'
2763 + '<br>'
2764 + '<div style="font-weight: bold;">'
2765 + '!tj -addstatus [name]:[duration]:[direction]:[message]'
2766 + '</div>'
2767 + '<li style="padding-left: 10px;">'
2768 + 'Add a status to the group of selected tokens, if it does not have the named status.'
2769 + '</li>'
2770 + '<li style="padding-left: 20px;">'
2771 + '<b>name</b> name of the status.'
2772 + '</li>'
2773 + '<li style="padding-left: 20px;">'
2774 + '<b>duration</b> duration of the status (numeric).'
2775 + '</li>'
2776 + '<li style="padding-left: 20px;">'
2777 + '<b>direction</b> + or - direction (+# or -#) indicating the increase or decrease of the the status\' duration when the token\'s turn comes up.'
2778 + '</li>'
2779 + '<li style="padding-left: 20px;">'
2780 + '<b>message</b> optional description of the status. If dice text, ie: 1d4 exist, it\'ll roll this result when the token\'s turn comes up.'
2781 + '</li>'
2782 + '<br>'
2783 + '<div style="font-weight: bold;">'
2784 + '!tj -removestatus [name]'
2785 + '</div>'
2786 + '<li style="padding-left: 10px;">'
2787 + 'Remove a status from a group of selected tokens given the name.'
2788 + '</li>'
2789 + '<br>'
2790 + '<div style="font-weight: bold;">'
2791 + '!tj -edit'
2792 + '</div>'
2793 + '<li style="padding-left: 10px;">'
2794 + 'Edit statuses on the selected tokens'
2795 + '</li>'
2796 + '<br>'
2797 + '<div style="font-weight: bold;">'
2798 + '!tj -addfav [name]:[duration]:[direction]:[message]'
2799 + '</div>'
2800 + '<li style="padding-left: 10px;">'
2801 + 'Add a favorite status for quick application to selected tokens later.'
2802 + '</li>'
2803 + '<br>'
2804 + '<div style="font-weight: bold;">'
2805 + '!tj -listfavs'
2806 + '</div>'
2807 + '<li style="padding-left: 10px;">'
2808 + 'Displays favorite statuses with options to apply or edit.'
2809 + '</li>'
2810 + '<br>'
2811 + '<div style="font-weight: bold;">'
2812 + '!eot'
2813 + '</div>'
2814 + '<li style="padding-left: 10px;">'
2815 + 'Ends a player\'s turn and advances the tracker if the player has control of the current turn\'s token. Player usable command.'
2816 + '</li>'
2817 + '</div>'
2818 + '</div>';
2819
2820 sendFeedback(content);
2821 };
2822
2823 /**
2824 * Send public message
2825 */
2826 var sendPublic = function(msg) {
2827 if (!msg)
2828 {return undefined;}
2829 var content = '/desc ' + msg;
2830 sendChat('',content,null,(flags.archive ? {noarchive:true}:null));
2831 };
2832
2833 /**
2834 * Fake message is fake!
2835 */
2836 var sendFeedback = function(msg) {
2837 var content = '/w GM '
2838 + '<div style="position: absolute; top: 4px; left: 5px; width: 26px;">'
2839 + '<img src="' + fields.feedbackImg + '">'
2840 + '</div>'
2841 + msg;
2842
2843 sendChat(fields.feedbackName,content,null,(flags.archive ? {noarchive:true}:null));
2844 };
2845
2846 /**
2847 * Sends a response
2848 */
2849 var sendResponse = function(pid,msg,as,img) {
2850 if (!pid || !msg)
2851 {return null;}
2852 var player = getObj('player',pid),
2853 to;
2854 if (player) {
2855 to = '/w "' + player.get('_displayname') + '" ';
2856 }
2857 else
2858 {throw ('could not find player: ' + to);}
2859 var content = to
2860 + '<div style="position: absolute; top: 4px; left: 5px; width: 26px;">'
2861 + '<img src="' + (img ? img:fields.feedbackImg) + '">'
2862 + '</div>'
2863 + msg;
2864 sendChat((as ? as:fields.feedbackName),content);
2865 };
2866
2867 var sendResponseError = function(pid,msg,as,img) {
2868 sendResponse(pid,'<span style="color: red; font-weight: bold;">'+msg+'</span>',as,img);
2869 };
2870
2871 /**
2872 * Send an error
2873 */
2874 var sendError = function(msg) {
2875 sendFeedback('<span style="color: red; font-weight: bold;">'+msg+'</span>');
2876 };
2877
2878 /**
2879 * Handle chat message event
2880 */
2881 var handleChatMessage = function(msg) {
2882 var args = msg.content,
2883 senderId = msg.playerid,
2884 selected = msg.selected;
2885
2886 if (msg.type === 'api'
2887 && playerIsGM(senderId)
2888 && args.indexOf('!tj') === 0) {
2889 args = args.replace('!tj','').trim();
2890 if (args.indexOf('-start') === 0) {
2891 doStartTracker();
2892 } else if (args.indexOf('-stop') === 0) {
2893 doStopTracker();
2894 } else if (args.indexOf('-pause') === 0) {
2895 doPauseTracker();
2896 } else if (args.indexOf('-reset') === 0) {
2897 args = args.replace('-reset','').trim();
2898 doResetTurnorder(args);
2899 } else if (args.indexOf('-addstatus') === 0) {
2900 args = args.replace('-addstatus','').trim();
2901 doAddStatus(args,selected);
2902 } else if (args.indexOf('-removestatus') === 0) {
2903 args = args.replace('-removestatus','').trim();
2904 doRemoveStatus(args,selected);
2905 } else if (args.indexOf('-clear') === 0) {
2906 doClearTurnorder();
2907 } else if (args.indexOf('-s_marker') === 0) {
2908 doShowMarkers();
2909 } else if (args.indexOf('-dispmarker') === 0) {
2910 args = args.replace('-dispmarker','').trim();
2911 doDisplayMarkers(args);
2912 } else if (args.indexOf('-marker') === 0) {
2913 args = args.replace('-marker','').trim();
2914 doDirectMarkerApply(args);
2915 } else if (args.indexOf('-disptokenconfig') === 0) {
2916 args = args.replace('-disptokenconfig','').trim();
2917 doDisplayTokenConfig(args);
2918 } else if (args.indexOf('-dispstatusconfig') === 0) {
2919 // dirty fix
2920 args = args.replace('-dispstatusconfig','');
2921 doDisplayStatusConfig(args);
2922 } else if (args.indexOf('-listfav') === 0) {
2923 doDisplayFavConfig();
2924 } else if (args.indexOf('-dispmultistatusconfig') === 0) {
2925 args = args.replace('-dispmultistatusconfig','').trim();
2926 doDisplayMultiStatusConfig(args);
2927 } else if (args.indexOf('-edit_status') === 0) {
2928 args = args.replace('-edit_status','').trim();
2929 doEditStatus(args);
2930 } else if (args.indexOf('-edit_multi_status') === 0) {
2931 args = args.replace('-edit_multi_status','').trim();
2932 doEditMultiStatus(args);
2933 } else if (args.indexOf('-edit') === 0) {
2934 args = args.replace('-edit','').trim();
2935 doMultiEditTokenStatus(selected);
2936 } else if (args.indexOf('-addfav') === 0) {
2937 args = args.replace('-addfav','').trim();
2938 doAddFavorite(args);
2939 } else if (args.indexOf('-applyfav') === 0) {
2940 args = args.replace('-applyfav','').trim();
2941 doApplyFavorite(args,selected);
2942 } else if (args.indexOf('-relay') === 0) {
2943 args = args.replace('-relay','').trim();
2944 doRelay(args,senderId);
2945 } else if (args.indexOf('-help') === 0) {
2946 showHelp();
2947 } else {
2948 sendFeedback('<span style="color: red;">Invalid command " <b>'+msg.content+'</b> "</span>');
2949 showHelp();
2950 }
2951 } else if (msg.type === 'api') {
2952 if (args.indexOf('!eot') === 0) {
2953 doPlayerAdvanceTurn(senderId);
2954 } else if (args.indexOf('!tj -addstatus') === 0) {
2955 args = args.replace('!tj -addstatus','').trim();
2956 doPlayerAddStatus(args,selected,senderId);
2957 } else if (args.indexOf('!tj -relay') === 0) {
2958 args = args.replace('!tj -relay','').trim();
2959 doRelay(args,senderId);
2960 }
2961 }
2962 };
2963
2964 /**
2965 * Handle turn order change event
2966 */
2967 var handleChangeCampaignTurnorder = function(obj,prev) {
2968 handleAdvanceTurn(obj.get('turnorder'),prev.turnorder);
2969 };
2970
2971 var handleChangeCampaignInitativepage = function(obj,prev) {
2972 if (obj.get('initiativepage')) {
2973 prepareTurnorder(obj.get('turnorder'));
2974 } else {
2975 if (flags.clearonclose)
2976 {doClearTurnorder();}
2977 }
2978 };
2979
2980 /**
2981 * Handle Graphic movement events
2982 */
2983 var handleChangeGraphicMovement = function(obj,prev) {
2984 if (!flags.image || flags.tj_state === TJ_StateEnum.STOPPED)
2985 {return;}
2986 var graphic = findTrackerGraphic(),
2987 curToken = findCurrentTurnToken(),
2988 maxsize = 0;
2989
2990 if (!curToken || curToken.get('_id') !== obj.get('_id'))
2991 {return;}
2992
2993 maxsize = Math.max(parseInt(curToken.get('width')),parseInt(curToken.get('height')));
2994 graphic.set('layer','gmlayer');
2995 graphic.set('left',curToken.get('left'));
2996 graphic.set('top',curToken.get('top'));
2997 graphic.set('width',maxsize*fields.trackerImgRatio);
2998 graphic.set('height',maxsize*fields.trackerImgRatio);
2999 if (flags.tj_state === TJ_StateEnum.ACTIVE)
3000 {flags.tj_state = TJ_StateEnum.FROZEN;}
3001 setTimeout(function() {
3002 if (graphic) {
3003 if (curToken.get('layer') === 'gmlayer') {
3004 graphic.set('layer','gmlayer');
3005 toBack(graphic);
3006 } else {
3007 graphic.set('layer','map');
3008 toFront(graphic);
3009 }
3010 if (flags.tj_state === TJ_StateEnum.FROZEN)
3011 {flags.tj_state = TJ_StateEnum.ACTIVE;}
3012 }
3013 },500);
3014 };
3015
3016 /**
3017 * Register and bind event handlers
3018 */
3019 var registerAPI = function() {
3020 on('chat:message',handleChatMessage);
3021 on('change:campaign:turnorder',handleChangeCampaignTurnorder);
3022 on('change:campaign:initiativepage',handleChangeCampaignInitativepage);
3023 on('change:graphic:top',handleChangeGraphicMovement);
3024 on('change:graphic:left',handleChangeGraphicMovement);
3025 on('change:graphic:layer',handleChangeGraphicMovement);
3026 };
3027
3028 return {
3029 init: init,
3030 registerAPI: registerAPI
3031 };
3032
3033}());
3034
3035on("ready", function() {
3036 'use strict';
3037 TrackerJacker.init();
3038 TrackerJacker.registerAPI();
3039});