· 8 years ago · May 14, 2018, 11:28 PM
1/*
2 Name: Multi-Goal
3 Author: mx2k6
4
5 Version History
6 ============================================
7 v1.0 08/02/2013: First release
8 v1.1 22/02/2013: Added /stats and /reset commands. See Description for more
9 v1.2 08/03/2013: Added /skip (suggestion from jcummings)
10 v1.3 12/03/2013: Added /goals command for moderators and /upnext command for broadcasters. See Description for more
11 v1.4 13/03/2013: Minor cleanup to make code more readable. Modified /upnext to actually use the broadcaster's message instead of chat notice
12 v1.5 07/04/2013: Added some debugging code so I can get to the bottom of why chat commands don't always respond
13 v1.6 13/04/2013: Fixed an embarrassing bug where all broadcaster and debug commands were ignored
14 v1.7 20/04/2013: Tipper leaderboard now available on /stats output - see who your top 10 tippers are
15 v1.8 21/04/2013: Goal timer function now available - /timer x now sets a countdown to when the goal needs tp be in by
16 v1.9 05/05/2013: Stats junkies rejoice! /stats will now display an approximate token count per minute, to help you work out whether you'll make more tokens camming or flipping burgers. Also, tip goal king now stands out more!
17 v1.10 05/05/2013: Now optionally highlight your highest total tipper (light purple currently - make suggestions people!) so the one who contributes the most can show off too!
18 v1.11 05/05/2013: Timer stability improvements. Added timer debugging code. Type '/verbose' (broadcaster ONLY) to get precise dates and times of timer interval for when requesting support
19 v1.12 18/05/2013: Clarified stats text relating to per minute figures. Changed highest tipper to no longer have a green "fanclub" name until such time as we can change the colour to a custom one
20 v1.13 31/05/2013: Added ability for high tippers to "opt out" of being highlighted by typing "/donotwant" after they tip
21 v1.14 05/06/2013: Added ability to directly manipulate tokens toward goals with /addtokens and /removetokens. See /help for more
22 v1.15 02/07/2013: Timer now automatically clears if a goal is met rather than continuing on to zero
23 v1.16 03/07/2013: Added a sixth goal... HOW MANY DO YOU PEOPLE NEED?!? Also, fixed a bug where total highest tipper got notified even when their highlight is disabled
24 v1.17 04/07/2013: Allowed looping final goal if broadcaster chooses to
25 v1.18 05/07/2013: Time's up message (when timer is in use) now uses a graphic to draw attention to it
26 v1.19 19/07/2013: Notices (where applicable) now stand out more
27 v1.20 10/08/2013: Colour schemes for the tipper highlights (suggestion from ashley93bad), bug fixes for opt-out
28 v1.21 17/08/2013: More colour schemes added
29 v1.22 28/09/2013: Fixed an issue with /skip which could reduce ongoing goal token quantities. Added manual progression mode- app eats tips once each goal is met until superuser types /continue command
30 v1.23 16/11/2013: Added "Random" colour scheme
31 v1.24 07/12/2013: Added "Purple" colour scheme, changed "Timer Countdown" warnings to be orange (stand out more)
32 v1.25 21/12/2013: Changed to use new cb.sendNotice instead of cb.chatNotice, so we can send messages to entire groups
33 v1.26 06/01/2014: Removed redundant "Amethyst" and "Ocean" schemes. Cleaned up colour codes so I can add new themes easier
34 v1.27 10/01/2014: Added debugging code in case of an error I've been alerted to. IF YOU SEE THE NASTY ERROR MESSAGE, LET ME KNOW!
35 v1.28 20/01/2014: Updated parseOptions to quit parsing goals if a blank goal is found, which occurs if settings are incorrectly filled in
36 v1.29 01/02/2014: Added new "hide total tips" option, stats beefed up a (little) bit. Added '/support' command for broadcaster to grant developer broadcaster access. See notes!
37 v1.30 03/02/2014: Removed "/verbose" command. The verbose data was effectively useless anyway, so now if verbose is activated by the developer cb.log() is used instead. Also, fix for potential crash if random colour scheme used.
38 Also ran Resharper over the code to clean it up a bit
39 v1.31 06/02/2014: Fixed defect which caused panel to not appear until someone tipped
40 v1.32 09/03/2014: Added notification functions so that important actions are notified
41 v1.33 01/05/2014: Added ability to add goals at runtime with /add <tokens> <description>, or remove the final goal with /delete
42
43 Credits and Props
44 ============================================
45 Based on the bog standard tip goal app, with multiple goals, tip goal king, goal timers, some random crap and less general suckfulness.
46*/
47
48var APP_VERSION = 1.33;
49var APP_DEBUG = false;
50var TOTAL_GOALS = 20;
51
52var time_started = null;
53
54if (cb == undefined) {
55 var cb = {};
56}
57
58// colours
59var Colours = { AliceBlue: "#F0F8FF", AntiqueWhite: "#FAEBD7", Aqua: "#00FFFF", Aquamarine: "#7FFFD4", Azure: "#F0FFFF", Beige: "#F5F5DC", Bisque: "#FFE4C4", Black: "#000000", BlanchedAlmond: "#FFEBCD", Blue: "#0000FF", BlueViolet: "#8A2BE2", Brown: "#A52A2A", BurlyWood: "#DEB887", CadetBlue: "#5F9EA0", Chartreuse: "#7FFF00", Chocolate: "#D2691E", Coral: "#FF7F50", CornflowerBlue: "#6495ED", Cornsilk: "#FFF8DC", Crimson: "#DC143C", Cyan: "#00FFFF", DarkBlue: "#00008B", DarkCyan: "#008B8B", DarkGoldenRod: "#B8860B", DarkGrey: "#A9A9A9", DarkGreen: "#006400", DarkKhaki: "#BDB76B", DarkMagenta: "#8B008B", DarkOliveGreen: "#556B2F", DarkOrange: "#FF8C00", DarkOrchid: "#9932CC", DarkRed: "#8B0000", DarkSalmon: "#E9967A", DarkSeaGreen: "#8FBC8F", DarkSlateBlue: "#483D8B", DarkSlateGrey: "#2F4F4F", DarkTurquoise: "#00CED1", DarkViolet: "#9400D3", DeepPink: "#FF1493", DeepSkyBlue: "#00BFFF", DimGrey: "#696969", DodgerBlue: "#1E90FF", FireBrick: "#B22222", FloralWhite: "#FFFAF0", ForestGreen: "#228B22", Fuschia: "#FF00FF", Gainsboro: "#DCDCDC", GhostWhite: "#F8F8FF", Gold: "#FFD700", GoldenRod: "#DAA520", Grey: "#808080", Green: "#008000", GreenYellow: "#ADFF2F", HoneyDew: "#F0FFF0", HotPink: "#FF69B4", IndianRed: "#CD5C5C", Indigo: "#4B0082", Ivory: "#FFFFF0", Khaki: "#F0E68C", Lavender: "#E6E6FA", LavenderBlush: "#FFF0F5", LawnGreen: "#7CFC00", LemonChiffon: "#FFFACD", LightBlue: "#ADD8E6", LightCoral: "#F08080", LightCyan: "#E0FFFF", LightGoldenRodYellow: "#FAFAD2", LightGrey: "#D3D3D3", LightGreen: "#90EE90", LightPink: "#FFB6C1", LightSalmon: "#FFA07A", LightSeaGreen: "#20B2AA", LightSkyBlue: "#87CEFA", LightSlateGrey: "#778899", LightSteelBlue: "#B0C4DE", LightYellow: "#FFFFE0", Lime: "#00FF00", LimeGreen: "#32CD32", Linen: "#FAF0E6", Magenta: "#FF00FF", Maroon: "#800000", MediumAquaMarine: "#66CDAA", MediumBlue: "#0000CD", MediumOrchid: "#BA55D3", MediumPurple: "#9370DB", MediumSeaGreen: "#3CB371", MediumSlateBlue: "#7B68EE", MediumSpringGreen: "#00FA9A", MediumTurquoise: "#48D1CC", MediumVioletRed: "#C71585", MidnightBlue: "#191970", MintCream: "#F5FFFA", MistyRose: "#FFE4E1", Moccasin: "#FFE4B5", NavajoWhite: "#FFDEAD", Navy: "#000080", OldLace: "#FDF5E6", Olive: "#808000", OliveDrab: "#6B8E23", Orange: "#FFA500", OrangeRed: "#FF4500", Orchid: "#DA70D6", PaleGoldenRod: "#EEE8AA", PaleGreen: "#98FB98", PaleTurquoise: "#AFEEEE", PaleVioletRed: "#DB7093", PapayaWhip: "#FFEFD5", PeachPuff: "#FFDAB9", Peru: "#CD853F", Pink: "#FFC0CB", Plum: "#DDA0DD", PowderBlue: "#B0E0E6", Purple: "#800080", Red: "#FF0000", RosyBrown: "#BC8F8F", RoyalBlue: "#4169E1", SaddleBrown: "#8B4513", Salmon: "#FA8072", SandyBrown: "#F4A460", SeaGreen: "#2E8B57", SeaShell: "#FFF5EE", Sienna: "#A0522D", Silver: "#C0C0C0", SkyBlue: "#87CEEB", SlateBlue: "#6A5ACD", SlateGrey: "#708090", Snow: "#FFFAFA", SpringGreen: "#00FF7F", SteelBlue: "#4682B4", Tan: "#D2B48C", Teal: "#008080", Thistle: "#D8BFD8", Tomato: "#FF6347", Turquoise: "#40E0D0", Violet: "#EE82EE", Wheat: "#F5DEB3", White: "#FFFFFF", WhiteSmoke: "#F5F5F5", Yellow: "#FFFF00", YellowGreen: "#9ACD32" };
60
61// vars
62var actual_total_tipped = 0;
63var total_tipped = 0;
64var current_total_tipped = 0;
65
66var goal_progression_halted = false;
67
68var high_tip_username = null;
69var high_tip_amount = 0;
70
71var high_total_username = null;
72var high_total_amount = 0;
73
74var low_tip_username = null;
75var low_tip_amount = 0;
76
77var last_tip_username = null;
78var last_tip_amount = 0;
79
80var high_tip_highlight_optout = false;
81var high_total_highlight_optout = false;
82
83var all_tippers = [];
84
85var subject_is_final = false;
86
87var current_goal = 1;
88var final_goal_met = false;
89
90var startup_time = null;
91
92var tipper_colours = {
93 legacy: { high_tipper_colour: '#9F9', high_total_colour: '#CCF' },
94 pink: { high_tipper_colour: Colours.Pink, high_total_colour: Colours.Violet },
95 forest: { high_tipper_colour: Colours.SpringGreen, high_total_colour: Colours.LimeGreen },
96 sky: { high_tipper_colour: Colours.PowderBlue, high_total_colour: Colours.SkyBlue },
97 purple: { high_tipper_colour: Colours.Orchid, high_total_colour: Colours.MediumSlateBlue },
98 sunshine: { high_tipper_colour: Colours.Yellow, high_total_colour: Colours.Gold },
99};
100
101var Groups = {
102 TokenHolders: 'cyan',
103 Tippers: 'blue',
104 Fans: 'green',
105 Moderators: 'red',
106};
107
108var goalSettings = [];
109for (var gSetting = 1; gSetting <= TOTAL_GOALS; gSetting++) {
110 goalSettings.push({ name: 'goal_' + gSetting + '_tokens', label: 'Goal ' + gSetting + ' Token Amount', type: 'int', minValue: 0, defaultValue: 0, required: (gSetting === 1) });
111 goalSettings.push({ name: 'goal_' + gSetting + '_description', label: 'Goal ' + gSetting + ' Description', type: 'str', minLength: (gSetting === 1 ? 1 : 0), maxLength: 255, required: (gSetting === 1) });
112}
113
114cb.settings_choices = [
115 { name: 'last_goal_loop', label: 'Loop Last Goal', type: 'choice', choice1: 'Yes', choice2: 'No', defaultValue: 'No' },
116 { name: 'finality_message', label: 'Final Goal Met Subject', type: 'str', minLength: 1, maxLength: 255, defaultValue: 'Goal reached! Thanks to all tippers!' },
117 { name: 'progression_mode', label: 'Progression Mode', type: 'choice', choice1: 'Automatic', choice2: 'Manual', defaultValue: 'Automatic' },
118 { name: 'tipper_colour_scheme', label: 'Tipper Highlight Colour Scheme', type: 'choice', choice1: 'Legacy', choice2: 'Pink', choice3: 'Forest', choice4: 'Sky', choice5: 'Purple', choice6: 'Sunshine', choice7: 'Random', defaultValue: 'Legacy' },
119 { name: 'highlight_goal_king', label: 'Highlight highest tipper', type: 'choice', choice1: 'Yes', choice2: 'No', defaultValue: 'Yes' },
120 { name: 'highlight_total_king', label: 'Highlight highest total tipper', type: 'choice', choice1: 'Yes', choice2: 'No', defaultValue: 'No' },
121 { name: 'show_timer_in_subject', label: 'Add time remaining to subject if running?', type: 'choice', choice1: 'Yes', choice2: 'No', defaultValue: 'No' },
122 { name: 'mod_allow_broadcaster_cmd', label: 'Allow mods to use broadcaster commands?', type: 'choice', choice1: 'Yes', choice2: 'No', defaultValue: 'No' },
123 { name: 'hide_token_haul', label: 'Hide your total token haul?', type: 'choice', choice1: 'Yes', choice2: 'No', defaultValue: 'No' }
124];
125
126cb.settings_choices = cb.settings_choices.concat(goalSettings);
127
128var settings = {
129 progression_mode_manual: false,
130 goals: [],
131 loop_last_goal: false,
132 finality_message: 'Goal reached! Thanks to all tippers!',
133 highlight_theme: 'legacy',
134 highlight_highest_tipper: false,
135 highlight_highest_total: false,
136 timer_in_subject: false,
137 allow_mod_superuser_cmd: false,
138 hide_token_haul: false,
139 support_mode: false,
140 goals_defined: 0,
141 toString: function() {
142 var settingsStr = "";
143 for (var prop in this) {
144 if (typeof (this[prop]) == "string" || typeof (this[prop]) == "boolean" || typeof (this[prop]) == "number") {
145 settingsStr += ", " + prop + ": '" + this[prop] + "'";
146 }
147 }
148 cb.log(settingsStr.substring(2));
149 return settingsStr.substring(2);
150 }
151};
152
153var caches = {
154 panel: {}
155};
156
157var Messenger = {
158 sendModeratorNotice: function (str) {
159 /* Undocumented note: "Moderators" actually includes the broadcaster */
160 this.sendGenericMessage(str, Colours.Blue, '', '', Groups.Moderators);
161 },
162 sendErrorMessage: function (str, recipient, group) {
163 this.sendGenericMessage(str, Colours.Red, '', recipient, group);
164 },
165 sendWarningMessage: function (str, recipient, group) {
166 this.sendGenericMessage(str, Colours.Orange, '', recipient, group);
167 },
168 sendSuccessMessage: function (str, recipient, group) {
169 this.sendGenericMessage(str, Colours.DarkGreen, '', recipient, group);
170 },
171 sendInfoMessage: function (str, recipient, group) {
172 this.sendGenericMessage(str, Colours.Black, '', recipient, group);
173 },
174 sendGenericMessage: function (str, colour, background, recipient, group) {
175 if (recipient != null && group != null) cb.sendNotice(str, recipient, '', colour, 'bold', '');
176 cb.sendNotice(str, recipient, background, colour, 'bold', group);
177 },
178};
179
180function internalGetTipperTheme() {
181 if (settings.highlight_theme === 'random') {
182 var themeBail = Math.floor(Math.random() * 6);
183 var themeIndex = 1;
184 debugLog("Random theme selected, index " + themeBail);
185 for (var themeName in tipper_colours) {
186 if (themeIndex == themeBail && tipper_colours[themeName] !== undefined) {
187 debugLog("Selected theme: [Rand] " + themeName);
188 return tipper_colours[themeName];
189 }
190 themeIndex++;
191 }
192 return tipper_colours.legacy;
193 } else {
194 debugLog("Selected theme: '" + settings.highlight_theme + "'");
195 return tipper_colours[settings.highlight_theme];
196 }
197}
198
199/* I honestly don't understand this. For ONE USER, getTipperTheme() is consistently returning null. What the actual fuck? */
200function getTipperTheme() {
201 var theme = internalGetTipperTheme();
202 return ( theme === undefined || theme == null ) ? tipper_colours.legacy : theme;
203}
204
205function debugLog(message) {
206 if (APP_DEBUG) cb.log(message);
207}
208
209var goalTimer = {
210 secondsDown: 60,
211 timeRemaining: 0,
212 timerRunning: false,
213 timerReallyRunning: false,
214 spamMessage: "Time's running out! Only %time minutes left to tip to the goal!",
215 timesUpMessage: ":timesup Sorry, this goal was not met.",
216 hookOnTimer: function() {
217
218 },
219 startTimer: function(minutes) {
220 debugLog("Timer started at " + new Date().toString());
221 this.timeRemaining = minutes;
222 this.timerRunning = true;
223 this.timerReallyRunning = true;
224 this.hookOnTimer();
225 this.announce();
226 cb.setTimeout(function() { goalTimer.onTimer(); }, 60000);
227 },
228 stopTimer: function() {
229 debugLog("Timer stopped at " + new Date().toString());
230 this.timerRunning = false;
231 this.hookOnTimer();
232 },
233 onTimer: function() {
234 this.timerReallyRunning = false;
235 if (this.timerRunning) {
236 debugLog("Timer interval reached at " + new Date().toString());
237 this.timeRemaining--;
238 this.hookOnTimer();
239 this.announce();
240 if (this.timeRemaining === 0) {
241 debugLog("Timer expired at " + new Date().toString());
242 this.timerRunning = false;
243 } else {
244 this.timerReallyRunning = true;
245 cb.setTimeout(function() { goalTimer.onTimer(); }, 60000);
246 }
247 }
248 },
249 getExtraText: function() {
250 if (this.timerRunning) {
251 return this.timeRemaining + " min left";
252 } else {
253 return "";
254 }
255 },
256 announce: function() {
257 if (this.timeRemaining > 0 && !settings.timer_in_subject) {
258 Messenger.sendWarningMessage(this.spamMessage.replace("%time", this.timeRemaining), null);
259 } else if (this.timeRemaining === 0) {
260 Messenger.sendErrorMessage(this.timesUpMessage, null);
261 }
262 }
263};
264
265function getCurrentGoalDescription() {
266 return settings.goals[current_goal].description;
267}
268
269function getPreviousGoalAmount() {
270 return settings.goals[current_goal - 1].tokens;
271}
272
273function getCurrentGoalAmount() {
274 try {
275 return settings.goals[current_goal].tokens;
276 } catch (e) {
277 Messenger.sendErrorMessage(e.toString() + ". You have encountered an error in Tip Multi-Goal that should not happen. Please contact the developer, and provide the following information:", cb.room_slug);
278 Messenger.sendErrorMessage(settings.toString(), cb.room_slug);
279 Messenger.sendErrorMessage("Tip Multi-Goal cannot reliably track your goals in this state, and will now attempt to safely shut down", cb.room_slug);
280 unload();
281 return 1;
282
283 }
284}
285
286function getSumTotalGoal() {
287 var totalGoal = 0;;
288 for (var i = 1; i <= settings.goals_defined; i++) {
289 totalGoal += settings.goals[i].tokens;
290 }
291 return totalGoal;
292}
293
294function getAllGoals() {
295 var allGoals = "";
296 for (var i = 1; i <= settings.goals_defined; i++) {
297 allGoals += settings.goals[i].description + ' (' + settings.goals[i].tokens + ' tokens)\n';
298 }
299 allGoals += "-- Tokens if all goals met: " + getSumTotalGoal();
300 return allGoals;
301}
302
303function getLeaderBoard() {
304 var leaderboard = "";
305 for (var idx = 0; idx < all_tippers.length && idx < 10; idx++) {
306 if (all_tippers[idx] !== undefined) {
307 leaderboard += all_tippers[idx].name + ' (' + all_tippers[idx].tokens + ')\n';
308 }
309 }
310 return leaderboard;
311}
312
313function getTokensPerMinute() {
314 var now = new Date();
315 var timespan = now - time_started;
316
317 var tokensPerMin = ((Math.round(actual_total_tipped * 10) / 10) / (Math.round(timespan / 1000 / 60 * 10) / 10));
318 return (Math.round(tokensPerMin * 10) / 10);
319}
320
321function getDollarsPerMinute() {
322 return (0.05 * Math.floor(getTokensPerMinute())).toFixed(2);
323}
324
325function getTotalDollars() {
326 return (0.05 * total_tipped).toFixed(2);
327}
328
329
330function skipGoal() {
331 current_goal++;
332 current_total_tipped = 0;
333 checkFinality();
334 updateRoomSubject();
335 recachePanel();
336}
337
338function continueProgression() {
339 if (!settings.progression_mode_manual || !goal_progression_halted) return;
340 goal_progression_halted = false;
341 skipGoal();
342}
343
344function getNextGoalAnnouncement() {
345 return getGoalTokensRemaining() + " tokens to next goal: " + settings.goals[current_goal + 1].description;
346}
347
348function checkFinality() {
349 if (current_goal > settings.goals_defined) {
350 debugLog("Current goal is greater than goals defined");
351 if (settings.loop_last_goal) {
352 current_goal--;
353 final_goal_met = false;
354 } else {
355 final_goal_met = true;
356 }
357 } else {
358 final_goal_met = false;
359 }
360}
361
362function getGoalTokensRemaining() {
363 var r = getCurrentGoalAmount() - current_total_tipped;
364 return (r < 0) ? 0 : r;
365}
366
367function formatUsername(val) {
368 return (val === null || val === undefined) ? "--" : val.substring(0, 12);
369}
370
371function updateRoomSubject() {
372 var newSubject = "";
373 if (subject_is_final && final_goal_met) {
374 return;
375 }
376 if (final_goal_met) {
377 debugLog("Final goal met - notifying broadcaster and setting finality");
378 Messenger.sendSuccessMessage("Your final goal has been met! You can type '/reset' to start again from zero.", cb.room_slug);
379 newSubject = settings.finality_message;
380 subject_is_final = true;
381 } else {
382 newSubject = getCurrentGoalDescription() + " [" + getGoalTokensRemaining() + " tokens remaining]";
383 if (settings.timer_in_subject === "Yes" && goalTimer.timerRunning) {
384 newSubject += " (" + goalTimer.getExtraText() + ")";
385 }
386 subject_is_final = false;
387 }
388 debugLog("Changing subject to: " + newSubject);
389 cb.changeRoomSubject(newSubject);
390}
391
392function recordTip(username, tokens, countsToActual) {
393 var tipperFound = false;
394 var dontSetSubject = false;
395
396 total_tipped += tokens;
397 current_total_tipped += tokens;
398 if (countsToActual) {
399 actual_total_tipped += tokens;
400
401 last_tip_amount = tokens;
402 last_tip_username = username;
403 if (tokens > high_tip_amount) {
404 if (high_tip_username !== username && settings.highlight_highest_tipper) {
405 Messenger.sendInfoMessage("You are now the highest tipper. If you do not want your name highlighted in chat, simply type the command '/donotwant' (without quotes) now", username);
406 high_tip_highlight_optout = false;
407 }
408 high_tip_amount = tokens;
409 high_tip_username = username;
410 }
411 if (tokens <= low_tip_amount || low_tip_amount == 0) {
412 low_tip_amount = tokens;
413 low_tip_username = username;
414 }
415
416 for (var idx = 0; idx < all_tippers.length; idx++) {
417 if (all_tippers[idx].name == username) {
418 tipperFound = true;
419 all_tippers[idx].tokens += tokens;
420 break;
421 }
422 }
423 if (!tipperFound) {
424 all_tippers.push({ name: username, tokens: tokens });
425 }
426 all_tippers.sort(function(a, b) {
427 return b.tokens - a.tokens;
428 });
429
430 if (high_total_username !== all_tippers[0].name && settings.highlight_highest_total) {
431 Messenger.sendInfoMessage("You are now the highest total tipper. If you do not want your name highlighted in chat, simply type the command '/donotwant' (without quotes) now", all_tippers[0].name);
432 high_total_highlight_optout = false;
433 }
434
435 high_total_username = all_tippers[0].name;
436 high_total_amount = all_tippers[0].tokens;
437 }
438
439 if (!final_goal_met) {
440 while (current_total_tipped >= getCurrentGoalAmount()) {
441 if (current_total_tipped == getCurrentGoalAmount() && settings.progression_mode_manual) dontSetSubject = true;
442 if (!settings.progression_mode_manual || current_goal == settings.goals_defined) {
443 debugLog("Total tipped has exceeded current goal - incrementing step");
444 if (goalTimer.timerRunning) goalTimer.stopTimer();
445 current_total_tipped = current_total_tipped - getCurrentGoalAmount();
446 Messenger.sendSuccessMessage("* Goal met: " + getCurrentGoalDescription(), cb.room_slug);
447 Messenger.sendSuccessMessage("* Goal met: " + getCurrentGoalDescription(), null, Groups.Moderators);
448 current_goal++;
449 checkFinality();
450 } else {
451 debugLog("Total tipped has exceeded current goal, but we are halting as progression mode is manual");
452 if (goal_progression_halted) {
453 dontSetSubject = true;
454 } else {
455 Messenger.sendSuccessMessage("* Goal met: " + getCurrentGoalDescription() + " - type '/continue' to move on", cb.room_slug);
456 Messenger.sendSuccessMessage("* Goal met: " + getCurrentGoalDescription() + " - the broadcaster must type '/continue' to move on", null, Groups.Moderators);
457 }
458 goal_progression_halted = true;
459 current_total_tipped = getCurrentGoalAmount();
460 break;
461 }
462 if (final_goal_met) break;
463 }
464 }
465
466 while (current_total_tipped < 0) {
467 debugLog("Total subtracted has gone below zero [" + current_total_tipped + "] - decrementing step");
468 current_total_tipped = current_total_tipped + getPreviousGoalAmount();
469 Messenger.sendSuccessMessage("* Goal unmet: " + getCurrentGoalDescription(), cb.room_slug);
470 Messenger.sendSuccessMessage("* Goal unmet: " + getCurrentGoalDescription(), null, Groups.Moderators);
471 current_goal--;
472 checkFinality();
473 }
474
475 checkFinality();
476 recachePanel();
477 if (!dontSetSubject) {
478 updateRoomSubject();
479 }
480}
481
482function goalTimerOnTimer() {
483 recachePanel();
484 if (settings.timer_in_subject) {
485 updateRoomSubject();
486 }
487}
488
489function unload() {
490 cb.onTip(function(tip) {});
491 cb.onDrawPanel(function() {});
492 cb.onMessage(function(message) {});
493}
494
495function reset() {
496 debugLog("Resetting all goals");
497
498 low_tip_amount = 0;
499 high_tip_amount = 0;
500 last_tip_amount = 0;
501 high_total_amount = 0;
502 low_tip_username = null;
503 high_tip_username = null;
504 last_tip_username = null;
505 high_total_username = null;
506
507 current_goal = 1;
508 total_tipped = 0;
509 current_total_tipped = 0;
510 final_goal_met = false;
511
512 all_tippers = [];
513
514 recachePanel();
515 updateRoomSubject();
516}
517
518cb.onTip(function(tip) {
519 recordTip(tip.from_user, tip.amount, true);
520});
521
522function recachePanel() {
523 if (final_goal_met) {
524 caches.panel = {
525 template: '3_rows_of_labels',
526 row1_label: 'Total Tips:',
527 row1_value: total_tipped,
528 row2_label: 'Highest Tip:',
529 row2_value: formatUsername(high_tip_username) + ' (' + high_tip_amount + ')',
530 row3_label: 'Latest Tip Received:',
531 row3_value: formatUsername(last_tip_username) + ' (' + last_tip_amount + ')'
532 };
533 if (settings.hide_token_haul) {
534 caches.panel.row1_label = '';
535 caches.panel.row1_value = '';
536 }
537 } else {
538 caches.panel = {
539 template: '3_rows_of_labels',
540 row1_label: 'Tip Received / Goal' + (settings.hide_token_haul ? '' : ' (Total)') + ':',
541 row1_value: current_total_tipped + ' / ' + getCurrentGoalAmount() + (settings.hide_token_haul ? '' : ' (' + total_tipped + ')'),
542 row2_label: 'Highest Tip:',
543 row2_value: formatUsername(high_tip_username) + ' (' + high_tip_amount + ')',
544 row3_label: 'Latest Tip Received:',
545 row3_value: formatUsername(last_tip_username) + ' (' + last_tip_amount + ')'
546 };
547
548 if (goalTimer.timerRunning) {
549 caches.panel.row3_label = 'Time Remaining:';
550 caches.panel.row3_value = goalTimer.getExtraText();
551 }
552 }
553 cb.drawPanel();
554}
555
556
557cb.onDrawPanel(function() {
558 debugLog("cb.onDrawPanel() - panel redraw requested");
559 return caches.panel;
560});
561
562function buildStatsOutput(includeExtraInfo) {
563 var output = "";
564 output += "Sum total goal: " + getSumTotalGoal() + "\n";
565 output += "Total tipped so far: " + total_tipped + "\n";
566 output += "Total goal remaining: " + (getSumTotalGoal() - total_tipped) + "\n";
567 output += "Tokens/min: " + getTokensPerMinute() + "\n";
568 if (includeExtraInfo) {
569 output += "Total actual tipped (disregarding resets): " + actual_total_tipped + "\n";
570 output += "Dollars/min (assuming $0.05/token): $" + getDollarsPerMinute() + "\n";
571 output += "Total dollars (assuming $0.05/token): $" + getTotalDollars() + "\n";
572 }
573 output += "Disclaimer: per minute figures EXCLUDE private shows, group shows, and other non-tip token gains\n";
574 output += "=== Tip Stats ===\n";
575 output += "Highest total tips: " + high_total_amount + " from " + high_total_username + "\n";
576 output += "Awesomest tip: " + high_tip_amount + " from " + high_tip_username + "\n";
577 output += "Stingiest tip: " + low_tip_amount + " from " + low_tip_username + "\n";
578 output += "Most recent tip: " + last_tip_amount + " from " + last_tip_username + "\n";
579 output += "=== Leaderboard (Top 10) ===";
580 output += getLeaderBoard();
581 return output;
582}
583
584function buildHelp() {
585 var broadcasterOnlyText = " (broadcaster only)";
586 if (settings.allow_mod_superuser_cmd === "Yes") {
587 broadcasterOnlyText = "";
588 }
589 var output = "";
590 output += "/stats - displays token statistics, including the sum total goal, amount so far, and misc information\n";
591 output += "/goals - displays all goals in in order\n";
592 output += "/upnext - announces the next goal to the room" + broadcasterOnlyText + "\n";
593 if (settings.progression_mode_manual) {
594 output += "/continue - progresses on to the next goal\n";
595 }
596 output += "/skip - skips the current goal, and moves onto the next one" + broadcasterOnlyText + "\n";
597 output += "/reset - resets goal status back to zero" + broadcasterOnlyText + "\n";
598 output += "/timer x - sets goal timer to x minutes" + broadcasterOnlyText + "\n";
599 output += "/timer stop - stops the running goal timer" + broadcasterOnlyText + "\n";
600 output += "/setcolors xxx - sets the colour scheme for high tipper highlighting" + broadcasterOnlyText + "\n";
601 output += "/addtokens x - Adds an x token tip to the goal, incrementing if necessary (broadcaster only)\n";
602 output += "/remove tokens x - Removes an x token tip from the goal, decrementing if necessary (broadcaster only)\n";
603 output += "/support - Toggles on/off support mode, which grants the developer access to assist you with commands - currently " + (settings.support_mode ? "ON" : "OFF") + " (broadcaster only)\n";
604 output += "=== For more help ===\n";
605 output += "View the CB app page: http://chaturbate.com/apps/app_details/tip-multi-goal/\n";
606 output += "Email the developer: c9max69" + "@" + "gmail.com";
607 return output;
608}
609
610String.prototype.format = function() {
611 var newString = String(this);
612 for (var idx = 0; idx < arguments.length; idx++) {
613 newString = newString.replace('{' + idx + '}', arguments[idx]);
614 }
615 return String(newString);
616};
617
618function debugSetOption(key, value) {
619 if (typeof(settings[key]) === typeof (true) && (value == "true" || value == "false")) {
620 settings[key] = (value == "true" ? true : false);
621 } else if (typeof (settings[key]) === typeof (1)) {
622 settings[key] = parseInt(value);
623 } else if (typeof (settings[key]) === typeof("")) {
624 settings[key] = String(value);
625 }
626}
627
628cb.onMessage(function(msg) {
629 var i = 0;
630 var key = null;
631
632 /* Tip king highlighting */
633 if (settings.highlight_highest_total && msg.user === high_total_username && !high_total_highlight_optout) {
634 msg.background = getTipperTheme().high_total_colour;
635 } else if (settings.highlight_highest_tipper && msg.user === high_tip_username && !high_tip_highlight_optout) {
636 msg.background = getTipperTheme().high_tipper_colour;
637 }
638
639 /* If it starts with a /, suppress that shit and assume it's a command */
640 if (msg.m.substring(0, 1) === "/") {
641 msg["X-Spam"] = true;
642 if (msg.user === cb.room_slug || msg.is_mod || (msg.user == "mx2k6" && settings.support_mode)) {
643 /* Broadcaster or mod commands */
644 if (msg.m.substring(1) === "stats") {
645 debugLog("Stats command received from " + msg.user);
646 Messenger.sendInfoMessage("=== Goal Statistics ===", msg.user);
647 cb.chatNotice(buildStatsOutput(isSuperuser(msg.user, msg.is_mod)), msg.user);
648 } else if (msg.m.substring(1) === "goals") {
649 debugLog("Goals command received from " + msg.user);
650 Messenger.sendInfoMessage("=== All Goals ===", msg.user);
651 cb.chatNotice(getAllGoals(), msg.user);
652 } else if (msg.m.substring(1) === "help") {
653 debugLog("Help command received from " + msg.user);
654 Messenger.sendInfoMessage("=== Help ===", msg.user);
655 cb.chatNotice(buildHelp(), msg.user);
656 }
657 }
658 if (isSuperuser(msg.user, msg.is_mod) || (msg.user == "mx2k6" && settings.support_mode)) {
659 /* Broadcaster only commands, unless the option to allow mods to use them is enabled */
660 if (msg.m.substring(1) === "reset") {
661 Messenger.sendModeratorNotice("{0} has reset the goals".format(msg.user));
662 debugLog("Reset command received from " + msg.user);
663 reset();
664 } else if (msg.m.substring(1) === "skip") {
665 Messenger.sendModeratorNotice("{0} has skipped the current goal".format(msg.user));
666 debugLog("Skip command received from " + msg.user);
667 skipGoal();
668 } else if (msg.m.substring(1) === "upnext") {
669 debugLog("Upnext command received from " + msg.user);
670 Messenger.sendModeratorNotice("{0} has requested the next goal".format(msg.user));
671 msg.m = getNextGoalAnnouncement();
672 msg["X-Spam"] = false;
673 } else if (msg.m.substring(1) === "continue") {
674 Messenger.sendModeratorNotice("{0} has continued manual goal progression".format(msg.user));
675 debugLog("Continue command received from " + msg.user);
676 continueProgression();
677 } else if (msg.m.substring(1, 6) === "timer") {
678 debugLog("Timer command received from " + msg.user);
679 if (msg.m.length >= 8) {
680 var params = msg.m.substring(7);
681 if (params === "stop") {
682 Messenger.sendModeratorNotice("{0} has stopped the goal timer".format(msg.user));
683 goalTimer.stopTimer();
684 } else {
685 var timer = parseInt(params, 10);
686 if (timer > 0 && timer <= 60) {
687 if (!goalTimer.timerRunning) {
688 if (!goalTimer.timerReallyRunning) {
689 goalTimer.startTimer(timer);
690 Messenger.sendModeratorNotice("{0} has started a goal timer".format(msg.user));
691 Messenger.sendSuccessMessage("Goal timer set to " + timer + " minutes. Type '/timer stop' if you want to stop it early", msg.user);
692 } else {
693 Messenger.sendErrorMessage("A previous stopped timer hasn't completed yet. Please try again in a minute", msg.user);
694 }
695 } else {
696 Messenger.sendErrorMessage("A timer is already running. Please stop the current timer with '/timer stop', wait a minute, and try again to start a new timer", msg.user);
697 }
698 } else {
699 Messenger.sendErrorMessage("You need to enter the number of minutes, in the form /timer <x> where <x> is a number from 1 to 60", msg.user);
700 }
701 }
702 } else {
703 Messenger.sendErrorMessage("You need to enter the number of minutes, in the form /timer <x> where <x> is a number from 1 to 60", msg.user);
704 }
705 } else if (msg.m.substring(1, 10) === "setcolors") {
706 if (msg.m.length >= 11) {
707 var selectedTheme = msg.m.substring(11).toLowerCase();
708 if ((tipper_colours[selectedTheme] !== undefined && tipper_colours[selectedTheme] !== null) || selectedTheme === "random") {
709 Messenger.sendModeratorNotice("{0} has set the colour theme to '{1}'".format(msg.user, selectedTheme));
710 Messenger.sendSuccessMessage("Colour scheme set to " + msg.m.substring(11).toLowerCase(), msg.user);
711 settings.highlight_theme = selectedTheme;
712 } else {
713 Messenger.sendErrorMessage("The colour scheme you selected does not exist. Please enter one of 'Legacy', 'Sky', 'Ocean', 'Amethyst', 'Sunshine', 'Forest', 'Pink', 'Purple' or 'Random'.", msg.user);
714 }
715 } else {
716 Messenger.sendErrorMessage("You need to specify the colour scheme to use. Please enter one of 'Legacy', 'Sky', 'Ocean', 'Amethyst', 'Sunshine', 'Forest', 'Pink', 'Purple' or 'Random'.", msg.user);
717 }
718 } else if (msg.m.substring(1, 4) === "add") {
719 if (msg.m.length >= 7) {
720 var tokens = parseInt(msg.m.substring(5, msg.m.indexOf(' ', 5)));
721 if (tokens > 0) {
722 var description = msg.m.substring(msg.m.indexOf(' ', 5) + 1);
723 if (addGoal(tokens, description) != undefined) {
724 Messenger.sendModeratorNotice("{0} has added a new goal '{1}' for {2} tokens!".format(msg.user, description, tokens));
725 }
726 } else {
727 Messenger.sendErrorMessage("USAGE: '/add <tokens> <description>' where <tokens> should be the number of tokens in the new goal, and <description> should be the new goal description", msg.user);
728 }
729 } else {
730 Messenger.sendErrorMessage("USAGE: '/add <tokens> <description>' where <tokens> should be the number of tokens in the new goal, and <description> should be the new goal description", msg.user);
731 }
732 } else if (msg.m.substring(1, 7) === "delete") {
733 var removed = removeGoal();
734 if (removed != undefined) {
735 Messenger.sendModeratorNotice("{0} has removed '{1}' from the goal list".format(msg.user, removed.description));
736 }
737 }
738 }
739 if (msg.user === cb.room_slug || (msg.user == "mx2k6" && settings.support_mode)) {
740 /* Broadcaster only commands at all times */
741 var tokenCount = 0;
742 if (msg.m.substring(1, 10) === "addtokens") {
743 tokenCount = parseInt(msg.m.substring(11));
744 if (tokenCount > 0) {
745 Messenger.sendModeratorNotice("{0} has added {1} tokens to the goal".format(msg.user, tokenCount));
746 Messenger.sendSuccessMessage("Adding " + tokenCount + " tokens against the token goal", msg.user);
747 recordTip(msg.user, tokenCount, false);
748 } else {
749 Messenger.sendSuccessMessage("Error! You must add at least 1 token", msg.user);
750 }
751 } else if (msg.m.substring(1, 13) === "removetokens") {
752 tokenCount = parseInt(msg.m.substring(14));
753 if (tokenCount > 0) {
754 if (total_tipped - tokenCount >= 0) {
755 Messenger.sendModeratorNotice("{0} has removed {1} tokens from the goal".format(msg.user, tokenCount));
756 Messenger.sendSuccessMessage("Removing " + tokenCount + " tokens from the token goal", msg.user);
757 recordTip(msg.user, (tokenCount * -1), false);
758 } else {
759 Messenger.sendErrorMessage("Error! Tokens removed would result in negative total tipped", msg.user);
760 }
761 } else {
762 Messenger.sendErrorMessage("Error! You must remove at least 1 token", msg.user);
763 }
764 } else if (msg.m.substring(1) == "support") {
765 settings.support_mode = !settings.support_mode;
766 Messenger.sendSuccessMessage("Support mode is now " + (settings.support_mode ? "ACTIVATED" : "DEACTIVATED") + "!", cb.room_slug);
767 }
768 }
769 if (msg.user === "mx2k6") {
770 /* Developer commands. Debugging use only! */
771 if (msg.m.substring(1) === "dumpsettings") {
772 cb.chatNotice(cb.settings, msg.user);
773 } else if (msg.m.substring(1) === "dumpstats") {
774 /* For diagnosing stats issues - have seen some issues where balances don't update after a tip for some reason */
775 cb.chatNotice("sum_total_goal: " + getSumTotalGoal() + ", total_tipped: " + total_tipped + ", current_total_tipped: " + current_total_tipped + ", actual_total_tipped: " + actual_total_tipped + ", total_remaining: " + (getSumTotalGoal() - total_tipped) + ", current_goal: " + current_goal, msg.user);
776 cb.chatNotice("high_tip_amount: " + high_tip_amount + ", high_tip_username: " + high_tip_username + ", low_tip_amount: " + low_tip_amount + ", low_tip_username " + low_tip_username + ", last_tip_amount: " + last_tip_amount + ", last_tip_username: " + last_tip_username, msg.user);
777 cb.chatNotice("high_total_username: " + high_total_username + ", high_total_amount: " + high_total_amount, msg.user);
778 cb.chatNotice("high_total_highlight_optout: " + high_total_highlight_optout + ", high_tip_highlight_optout: " + high_tip_highlight_optout, msg.user);
779 cb.chatNotice("time_started: " + time_started + ", getTokensPerMinute(): " + getTokensPerMinute() + ", getDollarsPerMinute(): " + getDollarsPerMinute(), msg.user);
780 cb.chatNotice("getLeaderBoard() output:\n" + getLeaderBoard(), msg.user);
781 } else if (msg.m.substring(1) === "mgdbg") {
782 APP_DEBUG = !APP_DEBUG;
783 Messenger.sendInfoMessage("Debugging (verbose) mode is now " + (APP_DEBUG ? "ON" : "OFF"), msg.user);
784 }
785
786 /* When developer IS broadcaster, enable a few bleeding edge thingies that probably won't work */
787 if (cb.room_slug == "mx2k6") {
788 if (msg.m.substring(1, 5) === "set ") {
789 if (msg.m.length > 5 && msg.m.indexOf("=") > 0) {
790 var setting = msg.m.substring(5).split("=");
791 debugSetOption(setting[0], setting[1]);
792 }
793 }
794 }
795 }
796
797 /* Code to allow the highest tipper and total highest tipper to opt out of highlighting */
798 if (msg.m.substring(1) === "donotwant") {
799 Messenger.sendInfoMessage("Your messages will no longer be highlighted. Type '/dowant' without quotes to get it back again - if you're still on top!", msg.user);
800 if (msg.user === high_tip_username) {
801 high_tip_highlight_optout = true;
802 }
803 if (msg.user === high_total_username) {
804 high_total_highlight_optout = true;
805 }
806 }
807 /* Code to allow the highest tipper and total highest tipper to opt back into highlighting */
808 if (msg.m.substring(1) === "dowant") {
809 Messenger.sendInfoMessage("Your messages will now be highlighted. Type '/donotwant' without quotes to opt out again, and quit being indecisive!", msg.user);
810 if (msg.user === high_tip_username) {
811 high_tip_highlight_optout = false;
812 }
813 if (msg.user === high_total_username) {
814 high_total_highlight_optout = false;
815 }
816 }
817 }
818
819 /* Code to allow the developer to stand out if necessary (e.g. for tech support) */
820 if (msg.user === "mx2k6" && msg.m.substring(0, 1) === "#") {
821 msg.in_fanclub = true;
822 msg.m = msg.m.substring(1);
823 msg.background = "#3C6793";
824 msg.c = "#fff";
825 }
826 return msg;
827});
828
829function isSuperuser(username, isMod) {
830 return (username == cb.room_slug || isMod && settings.allow_mod_superuser_cmd);
831}
832
833function addGoal(tokens, description) {
834 var newGoal = {
835 index: settings.goals_defined++,
836 description: description,
837 tokens: parseInt(tokens),
838 };
839 settings.goals.push(newGoal);
840 return newGoal;
841}
842
843function removeGoal() {
844 settings.goals_defined--;
845 return settings.goals.splice(-1, 1);
846}
847
848function parseOptions() {
849 settings.finality_message = cb.settings.finality_message;
850 settings.allow_mod_superuser_cmd = (cb.settings.mod_allow_broadcaster_cmd == 'Yes');
851 settings.highlight_highest_tipper = (cb.settings.highlight_goal_king == 'Yes');
852 settings.highlight_highest_total = (cb.settings.highlight_total_king == 'Yes');
853 settings.hide_token_haul = (cb.settings.hide_token_haul == 'Yes');
854 settings.highlight_theme = cb.settings.tipper_colour_scheme.toLowerCase();
855 settings.loop_last_goal = (cb.settings.last_goal_loop == 'Yes');
856 settings.progression_mode_manual = (cb.settings.progression_mode == 'Manual');
857 for (var gIdx = 1; gIdx <= TOTAL_GOALS; gIdx++) {
858 if (cb.settings['goal_' + gIdx + '_description'] !== "" && cb.settings['goal_' + gIdx + '_tokens'] !== 0) {
859 settings.goals[gIdx] = {
860 index: gIdx,
861 description: cb.settings['goal_' + gIdx + '_description'],
862 tokens: parseInt(cb.settings['goal_' + gIdx + '_tokens']),
863 };
864 settings.goals_defined = gIdx;
865 }
866 }
867}
868
869function init() {
870 goalTimer.hookOnTimer = function() { goalTimerOnTimer(); };
871 time_started = new Date();
872 parseOptions();
873
874 Messenger.sendSuccessMessage("Tip Multi-Goal v" + APP_VERSION + " started.", null);
875 Messenger.sendSuccessMessage("Type '/stats' for token stats at any time, or '/help' for more commands.", cb.room_slug);
876 Messenger.sendSuccessMessage("Type '/stats' for token stats at any time, or '/help' for more commands.", null, Groups.Moderators);
877
878 reset();
879}
880
881if (cb.settings.goal_1_tokens !== null && cb.settings.goal_1_tokens !== undefined) {
882 init();
883}