· 7 years ago · Dec 05, 2018, 03:24 AM
1// ==UserScript==
2// @name BombParty Overlay
3// @version 1.7.6
4// @description Overlay + Utilities for BombParty!
5// @icon https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/icon.png
6// @icon64 https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/icon64.png
7// @downloadURL https://github.com/MrInanimated/bp-overlay/raw/master/dist/bpoverlay.user.js
8// @author MrInanimated and Skandalabrandur
9// @match http://bombparty.sparklinlabs.com/play/*
10// @resource twitch_global http://twitchemotes.com/api_cache/v2/global.json
11// @resource twitch_subscriber http://twitchemotes.com/api_cache/v2/subscriber.json
12// @resource ffz_emotes http://api.frankerfacez.com/v1/set/global
13// @resource autoScrollOn https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/chatdown.png
14// @resource autoScrollOff https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/chatdownoff.png
15// @resource autoFocusOn https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/focusOn.png
16// @resource autoFocusOff https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/focusOff.png
17// @resource dragOff https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/dragOff.png
18// @resource dragOn https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/dragOn.png
19// @resource hideDeadOn https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/hideDeadOn.png
20// @resource hideDeadOff https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/hideDeadOff.png
21// @resource contextMenuPlugin https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/contextmenu.js
22// @resource contextMenuPluginCSS https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/dist/contextmenu.css
23// @grant GM_getResourceText
24// @grant GM_getResourceURL
25// @grant GM_getValue
26// @grant GM_setValue
27// @grant GM_xmlhttpRequest
28// ==/UserScript==
29
30//@require is not allowed under chrome (according to stackoverflow) and other suggested solutions have yet to work for me
31// a function that loads jQuery and calls a callback function when jQuery has finished loading
32function addJQuery(callback) {
33 var script = document.createElement("script");
34 script.setAttribute("src", "//ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js");
35 script.addEventListener('load', function() {
36 var script = document.createElement("script");
37 script.textContent = "window.jQ=jQuery.noConflict(true);(" + callback.toString() + ")();";
38 document.body.appendChild(script);
39
40 // Load the context menu plugin stuff
41 // This is a pretty simple task to require a whole plugin for, but I want to futureproof for now
42 var script = document.createElement("script");
43 script.textContent = "(function(jQuery){" + GM_getResourceText("contextMenuPlugin") + ";})(jQ);";
44 document.body.appendChild(script);
45
46 var css = document.createElement("style");
47 css.textContent = GM_getResourceText("contextMenuPluginCSS");
48 document.head.appendChild(css);
49 }, false);
50 document.body.appendChild(script);
51}
52
53// load jQuery and execute the main function
54addJQuery(function () {});
55
56// JSON Parser, by Douglas Crockford
57// This is necessary because in this environment I don't have access to the JSON object
58var json_parse=function(){"use strict";var e,t,n={'"':'"',"\\":"\\","/":"/",b:"\b",f:"\f",n:"\n",r:"\r",t:" "},r,i=function(t){throw{name:"SyntaxError",message:t,at:e,text:r}},s=function(n){if(n&&n!==t){i("Expected '"+n+"' instead of '"+t+"'")}t=r.charAt(e);e+=1;return t},o=function(){var e,n="";if(t==="-"){n="-";s("-")}while(t>="0"&&t<="9"){n+=t;s()}if(t==="."){n+=".";while(s()&&t>="0"&&t<="9"){n+=t}}if(t==="e"||t==="E"){n+=t;s();if(t==="-"||t==="+"){n+=t;s()}while(t>="0"&&t<="9"){n+=t;s()}}e=+n;if(!isFinite(e)){i("Bad number")}else{return e}},u=function(){var e,r,o="",u;if(t==='"'){while(s()){if(t==='"'){s();return o}if(t==="\\"){s();if(t==="u"){u=0;for(r=0;r<4;r+=1){e=parseInt(s(),16);if(!isFinite(e)){break}u=u*16+e}o+=String.fromCharCode(u)}else if(typeof n[t]==="string"){o+=n[t]}else{break}}else{o+=t}}}i("Bad string")},a=function(){while(t&&t<=" "){s()}},f=function(){switch(t){case"t":s("t");s("r");s("u");s("e");return true;case"f":s("f");s("a");s("l");s("s");s("e");return false;case"n":s("n");s("u");s("l");s("l");return null}i("Unexpected '"+t+"'")},l,c=function(){var e=[];if(t==="["){s("[");a();if(t==="]"){s("]");return e}while(t){e.push(l());a();if(t==="]"){s("]");return e}s(",");a()}}i("Bad array")},h=function(){var e,n={};if(t==="{"){s("{");a();if(t==="}"){s("}");return n}while(t){e=u();a();s(":");if(Object.hasOwnProperty.call(n,e)){i('Duplicate key "'+e+'"')}n[e]=l();a();if(t==="}"){s("}");return n}s(",");a()}}i("Bad object")};l=function(){a();switch(t){case"{":return h();case"[":return c();case'"':return u();case"-":return o();default:return t>="0"&&t<="9"?o():f()}};return function(n,s){var o;r=n;e=0;t=" ";o=l();a();if(t){i("Syntax error")}return typeof s==="function"?function u(e,t){var n,r,i=e[t];if(i&&typeof i==="object"){for(n in i){if(Object.prototype.hasOwnProperty.call(i,n)){r=u(i,n);if(r!==undefined){i[n]=r}else{delete i[n]}}}}return s.call(e,t,i)}({"":o},""):o}}()
59
60var loadJSON = function (resourceName, variableName) {
61 var resource = "undefined";
62 try {
63 resource = GM_getResourceText(resourceName);
64 json_parse(resource);
65 }
66 catch (e) {
67 console.log("Error loading " + resourceName);
68 resource = "undefined";
69 }
70 finally {
71 var script = document.createElement("script");
72 script.setAttribute("type", "application/javascript");
73 script.textContent = "var " + variableName + " = " + resource + ";";
74 document.body.appendChild(script);
75 document.body.removeChild(script);
76 }
77}
78
79loadJSON("twitch_global", "twitch_global");
80loadJSON("twitch_subscriber", "twitch_subscriber");
81loadJSON("ffz_emotes", "ffz_emotes");
82
83var te = document.createElement('script');
84te.setAttribute("type", "application/javascript");
85te.textContent = 'var bpImgUrls = {' +
86 'autoScrollOn : "' + GM_getResourceURL("autoScrollOn") + '",' +
87 'autoScrollOff : "' + GM_getResourceURL("autoScrollOff") + '",' +
88 'autoFocusOn : "' + GM_getResourceURL("autoFocusOn") + '",' +
89 'autoFocusOff : "' + GM_getResourceURL("autoFocusOff") + '",' +
90 'dragOff : "' + GM_getResourceURL("dragOff") + '",' +
91 'dragOn : "' + GM_getResourceURL("dragOn") + '",' +
92 'hideDeadOn : "' + GM_getResourceURL("hideDeadOn") + '",' +
93 'hideDeadOff : "' + GM_getResourceURL("hideDeadOff") + '",' +
94'};';
95document.body.appendChild(te);
96document.body.removeChild(te);
97
98// All the code written within this function RUNS WITHIN THE SCOPE OF THE PAGE.
99// Everything outside it do NOT have access to environment variables.
100var source = function() {
101 // If the window already has a BPOverlay, don't run again
102 if (window.hasOwnProperty('BPOverlayHasRun')) {} else {
103 window.BPOverlayHasRun = true;
104
105 var main = function() {
106
107 // Since this is running via a script loaded on page load, it's difficult ensure the overlay runs after everything has loaded
108 // This piece of code makes sure that all relevant things are loaded before executing the rest of the code
109 // We may need to add more to this long-ass if statement if we add more features in the future
110 if (!(window.hasOwnProperty("channel") && channel.socket && channel.data && channel.appendToChat && channel.socket.listeners("chatMessage").length && channel.socket.listeners("setActivePlayerIndex").length && channel.socket.listeners("winWord").length && channel.socket.listeners("setPlayerLives").length && channel.socket.listeners("setPlayerState").length && channel.socket.listeners("endGame").length && window.hasOwnProperty("JST") && JST["nuclearnode/chatMessage"] && document.getElementById("Sidebar") && document.getElementById("ChatLog") && window.jQ)) {
111 console.log("Everything's not loaded yet, trying again in a second...");
112 setTimeout(main, 1000);
113 return;
114 }
115
116 /*!
117 * Autolinker.js
118 * 0.15.0
119 *
120 * Copyright(c) 2014 Gregory Jacobs <greg@greg-jacobs.com>
121 * MIT Licensed. http://www.opensource.org/licenses/mit-license.php
122 *
123 * https://github.com/gregjacobs/Autolinker.js
124 */
125 !function(a,b){"function"==typeof define&&define.amd?define([],function(){return a.returnExportsGlobal=b()}):"object"==typeof exports?module.exports=b():a.Autolinker=b()}(this,function(){var a=function(b){a.Util.assign(this,b),this.matchValidator=new a.MatchValidator};return a.prototype={constructor:a,urls:!0,email:!0,twitter:!0,newWindow:!0,stripPrefix:!0,className:"",htmlCharacterEntitiesRegex:/( | |<|<|>|>)/gi,matcherRegex:function(){var a=/(^|[^\w])@(\w{1,15})/,b=/(?:[\-;:&=\+\$,\w\.]+@)/,c=/(?:[A-Za-z][-.+A-Za-z0-9]+:(?![A-Za-z][-.+A-Za-z0-9]+:\/\/)(?!\d+\/?)(?:\/\/)?)/,d=/(?:www\.)/,e=/[A-Za-z0-9\.\-]*[A-Za-z0-9\-]/,f=/\.(?:international|construction|contractors|enterprises|photography|productions|foundation|immobilien|industries|management|properties|technology|christmas|community|directory|education|equipment|institute|marketing|solutions|vacations|bargains|boutique|builders|catering|cleaning|clothing|computer|democrat|diamonds|graphics|holdings|lighting|partners|plumbing|supplies|training|ventures|academy|careers|company|cruises|domains|exposed|flights|florist|gallery|guitars|holiday|kitchen|neustar|okinawa|recipes|rentals|reviews|shiksha|singles|support|systems|agency|berlin|camera|center|coffee|condos|dating|estate|events|expert|futbol|kaufen|luxury|maison|monash|museum|nagoya|photos|repair|report|social|supply|tattoo|tienda|travel|viajes|villas|vision|voting|voyage|actor|build|cards|cheap|codes|dance|email|glass|house|mango|ninja|parts|photo|shoes|solar|today|tokyo|tools|watch|works|aero|arpa|asia|best|bike|blue|buzz|camp|club|cool|coop|farm|fish|gift|guru|info|jobs|kiwi|kred|land|limo|link|menu|mobi|moda|name|pics|pink|post|qpon|rich|ruhr|sexy|tips|vote|voto|wang|wien|wiki|zone|bar|bid|biz|cab|cat|ceo|com|edu|gov|int|kim|mil|net|onl|org|pro|pub|red|tel|uno|wed|xxx|xyz|ac|ad|ae|af|ag|ai|al|am|an|ao|aq|ar|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|bi|bj|bm|bn|bo|br|bs|bt|bv|bw|by|bz|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|co|cr|cu|cv|cw|cx|cy|cz|de|dj|dk|dm|do|dz|ec|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gp|gq|gr|gs|gt|gu|gw|gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|in|io|iq|ir|is|it|je|jm|jo|jp|ke|kg|kh|ki|km|kn|kp|kr|kw|ky|kz|la|lb|lc|li|lk|lr|ls|lt|lu|lv|ly|ma|mc|md|me|mg|mh|mk|ml|mm|mn|mo|mp|mq|mr|ms|mt|mu|mv|mw|mx|my|mz|na|nc|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|pa|pe|pf|pg|ph|pk|pl|pm|pn|pr|ps|pt|pw|py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sx|sy|sz|tc|td|tf|tg|th|tj|tk|tl|tm|tn|to|tp|tr|tt|tv|tw|tz|ua|ug|uk|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|ye|yt|za|zm|zw)\b/,g=/[\-A-Za-z0-9+&@#\/%=~_()|'$*\[\]?!:,.;]*[\-A-Za-z0-9+&@#\/%=~_()|'$*\[\]]/;return new RegExp(["(",a.source,")","|","(",b.source,e.source,f.source,")","|","(","(?:","(",c.source,e.source,")","|","(?:","(.?//)?",d.source,e.source,")","|","(?:","(.?//)?",e.source,f.source,")",")","(?:"+g.source+")?",")"].join(""),"gi")}(),charBeforeProtocolRelMatchRegex:/^(.)?\/\//,link:function(b){var c=this,d=this.getHtmlParser(),e=this.htmlCharacterEntitiesRegex,f=0,g=[];return d.parse(b,{processHtmlNode:function(a,b,c){"a"===b&&(c?f=Math.max(f-1,0):f++),g.push(a)},processTextNode:function(b){if(0===f)for(var d=a.Util.splitAndCapture(b,e),h=0,i=d.length;i>h;h++){var j=d[h],k=c.processTextNode(j);g.push(k)}else g.push(b)}}),g.join("")},getHtmlParser:function(){var b=this.htmlParser;return b||(b=this.htmlParser=new a.HtmlParser),b},getTagBuilder:function(){var b=this.tagBuilder;return b||(b=this.tagBuilder=new a.AnchorTagBuilder({newWindow:this.newWindow,truncate:this.truncate,className:this.className})),b},processTextNode:function(a){var b=this;return a.replace(this.matcherRegex,function(a,c,d,e,f,g,h,i,j){var k=b.processCandidateMatch(a,c,d,e,f,g,h,i,j);if(k){var l=b.createMatchReturnVal(k.match,k.matchStr);return k.prefixStr+l+k.suffixStr}return a})},processCandidateMatch:function(b,c,d,e,f,g,h,i,j){var k,l=i||j,m="",n="";if(c&&!this.twitter||f&&!this.email||g&&!this.urls||!this.matchValidator.isValidMatch(g,h,l))return null;if(this.matchHasUnbalancedClosingParen(b)&&(b=b.substr(0,b.length-1),n=")"),f)k=new a.match.Email({matchedText:b,email:f});else if(c)d&&(m=d,b=b.slice(1)),k=new a.match.Twitter({matchedText:b,twitterHandle:e});else{if(l){var o=l.match(this.charBeforeProtocolRelMatchRegex)[1]||"";o&&(m=o,b=b.slice(1))}k=new a.match.Url({matchedText:b,url:b,protocolUrlMatch:!!h,protocolRelativeMatch:!!l,stripPrefix:this.stripPrefix})}return{prefixStr:m,suffixStr:n,matchStr:b,match:k}},matchHasUnbalancedClosingParen:function(a){var b=a.charAt(a.length-1);if(")"===b){var c=a.match(/\(/g),d=a.match(/\)/g),e=c&&c.length||0,f=d&&d.length||0;if(f>e)return!0}return!1},createMatchReturnVal:function(b,c){var d;if(this.replaceFn&&(d=this.replaceFn.call(this,this,b)),"string"==typeof d)return d;if(d===!1)return c;if(d instanceof a.HtmlTag)return d.toString();var e=this.getTagBuilder(),f=e.build(b);return f.toString()}},a.link=function(b,c){var d=new a(c);return d.link(b)},a.match={},a.Util={abstractMethod:function(){throw"abstract"},assign:function(a,b){for(var c in b)b.hasOwnProperty(c)&&(a[c]=b[c]);return a},extend:function(b,c){var d=b.prototype,e=function(){};e.prototype=d;var f;f=c.hasOwnProperty("constructor")?c.constructor:function(){d.constructor.apply(this,arguments)};var g=f.prototype=new e;return g.constructor=f,g.superclass=d,delete c.constructor,a.Util.assign(g,c),f},ellipsis:function(a,b,c){return a.length>b&&(c=null==c?"..":c,a=a.substring(0,b-c.length)+c),a},indexOf:function(a,b){if(Array.prototype.indexOf)return a.indexOf(b);for(var c=0,d=a.length;d>c;c++)if(a[c]===b)return c;return-1},splitAndCapture:function(a,b){if(!b.global)throw new Error("`splitRegex` must have the 'g' flag set");for(var c,d=[],e=0;c=b.exec(a);)d.push(a.substring(e,c.index)),d.push(c[0]),e=c.index+c[0].length;return d.push(a.substring(e)),d}},a.HtmlParser=a.Util.extend(Object,{htmlRegex:function(){var a=/[0-9a-zA-Z][0-9a-zA-Z:]*/,b=/[^\s\0"'>\/=\x01-\x1F\x7F]+/,c=/(?:".*?"|'.*?'|[^'"=<>`\s]+)/,d=b.source+"(?:\\s*=\\s*"+c.source+")?";return new RegExp(["(?:","<(!DOCTYPE)","(?:","\\s+","(?:",d,"|",c.source+")",")*",">",")","|","(?:","<(/)?","("+a.source+")","(?:","\\s+",d,")*","\\s*/?",">",")"].join(""),"gi")}(),parse:function(a,b){b=b||{};for(var c,d=b.processHtmlNode||function(){},e=b.processTextNode||function(){},f=this.htmlRegex,g=0;null!==(c=f.exec(a));){var h=c[0],i=c[1]||c[3],j=!!c[2],k=a.substring(g,c.index);k&&e(k),d(h,i.toLowerCase(),j),g=c.index+h.length}if(g<a.length){var l=a.substring(g);l&&e(l)}}}),a.HtmlTag=a.Util.extend(Object,{whitespaceRegex:/\s+/,constructor:function(b){a.Util.assign(this,b),this.innerHtml=this.innerHtml||this.innerHTML},setTagName:function(a){return this.tagName=a,this},getTagName:function(){return this.tagName||""},setAttr:function(a,b){var c=this.getAttrs();return c[a]=b,this},getAttr:function(a){return this.getAttrs()[a]},setAttrs:function(b){var c=this.getAttrs();return a.Util.assign(c,b),this},getAttrs:function(){return this.attrs||(this.attrs={})},setClass:function(a){return this.setAttr("class",a)},addClass:function(b){for(var c,d=this.getClass(),e=this.whitespaceRegex,f=a.Util.indexOf,g=d?d.split(e):[],h=b.split(e);c=h.shift();)-1===f(g,c)&&g.push(c);return this.getAttrs()["class"]=g.join(" "),this},removeClass:function(b){for(var c,d=this.getClass(),e=this.whitespaceRegex,f=a.Util.indexOf,g=d?d.split(e):[],h=b.split(e);g.length&&(c=h.shift());){var i=f(g,c);-1!==i&&g.splice(i,1)}return this.getAttrs()["class"]=g.join(" "),this},getClass:function(){return this.getAttrs()["class"]||""},hasClass:function(a){return-1!==(" "+this.getClass()+" ").indexOf(" "+a+" ")},setInnerHtml:function(a){return this.innerHtml=a,this},getInnerHtml:function(){return this.innerHtml||""},toString:function(){var a=this.getTagName(),b=this.buildAttrsStr();return b=b?" "+b:"",["<",a,b,">",this.getInnerHtml(),"</",a,">"].join("")},buildAttrsStr:function(){if(!this.attrs)return"";var a=this.getAttrs(),b=[];for(var c in a)a.hasOwnProperty(c)&&b.push(c+'="'+a[c]+'"');return b.join(" ")}}),a.MatchValidator=a.Util.extend(Object,{invalidProtocolRelMatchRegex:/^[\w]\/\//,hasFullProtocolRegex:/^[A-Za-z][-.+A-Za-z0-9]+:\/\//,uriSchemeRegex:/^[A-Za-z][-.+A-Za-z0-9]+:/,hasWordCharAfterProtocolRegex:/:[^\s]*?[A-Za-z]/,isValidMatch:function(a,b,c){return b&&!this.isValidUriScheme(b)||this.urlMatchDoesNotHaveProtocolOrDot(a,b)||this.urlMatchDoesNotHaveAtLeastOneWordChar(a,b)||this.isInvalidProtocolRelativeMatch(c)?!1:!0},isValidUriScheme:function(a){var b=a.match(this.uriSchemeRegex)[0];return"javascript:"!==b&&"vbscript:"!==b},urlMatchDoesNotHaveProtocolOrDot:function(a,b){return!(!a||b&&this.hasFullProtocolRegex.test(b)||-1!==a.indexOf("."))},urlMatchDoesNotHaveAtLeastOneWordChar:function(a,b){return a&&b?!this.hasWordCharAfterProtocolRegex.test(a):!1},isInvalidProtocolRelativeMatch:function(a){return!!a&&this.invalidProtocolRelMatchRegex.test(a)}}),a.AnchorTagBuilder=a.Util.extend(Object,{constructor:function(b){a.Util.assign(this,b)},build:function(b){var c=new a.HtmlTag({tagName:"a",attrs:this.createAttrs(b.getType(),b.getAnchorHref()),innerHtml:this.processAnchorText(b.getAnchorText())});return c},createAttrs:function(a,b){var c={href:b},d=this.createCssClass(a);return d&&(c["class"]=d),this.newWindow&&(c.target="_blank"),c},createCssClass:function(a){var b=this.className;return b?b+" "+b+"-"+a:""},processAnchorText:function(a){return a=this.doTruncate(a)},doTruncate:function(b){return a.Util.ellipsis(b,this.truncate||Number.POSITIVE_INFINITY)}}),a.match.Match=a.Util.extend(Object,{constructor:function(b){a.Util.assign(this,b)},getType:a.Util.abstractMethod,getMatchedText:function(){return this.matchedText},getAnchorHref:a.Util.abstractMethod,getAnchorText:a.Util.abstractMethod}),a.match.Email=a.Util.extend(a.match.Match,{getType:function(){return"email"},getEmail:function(){return this.email},getAnchorHref:function(){return"mailto:"+this.email},getAnchorText:function(){return this.email}}),a.match.Twitter=a.Util.extend(a.match.Match,{getType:function(){return"twitter"},getTwitterHandle:function(){return this.twitterHandle},getAnchorHref:function(){return"https://twitter.com/"+this.twitterHandle},getAnchorText:function(){return"@"+this.twitterHandle}}),a.match.Url=a.Util.extend(a.match.Match,{urlPrefixRegex:/^(https?:\/\/)?(www\.)?/i,protocolRelativeRegex:/^\/\//,protocolPrepended:!1,getType:function(){return"url"},getUrl:function(){var a=this.url;return this.protocolRelativeMatch||this.protocolUrlMatch||this.protocolPrepended||(a=this.url="http://"+a,this.protocolPrepended=!0),a},getAnchorHref:function(){var a=this.getUrl();return a.replace(/&/g,"&")},getAnchorText:function(){var a=this.getUrl();return this.protocolRelativeMatch&&(a=this.stripProtocolRelativePrefix(a)),this.stripPrefix&&(a=this.stripUrlPrefix(a)),a=this.removeTrailingSlash(a)},stripUrlPrefix:function(a){return a.replace(this.urlPrefixRegex,"")},stripProtocolRelativePrefix:function(a){return a.replace(this.protocolRelativeRegex,"")},removeTrailingSlash:function(a){return"/"===a.charAt(a.length-1)&&(a=a.slice(0,-1)),a}}),a});
126
127 // Let's write an additional thing for multi-language text strings!
128 // I kinda wanted to call this i18n, but that's taken
129 var tran = {
130 strings: {
131 en: {
132 timeText: "Elapsed time | ",
133 wordCountText: "Word Count: ",
134 showHideButtonTitle: "Show/Hide dead players.",
135 flipsText: "Flips",
136 flipsTitle: "Lives gained by using all the bonus letters.",
137 uFlipsText: "U-Flips",
138 uFlipsTitle: "Lives gained whilst already at 3 lives, making the \"flip\" unnecessary",
139 deathsText: "Deaths",
140 deathsTitle: "Lives lost in this game",
141 playersTitle: "Players:",
142 alphaText: "Alpha",
143 alphaTitle: "Which letter this player is on if he was playing alpha roulette. (The number is how many times they have completed the alphabet)",
144 wordsText: "Words",
145 wordsTitle: "How many words this player has used",
146 chatDownButtonTitle: "Automatically scroll the chat down whenever there is a new message.",
147 autoFocusButtonTitle: "Automatically focus the chat box after your turn.",
148 dragButtonTitle: "Have the scoreboard be in a draggable container instead.",
149 overlaySettingsButtonTitle: "BombParty Overlay Settings",
150 overlaySettingsText: "Overlay Settings",
151 notificationsText: "Notifications",
152 themeH2Text: "Themes",
153 easterText: "Easter Eggs",
154 chatOpText: "Chat Options",
155 playerListText: "Current Players",
156 creditsText: "Credits",
157 credits1: "Code Monkey",
158 credits2: "Code Amoeba",
159 credits3: "Frenchifier",
160 creditsContextMenu: "Context Menu",
161 creditsAutolinker: "Autolinker",
162 creditsTwitchEmotes: "Twitch Emotes",
163 creditsTextText: "With thanks to the English BombParty community",
164 containerSizeName: "Container Size",
165 containerSizeOptions: {
166 compact: "Compact Size",
167 fitToPlayers: "Fit To Players",
168 },
169 twitchEmotesName: "Twitch Emotes",
170 twitchEmotesOptions: {
171 on: "On",
172 off: "Off",
173 },
174 textAdventureName: "Text Adventure<sup>BETA</sup>",
175 textAdventureOptions: {
176 on: "On",
177 off: "Off",
178 },
179 hardModesName: "Hard Modes",
180 hardModesOptions: {
181 none: "None",
182 rev: "Reverse",
183 jqv: "J/Q/V",
184 az: "Alphabet",
185 xz: "X/Z",
186 },
187 themeName: "Theme<sup>BETA</sup>",
188 themeOptions: {
189 none: "None",
190 xmas: "Christmas",
191 custom: "Custom",
192 },
193 customThemeName: "Custom Theme",
194 particlesName: "Particles",
195 particlesOptions: {
196 high: "High",
197 low: "Low",
198 off: "Off",
199 },
200 notificationsName: "Notifications",
201 notificationVolume: "Notification Volume",
202 notificationOptions: {
203 on: "On",
204 off: "Off",
205 },
206 notificationAlias: "Aliases",
207 notificationAliasTitle: "Custom names that also trigger a notification.",
208 notificationAliasInputTitle: "Write your names one after another separated by semicolons here. e.g. MrInanimated;Inanimated;Animé;Inan",
209 endGameNotification: "Notify when a game ends",
210 endGameNotificationOptions: {
211 on: "On",
212 off: "Off",
213 },
214 alphaRouletteName: "Alpha Roulette Display",
215 alphaRouletteOptions: {
216 on: "On",
217 off: "Off",
218 },
219 wordCounterName: "Word Counter Display",
220 wordCounterOptions: {
221 on: "On",
222 off: "Off",
223 },
224 muted: "(muted)",
225 muteUser: "Mute",
226 unmuteUser: "Unmute",
227 areYouSure: "Are you sure?",
228 ignoringText: "Muted Users",
229 ignoringEmpty: "Nobody is currently muted.",
230 scoreName: "Leaderboard score",
231 scoreOption: {
232 on: "On",
233 off: "Off",
234 },
235 jqvText: "That word didn't contain J, Q nor V!",
236 azText: "You are on letter {l} Kappa!",
237 xzText: "That word didn't contain X nor Z!",
238 speechName: "Speech on Chrome<sup>BETA</sup>",
239 speechOptions: {
240 on: "On",
241 off: "Off",
242 },
243 voiceSelect: "Voice",
244 voiceOptions: {
245 us: "US",
246 ukMale: "GB Male",
247 ukFem: "GB Female",
248 fran: "FR",
249 },
250 twitchEmoteError: "Error: Twitch emotes have not been loaded.",
251 updateText: "BP Overlay v1.7.0 \nNew emote system: Twitch subscriber emotes no longer need square brackets (except a couple that might be used by accident).",
252 },
253 fr: {
254 timeText: "Temps Écoulé : ",
255 wordCountText: "Nombre de Mots : ",
256 showHideButtonTitle: "Afficher/Masquer les joueurs morts.",
257 flipsText: "V.R.",
258 flipsTitle: "Vies récupérées en utilisant toutes les lettres bonus",
259 uFlipsText: "V.R.I.",
260 uFlipsTitle: "Vies récupérées inutiles : vies récupérées tandis qu'on en possède trois, ce qui les rend \"inutiles\"",
261 deathsText: "Morts",
262 deathsTitle: "Vies perdues dans cette partie",
263 alphaText: "Alpha",
264 alphaTitle: "La lettre à laquelle ce joueur serait si il jouait en mode alphabet. (Le chiffre entre parenthèses est le nombre de fois où il a complété l'alphabet)",
265 wordsText: "Mots",
266 wordsTitle: "Le nombre total de mots utilsés par ce joueur",
267 playersTitle: "Joueurs :",
268 chatDownButtonTitle: "Forcer le tchat à défiler vers le bas quand il y a un nouveau message.",
269 autoFocusButtonTitle: "Positionner automatiquement le curseur sur le tchat après son tour",
270 dragButtonTitle: "Détacher le tableau des scores.",
271 overlaySettingsButtonTitle: "Paramètres de l'Overlay",
272 overlaySettingsText: "Paramètres",
273 notificationsText: "Notifications",
274 themeH2Text: "Thèmes",
275 easterText: "Easter eggs",
276 chatOpText: "Paramètres du Tchat",
277 playerListText: "Joueurs connectés",
278 creditsText: "Crédits",
279 credits1: "Code Monkey",
280 credits2: "Code Amoeba",
281 credits3: "Traduction",
282 creditsContextMenu: "Menu Contextuel",
283 creditsAutolinker: "Liens automatiques",
284 creditsTwitchEmotes: "Emoticones Twitch",
285 creditsTextText: "Remerciements à la communauté anglaise de BombParty",
286 containerSizeName: "Taille du tableau",
287 containerSizeOptions: {
288 compact: "Compacte",
289 fitToPlayers: "Ajustée selon les joueurs",
290 },
291 twitchEmotesName: "Emoticones Twitch",
292 twitchEmotesOptions: {
293 on: "Marche",
294 off: "Arrêt",
295 },
296 textAdventureName: "Text Adventure<sup>BETA</sup>",
297 textAdventureOptions: {
298 on: "Marche",
299 off: "Arrêt",
300 },
301 hardModesName: "Difficultés supplémentaires",
302 hardModesOptions: {
303 none: "Aucune",
304 rev: "Inversé",
305 jqv: "J/Q/V",
306 az: "Alphabet",
307 xz: "X/Z",
308 },
309 themeName: "Thème<sup>BETA</sup>",
310 themeOptions: {
311 none: "Aucun",
312 xmas: "Noël",
313 custom: "Personnalisé",
314 },
315 customThemeName: "Thème Personnalisé",
316 particlesName: "Particules",
317 particlesOptions: {
318 high: "Elevé",
319 low: "Faible",
320 off: "Désactivé",
321 },
322 notificationsName: "Notifications",
323 notificationVolume: "Volume de Notification",
324 notificationOptions: {
325 on: "Activé",
326 off: "Désactivé",
327 },
328 notificationAlias: "Pseudonymes",
329 notificationAliasTitle: "Noms personnalisés qui déclenchent également une notification.",
330 notificationAliasInputTitle: "Ecrivez vos noms l'un après l'autre, séparés par un point virgule. Par ex. MrInanimated;Inanimated;Animé;Inan",
331 endGameNotification: "Avertit quand une partie se termine",
332 endGameNotificationOptions: {
333 on: "Activé",
334 off: "Désactivé",
335 },
336 alphaRouletteName: "Affichage Mode Alphabet",
337 alphaRouletteOptions: {
338 on: "Activé",
339 off: "Désactivé",
340 },
341 wordCounterName: "Affichage Compteur de Mots",
342 wordCounterOptions: {
343 on: "Activé",
344 off: "Désactivé",
345 },
346 muted: "(muted)",
347 muteUser: "Ignorer",
348 unmuteUser: "Autoriser",
349 areYouSure: "Êtes-vous sûr ?",
350 ignoringText: "Utilisateurs ignorés",
351 ignoringEmpty: "Personne n'est ignoré actuellement.",
352 scoreName: "Score Classement",
353 scoreOption: {
354 on: "Activé",
355 off: "Désactivé",
356 },
357 jqvText: "Ce mot ne contient ni J, ni V, ni Q.",
358 azText: "Au tour de la lettre {l} Kappa !",
359 xzText: "Ce mot ne contient ni X, ni Z !",
360 speechName: "Vocale sur Chrome<sup>BETA</sup>",
361 speechOptions: {
362 on: "Activé",
363 off: "Désactivé",
364 },
365 voiceSelect: "Voix",
366 voiceOptions: {
367 us: "US",
368 ukMale: "GB Homme",
369 ukFem: "GB Femme",
370 fran: "FR",
371 },
372 twitchEmoteError: "Erreur : Les émoticones Twitch n'ont pas été chargées.",
373 updateText: "BP Overlay v1.7.0",
374 },
375 },
376 language: (document.cookie.indexOf("i18next=fr") !== -1 ? "fr" : "en"),
377 fallback: "en",
378 t: function (accessor) {
379 var rStr;
380
381 try {
382 if ((rStr = eval("this.strings." + this.language + "." + accessor)) !== undefined) {
383 return rStr;
384 }
385 }
386 catch (e) {
387 if (!(e instanceof TypeError)) {
388 throw e;
389 }
390 }
391
392 try {
393 if ((rStr = eval("this.strings." + this.fallback + "." + accessor)) !== undefined) {
394 return rStr;
395 }
396 }
397 catch (e) {
398 if (!(e instanceof TypeError)) {
399 throw e;
400 }
401 }
402
403 return accessor;
404 },
405 };
406
407 // Tidy container for storing "global" variables.
408 bpOverlay = {
409 playerNames: {}, // Stores player name by actor index
410 playerScores: Array.apply(null, new Array(20)).map(Number.prototype.valueOf,0), // Stores player's score by actor index
411 playerAuthId: {}, // Stores actor index by authId
412 lostLives: {}, // Stores lost lives by actor index
413 flips: {}, // Stores flips by actor index
414 uFlips: {}, // Stores u-flips by actor index
415 alpha: {}, // Stores progress across the alphabet
416 words: {}, // Stores number of words used per player
417 wordCount: 0, // Stores word count for current round
418 startTime: 0, // Stores (in milliseconds since the epoch) the starting time of the round
419 timeText: "", // Stores the text used to display the time on the overlay
420
421 dragBoxHasBeenCreated: false, //Indicates that the dragbox is 4 real
422 boxHasBeenCreated: false, // True when a DOM object with the id "infoBox" exists
423 firstRun: true, // Set to true every time a game ends. If this is true a new infoBox is created, and this is set to false
424
425 prevPlayerNames: {}, // Stores player name by actor index
426 prevLostLives: {}, // Stores lost lives by actor index
427 prevFlips: {}, // Stores flips by actor index
428 prevUFlips: {}, // Stores u-flips by actor index
429 prevTimeText: "", // Stores the text used to display the time on the overlay
430 prevWordCount: 0, // Stores the word count for the last round
431
432 hideDead: false, // If true, hides dead players' rows on the scoreboard table
433 dragonDrop: false, // If true, indicates that the user has a wish to become or remain a dragon.
434 // dragBoxHasBeenCreated will not replace this
435
436 autoScroll: true, // If true, automatically scrolls that chat down whenever there is a new chat message.
437 autoFocus: true, // If true, automatically switches focus to the chatbox after the user's turn
438
439 focusNext: false, // If true, it is the user's turn (and so we should focus to the chatBox if autoFocus is true after the user's turn)
440
441 twitchOn: true, // If true, twitch emoticons will be displayed.
442
443 markupOn: true, // A dummy, nothing changes this setting just yet
444
445 adventureTextMode: false, //Self explanatory boolean for text adventure toggle
446
447 adventureFirstRun: false,
448 adventureLevels: ["A noob", "A beginner", "A novice", "A student of flips", "A graduated student of flips", "An expert flipper", "An incredible flipper", "A master flipper", "A scrub tier immortal", "A near immortal", "An immortal", "A massive flipping faggot", "A strong contender to the 'hang in there kitty'", "An immeasurable faggot of flips", "A blackhole tier faggot", "A legendary immortal faggot flipper", "A supermassive faggot with more flips than a herd of dolphins", "Silly rabbit. Flips are for kids!", "An undefeatable flipping gaylord of +5 anal strength", "An ascended immortal queen faggot cockmunch godly overlord of flips", "Zip zop zippety boop flip floop", "You're now the gayest man on earth", "Aren't you fagged out after all that flipping?", "A lifesize Johnboat", "A lifesize Catboat", "The Jesus figure in the underworld of extreme flip fetish", "On fliproids!", "Why don't you slip into something more comfortable... like a coma?", "A divine elder scrub god that manifests himself as fine-cuisine salt", "The nicest guy. Didn't expect that did you?", "Hungry hungry heart hoarder!", "A bird in the hand is worth zemstvo in the quush", "You're good. Actually very good. Goddamn", "Still going. Holy crap!", "Your Anaconda dont want none unless you got equivocations hun", "A budding plenipotentiary", "Antidisestablishmentarinism in the flesh", "Suffering from pseudopseudohypoparathyroidism", "Arrete les flips tu connard!", "A disciple of Pingu", "A man of a thousand flips... well... 40", "The god of bombparty", "Insert Douglas Adams reference here because 42 flips", "An Alpaca. Yes. An Alpaca. The flippest of animals besides giraffes", "A giraffe. The flippest of the animals in the animal kingdom", "You deserve a movie about you. The title 'Rainman' is taken though", "If faggots were mountains, you'd be a volcano!"],
449
450 //The alphabet variables for alphabet hard mode
451 alphabet: ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"],
452 alphapos: 0,
453
454 isThemed: false, // Is the game currently themed
455 particleSpawnRate: "high",
456
457 alias: [],
458 notificationSound: new Audio("http://bombparty.sparklinlabs.com/sounds/myTurn.wav"),
459 notifications: true,
460
461 endGameNotification: false,
462
463 ignoring: {},
464
465 speechName: "Google UK English Male", //Default voice
466
467 //Letter scores based on the formula ((10 - (pure integer value of percentage))*3). floored at 1. Why this formula? Just because. :D
468 letterScore: { a: 5, b: 25, c: 21, d: 17, e: 1, f: 23, g: 24, h: 12, i: 9, j: 30, k:27, l:18, m:22, n:10, o:7, p:24, q:30, r:12, s:11, t:3, u:22, v:27, w:23, x:30, y:24, z:30},
469 scoreMode: false,
470
471 is_hidden: false,
472 };
473
474 // Store all the game images so they can be changed
475 gameImages = {};
476
477 window.addEventListener("blur", function () { bpOverlay.is_hidden = true; });
478 window.addEventListener("focus", function () { bpOverlay.is_hidden = false; });
479
480 // So, um, the code here is pretty distributed, but I'll explain the "hack" I've found here
481 // Elisee has *kindly* wrapped up all the images he used in a closure
482 // (I totally didn't spend about 3 hours hunting how to do this before finding this hack)
483 // Which means there's normally no way to get at those images and change them
484 // The one point of contact those variables have with the outside environment is the context.drawImage method
485 // So of course that's exploited.
486 // Now, not all the images will be sent to drawImage at once
487 // Hence the need for a missing game images list.
488 var missingGameImgs = [
489 "avatarShadow",
490 "heart",
491 "heartEmpty",
492 "arrow",
493 "bomb",
494 "sparkle",
495 "avatarPlaceHolder",
496 "letter",
497 ]
498
499 // Relacement src's go here
500 var replacementImages = {
501 };
502
503 // Replacement offsets go here
504 var replacementOffsets = {
505 };
506
507 // This is a backup of the original src's of the images
508 // So if ever need to flip back to the default
509 // We use the src's listed here
510 var backupSources = {
511 };
512
513 var canvasContext = document.getElementById("GameCanvas").getContext("2d");
514
515 // Adventure Mode Text formatter!
516 // Use this to have a pool of text messages to randomly choose from.
517
518 // USAGE:
519 // adventureTextFormat.chooseText(textBankName, *arguments)
520 // textBankName: the text bank you want to choose the text from.
521 // *arguments: The arguments you want to fill the empty spaces in your text with.
522 // (It's not tremendously clear what this does, but I'll try to explain it anyway)
523 // You provide it with a string like this:
524 // "The heroic, {0}, faces the mighty {1}."
525 // And then you call adventureTextFormat.chooseText($TextBankWhichTheStringAboveIsIn, "Pingu", "GOS")
526 // It returns "The heroic, Pingu, faces the mighty GOS."
527 // It replaces the numbers inside the braces with the relevant text supplied.
528 // You can have the numbers in any order, so:
529 // "A fierce {1} roars greatly at {0}!" -> "A fierce GOS roars greatly at Pingu!"
530 // You can miss out numbers all together, so:
531 // "{0} takes their stand against the foul beast." -> "Pingu takes their stand against the foul beast."
532
533 var adventureTextFormat = {
534 format: function(format, args) {
535 return format.replace(/{(\d+)}/g, function(match, number) {
536 return typeof args[number] != 'undefined' ? args[number] : match;
537 });
538 },
539
540 chooseText : function (textBankName) {
541 var args = Array.prototype.slice.call(arguments, 1);
542 var textBank = this.textBanks[textBankName];
543 var chosenString = textBank[Math.floor(Math.random() * textBank.length)];
544 return this.format(chosenString, args);
545 },
546
547 // These messages will be randomly selected.
548 // Note: I've never read Shakespeare. If you're a fan of his then you're not of this.
549 // You'd think that someone good with words would be good with sentences but it ain't so
550 textBanks : {
551 newRound : [
552 "Hark, the wheel turns yet again!",
553 "Lo and behold, a new round has cometh!",
554 "The crack of a new bomb, stand ready and begin the new day!",
555 "The whole bomb's a stage, and all the men and women merely nerds!",
556 "Give heed! The wordgrimage is set about, selectees!",
557 "With thine vocable, set forth and slay that which is fragmentary!",
558 "Words are swords, prompts be stomped!",
559 "Sticks and stones will break thy bones / And prompts will surely kill thee!",
560 ],
561 userTurn : [
562 "It is thy turn, squire. You are facing off against {0}. Do thine worst!",
563 "A vile horror seeks thee. What is thine plan for {0}?",
564 "Nay, {0} fixes to tender thine flame. Take action!",
565 "A wild {0} appears!",
566 "Shalt thou stand and accomplish nothing against {0}?",
567 "{0} is about to wound thee. Time for a curt attack!",
568 "Heedeth thou the mighty {0}! Distress or scuffle is forthcoming!",
569 "Art thou harefooted or harebrained! Observe, it is {0}!",
570 ],
571 playerTurn : [
572 "The heroic, {0}, faces the mighty {1}.",
573 "A ferocious roar as {1} corners {0}!",
574 "{0} needeth stand firm or be struck by {1}",
575 "{1} hath hunger who {0} might satisfy, lest!",
576 "{0} faces down the rat that is {1}!",
577 "{0} challenges the verminous {1}!",
578 "Behold the ravenous {1}! Will {0} succeed?",
579 "{0} versus {1}! The trepidation is palpable!",
580
581 ],
582 userLevelUp : [
583 "LEVEL UP: Thou art now {0}",
584 "UPLEVEL : Now thou art {0}",
585 "RISE SIR, AS '{0}'",
586 "YOU FIT A NEW TITLE: {0}",
587 "THOU HAST EVOLVED INTO {0}",
588 "EXPERIENCE MOLDS YOU INTO {0}",
589 "YOU FEEL STRONGER: You become '{0}'",
590 "REJOICE, YOU ARE: {0}",
591 ],
592 levelUp : [
593 "{0} levelled up to {1}",
594 "{0} has become {1}",
595 "{0} is now titled {1}",
596 "{0} has a new name: {1}",
597 "There is now a '{1}' among us",
598 "{0} is levelling up. What's your excuse, faggoth?",
599 "Out of his cocoon, {0} crawls like a majestic buttefly. How can I compare 'levelling up' to a summer's day?",
600 "{0} is evolving! Don't cream your pants, Pokémon faggots!",
601 ],
602 userWinWord : [
603 "Thou hast slain the beast with your {0} and gained {1} EXP!",
604 "The hellion shatters for thine knowledge is vast. A well deserved {1} EXP!",
605 "Smite triumphed over bite! The prompt is no more. You gain {1} EXP!",
606 "Twas foolish of the beast to underestimate you. You gain {1} EXP",
607 "By Jove, the miscreant is rend like a brittle biscuit from your wallop. I say, good show!",
608 "No mercy have thou for these ogres. Let {0} be a lesson. You gain {1} EXP",
609 "Blasted vermin recoil in fear at thine mighty {0}. {1} EXP to thee!",
610 "Idle barking is met with a firm {0}.",
611 ],
612 winWord : [
613 "{0} has killed the beast with the mighty shout {1} and gained {2} EXP!",
614 "{0} fears no ghosts. His guiding {1} has fled them from the shadows!",
615 "{1}, an apt warcry from {0} who gained {2} EXP!",
616 "{0} decimates the hoodlum with {1} and gains {2} EXP",
617 "{1}, and then the dragon is no more! {0} feels stronger by {2} points!",
618 "{0} delivers the final blow to the whale with his {1}!",
619 "{1} was {0}'s weapon of choice and he earns {2} EXP",
620 "{0} shows no mercy and swings his {1}!",
621 ],
622 userLostLife : [
623 "Thine ignorance woundeth and thou hast been hurt",
624 "Incompetent fool, you were lacerated by a mere rodent!",
625 "Thou areth assrapeth so fervently you lose heart!",
626 "If thy stuttereth again, thou shalt surely receive another blow such as this!",
627 "Nay, thou hast been set upon and crushed by that wild thing!",
628 "Is thy fighting as good as thy 'not-being-a-faggot' skill? Thine heart bleeds!",
629 "Thine tongue utters nothing and so thou art skewered like a pig! The agony!",
630 "Thou art badly contused by the assailing prompt. Man up or die, cutter!",
631 ],
632 lostLife : [
633 "Alas... for poor {0} hath been hurt",
634 "{0} has been struck to the ground!",
635 "{0} cries can be heard echoing through these halls!",
636 "{0} receives hefty damage!",
637 "The horror kill and goes forth to claim its next victim",
638 "Sacre bleu. Les monstrosite c'est trop. {0} ne pas victoirement!",
639 "Don't look. {0} was badly mauled!",
640 "Oh lord gawd. {0} got roughed up!",
641 "{0} was generously harmed!",
642 ],
643 userDeath : [
644 "and a grave loss for thou art dead. {0}, may thee rest in peace!",
645 "yet-th, thineth lifeth hateth endeth. Noth. Whyth?",
646 "but this is the end for you. Rest in pieces, sweet prince",
647 "however you face oblivion.",
648 "and thine life flasheth before thine eyes and thou realizes what a dead faggot thou art!",
649 "now you have left the realm of the living and enter the realm of spectators!",
650 "here, you expire like milk and smell like it too! Smelly dead {0}!",
651 "but bonk bonk you conk into the casket. The wonk was too stronk!",
652 ],
653 death : [
654 "and weepeth, thee morn, for {0} hath left yonder mortal coil!",
655 "{0} exits the stage!",
656 "{0} is snuffed like a candle",
657 "{0} is no more, like, jeez, got dun dead good!",
658 "meow meow meow 'COMPLETELY DEAD' meow meow '{0}'",
659 "{0} kicks the bucket, knocks it over and swallows shit!",
660 "{0}, oh, in the prime of his 'not being able to answer prompts like the faggot he is', is deceased!",
661 "{0}, bites the dust, goes the way of all flesh, gives up the ghost, drops off. You know what I mean",
662 ],
663 endRound : [
664 "All things must end and so it does with {0}!",
665 "The ordeal is over!",
666 "{0} was the death of all but one!",
667 "The seemingly unending wave of prompts comes to a halt!",
668 "Now, caged are the dogs of war!",
669 "And on that bombshell, {0}, we have to end!",
670 "Many have fallen! But not all!",
671 "Nothing lasts forever!, Yet...",
672
673 ],
674 winner : [
675 "Rising from the ashes of felled brethren is the victorious {0}!",
676 "A knight in tested armor, still breathing, the glorious {0}!",
677 "Praise be the heroic {0} for that human/otherkin/refridgerator has not be vanquished!",
678 "The surviving faggot was indeed {0}",
679 "Ahoyhoy and a hullo for the indissoluble {0}",
680 "Is it a bird? Is it a plane? Whatever it is, it's {0}",
681 "Doctors hate {0}! The secret doctors don't want you to know is \"don't be a scrub\"!",
682 "{0} the unending, the ceaseless, the permanent, the unfading; the sole survivor!",
683 ],
684 },
685 }
686
687
688 //I'm so proud of this ugly node constructor
689 //I'm certain it's unconventional, but it works so... yippee
690 //It allows for creating images as nodes within an object and
691 //defining attributes at the same time. LALALALALALALAL HAPPY HAPPY THIS
692 //TOOK SO LONG TO FIGURE OUT!!!! AAAAAH. I'M DUMB AND SMART
693
694 //USAGE: x = imNodeConstructor(type, attributes);
695 // 'type' is the type of element on the form document.createElement("whatever");
696 // 'attributes' are a list of attributes on the object form { att1: value1, att2: value2, ... , attN: valueN }
697 // Post: x is of the type "whatever" with the attributes att1, att2, ... , attN
698 var imgNodeConstructor = function(node, attributes) {
699 for (x in attributes) {
700 node[x] = attributes[x];
701 }
702 return node;
703 };
704
705 // Storage of images and their attributes used by the overlay.
706 bpOverlayImgs = {
707
708 //AutoScroll button on state
709 on: imgNodeConstructor(document.createElement("img"), {
710 width: 30,
711 height: 30,
712 src: bpImgUrls.autoScrollOn,
713 }),
714
715 //AutoScroll button off state
716 off: imgNodeConstructor(document.createElement("img"), {
717 width: 30,
718 height: 30,
719 src: bpImgUrls.autoScrollOff,
720 }),
721
722 //AutoFocus button on state
723 autoFocusOn: imgNodeConstructor(document.createElement("img"), {
724 width: 30,
725 height: 30,
726 src: bpImgUrls.autoFocusOn,
727 }),
728
729 //AutoFocus button off state
730 autoFocusOff: imgNodeConstructor(document.createElement("img"), {
731 width: 30,
732 height: 30,
733 src: bpImgUrls.autoFocusOff,
734 }),
735
736 //Dragon button off state
737 dragOff: imgNodeConstructor(document.createElement("img"), {
738 width: 30,
739 height: 30,
740 src: bpImgUrls.dragOff,
741 }),
742
743 //Dragon button on state
744 dragOn: imgNodeConstructor(document.createElement("img"), {
745 width: 30,
746 height: 30,
747 src: bpImgUrls.dragOn,
748 }),
749
750 //These buttons not constructed because they appear solidary and have already been coded with the old method
751 //No changes necessary unless we intend to add more buttons in the players window
752 hideDeadOn: null, // HideDead button on state
753 hideDeadOff: null, // HideDead button off state
754
755 };
756
757
758 //START - A collection of functions for the dragonDrop
759 //The functions basically name themselves and do what they're told
760 //They are then activated by listeneres when a)User starts dragging the 'draggable'
761 // b)User drags over to somewhere (not default behaviour of linkamajigging)
762 // c)User drops the box.
763
764 //window resize meow
765 window.onresize = resize;
766
767 //Called when browser is resized
768 function resize() {
769 //Let's patch this to do nothing if the box hasn't been created.
770 //Hooray for avoiding erors. HOORAY!
771 if(bpOverlay.dragboxHasBeenCreated) {
772 //Let's get the window size
773 var height = window.innerHeight,
774 width = window.innerWidth;
775
776 //Let's get the dragOn size
777 var infoBox = document.getElementById("infoBox");
778 var dragW = infoBox.clientWidth;
779 var dragH = infoBox.clientHeight;
780
781 //Let's get the dragOn... wrawrrr
782 var dragOn = document.getElementById("dragonDrop");
783
784 //... and his coordinates. Nifty. We can use parseInt direclty. directly*
785 var dragX = parseInt(dragOn.style.left);
786 var dragY = parseInt(dragOn.style.top);
787
788 //Resize widthwise
789 if(dragW + dragX > width) {
790 dragOn.style.left = width - dragW + 'px';
791 }
792
793 //Resize heightwise (also pushing it beyond check which isn't needed on the widthwise
794 if(dragY < 0) {
795 dragOn.style.top = "0px";
796 } else if (dragH + dragY > height) {
797 dragOn.style.top = height - dragH + 'px';
798 }
799 }
800 }
801
802 //a: establish the drag and the starting parameters
803 function drag_start(event) {
804 var style = window.getComputedStyle(event.target, null);
805 event.dataTransfer.setData("text/plain", (parseInt(style.getPropertyValue("left"), 10) - event.clientX) + ',' + (parseInt(style.getPropertyValue("top"), 10) - event.clientY));
806 }
807
808 //b: strange shit wont happen because things wont interact
809 // you can however act as if you're dropping the boxonto the ad and it will revert
810 // so this bug is now a feature: "CANNOT ADBLOCK BY DESIGN"
811 function drag_over(event) {
812 event.preventDefault();
813 return false;
814 }
815
816 //c: computes the offset from the drag and adds it to the style.top & style.left
817 function drop(event) {
818 var offset = event.dataTransfer.getData("text/plain").split(',');
819 var dm = document.getElementById('dragonDrop');
820 var addLeft = event.clientX + parseInt(offset[0], 10);
821 var addTop = event.clientY + parseInt(offset[1], 10);
822
823 //Let's see if we can prevent the jigger from flying offscreen by doing something lazy
824 //heigt and width answer "what is the area considered not to be outside of the game?"
825 var height = window.innerHeight,
826 width = window.innerWidth;
827
828 //((Resize friendly, that is, if we want to make this resizable in the future this should hold)).
829 //We seem to need to acquire this through the document.getElementById, it returns 0 otherwise
830 var dragH = document.getElementById("infoBox").clientHeight;
831 var dragW = document.getElementById("infoBox").clientWidth;
832
833
834 //If the drag sends the dragon offscreen, put it to the edge instead
835 //Otherwise let the user do whatever
836 if (addLeft < 0) {
837 dm.style.left = "0px";
838 } else if (addLeft + dragW > width) {
839 dm.style.left = (width - dragW) + 'px';
840 } else {
841 dm.style.left = addLeft + 'px';
842 }
843
844 //Same but for the height
845 if (addTop < 0) {
846 dm.style.top = "0px";
847 } else if (addTop + dragH > height) {
848 dm.style.top = (height - dragH) + 'px';
849 } else {
850 dm.style.top = addTop + 'px';
851 }
852 event.preventDefault();
853 return false;
854 }
855
856
857
858
859 //The body needs to be able to accept the new positions
860 //So naturally the listeners are added to the body
861 //(Imagine how weird this paragraph above would be in a medical paper)
862 document.body.addEventListener('dragover', drag_over, false);
863 document.body.addEventListener('drop', drop, false);
864
865
866 //Configure and style the draggable box, then add to the body
867 var dragAside = document.createElement("whatever");
868 dragAside.id = "dragonDrop";
869 dragAside.draggable = "true";
870 dragAside.style.position = "absolute";
871 dragAside.style.left = "100px"; //starting coordinates
872 dragAside.style.top = "100px"; //eh whatevs, nice round number
873 dragAside.style.width = "300px";
874 dragAside.style.background = "rgb(20, 20, 20)";
875 dragAside.addEventListener('dragstart', drag_start, false);
876 document.body.appendChild(dragAside);
877
878 //BEGIN: Functions for the adventureText thing
879 //////////////////////////////////////////////
880
881 var toggleTextAdventure = function(toggle) {
882
883 if(toggle) {
884
885 bpOverlay.adventureFirstRun = true;
886 bpOverlay.adventureTextMode = true;
887
888 //Hide the old
889 var gameCanvas = document.getElementById("GameCanvas");
890 gameCanvas.parentNode.style.backgroundColor="rgb(20,20,20)";
891 gameCanvas.style.display="none";
892
893 //In with the new
894 var textAdventureDiv = document.createElement("DIV");
895 textAdventureDiv.id="adventure";
896 textAdventureDiv.className="adventureMeow";
897 //textAdventureDiv.style="position: relative; padding: 0.25em; -webkit-box-flex: 1; -moz-box-flex: 1; -o-box-flex: 1; box-flex: 1; -webkit-flex: 1; -ms-flex: 1; flex: 1; -webkit-box-orient: vertical; -moz-box-orient: vertical; -o-box-orient: vertical; -webkit-flex-direction: column; -ms-flex-direction: column; flex-direction: column; text-align: left;";
898 textAdventureDiv.style.backgroundColor="rgb(20,20,20)";
899
900
901
902 //Player info
903 var textAdventurePINFO = document.createElement("P");
904 textAdventurePINFO.id = "adventurePINFO";
905 textAdventurePINFO.className = "adventureMeow";
906 var playerLives=0;
907 var playerLetters=["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v"];
908 //What level? How many letters?
909 var playerIndex = 200; //for undefined
910 for(i=0; i < channel.data.actors.length; i++) {
911 if(channel.data.actors[i].authId === window.app.user.authId) {
912 playerLives = channel.data.actors[i].lives;
913 playerLetters = channel.data.actors[i].lockedLetters;
914 playerIndex = i;
915 }
916 }
917
918 if(playerIndex == 200) {
919 var level = 0;
920 } else if(bpOverlay.flips[playerIndex] >= bpOverlay.adventureLevels.length) {
921 var level = bpOverlay.adventureLevels.length - 1;
922 } else {
923 var level = bpOverlay.flips[playerIndex];
924 }
925
926 textAdventurePINFO.innerHTML = window.app.user.displayName + " : " + bpOverlay.adventureLevels[level];
927 textAdventurePINFO.style.color="rgb(255, 255, 51)";
928
929 var remaining="";
930 //Player remaining letters
931 for(i in playerLetters) {
932 remaining+=playerLetters[i].toUpperCase();
933 }
934
935 var textAdventureEXPINFO = document.createElement("DIV");
936 textAdventureEXPINFO.id="adventureEXPINFO";
937 textAdventureEXPINFO.className = "adventureMeow";
938
939 textAdventureEXPINFO.innerHTML = "Experience needed: " + remaining;
940 textAdventureEXPINFO.style.color="rgb(255,255,255)";
941
942
943 textAdventureDiv.appendChild(textAdventureEXPINFO);
944 textAdventureDiv.appendChild(textAdventurePINFO);
945
946 //Player Container
947 var textAdventurePlayer = document.createElement("DIV");
948 textAdventurePlayer.id = "adventurePlayer";
949 textAdventurePlayer.className="adventureMeow";
950 textAdventurePlayer.style = "position: relative";
951 textAdventureDiv.appendChild(textAdventurePlayer);
952
953 //Player image container
954 var textAdventureAvatar = document.createElement("IMG");
955 textAdventureAvatar.id = "adventureAvatar";
956 textAdventureAvatar.className = "adventureMeow";
957 if(typeof window.app.user.pictureURL === "undefined") {
958 textAdventureAvatar.src = "http://bombparty.sparklinlabs.com/images/AvatarPlaceholder.png";
959 } else {
960 textAdventureAvatar.src = window.app.user.pictureURL;
961 }
962 textAdventureAvatar.style = "position: relative; float: right";
963 textAdventurePlayer.appendChild(textAdventureAvatar);
964
965 //Player bar container
966 var textAdventureBars = document.createElement("DIV");
967 textAdventureBars.id = "adventureBars";
968 textAdventureBars.className = "adventureMeow";
969 textAdventureBars.style="position: relative; float: right;";
970 textAdventurePlayer.appendChild(textAdventureBars);
971
972 //Player health bar
973 var textAdventureHealth = document.createElement("DIV");
974 textAdventureHealth.id = "adventureHealth";
975 textAdventureHealth.className = "adventureMeow";
976 textAdventureHealth.style="position: absolute;";
977 textAdventureHealth.style.width= (10 + playerLives * 70) + "px";
978 textAdventureBars.appendChild(textAdventureHealth);
979
980 //Player exp bar
981 var textAdventureExp = document.createElement("DIV");
982 textAdventureExp.id = "adventureExp";
983 textAdventureExp.className = "adventureMeow";
984 textAdventureExp.style="position: absolute;";
985 textAdventureExp.style.width= (10 + (21 - playerLetters.length) * 10) + "px";
986 textAdventureBars.appendChild(textAdventureExp);
987
988 //Turn container
989 var textAdventureTurns = document.createElement("DIV");
990 textAdventureTurns.id = "adventureTurns";
991 textAdventureTurns.className = "adventureMeow";
992 textAdventureTurns.style = "position: relative; float: right; color: #FFF";
993 textAdventurePlayer.appendChild(textAdventureTurns);
994
995 //Message container
996 var textAdventureMessages = document.createElement("DIV");
997 textAdventureMessages.id = "adventureMessages";
998 textAdventureMessages.className="adventureMeow";
999 textAdventureMessages.align="center";
1000 textAdventureMessages.style="position: relative;";
1001 textAdventureDiv.appendChild(textAdventureMessages);
1002
1003 //We need input... and on the project too. Like, comment and subscribe. How? Magic.
1004 //Or maybe not. Let's keep this though if we wanna customize more later.
1005 //var textAdventureInput = document.createElement("INPUT");
1006 //textAdventureInput.id="adventureInput";
1007 //textAdventureInput.className="adventureMeow";
1008 //textAdventureInput.style.outline="none";
1009 //textAdventureInput.style.border="none";
1010 //textAdventureInput.style.backgroundColor="rgb(20,20,20)";
1011 //textAdventureInput.style.color="rgb(90, 249, 12)";
1012 //textAdventureMessages.appendChild(textAdventureInput);
1013
1014 gameCanvas.parentNode.insertBefore(textAdventureDiv, gameCanvas);
1015
1016 //I hate CSS. This seems to only work after
1017 textAdventureMessages.style.marginLeft="40px";
1018 textAdventureMessages.style.marginTop="40px";
1019 textAdventureMessages.style.clear="both";
1020 textAdventurePlayer.style.marginLeft="40px";
1021 textAdventurePlayer.style.marginTop="5px"
1022 textAdventurePlayer.style.overflow="hidden";
1023 textAdventurePINFO.style.marginLeft="40px";
1024 textAdventureEXPINFO.style.marginLeft="40px";
1025 textAdventureEXPINFO.style.marginTop="40px";
1026 textAdventureAvatar.style.height="76px";
1027 textAdventureAvatar.style.width="76px";
1028 textAdventureAvatar.style.float="left";
1029 textAdventureBars.style.marginLeft="10px";
1030 textAdventureBars.style.height="75px";
1031 textAdventureBars.style.width="400px";
1032 textAdventureBars.style.border="1px solid #141414";
1033 textAdventureBars.style.float="left";
1034 textAdventureHealth.style.backgroundColor="rgb(255,255,0)";
1035 textAdventureHealth.style.border="10px solid red";
1036 textAdventureHealth.style.marginTop="8px";
1037 textAdventureHealth.style.marginBottom="14px";
1038 textAdventureExp.style.backgroundColor="rgb(0,0,204)";
1039 textAdventureExp.style.border="10px solid blue";
1040 textAdventureExp.style.marginTop="14px";
1041 textAdventureExp.style.marginBottom="8px";
1042 textAdventureTurns.style.marginLeft="20px";
1043 textAdventureTurns.style.float="left";
1044
1045 } else {
1046
1047 bpOverlay.adventureTextMode = false;
1048 bpOverlay.adventureFirstRun = false; //Probably not needed but I like symmetry
1049
1050 //Out with the old
1051 var old = document.getElementsByClassName("adventureMeow");
1052 for(i = 0; i < old.length; i++) {
1053 old[i].parentNode.removeChild(old[i]);
1054 }
1055
1056 //In with the previously old, ehhh.... default.
1057 //Pretty neat that you can reset this with an empty string. Oh boy.
1058 var gameCanvas = document.getElementById("GameCanvas");
1059 gameCanvas.parentNode.style.backgroundColor="";
1060 gameCanvas.style.display="";
1061
1062 }
1063
1064 }
1065
1066 //msg is the string to be displayed
1067 //formatter is a color code on the form "rgb(x,y,z)" where 0<=x,y,z<=255
1068 var sendAdventureMessage = function(msg, formatter) {
1069 if(bpOverlay.adventureTextMode) {
1070 var textAdventureMessages = document.getElementById("adventureMessages");
1071 var textAdventureMsg = document.createElement("P");
1072 textAdventureMsg.style.color=formatter;
1073 textAdventureMsg.innerHTML=msg;
1074
1075 //We don't want the messages to extend out of the page... now do we?
1076 if(textAdventureMessages.children.length > 10) {
1077 textAdventureMessages.appendChild(textAdventureMsg);
1078 textAdventureMessages.removeChild(textAdventureMessages.firstChild);
1079 } else {
1080 textAdventureMessages.appendChild(textAdventureMsg);
1081 }
1082
1083
1084 }
1085 }
1086 //////////////////////////////////////////////
1087 //END functions for the adventure text thing
1088
1089 // It's my turn to write a long completely unnecessary feature!
1090 //////////////////////////////////////////////
1091 var particleEmitters = []; // An array to store the emitters
1092 var particleArray = []; // An array for actual particles
1093 var particleImgs = []; // An array for particle images
1094 var rafID; // For storing the ID for requestAnimationFrame, so we can stop it
1095
1096 var textColors = { // For converting colours into text specifiers
1097 "#cccccc": "statusText",
1098 "#ffffff": "promptText",
1099 "#a0a0a0": "wordText",
1100 "#60aa60": "highlightedText",
1101 "#404040": "bonusLetterText",
1102 };
1103
1104 // Bleh, it turns out that browsers haven't completely agreed on a single function for requestAnimationFrame/cancelAnimationFrame
1105 var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || window.msRequestAnimationFrame;
1106 var cancelAnimationFrame = window.cancelAnimationFrame || window.mozCancelAnimationFrame;
1107
1108 // This needs to be exposed because it needs to be called by the GM script
1109 loadCustomTheme = function (themeObj) {
1110 if (themeObj && !bpOverlay.isThemed) {
1111 bpOverlay.isThemed = true;
1112
1113 // Grab the canvas and its context
1114 var canvas = document.getElementById("GameCanvas");
1115 var ctx = canvas.getContext("2d");
1116
1117 // Add and replace images if necessary
1118 if (themeObj.images) {
1119 for (var i in themeObj.images) {
1120 // Validation
1121 if (!themeObj.images[i].src) {
1122 console.log("Error: source is not defined for image " + i + ". Will try to execute anyway");
1123 }
1124
1125 replacementImages[i] = themeObj.images[i].src;
1126
1127 if (themeObj.images[i].xOffset || themeObj.images[i].yOffset) {
1128 var offset = {x: 0, y: 0};
1129 if (themeObj.images[i].xOffset) {
1130 offset.x = themeObj.images[i].xOffset;
1131 }
1132 if (themeObj.images[i].yOffset) {
1133 offset.y = themeObj.images[i].yOffset
1134 }
1135
1136 replacementOffsets[i] = offset;
1137 }
1138
1139 replaceImage(i);
1140 }
1141 }
1142
1143 // Create a style if there is one.
1144 if (themeObj.css) {
1145 if (themeObj.css.text) {
1146 var style = document.createElement('style');
1147 style.id = "customThemeStyle";
1148 style.appendChild(document.createTextNode(themeObj.css.text));
1149 document.getElementsByTagName('head')[0].appendChild(style);
1150 }
1151 else if (themeObj.css.src) {
1152 var style = document.createElement('link');
1153 style.id = "customThemeStyle";
1154 style.rel = "stylesheet";
1155 style.type = "text/css";
1156 style.href = themeObj.css.src;
1157 document.getElementsByTagName('head')[0].appendChild(style);
1158 }
1159 else {
1160 console.log("Error: css field declared but no valid css provided. Will try to execute anyway");
1161 }
1162 }
1163
1164 // If there is particles, then do this whole canvas drawing shebang
1165 if (themeObj.particles) {
1166 // Load up the images
1167 if (themeObj.particles.images && themeObj.particles.images.length) {
1168 for (i = 0; i < themeObj.particles.images.length; i++) {
1169 var img = new Image;
1170 img.src = themeObj.particles.images[i];
1171 particleImgs.push(img);
1172 }
1173 }
1174 else {
1175 console.log("Error: no particle images defined. Will try to execute anyway");
1176 }
1177
1178 var lastTime = (new Date).getTime();
1179
1180 if (themeObj.particles.emitters && themeObj.particles.emitters.length) {
1181 var emitters = themeObj.particles.emitters;
1182 for (i = 0; i < emitters.length; i++) {
1183 var e = emitters[i];
1184 e.spawnParticle = function () {
1185 if (Math.random() < e.spawnRate[bpOverlay.particleSpawnRate]) {
1186 // This bit's quite involved because the data entered into the JSON could take quite a few forms...
1187
1188 var rawSize = Math.random();
1189
1190 // Determine x-position
1191 var pos_x = e.position.x * canvas.width;
1192 if (e.position.width) {
1193 pos_x += Math.random() * e.position.width * canvas.width;
1194 }
1195
1196 // determine y-position
1197 var pos_y = e.position.y * canvas.height;
1198 if (e.position.height) {
1199 pos_y += Math.random() * e.position.height * canvas.height;
1200 }
1201
1202 // Determine x-velocity
1203 var vel_x;
1204 if (typeof(e.velocity.x) === "number") {
1205 vel_x = e.velocity.x;
1206 }
1207 else {
1208 vel_x = e.velocity.x.min + (e.velocity.linkedToSize ? rawSize : Math.random()) * (e.velocity.x.max - e.velocity.x.min);
1209 }
1210
1211 // Determine y-velocity
1212 var vel_y;
1213 if (typeof(e.velocity.y) === "number") {
1214 vel_y = e.velocity.y;
1215 }
1216 else {
1217 vel_y = e.velocity.y.min + (e.velocity.linkedToSize ? rawSize : Math.random()) * (e.velocity.y.max - e.velocity.y.min);
1218 }
1219
1220 // Determine size
1221 var size;
1222 if (typeof(e.size) === "number") {
1223 size = e.size;
1224 }
1225 else {
1226 size = e.size.min + rawSize * (e.size.max - e.size.min);
1227 }
1228
1229 // Determine rotation
1230 var rot;
1231 if (typeof(e.rotation) === "number") {
1232 rot = e.rotation;
1233 }
1234 else {
1235 rot = e.rotation.min + (e.rotation.linkedToSize ? rawSize : Math.random()) * (e.rotation.max - e.rotation.min);
1236 }
1237
1238 // Determine angular velocity
1239 var rotV;
1240 if (typeof(e.angularVelocity) === "number") {
1241 rotV = e.angularVelocity;
1242 }
1243 else {
1244 rotV = e.angularVelocity.min + (e.angularVelocity.linkedToSize ? rawSize : Math.random()) * (e.angularVelocity.max - e.angularVelocity.min);
1245 }
1246
1247 if (e.gravity) {
1248 // Determine x-gravity
1249 var grav_x = e.gravity.x;
1250 // Determine y-gravity
1251 var grav_y = e.gravity.y;
1252 }
1253 else {
1254 var grav_x = 0;
1255 var grav_y = 0;
1256 }
1257
1258 // Put all of this into a particle object
1259 particleArray.push({
1260 imageId: Math.floor(Math.random() * particleImgs.length),
1261 position: {
1262 x: pos_x,
1263 y: pos_y,
1264 },
1265 velocity: {
1266 x: vel_x,
1267 y: vel_y,
1268 },
1269 size: size,
1270 rotation: rot,
1271 angularVelocity: rotV,
1272 gravity: {
1273 x: grav_x,
1274 y: grav_y,
1275 },
1276 alpha: 1,
1277 });
1278
1279 }
1280 }
1281 }
1282 }
1283 else if (themeObj.particles.emitters === [] || !themeObj.particles.emitters) {
1284 console.log("Error: Particles declared but no emitters defined. Will try to execute anyway");
1285 }
1286
1287 // Actual function used for drawing the particles
1288 particleAnimate = function () {
1289 // Run spawning thing on all the emitters
1290 var emitters = themeObj.particles.emitters;
1291 for (i = 0; i < emitters.length; i++) {
1292 emitters[i].spawnParticle();
1293 }
1294
1295 // Time params
1296 var current = (new Date).getTime();
1297 var dt = Math.min((current - lastTime) / 1000, 1);
1298 lastTime = current;
1299
1300 // Euh, reset the scaling
1301 ctx.setTransform(1, 0, 0, 1, 0, 0);
1302
1303 // Loop through all the particles
1304 // We're looping backwards because we want to be able to remove particles
1305 // Without messing up list indices
1306 for (i = particleArray.length - 1; i >= 0; i--) {
1307 var particle = particleArray[i];
1308 var img = particleImgs[particle.imageId];
1309
1310 // Update its parameters
1311 particle.position.x += particle.velocity.x * dt;
1312 particle.position.y += particle.velocity.y * dt;
1313 particle.rotation += particle.angularVelocity * dt;
1314 particle.rotation %= Math.PI * 2;
1315 particle.velocity.x += particle.gravity.x * dt;
1316 particle.velocity.y += particle.gravity.y * dt;
1317
1318 // set the alpha to something lower if it's over the prompt or the locked letters.
1319 if (Math.abs(particle.position.x - canvas.width / 2) < 100 && Math.abs(particle.position.y - canvas.height /2) < 200 ||
1320 particle.position.x + width/2 > canvas.width - Math.min(100, canvas.height / 11 * 2)) {
1321 particle.alpha = Math.max(0.1, particle.alpha - 0.05);
1322 }
1323 else if (particle.alpha < 1) {
1324 particle.alpha = Math.min(1, particle.alpha + 0.05);
1325 }
1326
1327 if (particle.alpha < 1) {
1328 var ga = ctx.globalAlpha;
1329 ctx.globalAlpha = particle.alpha;
1330 }
1331
1332 var width = img.width * particle.size;
1333 var height = img.height * particle.size;
1334
1335 // Draw the particle
1336 // Apparently, to rotate things, you have to transform the entire canvas context
1337 ctx.translate(particle.position.x, particle.position.y);
1338 ctx.rotate(particle.rotation);
1339 ctx.drawImage(img, -width/2, -height/2, width, height);
1340 ctx.rotate(-particle.rotation);
1341 ctx.translate(-particle.position.x, -particle.position.y);
1342
1343 if (particle.alpha < 1) {
1344 ctx.globalAlpha = ga;
1345 }
1346
1347 // Remove the particle if necessary
1348 if (particle.x < -0.2 * canvas.width ||
1349 particle.position.y < -0.2 * canvas.height ||
1350 particle.position.x > 1.2 * canvas.width ||
1351 particle.position.y > 1.2 * canvas.height) {
1352 particleArray.splice(i, 1);
1353 }
1354 }
1355
1356 // Request it again so
1357 rafID = requestAnimationFrame(particleAnimate);
1358 }
1359
1360 particleAnimate();
1361 }
1362
1363 // Do the text styles
1364 if (themeObj.textStyles) {
1365 ctx.fillTextRedux = ctx.fillText;
1366 ctx.fillText = function () {
1367 var tc = ctx.shadowColor;
1368 var tb = ctx.shadowBlur;
1369 var fs = ctx.fillStyle;
1370 var f = ctx.font;
1371
1372 var fontA = f.split("px ");
1373 var fontSize = fontA[0];
1374 var fontFamily = fontA[1];
1375
1376 var thisStyle = themeObj.textStyles[textColors[fs]];
1377 if (thisStyle) {
1378 if (thisStyle.color) {
1379 ctx.fillStyle = thisStyle.color;
1380 }
1381 if (thisStyle.shadow) {
1382 if (typeof thisStyle.shadow === "string")
1383 ctx.shadowColor = thisStyle.shadow;
1384 else
1385 ctx.shadowColor = "#000";
1386
1387 ctx.shadowBlur = 10;
1388 }
1389
1390 var newFont = [fontSize, fontFamily];
1391
1392 if (textColors[fs] === "wordText" || textColors[fs] === "highlightedText") {
1393 if (fontSize === "20") {
1394 if (thisStyle.majorFontSize) {
1395 newFont[0] = thisStyle.majorFontSize;
1396 }
1397 }
1398 else if (fontSize === "14") {
1399 if (thisStyle.minorFontSize) {
1400 newFont[0] = thisStyle.minorFontSize;
1401 }
1402 }
1403 }
1404 else {
1405 if (thisStyle.fontSize) {
1406 newFont[0] = thisStyle.fontSize;
1407 }
1408 }
1409
1410 if (thisStyle.fontFamily) {
1411 newFont[1] = thisStyle.fontFamily;
1412 }
1413
1414 ctx.font = newFont.join("px ");
1415 }
1416
1417 ctx.fillTextRedux.apply(this, arguments);
1418
1419 ctx.font = f;
1420 ctx.fillStyle=fs;
1421 ctx.shadowColor=tc;
1422 ctx.shadowBlur=tb;
1423 };
1424
1425 // Since I'm changing the text size and style sometimes, I need to adjust ctx.measureText
1426 ctx.measureTextRedux = ctx.measureText;
1427 ctx.measureText = function () {
1428 var f = ctx.font;
1429 var fs = ctx.fillStyle;
1430
1431 var fontA = f.split("px ");
1432 var fontSize = fontA[0];
1433 var fontFamily = fontA[1];
1434
1435 var thisStyle = themeObj.textStyles[textColors[fs]];
1436 if (thisStyle) {
1437 var newFont = [fontSize, fontFamily];
1438
1439 // Helpfully, the only time the measureText is actually used
1440 // In Elisee's code is when the words below the players are being drawn
1441 if (textColors[fs] === "wordText" || textColors[fs] === "highlightedText") {
1442 if (fontSize === "20") {
1443 if (thisStyle.majorFontSize) {
1444 newFont[0] = thisStyle.majorFontSize;
1445 }
1446 }
1447 else if (fontSize === "14") {
1448 if (thisStyle.minorFontSize) {
1449 newFont[0] = thisStyle.minorFontSize;
1450 }
1451 }
1452 }
1453
1454 if (thisStyle.fontFamily) {
1455 newFont[1] = thisStyle.fontFamily;
1456 }
1457
1458 ctx.font = newFont.join("px ");
1459 }
1460
1461 var result = ctx.measureTextRedux.apply(this, arguments);
1462
1463 ctx.font = f;
1464
1465 return result;
1466 }
1467
1468 }
1469
1470 for (var i in gameImages) {
1471 replaceImage(i);
1472 }
1473
1474 }
1475 else if (!themeObj && bpOverlay.isThemed) {
1476 bpOverlay.isThemed = false;
1477
1478 // Get rid of the custom style sheet if there is one
1479 if (style = document.getElementById("customThemeStyle")) {
1480 style.parentNode.removeChild(style);
1481 }
1482
1483 // Cancel the particle animation
1484 if (rafID !== undefined) {
1485 cancelAnimationFrame(rafID);
1486 rafID = undefined;
1487 }
1488
1489 var ctx = document.getElementById("GameCanvas").getContext("2d");
1490
1491 if (ctx.fillTextRedux) {
1492 ctx.fillText = ctx.fillTextRedux;
1493 }
1494 if (ctx.measureTextRedux) {
1495 ctx.measureText = ctx.measureTextRedux;
1496 }
1497
1498 for (var i in gameImages) {
1499 unreplaceImage(i);
1500 }
1501
1502 particleImgs = [];
1503 particleArray = [];
1504
1505 // Reset so nothing else gets replaced
1506 replacementImages = {};
1507 replacementOffsets = {};
1508 }
1509 };
1510
1511 // Replace image function
1512 // Call this when a new image is being drawn or when custom theme mode is being activated
1513 var replaceImage = function (imageName) {
1514 if (replacementImages[imageName] && gameImages[imageName]) {
1515 gameImages[imageName].src = replacementImages[imageName];
1516 }
1517 };
1518
1519 // Replace the images back with the originals
1520 // Call when custom theme mode is deactivated
1521 var unreplaceImage = function (imageName) {
1522 if (backupSources[imageName] && gameImages[imageName]) {
1523 gameImages[imageName].src = backupSources[imageName];
1524 }
1525 };
1526
1527 //////////////////////////////////////////////
1528 // END of most of the custom theme code
1529
1530 //Creates a score notifier in ca. the middle of the screen
1531 var scoreNotifier = function(points) {
1532 var scoreDiv = document.createElement("whatever");
1533 scoreDiv.innerHTML="<p style='color: yellow; font-size: 300%'>" + points;
1534 scoreDiv.id = "scoreDiv";
1535 scoreDiv.draggable = "true";
1536 scoreDiv.style.position = "absolute";
1537 scoreDiv.style.left = window.innerWidth / 3 + "px";
1538 scoreDiv.style.top = window.innerHeight / 2 + "px";
1539 scoreDiv.style.width = window.innerWidth / 2 + "px";
1540 scoreDiv.style.background = "rgb(20, 20, 20, 0)";
1541 document.body.appendChild(scoreDiv);
1542
1543 jQ('#scoreDiv').animate({"top":"20px", "opacity":"0"}, 1000, function() {
1544 document.getElementById("scoreDiv").parentNode.removeChild(document.getElementById("scoreDiv"));
1545 });
1546 };
1547
1548 var updateScores = function() {
1549
1550 var stupidSort = [];
1551 for(i=0; i<Object.keys(bpOverlay.playerNames).length; i++) {
1552 var temp = {names: bpOverlay.playerNames[i], score: bpOverlay.playerScores[i]};
1553 stupidSort.push(temp);
1554 }
1555
1556 stupidSort.sort(function(a, b) {
1557 return b.score - a.score;
1558 });
1559
1560 var lTab = document.getElementById("LeaderboardTab");
1561 lTab.innerHTML = "<table>";
1562 var names="";
1563 for(i=0; i<Object.keys(bpOverlay.playerNames).length; i++) {
1564 if(stupidSort[i].names.length > 14) {
1565 names = stupidSort[i].names.substring(0,14);
1566 } else {
1567 names = stupidSort[i].names;
1568 }
1569 lTab.innerHTML += "<br><tr><td>" + names + "</td><td> --- </td><td>" + stupidSort[i].score + "</td></tr>";
1570 }
1571 lTab.innerHTML += "</table>";
1572 };
1573
1574
1575 // This function is called whenever a new round begins.
1576 var generateActorConditions = function() {
1577 // If there is already a box, get rid of it
1578 if (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated) {
1579 var infoBox = document.getElementById("infoBox");
1580
1581 //remove infoBox from wherever it is
1582 var meow = infoBox.parentNode;
1583 meow.removeChild(infoBox);
1584
1585 //If if then more lines hence both.
1586 bpOverlay.boxHasBeenCreated = false;
1587 bpOverlay.dragBoxHasBeenCreated = false;
1588 }
1589
1590 // Shift current round's variables onto previous round's variables
1591 bpOverlay.prevPlayerNames = bpOverlay.playerNames;
1592 bpOverlay.prevLostLives = bpOverlay.lostLives;
1593 bpOverlay.prevFlips = bpOverlay.flips;
1594 bpOverlay.prevUFlips = bpOverlay.uFlips;
1595 bpOverlay.prevWords = bpOverlay.words;
1596 bpOverlay.prevTimeText = bpOverlay.timeText;
1597 bpOverlay.prevWordCount = bpOverlay.wordCount;
1598
1599 // Reset current round variables
1600 bpOverlay.playerNames = {};
1601 bpOverlay.playerAuthId = {};
1602 bpOverlay.lostLives = {};
1603 bpOverlay.flips = {};
1604 bpOverlay.uFlips = {};
1605 bpOverlay.alpha = {};
1606 bpOverlay.words = {};
1607
1608 actors = channel.data.actors;
1609
1610 // Loop through current round's actors and log data accordingly
1611 for (i = 0; i < actors.length; i++) {
1612 bpOverlay.playerNames[i] = actors[i].displayName;
1613 bpOverlay.playerAuthId[actors[i].authId] = i;
1614 bpOverlay.flips[i] = 0;
1615 bpOverlay.uFlips[i] = 0;
1616 bpOverlay.lostLives[i] = 0;
1617 bpOverlay.alpha[i] = {progress: 0, completed: 0};
1618 bpOverlay.words[i] = 0;
1619 }
1620
1621 // More resetting...
1622 bpOverlay.wordCount = 0;
1623 bpOverlay.timeText = tran.t("timeText") + "0:00";
1624
1625 var d = new Date();
1626 bpOverlay.startTime = d.getTime();
1627
1628 // Create the infoBox.
1629 var infoBox = document.createElement("DIV");
1630 infoBox.id = "infoBox";
1631
1632 // Create the time text display
1633 var timeElement = document.createElement("H2");
1634 timeElement.id = "infoBoxTimer";
1635 timeElement.align = "center"; //Might get overridden if not in drag-mode but who cares :D
1636 timeElement.style.color = "rgb(200,200,200)"; //this as well but that's a good thing
1637 timeElement.textContent = bpOverlay.timeText;
1638 infoBox.appendChild(timeElement);
1639
1640 // Create the word counter display
1641 var wordCounterElement = document.createElement("H2");
1642 wordCounterElement.align = "center";
1643 wordCounterElement.id = "infoWordCounter";
1644 wordCounterElement.style.color = "rgb(200,200,200)";
1645 wordCounterElement.textContent = tran.t("wordCountText") + "0";
1646 infoBox.appendChild(wordCounterElement);
1647
1648 // Oh boy, a horizontal rule! Gee willikers!
1649 var horizontalRule = document.createElement("hr");
1650 infoBox.appendChild(horizontalRule);
1651
1652 // Contain the scoreboard table in a div to allow for scrolling
1653 var infoTableDiv = document.createElement("DIV");
1654 infoTableDiv.className = "infoTableDiv";
1655 infoTableDiv.align = "center"
1656
1657 //Retain choices between rounds from the settings meow
1658 //Ah... this may not exist. Let's patch it.
1659 if(document.getElementById("containerSelect")) {
1660 var cont = document.getElementById("containerSelect");
1661 if(cont.value === "fitToPlayers") {
1662 infoTableDiv.style.maxHeight = "1000px"; //autoflow hinders big meow meow meow yappety yak
1663 } else {
1664 infoTableDiv.style.maxHeight = "100px";
1665 }
1666 }
1667 infoTableDiv.style.overflowY = "auto";
1668
1669 // Make the actual scoreboard table
1670 var infoTable = document.createElement("table");
1671 infoTable.style.tableLayout = "fixed";
1672 infoTable.style.width = "100%";
1673 infoTableDiv.appendChild(infoTable);
1674
1675 // First row is a button and column headers.
1676 var firstRow = document.createElement("tr");
1677
1678 // The first element in the first row is a container for the show/hide button
1679 var showHideContainer = document.createElement("td");
1680
1681 // Make the show_hide button
1682 var showHideButton = document.createElement("BUTTON");
1683 var showHideButtonDiv = document.createElement("DIV");
1684 showHideButtonDiv.className = "headerButtonDiv";
1685 showHideButton.id = "autoFocusButton";
1686 showHideButton.className = "headerButton";
1687 showHideButton.title = tran.t("showHideButtonTitle");
1688
1689 showHideButton.onclick = function() {
1690 // Flip the state of hideDead
1691 bpOverlay.hideDead = !bpOverlay.hideDead;
1692
1693 if (bpOverlay.hideDead) {
1694 // if hideDead is true, remove the off state image and add the on state image
1695 showHideButton.removeChild(bpOverlayImgs.hideDeadOff);
1696 showHideButton.appendChild(bpOverlayImgs.hideDeadOn);
1697
1698 // Hide all the grayed out players
1699 // A bit of a hack, storing information in colours
1700 var rows = document.getElementsByClassName("playerRow");
1701 for (i = 0; i < rows.length; i++) {
1702 if (rows[i].style.color == "rgb(102, 102, 102)") {
1703 rows[i].style.display = "none";
1704 }
1705 }
1706 } else {
1707 // if hideDead is false, remove the on state image and add the off state image
1708 showHideButton.removeChild(bpOverlayImgs.hideDeadOn);
1709 showHideButton.appendChild(bpOverlayImgs.hideDeadOff);
1710
1711 // Since showing the overlay will scroll the chat up, we might want to check if
1712 // the user needs to have the chat scrolled down after expansion
1713 var chatLog = document.getElementById("ChatLog");
1714 var scrollDown = (bpOverlay.autoScroll || chatLog.scrollTop == chatLog.scrollHeight)
1715
1716 // Show all the hidden rows
1717 var rows = document.getElementsByClassName("playerRow");
1718 for (i = 0; i < rows.length; i++) {
1719 if (rows[i].style.display == "none") {
1720 rows[i].style.display = "table-row";
1721 }
1722 }
1723
1724 //Let's add a call to the window.onresize if this is done while the dragon is at the bottom
1725 //This prevents overflow
1726 var funky = window.onresize;
1727 funky();
1728
1729 // If the chat does need scrolling down then scroll it down
1730 if (scrollDown) {
1731 chatLog.scrollTop = chatLog.scrollHeight;
1732 }
1733 }
1734 };
1735
1736 // Add the appropriate image based on the current setting of hideDead
1737 if (bpOverlay.hideDead) {
1738 showHideButton.appendChild(bpOverlayImgs.hideDeadOn);
1739 } else {
1740 showHideButton.appendChild(bpOverlayImgs.hideDeadOff);
1741 }
1742
1743 // ...and append it all into the first row. Phew!
1744 showHideButtonDiv.appendChild(showHideButton);
1745 showHideContainer.appendChild(showHideButtonDiv);
1746 firstRow.appendChild(showHideContainer);
1747
1748 // Make headers for the columns, and append to the first row
1749 var flipColumnHeader = document.createElement("td");
1750 flipColumnHeader.textContent = tran.t("flipsText");;
1751 flipColumnHeader.style.color = "rgb(200,200,200)";
1752 flipColumnHeader.align = "center";
1753 flipColumnHeader.style.padding = "2px";
1754 flipColumnHeader.style.fontSize = "11px";
1755 flipColumnHeader.style.width = "40px";
1756 flipColumnHeader.title = tran.t("flipsTitle");
1757 firstRow.appendChild(flipColumnHeader);
1758 var uFlipColumnHeader = document.createElement("td");
1759 uFlipColumnHeader.textContent = tran.t("uFlipsText");
1760 uFlipColumnHeader.style.color = "rgb(200,200,200)";
1761 uFlipColumnHeader.align = "center";
1762 uFlipColumnHeader.style.padding = "2px";
1763 uFlipColumnHeader.style.fontSize = "11px";
1764 uFlipColumnHeader.style.width = "40px";
1765 uFlipColumnHeader.title = tran.t("uFlipsTitle");
1766 firstRow.appendChild(uFlipColumnHeader);
1767 var lostLivesColumnHeader = document.createElement("td");
1768 lostLivesColumnHeader.textContent = tran.t("deathsText");
1769 lostLivesColumnHeader.style.color = "rgb(200,200,200)";
1770 lostLivesColumnHeader.align = "center";
1771 lostLivesColumnHeader.style.padding = "2px";
1772 lostLivesColumnHeader.style.fontSize = "11px";
1773 lostLivesColumnHeader.style.width = "40px";
1774 lostLivesColumnHeader.title = tran.t("deathsTitle");
1775 firstRow.appendChild(lostLivesColumnHeader);
1776 infoTable.appendChild(firstRow);
1777
1778 // add the alphabet thing
1779 var alphaColumnHeader = document.createElement("td");
1780 alphaColumnHeader.textContent = tran.t("alphaText");
1781 alphaColumnHeader.className = "alphaColumn";
1782 alphaColumnHeader.style.color = "rgb(200,200,200)";
1783 alphaColumnHeader.align = "center";
1784 alphaColumnHeader.style.padding = "2px";
1785 alphaColumnHeader.style.fontSize = "11px";
1786 alphaColumnHeader.style.width = "40px";
1787 alphaColumnHeader.title = tran.t("alphaTitle");
1788 firstRow.appendChild(alphaColumnHeader);
1789
1790 var wordsColumnHeader = document.createElement("td");
1791 wordsColumnHeader.textContent = tran.t("wordsText");
1792 wordsColumnHeader.className = "wordsColumn";
1793 wordsColumnHeader.style.color = "rgb(200,200,200)";
1794 wordsColumnHeader.align = "center";
1795 wordsColumnHeader.style.padding = "2px";
1796 wordsColumnHeader.style.fontSize = "11px";
1797 wordsColumnHeader.style.width = "40px";
1798 wordsColumnHeader.title = tran.t("wordsTitle");
1799 firstRow.appendChild(wordsColumnHeader);
1800
1801 // Loop through the players, making a new row for each one
1802 for (i = 0; i < actors.length; i++) {
1803 var playerRow = document.createElement("tr");
1804 playerRow.id = i + " row"; // Used to reference this row later on
1805 playerRow.className = "playerRow";
1806
1807 if (actors[i].authId === app.user.authId) {
1808 playerRow.style.background = "rgba(255,0,0,0.1)";
1809 }
1810
1811 // If the player this row represents is dead, grey it out
1812 if (actors[i].state == "dead") {
1813 playerRow.style.color = "#666";
1814 if (bpOverlay.hideDead) {
1815 playerRow.style.display = "none";
1816 }
1817 } else {
1818 playerRow.style.color = "rgb(210,210,210)";
1819 }
1820
1821 // Make the cell containing the name
1822 var nameData = document.createElement("td");
1823 var name = bpOverlay.playerNames[i];
1824
1825 nameData.textContent = name;
1826 nameData.align = "center";
1827 nameData.style.whiteSpace = "nowrap";
1828 nameData.style.overflow = "hidden";
1829 nameData.style.textOverflow = "ellipsis";
1830 playerRow.appendChild(nameData)
1831
1832 // Make the cell containing the number of flips
1833 var flipData = document.createElement("td");
1834 flipData.id = i + " flips"; // used to reference this cell later
1835 flipData.textContent = "0";
1836 flipData.align = "center";
1837 playerRow.appendChild(flipData);
1838
1839 // Make the cell containing the # of u-flips
1840 var uFlipData = document.createElement("td");
1841 uFlipData.id = i + " uFlips"; // used to reference this cell later
1842 uFlipData.textContent = "0";
1843 uFlipData.align = "center";
1844 playerRow.appendChild(uFlipData);
1845
1846 // Make the cell containing the # of deaths
1847 var lostLivesData = document.createElement("td");
1848 lostLivesData.id = i + " lives"; // used to reference this cell later
1849 lostLivesData.textContent = "0";
1850 lostLivesData.align = "center";
1851 playerRow.appendChild(lostLivesData);
1852
1853 // Make the alpha thing
1854 var alphaData = document.createElement("td");
1855 alphaData.className = "alphaColumn";
1856 alphaData.id = i + " alpha"; // used to reference this cell later
1857 alphaData.innerHTML = "A (0)";
1858 alphaData.align = "center";
1859 playerRow.appendChild(alphaData);
1860
1861 var wordsData = document.createElement("td");
1862 wordsData.className = "wordsColumn";
1863 wordsData.id = i + " words";
1864 wordsData.innerHTML = "0";
1865 wordsData.align = "center";
1866 playerRow.appendChild(wordsData);
1867
1868 // Append the row to the table
1869 infoTable.appendChild(playerRow);
1870 }
1871
1872 // Append the table to the container...
1873 infoBox.appendChild(infoTableDiv);
1874
1875
1876
1877 //Creates either a docked infoBox or a draggable one
1878 if (bpOverlay.dragonDrop) {
1879 //Created if user wishes dragonDrop
1880 var deDragonDrop = document.getElementById("dragonDrop");
1881 deDragonDrop.appendChild(infoBox);
1882 bpOverlay.dragBoxHasBeenCreated = true;
1883
1884
1885 } else {
1886 //otherwise this is created
1887 var sideBar = document.getElementById("Sidebar");
1888 sideBar.insertBefore(infoBox, sideBar.firstChild);
1889
1890 bpOverlay.boxHasBeenCreated = true;
1891 }
1892
1893 //And yet another check of overflow needed in case the dragon is on the bottom and a new round starts
1894 //with more players than the previous round. This is outside the if(bpOverlay.dragonDrop) because the user might wish
1895 //to switch sides after starting a new round in docked mode. Bugfixes ahoy hoy yay yay
1896 var funky = window.onresize;
1897 funky();
1898
1899 // ..and finally, if autoScrolling is on, scroll the chat back down since this would've caused the chat to scroll up
1900 if (bpOverlay.autoScroll) {
1901 var chatLog = document.getElementById("ChatLog");
1902 chatLog.scrollTop = chatLog.scrollHeight;
1903 }
1904
1905 }
1906
1907 //Usage: generateSettingsElement(itemText, options, selectId, settingsFunction)
1908 //string 'itemText' is the text to the right of the drop down options pane
1909 //object 'options' is an object {value: Text, value2: Text2, ... , valueN: TextN}
1910 // 'value, ..., valueN' are the value we can compare from selectElement.value
1911 // 'Text, ..., TextN' are the strings that the user see when selecting options
1912 //string 'selectId': for your function you probably want to use document.getElementById(selectId)
1913 //function 'settingsFunction' is the function that is called on selectElement.onchange
1914 var generateSettingsElement = function(itemText, options, selectId, locatorId, settingsFunction) {
1915 //Create the text item
1916 //Oh god the horrors of navigating the dom DOM DOOOOM
1917
1918 // Made it so it keeps appending rows to the same table
1919 // As far as I'm aware, I don't think you need a tbody element here
1920 var sTabTable = document.getElementById(locatorId);
1921 var sTabTr = document.createElement("TR");
1922 sTabTable.appendChild(sTabTr);
1923 var sTabTd = document.createElement("TD");
1924 sTabTd.innerHTML = itemText;
1925 sTabTr.appendChild(sTabTd);
1926
1927 //Create the options DOM DOM POMPOM
1928 var sTabOptionsTd = document.createElement("TD");
1929 sTabTr.appendChild(sTabOptionsTd);
1930 var sTabSelect = document.createElement("SELECT");
1931 sTabSelect.id = selectId;
1932 sTabOptionsTd.appendChild(sTabSelect);
1933
1934 //Populate the select field with {Value: text} from options which is an object
1935 for(x in options) {
1936 var op = document.createElement("OPTION");
1937 op.textContent = options[x];
1938 op.value = x;
1939 sTabSelect.appendChild(op);
1940 }
1941
1942 //Add the function to the onchange listener for the newly created select
1943 sTabSelect.onchange = settingsFunction;
1944
1945 //OPTIONAL: Reflect on your lifechoices, such as programming when you should be studying
1946 }
1947
1948 //Usage: function(onSrc, offSrc, buttonId, buttonMessage, defaultState, buttonFunction)
1949 //img onSrc is the image for the on- state
1950 //img offSrc is the image for the off-state
1951 //string buttonId is the id for the created button item in the header
1952 //string buttonMessage is the hovermessage presented by the button
1953 //boolean defaultState: if true, then the button is created with the onSrc image, else the offSrc image
1954 //function buttonFunction is the function that is called when the button is clicked
1955 var makeHeaderButton = function(onSrc, offSrc, buttonId, buttonMessage, defaultState, buttonFunction) {
1956
1957 // Actually make the button, and its container div
1958 var button = document.createElement("BUTTON");
1959 var buttonDiv = document.createElement("DIV");
1960 buttonDiv.className = "headerButtonDiv";
1961
1962 // Insert the button container div into the header
1963 var header = document.getElementsByTagName("header")[0];
1964 var lastChild = header.lastChild;
1965 header.insertBefore(buttonDiv, lastChild);
1966
1967 var onElement = document.createElement("img");
1968 onElement = onSrc;
1969
1970 var offElement = document.createElement("img");
1971 offElement = offSrc;
1972
1973 // General "stylistic touches"
1974 button.id = buttonId;
1975 button.className = "headerButton";
1976 button.title = buttonMessage;
1977
1978 button.onclick = buttonFunction;
1979
1980 //Depending on defaultState, have the button start with the "on" image or the "off"
1981 if (defaultState) {
1982 button.appendChild(onSrc);
1983 button.dataset.state = true; // Turns out, data attributes are pretty useful in storing state
1984 } else {
1985 button.appendChild(offSrc);
1986 button.dataset.state = false;
1987 }
1988
1989 buttonDiv.appendChild(button);
1990 };
1991
1992
1993 // This function is called regularly to update the time text
1994 var updateTime = function() {
1995 // Don't bother doing anything if there's no game or the infoBox hasn't been created
1996 if (channel.data.state === 'playing' && (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated)) {
1997 // Timer code
1998 // Copied directly from Ice's bot
1999 var d = new Date();
2000 var seconds = Math.floor((d.getTime() - bpOverlay.startTime) / 1000);
2001 if ((seconds % 60) < 10) {
2002 var formatter = "0";
2003 } else {
2004 var formatter = "";
2005 }
2006
2007 bpOverlay.timeText = tran.t("timeText") + Math.floor(seconds / 60) + ":" + formatter + "" + (seconds % 60) + "";
2008
2009 // Umm, hmm, this if statement is redundant.
2010 // It looks like it anyway.
2011 // Yus - It most certainly is. I don't know what was going on
2012 //if (bpOverlay.boxHasBeenCreated) {
2013 // Update the infoBox timer text
2014 document.getElementById("infoBoxTimer").textContent = bpOverlay.timeText;
2015 //}
2016 }
2017 }
2018
2019 window.twitch = {
2020 emotes: {},
2021 templates: {},
2022 nonalphabetic: {},
2023 blacklist: {},
2024 };
2025
2026 // Process twitch emotes
2027 try {
2028 var globalEmotes = window.twitch_global;
2029 var subEmotes = window.twitch_subscriber;
2030 var ffzEmotes = window.ffz_emotes;
2031
2032 // TODO: Move this to a resource file somewhere
2033 var specialEmotes = {
2034 "D:": {
2035 src: "//cdn.betterttv.net/emote/55028cd2135896936880fdd7/1x",
2036 titlePrefix: "(bttv) ",
2037 },
2038 "(ditto)": {
2039 src: "//cdn.betterttv.net/emote/554da1a289d53f2d12781907/1x",
2040 titlePrefix: "(bttv) "
2041 },
2042 "(puke)": {
2043 src: "//cdn.betterttv.net/emote/550288fe135896936880fdd4/1x",
2044 titlePrefix: "(bttv) "
2045 },
2046 "MikuStare": {
2047 src: "//cdn.frankerfacez.com/emoticon/143791/1",
2048 titlePrefix: "(ffz) ",
2049 },
2050 "FrankerZed": {
2051 src: "//cdn.frankerfacez.com/emoticon/43060/1",
2052 titlePrefix: "(ffz) ",
2053 },
2054 "WooperZ": {
2055 src: "//cdn.frankerfacez.com/emoticon/18150/1",
2056 titlePrefix: "(ffz) ",
2057 },
2058 "KevinSquirtle": {
2059 src: "//cdn.frankerfacez.com/emoticon/18146/1",
2060 titlePrefix: "(ffz) ",
2061 },
2062 "TopPika": {
2063 src: "//cdn.frankerfacez.com/emoticon/18149/1",
2064 titlePrefix: "(ffz) ",
2065 },
2066 "FrenchNerd": {
2067 src: "//cdn.frankerfacez.com/emoticon/83539/1",
2068 titlePrefix: "(ffz) ",
2069 },
2070 "DontBully": {
2071 src: "//cdn.frankerfacez.com/emoticon/144330/1",
2072 titlePrefix: "(ffz) ",
2073 },
2074 };
2075
2076 twitch.templates.global = globalEmotes.template;
2077 twitch.templates.subscriber = subEmotes.template;
2078
2079 // Handle twitch based emotes
2080 jQ.each(globalEmotes.emotes, function (code, emote) {
2081 if (!emote.image_id) {
2082 throw "Missing image ID for " + code;
2083 }
2084 twitch.emotes[code] = { image_id: emote.image_id, type: "global" };
2085
2086 if (code.search(/\W/) > -1) {
2087 twitch.nonalphabetic[code] = twitch.emotes[code];
2088 }
2089 });
2090
2091 jQ.each(subEmotes.channels, function (channel, channelObj) {
2092 jQ.each(channelObj.emotes, function (index, emote) {
2093 var code = emote.code;
2094 if (/^([A-Z][a-z]*|[A-Z]+|[a-z]+)$/.test(code)) {
2095 twitch.blacklist[code] = {
2096 image_id: emote.image_id,
2097 channel: channel,
2098 type: "subscriber"
2099 };
2100 }
2101 else {
2102 twitch.emotes[code] = {
2103 image_id: emote.image_id,
2104 channel: channel,
2105 type: "subscriber"
2106 };
2107
2108 if (code.search(/\W/) > -1) {
2109 twitch.nonalphabetic[code] = twitch.emotes[code];
2110 }
2111 }
2112 });
2113 });
2114
2115 // Handle FFZ emotes
2116 jQ.each(ffzEmotes.sets, function (setName, set) {
2117 jQ.each(set.emoticons, function (index, emote) {
2118 var code = emote.name;
2119
2120 if (!emote.urls[1]) {
2121 throw "Missing source url for " + code;
2122 }
2123
2124 twitch.emotes[code] = {
2125 css: emote.css,
2126 margins: emote.margins,
2127 src: emote.urls[1],
2128 type: "ffz",
2129 };
2130
2131 if (code.search(/\W/) > -1) {
2132 twitch.nonalphabetic[code] = twitch.emotes[code];
2133 }
2134 });
2135 });
2136
2137 // Handle special emotes
2138 jQ.each(specialEmotes, function (code, emote) {
2139 twitch.emotes[code] = {
2140 src: emote.src,
2141 titlePrefix: emote.titlePrefix,
2142 type: "special",
2143 };
2144
2145 if (code.search(/\W/) > -1) {
2146 twitch.nonalphabetic[code] = twitch.emotes[code];
2147 }
2148 });
2149 }
2150 catch (e) {
2151 // Emote loading failed
2152 console.error(e);
2153 console.error("Twitch emotes have not loaded. This is likely to be an API error " +
2154 "(check http://twitchemotes.com/api_cache/v2/global.json " +
2155 "http://twitchemotes.com/api_cache/v2/subscriber.json and http://api.frankerfacez.com/v1/set/global )");
2156 bpOverlay.emoteError = true;
2157 }
2158
2159 var twitchify_new = function (message) {
2160 if (!bpOverlay.twitchOn || bpOverlay.emoteError)
2161 return message;
2162
2163 function getEmote(code, emote) {
2164 if (twitch.emotes[code] || emote) {
2165 emote = emote || twitch.emotes[code];
2166
2167 var src;
2168 var title;
2169 switch (emote.type) {
2170 case "ffz":
2171 src = emote.src;
2172 title = "(ffz) " + code;
2173 break;
2174 case "subscriber":
2175 src = twitch.templates.global.small.replace(
2176 "{image_id}", emote.image_id);
2177 title = emote.channel + " > " + code;
2178 break;
2179 case "global":
2180 src = twitch.templates.subscriber.small.replace(
2181 "{image_id}", emote.image_id);
2182 title = code;
2183 break;
2184 case "special":
2185 src = emote.src;
2186 title = emote.titlePrefix + code;
2187 break;
2188 }
2189
2190 var margins = emote.margins;
2191
2192 return '<img src="' + src + '" alt="' + code +
2193 '" title="' + title +
2194 (margins ? '" style="margin:' + margins : "") +
2195 '" class="emoticon"></img>';
2196 }
2197 else {
2198 return code;
2199 }
2200 }
2201
2202 // Deal with nonalphanumeric emotes first
2203 var words = message.split(/(<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>| )/);
2204 for (var i = 0; i < words.length; i++) {
2205 if (twitch.nonalphabetic[words[i]]) {
2206 words[i] = getEmote(words[i]);
2207 }
2208 }
2209 message = words.join("");
2210
2211 words = message.split(/(<(?:"[^"]*"['"]*|'[^']*'['"]*|[^'">])+>|\W)/);
2212 for (i = 0; i < words.length; i++) {
2213 words[i] = getEmote(words[i]);
2214 }
2215
2216 message = words.join("");
2217
2218 // Deal with blacklisted emotes
2219 return message.replace(/\[[^\]]+\]/g, function (match) {
2220 var code = match.substring(1, match.length - 1);
2221 if (twitch.blacklist[code]) {
2222 return getEmote(match, twitch.blacklist[code]);
2223 }
2224 return match;
2225 });
2226 };
2227 // It now makes more sense to have the twitch emotes in a separate function
2228 var twitchify = function (message) {
2229 message = twitchify_new(message);
2230 if (bpOverlay.markupOn) {
2231 // Quick and dirty
2232 // Undo the escaping the <b> <i> <s> and <u> tags.
2233 // Not sure if this is completely okay, but whatever for now
2234 message = message.replace(/<(\/?[BISUbisu])>/g, "<$1>");
2235 }
2236 return message;
2237 };
2238
2239 // Inline custom markdown support
2240 var inline = {
2241 escape: /^\\([\\`*{}\[\]()#+\-.!_>~])/,
2242 tag: /^<!--[\s\S]*?-->|^<\/?\w+(?:"[^"]*"|'[^']*'|[^'">])*?>/,
2243 strong: /^\*\*([\s\S]+?)\*\*(?!\*)/,
2244 underline: /^__([\s\S]+?)__(?!_)/,
2245 del: /^~~(?=\S)([\s\S]*?\S)~~/,
2246 em: /^\b_((?:__|[\s\S])+?)_\b|^\*(?=\S)((?:\*\*|\s+[^\*\s]|[^\s\*])*?[^\s\*])\*(?!\*)/,
2247 code: /^(`+)\s*([\s\S]*?[^`])\s*\1(?!`)/,
2248 br: /^ {2,}\n(?!\s*$)/,
2249 text: /^[\s\S]+?(?=[\\<!\[_*`~]| {2,}\n|$)/
2250 };
2251
2252 var inlineLex = function (src) {
2253 var out = "";
2254
2255 while (src) {
2256 // escape
2257 if (cap = inline.escape.exec(src)) {
2258 src = src.substring(cap[0].length);
2259 out += cap[1];
2260 continue;
2261 }
2262
2263 // tag
2264 if (cap = inline.tag.exec(src)) {
2265 // Leave it alone
2266 src = src.substring(cap[0].length);
2267 out += cap[0];
2268 continue;
2269 }
2270
2271 // strong
2272 if (cap = inline.strong.exec(src)) {
2273 src = src.substring(cap[0].length);
2274 out += "<strong>" + inlineLex(cap[2] || cap[1]) + "</strong>";
2275 continue;
2276 }
2277
2278 // underline
2279 if (cap = inline.underline.exec(src)) {
2280 src = src.substring(cap[0].length);
2281 // Fuck it, just going to use inline styles
2282 out += "<span style=\"text-decoration: underline\">" + inlineLex(cap[2] || cap[1]) + "</span>";
2283 continue;
2284 }
2285
2286 // em
2287 if (cap = inline.em.exec(src)) {
2288 src = src.substring(cap[0].length);
2289 out += "<em>" + inlineLex(cap[2] || cap[1]) + "</em>";
2290 continue;
2291 }
2292
2293 // del
2294 if (cap = inline.del.exec(src)) {
2295 src = src.substring(cap[0].length);
2296 out += "<del>" + inlineLex(cap[1]) + "</del>";
2297 continue;
2298 }
2299
2300 // code
2301 if (cap = inline.code.exec(src)) {
2302 src = src.substring(cap[0].length);
2303 out += "<code>" + inlineLex(cap[2] || cap[1]) + "</code>";
2304 continue;
2305 }
2306
2307 // br
2308 if (cap = inline.br.exec(src)) {
2309 src = src.substring(cap[0].length);
2310 out += "<br>";
2311 continue;
2312 }
2313
2314 // text
2315 if (cap = inline.text.exec(src)) {
2316 src = src.substring(cap[0].length);
2317 out += cap[0];
2318 continue;
2319 }
2320
2321 if (src) {
2322 throw new Error("Infinite loop starting at: " + src);
2323 }
2324 }
2325
2326 return out;
2327 };
2328
2329 var markdown = function (message) {
2330 try {
2331 return inlineLex(message);
2332 }
2333 catch (e) {
2334 console.log("An error occured whilst parsing markdown:\n" + e.message);
2335 return message;
2336 }
2337 };
2338
2339 // Since a lot of the functions the bot needs to do has to happen before the game updates the state of everything
2340 // We wrap the default game functions to force them to be called after our custom code.
2341 var wrapGameFunctions = function() {
2342
2343 // Context Menus!
2344 jQ(function () {
2345 jQ.contextMenu({
2346 selector: ".User",
2347 items: {
2348 "ban": {
2349 name: i18n.t("nuclearnode:chat.ban"),
2350 className: "contextMenuBanButton",
2351 callback: function () {
2352 var button = jQ(".contextMenuBanButton")[0];
2353 if (button.dataset.state === "1") {
2354 channel.socket.emit("banUser", {displayName: channel.data.usersByAuthId[this[0].dataset.authId].displayName, authId: this[0].dataset.authId});
2355 return true;
2356 }
2357 else {
2358 button.dataset.state = "1";
2359 button.textContent = tran.t("areYouSure");
2360 return false;
2361 }
2362 },
2363 disabled: function (key, opt) {
2364 return !(this[0].dataset.authId && channel.data.usersByAuthId[this[0].dataset.authId] && ["host", "moderator", "hubAdministrator"].indexOf(app.user.role) >= 0)
2365 },
2366 },
2367 "mod": {
2368 name: i18n.t("nuclearnode:chat.mod"),
2369 className: "contextMenuModButton",
2370 callback: function () {
2371 var button = jQ(".contextMenuModButton")[0];
2372 if (button.dataset.state === "1") {
2373 channel.socket.emit("modUser", {displayName: channel.data.usersByAuthId[this[0].dataset.authId].displayName, authId: this[0].dataset.authId});
2374 return true;
2375 }
2376 else {
2377 button.dataset.state = "1";
2378 button.textContent = tran.t("areYouSure");
2379 return false;
2380 }
2381 },
2382 disabled: function (key, opt) {
2383 return !(this[0].dataset.authId && channel.data.usersByAuthId[this[0].dataset.authId] && ["host", "hubAdministrator"].indexOf(app.user.role) >= 0)
2384 },
2385 },
2386 "mute": {
2387 name: "Mute",
2388 callback: function () {
2389 bpOverlay.ignoring[this[0].dataset.authId] = channel.data.usersByAuthId[this[0].dataset.authId].displayName;
2390 var toMute = jQ(".Author-" + this[0].dataset.authId.replace(/:/g, "_"));
2391 for (var i = 0; i < toMute.length; i++) {
2392 toMute[i].style.opacity = .4;
2393 }
2394 updateMuted();
2395 return true;
2396 },
2397 disabled: function (key, opt) {
2398 return !(this[0].dataset.authId && channel.data.usersByAuthId[this[0].dataset.authId])
2399 },
2400 },
2401 }
2402 });
2403
2404 jQ(document.body).on("contextmenu:blur", ".context-menu-item",
2405 function (e) {
2406 var contextMenuItems = jQ(".context-menu-item");
2407 for (var i = 0; i < contextMenuItems.length; i++) {
2408 var item = contextMenuItems[i];
2409 if (item.classList.contains("contextMenuBanButton")) {
2410 item.dataset.state = "0";
2411 item.textContent = i18n.t("nuclearnode:chat.ban");
2412 }
2413 else if (item.classList.contains("contextMenuModButton")) {
2414 item.dataset.state = "0";
2415 item.textContent = i18n.t("nuclearnode:chat.mod");
2416 }
2417 }
2418 }
2419 );
2420 });
2421
2422 // A little wrapper thing to make the canvas fill up the gameImages object
2423 var ctx = canvasContext;
2424 ctx.drawImageRedux = ctx.drawImage; // Do a little wrapping
2425 ctx.drawImage = function () {
2426 for (i = 0; i < missingGameImgs.length; i++) {
2427 if (arguments[0].src.toLowerCase().indexOf("/images/" + missingGameImgs[i].toLowerCase() + ".png") != -1) {
2428 // Fill up gameImages
2429 gameImages[missingGameImgs[i]] = arguments[0];
2430 backupSources[missingGameImgs[i]] = arguments[0].src;
2431 if (bpOverlay.isThemed) {
2432 replaceImage(missingGameImgs[i]);
2433 }
2434
2435 missingGameImgs.splice(i, 1);
2436 break;
2437 }
2438 }
2439
2440 // If there has been an offset specified, apply it
2441 var replaced = false;
2442 for (var i in replacementOffsets) {
2443 if (gameImages[i] && gameImages[i].src === arguments[0].src) {
2444 replaced = replacementOffsets[i];
2445 ctx.translate(replaced.x, replaced.y);
2446 break;
2447 }
2448 }
2449
2450 // Of course, we need to call the actual function first
2451 var val = ctx.drawImageRedux.apply(this, arguments);
2452
2453 // reset the offset
2454 if (replaced) {
2455 ctx.translate(-replaced.x, -replaced.y);
2456 }
2457 return val;
2458 };
2459
2460 // I guess I'm wrapping this as well
2461 channel.socket.listeners("chatMessage").pop();
2462 channel.socket.on("chatMessage", function(e) {
2463 if (!bpOverlay.ignoring[e.userAuthId]) {
2464 var notified = false;
2465 var index;
2466 if (e.text) {
2467 var lowercaseText = e.text.toLowerCase();
2468 if (bpOverlay.notifications) {
2469 if ((index = lowercaseText.indexOf(app.user.displayName.toLowerCase())) !== -1) {
2470 if ((index === 0 || lowercaseText[index-1].search(/\W/) !== -1) &&
2471 (index + app.user.displayName.length >= lowercaseText.length || lowercaseText[index + app.user.displayName.length].search(/\W/) !== -1)) {
2472 if (bpOverlay.is_hidden) {
2473 bpOverlay.notificationSound.play();
2474 }
2475 notified = true;
2476 }
2477 }
2478 else {
2479 var i = 0;
2480 for (; i < bpOverlay.alias.length; i++) {
2481 if ((index = lowercaseText.indexOf(bpOverlay.alias[i])) !== -1) {
2482 if ((index === 0 || lowercaseText[index-1].search(/\W/) !== -1) &&
2483 (index + bpOverlay.alias[i].length >= lowercaseText.length || lowercaseText[index + bpOverlay.alias[i].length].search(/\W/) !== -1)) {
2484 if (bpOverlay.is_hidden) {
2485 bpOverlay.notificationSound.play();
2486 }
2487 notified = true;
2488 break;
2489 }
2490 }
2491 }
2492 }
2493 }
2494 }
2495
2496 null != e.userAuthId ? channel.appendToChat("Message Author-" + e.userAuthId.replace(/:/g, "_") + (notified ? " highlighted" : ""), JST["nuclearnode/chatMessage"]({
2497 text: e.text,
2498 author: JST["nuclearnode/chatUser"]({
2499 user: channel.data.usersByAuthId[e.userAuthId],
2500 i18n: i18n,
2501 app: app
2502 })
2503 })) : channel.appendToChat("Info" + (notified ? " highlighted" : ""), i18n.t("nuclearnode:chat." + e.text))
2504 }
2505 });
2506
2507 // Screw your function for handling chat messages, Elisee
2508 // I'm going to make a better one! With blackjack! And hookers!
2509 JST["nuclearnode/chatMessage"] = function (e) {
2510 var t;
2511 var a = [];
2512 var n = e || {};
2513
2514 return function (e, n) {
2515 a.push(
2516 (null == (t = e) ? "" : t) +
2517 ': <span class="Content">' +
2518 markdown(twitchify(Autolinker.link(jade.escape(null == (t = n) ? "" : t), {
2519 className: "chatMessageLink"
2520 }))) + // This function is literally the exact same as before except for this line
2521 "</span>")
2522 }.call(this, "author" in n ? n.author : "undefined" != typeof author ? author : void 0, "text" in n ? n.text : "undefined" != typeof text ? text : void 0), a.join("")
2523 };
2524
2525 // Get rid of the annoying Ban and Mod buttons next to the chat message.
2526 JST["nuclearnode/chatUser"] = function (e) {
2527 // Again, this is just elisee's code with like just a tiny bit changed
2528 var t, a = [],
2529 n = e || {};
2530 return function (e, n, r) {
2531 var l = [];
2532 (function () {
2533 var t = e.serviceHandles;
2534 if ("number" == typeof t.length)
2535 for (var a = 0, r = t.length; r > a; a++) {
2536 var s = t[a];
2537 null == s && (s = n.t("nuclearnode:serviceHandlePlaceholder")), "guest" != a && l.push(n.t("nuclearnode:userHandleOnService", {
2538 handle: s,
2539 service: n.t("nuclearnode:loginServices." + a)
2540 }))
2541 } else {
2542 var r = 0;
2543 for (var a in t) {
2544 r++;
2545 var s = t[a];
2546 null == s && (s = n.t("nuclearnode:serviceHandlePlaceholder")), "guest" != a && l.push(n.t("nuclearnode:userHandleOnService", {
2547 handle: s,
2548 service: n.t("nuclearnode:loginServices." + a)
2549 }))
2550 }
2551 }
2552 })
2553 .call(this), a.push("<span" + jade.attr("title", l.join(", "), !0, !1) + (e.authId ? jade.attr("data-auth-id", e.authId, !0, !1) : "") + ' class="User">'), "" != e.role && a.push("<span" + jade.attr("title", n.t("nuclearnode:userRoles." + e.role), !0, !1) + jade.cls(["UserRole_" + e.role], [!0]) + "></span> "), a.push(jade.escape(null == (t = e.displayName) ? "" : t)), a.push("</span>")
2554 // Got rid of the Ban and Mod buttons from the line above
2555 }.call(this, "user" in n ? n.user : "undefined" != typeof user ? user : void 0, "i18n" in n ? n.i18n : "undefined" != typeof i18n ? i18n : void 0, "app" in n ? n.app : "undefined" != typeof app ? app : void 0), a.join("")
2556 }
2557 // Those buttons are being rid of so we don't accidentally ban people when trying to chat
2558 // A list of people that you can ban will be provided
2559
2560 // Chat message wrapper
2561 var gameChat = channel.appendToChat;
2562 channel.appendToChat = function(header, message) {
2563 // This stuff's in a try block because I want the default functions to go through even if my custom code fails
2564 try {
2565 // Since Info messages don't go through JST
2566 // This is needed
2567 if (header === "Info" && message.indexOf("class=\"User\"") === -1) {
2568 // Link using the autolinker library any links in the message.
2569 message = Autolinker.link(message, {
2570 className: "chatMessageLink"
2571 });
2572 // That should be fine, because I don't think the Autolinker library disturbs existing tags
2573
2574 message = twitchify(message);
2575
2576 // Pass the message through the markdown thingy
2577 message = markdown(message);
2578 }
2579
2580 // Scroll the chat down.
2581 if (bpOverlay.autoScroll) {
2582 var chatLog = document.getElementById("ChatLog");
2583 chatLog.scrollTop = chatLog.scrollHeight;
2584 }
2585 } finally {
2586 // Do the actual default chat message function
2587 gameChat(header, message);
2588 }
2589 };
2590
2591 // Process previous messages
2592 jQ("#ChatLog > li").each(function (i, el) {
2593 $el = jQ(el);
2594 if ($el.hasClass("Info") && $el.html().indexOf('class="User"') === -1) {
2595 var message = $el.html();
2596 message = Autolinker.link(message, {
2597 className: "chatMessageLink"
2598 });
2599 message = twitchify(message);
2600 message = markdown(message);
2601 $el.html(message);
2602 }
2603 else if ($el.hasClass("Message")) {
2604 $content = $el.find(".Content");
2605 var message = $content.html();
2606 message = Autolinker.link(message, {
2607 className: "chatMessageLink"
2608 });
2609 message = twitchify(message);
2610 message = markdown(message);
2611 $content.html(message);
2612 }
2613 });
2614
2615 // setActivePlayerIndex wrapper
2616 var gameSetActivePlayerIndex = channel.socket.listeners("setActivePlayerIndex").shift();
2617 channel.socket.on("setActivePlayerIndex", function(actor) {
2618 try {
2619 // Since the first event that fires when a game starts is the setActivePlayerIndex event,
2620 // We create the infoBox and any other first-run procedures here
2621 if (bpOverlay.firstRun) {
2622 generateActorConditions();
2623
2624 //reset the alphabet
2625 bpOverlay.alphapos=0;
2626
2627
2628 // Set firstRun to false so a new box is not created every time there's a turn change
2629 bpOverlay.firstRun = false;
2630
2631 if(bpOverlay.adventureTextMode) {
2632 document.getElementById("adventureMessages").innerHTML="";
2633 sendAdventureMessage(adventureTextFormat.chooseText("newRound"), "rgb(10,200,150)");
2634 }
2635
2636 bpOverlay.adventureFirstRun=true;
2637 }
2638
2639 // Hide the context menu if it is the user's turn
2640 if (channel.data.actors[actor].authId === window.app.user.authId) {
2641 jQ('#context-menu-layer').trigger('mousedown');
2642 }
2643
2644 //invisible break
2645 sendAdventureMessage("break", "rgb(20,20,20");
2646
2647
2648 // Chatbox autofocus code && hijacked for textAdventure
2649 // Which creates a strange conundrum of double checking the bpOverlay.autoFocus, but hey. Smaller code.
2650 // Nested checks ahoy because laziness.
2651 if (bpOverlay.autoFocus || bpOverlay.adventureTextMode) {
2652
2653 if(bpOverlay.adventureTextMode) {
2654 //I'm afraid to have these recalculations elsewhere, they aren't that costly anyhow.
2655 //Player info
2656 var playerLives=0;
2657 var playerLetters=["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v"];
2658 var playerIndex = 200;
2659 //What level? How many letters?
2660 for(i=0; i < channel.data.actors.length; i++) {
2661 if(channel.data.actors[i].authId === window.app.user.authId) {
2662 playerLives = channel.data.actors[i].lives;
2663 playerLetters = channel.data.actors[i].lockedLetters;
2664 playerIndex = i;
2665 }
2666 }
2667
2668 if(playerIndex == 200) {
2669 var level = 0;
2670 } else if(bpOverlay.flips[playerIndex] >= bpOverlay.adventureLevels.length) {
2671 var level = bpOverlay.adventureLevels.length - 1;
2672 } else {
2673 var level = bpOverlay.flips[playerIndex];
2674 }
2675
2676 var textAdventurePINFO = document.getElementById("adventurePINFO");
2677 textAdventurePINFO.innerHTML = window.app.user.displayName + " : " + bpOverlay.adventureLevels[level];
2678
2679 var textAdventureEXPINFO = document.getElementById("adventureEXPINFO");
2680 var remaining="";
2681 //Player remaining letters
2682 for(i in playerLetters) {
2683 remaining+=playerLetters[i].toUpperCase();
2684 }
2685
2686 textAdventureEXPINFO.innerHTML = "Experience needed: " + remaining;
2687
2688
2689
2690
2691 //Player health bar
2692 var textAdventureHealth = document.getElementById("adventureHealth");
2693 textAdventureHealth.style.width = (10 + playerLives * 70) + "px";
2694
2695 //Player exp bar
2696 var textAdventureExp = document.getElementById("adventureExp");
2697 textAdventureExp.style.width= (10 + (21 - playerLetters.length) * 10) + "px";
2698
2699 }
2700
2701
2702 //There is a very very very small chance that this will not work for the "first shift" of the "first round"
2703 //It does work as expected after that though. Almost flawless. People will probably not notice.
2704 //Either that or change the code... but who's unlazy enough? I mean
2705 //This works 90% of the time for the first shift and 100% after that.
2706 if (bpOverlay.focusNext && bpOverlay.autoFocus) {
2707 // If focusNext is true (i.e. it's immediately after the player's turn)
2708 // We set the focus to the chatbox, and reset focusNext.
2709 setTimeout(function() {
2710 // We set focus to the chat only if the user is not the current player
2711 if (channel.data.actors[channel.data.activePlayerIndex].authId !== app.user.authId) {
2712 document.getElementById("ChatInputBox").focus();
2713 }
2714 }, 400);
2715 bpOverlay.focusNext = false;
2716
2717 }
2718 //If first-run, then a small delay is needed to get a correct wordRoot.
2719 if(!bpOverlay.adventureFirstRun) {
2720 if (channel.data.actors[actor].authId === window.app.user.authId) {
2721 // If it's the user's turn, set focusNext to true so the next time
2722 // setActivePlayerIndex fires, we set focus to the chatbox
2723 if(bpOverlay.autoFocus) {
2724 bpOverlay.focusNext = true;
2725 }
2726
2727 sendAdventureMessage(
2728 adventureTextFormat.chooseText("userTurn", channel.data.wordRoot),
2729 "rgb(90, 250, 0)"
2730 );
2731
2732
2733 } else {
2734
2735 sendAdventureMessage(
2736 adventureTextFormat.chooseText("playerTurn", channel.data.actors[actor].displayName, channel.data.wordRoot),
2737 "rgb(255, 165, 0)"
2738 );
2739
2740 }
2741 }
2742
2743 }
2744 } finally {
2745 // Call the actual game function
2746 gameSetActivePlayerIndex(actor);
2747 if(bpOverlay.adventureFirstRun) {
2748 bpOverlay.adventureFirstRun = false;
2749 //The channel.data.wordRoot needs to update in the first run
2750 setTimeout(function() {
2751 if (channel.data.actors[actor].authId === window.app.user.authId) {
2752 // If it's the user's turn, set focusNext to true so the next time
2753 // setActivePlayerIndex fires, we set focus to the chatbox
2754 if(bpOverlay.autoFocus) {
2755 bpOverlay.focusNext = true;
2756 }
2757
2758 sendAdventureMessage(
2759 adventureTextFormat.chooseText("userTurn", channel.data.wordRoot),
2760 "rgb(90, 250, 0)"
2761 );
2762
2763
2764 } else {
2765
2766 sendAdventureMessage(
2767 adventureTextFormat.chooseText("playerTurn", channel.data.actors[actor].displayName, channel.data.wordRoot),
2768 "rgb(255, 165, 0)"
2769 );
2770
2771 }
2772 }, 100);
2773 }
2774 //We need to do this shortly after a shift because the channel needs to be updated first, every time.
2775 if(bpOverlay.adventureTextMode) {
2776 setTimeout(function() {
2777 for(i=0; i < channel.data.actors.length; i++) {
2778
2779 var turns = document.getElementById("adventureTurns");
2780 var index = channel.data.activePlayerIndex;
2781 if( index == i ) {
2782 if(typeof channel.data.actors[index].pictureURL === "undefined") {
2783 var imgSource = "http://bombparty.sparklinlabs.com/images/AvatarPlaceholder.png";
2784 } else {
2785 var imgSource = channel.data.actors[index].pictureURL;
2786 }
2787
2788 var EXP = (21 - channel.data.actors[index].lockedLetters.length);
2789 var hearts="";
2790 for(j=0; j < channel.data.actors[index].lives; j++) {
2791 hearts+="♥";
2792 }
2793 turns.innerHTML="<img src=' "+ imgSource + "' height='70px' width='70px' style='float:left; margin-right: 10px'></img><div style='float:right;'><p style='color: #DFA'>" + channel.data.actors[i].displayName + "<p style='color: orange'>Lives: <span style='color: red'>" + hearts + "</span></p><p style='color: #A746C7'>EXP: <span style='color: #7D8ADB'>" + EXP + "/21</div>";
2794 }
2795 }
2796 },
2797 100);
2798
2799 }
2800 }
2801 });
2802
2803 // winWord wrapper
2804 var gameWinWord = channel.socket.listeners("winWord").shift();
2805 channel.socket.on("winWord", function(actor) {
2806 try {
2807 // We have to manually determine if the user flips, because apparently there's no event
2808 // that fires when a player flips.
2809
2810 // t is the player we're considering.
2811 // Why t? I have no idea.
2812 var t = channel.data.actorsByAuthId[actor.playerAuthId];
2813 var playerNum = bpOverlay.playerAuthId[actor.playerAuthId];
2814 var lockedLetters = t.lockedLetters.slice();
2815 var lastWord = t.lastWord.toLowerCase();
2816 var prevExp = lockedLetters.length;
2817 var scoreSum = 0;
2818 // Remove the letters of the last word that a person used
2819 // from the letters they need to flip
2820 for (i = 0; i < lastWord.length; i++) {
2821 var index;
2822 if ((index = lockedLetters.indexOf(lastWord[i])) != -1) {
2823 lockedLetters.splice(index, 1);
2824 }
2825
2826 if(bpOverlay.scoreMode) {
2827 scoreSum += bpOverlay.letterScore[lastWord[i]];
2828 }
2829
2830
2831 }
2832 var experience = prevExp - lockedLetters.length;
2833
2834 if(bpOverlay.scoreMode) {
2835 scoreNotifier(bpOverlay.playerNames[playerNum] + " " + scoreSum);
2836 bpOverlay.playerScores[playerNum] += scoreSum;
2837 updateScores();
2838 }
2839
2840 // Update the words counter
2841 bpOverlay.words[playerNum]++;
2842 if (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated) {
2843 document.getElementById(playerNum + " words").textContent = bpOverlay.words[playerNum];
2844 }
2845
2846 // Update the alpha thing
2847 if (lastWord.toLowerCase()[0] === bpOverlay.alphabet[bpOverlay.alpha[playerNum].progress]) {
2848 bpOverlay.alpha[playerNum].progress++;
2849 if (bpOverlay.alpha[playerNum].progress >= bpOverlay.alphabet.length) {
2850 bpOverlay.alpha[playerNum].progress = 0;
2851 bpOverlay.alpha[playerNum].completed++;
2852 }
2853
2854 if (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated) {
2855 document.getElementById(playerNum + " alpha").textContent = bpOverlay.alphabet[bpOverlay.alpha[playerNum].progress].toUpperCase() + " (" + bpOverlay.alpha[playerNum].completed + ")";
2856 }
2857 }
2858
2859 // If the lockedLetters is empty after removing all those, letters, the player has flipped
2860 var flipped = (lockedLetters.length === 0);
2861
2862 if (flipped) {
2863 // Append one to the flip counter
2864 bpOverlay.flips[playerNum] += 1;
2865
2866 if(bpOverlay.scoreMode) {
2867 bpOverlay.playerScores[playerNum] += 100;
2868 scoreNotifier(bpOverlay.playerNames[playerNum] + " FLIP BONUS");
2869 }
2870
2871 // If the box is created, update it too
2872 if (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated) {
2873 document.getElementById(playerNum + " flips").textContent = bpOverlay.flips[playerNum];
2874 }
2875
2876 if (t.lives === 3) {
2877 // If the flip happened when the player's lives is already at three, it's an u-flip
2878 // Increment and update
2879 bpOverlay.uFlips[playerNum] += 1;
2880 if (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated) {
2881 document.getElementById(playerNum + " uFlips").textContent = bpOverlay.uFlips[playerNum];
2882 }
2883
2884 }
2885
2886 if(bpOverlay.flips[playerNum] >= bpOverlay.adventureLevels.length) {
2887 var level = bpOverlay.adventureLevels.length - 1;
2888 } else {
2889 var level = bpOverlay.flips[playerNum];
2890 }
2891
2892 if(channel.data.actors[playerNum].displayName === window.app.user.displayName) {
2893 sendAdventureMessage(
2894 adventureTextFormat.chooseText("userLevelUp", bpOverlay.adventureLevels[level]),
2895 "rgb(200, 200, 0"
2896 );
2897 } else {
2898 sendAdventureMessage(
2899 adventureTextFormat.chooseText("levelUp", channel.data.actors[playerNum].displayName, bpOverlay.adventureLevels[level]),
2900 "rgb(200, 200, 0)"
2901 );
2902 }
2903
2904 } else {
2905 if(channel.data.actors[playerNum].displayName === window.app.user.displayName) {
2906 sendAdventureMessage(adventureTextFormat.chooseText("userWinWord", t.lastWord.toUpperCase(), experience),
2907 "rgb(250, 0, 250)"
2908 );
2909
2910 } else {
2911 sendAdventureMessage(adventureTextFormat.chooseText("winWord", t.displayName, t.lastWord.toUpperCase(), experience),
2912 "rgb(250, 0, 250)"
2913 );
2914 }
2915 }
2916
2917 // Add one to the word count, and update the box if it's been created
2918 bpOverlay.wordCount += 1;
2919 if (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated) {
2920 document.getElementById("infoWordCounter").textContent = tran.t("wordCountText") + bpOverlay.wordCount;
2921 }
2922
2923 //Let the player get more alphabets
2924 if(channel.data.actors[playerNum].displayName === window.app.user.displayName) {
2925 if(bpOverlay.alphabet.length <= bpOverlay.alphapos) {
2926 bpOverlay.alphapos = 0;
2927 } else {
2928 bpOverlay.alphapos++;
2929 }
2930 }
2931 } finally {
2932 // Call the actual game function
2933 gameWinWord(actor);
2934 }
2935 });
2936
2937 // setPlayerLives wrapper
2938 var gameSetPlayerLives = channel.socket.listeners("setPlayerLives").shift();
2939 channel.socket.on("setPlayerLives", function(actor) {
2940 try {
2941 // Apparently, setPlayerLives is only used for decreasing a player's lives.
2942 // It unfortunately doesn't fire when a player flips.
2943
2944
2945 if(actor.playerAuthId === window.app.user.authId) {
2946 sendAdventureMessage(adventureTextFormat.chooseText("userLostLife"), "rgb(255,20,10)");
2947 } else {
2948 sendAdventureMessage(adventureTextFormat.chooseText("lostLife", channel.data.actorsByAuthId[actor.playerAuthId].displayName),
2949 "rgb(255,20,10)");
2950 }
2951
2952 var t = channel.data.actorsByAuthId[actor.playerAuthId];
2953 var playerNum = bpOverlay.playerAuthId[actor.playerAuthId];
2954
2955 // if the game data's lives is larger than the updated actor's lives, then the player lost a life
2956 if (t.lives > actor.lives) {
2957 // Increment and update
2958 bpOverlay.lostLives[playerNum] += 1;
2959 if (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated) {
2960 document.getElementById(playerNum + " lives").textContent = bpOverlay.lostLives[playerNum];
2961 }
2962 }
2963 } finally {
2964 // Call the actual game function
2965 gameSetPlayerLives(actor);
2966 }
2967 });
2968
2969 // setPlayerState wrapper
2970 var gameSetPlayerState = channel.socket.listeners("setPlayerState").shift();
2971 channel.socket.on("setPlayerState", function(actor) {
2972 // setPlayerState is really only used to make a player dead.
2973 try {
2974 if (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated) {
2975 if (actor.state == "dead") {
2976 // This code basically just greys out the dead player's row on the scoreboard
2977 var playerNum = bpOverlay.playerAuthId[actor.playerAuthId];
2978 var tableRow = document.getElementById(playerNum + " row");
2979 tableRow.style.color = "#666";
2980
2981 // and if hideDead is true, it hides 'em too
2982 if (bpOverlay.hideDead) {
2983 tableRow.style.display = "none";
2984 }
2985 if(actor.playerAuthId === window.app.user.authId) {
2986 sendAdventureMessage(adventureTextFormat.chooseText("userDeath", channel.data.actorsByAuthId[actor.playerAuthId].displayName)
2987 , "rgb(255,255,255");
2988 } else {
2989 sendAdventureMessage(adventureTextFormat.chooseText("death", channel.data.actorsByAuthId[actor.playerAuthId].displayName),
2990 "rgb(255,255,255");
2991 }
2992 }
2993 }
2994 } finally {
2995 // Call the actual game function
2996 gameSetPlayerState(actor);
2997 }
2998 });
2999
3000 // endGame wrapper
3001 var gameEndGame = channel.socket.listeners("endGame").shift();
3002 channel.socket.on("endGame", function(actorName) {
3003 try {
3004 // Set firstRun to true, because we want the box to be redraw next round
3005 bpOverlay.firstRun = true;
3006
3007 if(bpOverlay.adventureTextMode) {
3008 document.getElementById("adventureMessages").innerHTML="";
3009 document.getElementById("adventureTurns").innerHTML="";
3010 sendAdventureMessage(adventureTextFormat.chooseText("endRound", channel.data.wordRoot.toUpperCase()),
3011 "rgb(204, 255, 204)");
3012 }
3013
3014
3015
3016 // Oh, and set the focus to the chatBox if you need to as well
3017 if (bpOverlay.autoFocus) {
3018 if (bpOverlay.focusNext) {
3019 setTimeout(function() {
3020 document.getElementById("ChatInputBox").focus();
3021 }, 400);
3022 bpOverlay.focusNext = false;
3023 }
3024 }
3025
3026 // Play the notification sound if specified
3027 if (bpOverlay.endGameNotification && bpOverlay.is_hidden) {
3028 bpOverlay.notificationSound.play();
3029 }
3030
3031
3032 // Update the time timer as it might be 1 second behind
3033 updateTime();
3034
3035 //Reset the playerScores
3036 bpOverlay.playerScores = Array.apply(null, new Array(20)).map(Number.prototype.valueOf,0);
3037
3038 } finally {
3039 // Call the actual game function
3040 gameEndGame(actorName);
3041 setTimeout(function() {
3042 sendAdventureMessage(adventureTextFormat.chooseText("winner", channel.data.lastWinner),
3043 "rgb(24, 24, 255)");
3044 }, 100);
3045 }
3046 });
3047
3048 // This function makes the tooltip text that lists all the players in the room
3049 // when you hover over the player count.
3050 // Double function: construct the player list in the leaderboards tab
3051 var changePlayerText = function() {
3052 var playerCount = document.getElementsByClassName("ChannelUsers")[0];
3053 var title = tran.t("playersTitle");
3054 for (var i in channel.data.users) {
3055 title += "\n" + channel.data.users[i].displayName;
3056 }
3057 playerCount.title = title;
3058
3059 // Get the playerListDiv
3060 var playerListDiv = document.getElementById("PlayerList");
3061
3062 // Make the innerHTML
3063 var PDIH = "";
3064 for (var i in channel.data.users) {
3065 // e is our lucky letter today
3066 var e = channel.data.users[i];
3067 var t = e.serviceHandles;
3068 var a = [];
3069 var n = i18n;
3070 var r = app;
3071 var l = [];
3072
3073 // At this point, I kinda said eff it
3074 // So we'll just use Elisee's obfuscated code because it works perfectly well
3075 (function () {
3076 var t = e.serviceHandles;
3077 if ("number" == typeof t.length)
3078 for (var a = 0, r = t.length; r > a; a++) {
3079 var s = t[a];
3080 null == s && (s = n.t("nuclearnode:serviceHandlePlaceholder")), "guest" != a && l.push(n.t("nuclearnode:userHandleOnService", {
3081 handle: s,
3082 service: n.t("nuclearnode:loginServices." + a)
3083 }))
3084 } else {
3085 var r = 0;
3086 for (var a in t) {
3087 r++;
3088 var s = t[a];
3089 null == s && (s = n.t("nuclearnode:serviceHandlePlaceholder")), "guest" != a && l.push(n.t("nuclearnode:userHandleOnService", {
3090 handle: s,
3091 service: n.t("nuclearnode:loginServices." + a)
3092 }))
3093 }
3094 }
3095 })
3096 .call(this), a.push("<span" + jade.attr("title", l.join(", "), !0, !1) + jade.attr("data-auth-id", e.authId, !0, !1) + ' class="BpOS-User User">'), "" != e.role && a.push("<span" + jade.attr("title", n.t("nuclearnode:userRoles." + e.role), !0, !1) + jade.cls(["UserRole_" + e.role], [!0]) + "></span> "), a.push(jade.escape(null == (t = e.displayName) ? "" : t)), a.push('<span class="Actions">'), ("moderator" == r.user.role || "host" == r.user.role || "hubAdministrator" == r.user.role) && (a.push('<button' + jade.attr("data-auth-id", e.authId, !0, !1) + jade.attr("data-display-name", e.displayName, !0, !1) + ' class="BanUser">' + jade.escape(null == (t = n.t("nuclearnode:chat.ban")) ? "" : t) + "</button>"), ("host" == r.user.role || "hubAdministrator" == r.user.role) && a.push("<button" + jade.attr("data-auth-id", e.authId, !0, !1) + jade.attr("data-display-name", e.displayName, !0, !1) + ' class="ModUser">' + jade.escape(null == (t = n.t("nuclearnode:chat.mod")) ? "" : t) + "</button>")), a.push("<button" + jade.attr("data-auth-id", e.authId, !0, !1) + jade.attr("data-display-name", e.displayName, !0, !1) + ' class="MuteUser">' + tran.t("muteUser") + "</button>"), a.push("</span>"), a.push("</span>")
3097
3098 PDIH += a.join("");
3099 PDIH += "<br />";
3100 }
3101
3102 playerListDiv.innerHTML = PDIH;
3103 };
3104
3105 // We want it to fire now, when a user is added, and when a users is removed.
3106 channel.socket.on("addUser", changePlayerText);
3107 channel.socket.on("removeUser", changePlayerText);
3108 channel.socket.on("setUserRole", changePlayerText);
3109 changePlayerText();
3110
3111 // Update the muted player text
3112 var updateMuted = function () {
3113 var ignoringDiv = jQ("#IgnoringListDiv")[0];
3114 var innerHTML = "";
3115 var ignoring_any = false;
3116 for (var i in bpOverlay.ignoring) {
3117 innerHTML += "<span title=\"" + i + "\">" + bpOverlay.ignoring[i] + "<span class=\"Actions\"><button class=\"UnmuteUser\" data-auth-id=\"" + i + "\">" + tran.t("unmuteUser") + "</button></span></span><br />"
3118 ignoring_any = true;
3119 }
3120 if (!ignoring_any) {
3121 innerHTML = tran.t("ignoringEmpty");
3122 }
3123 ignoringDiv.innerHTML = innerHTML;
3124 ignoringDiv.dataset.json = JSON.stringify(bpOverlay.ignoring);
3125 }
3126
3127 // Expose this to the globals so the GM script has access to it
3128 setMuted = function (jsonString) {
3129 bpOverlay.ignoring = JSON.parse(jsonString);
3130 for (var i in bpOverlay.ignoring) {
3131 var toMute = jQ(".Author-" + i.replace(/:/g, "_"));
3132 for (var j = 0; j < toMute.length; i++) {
3133 toMute[j].style.opacity = .4;
3134 }
3135 }
3136 updateMuted();
3137 }
3138
3139 // Click wrapper
3140 // Pls put any document.click actions in here
3141 document.addEventListener("click", function (e) {
3142 if ("BUTTON" === e.target.tagName) {
3143 if (e.target.className === "MuteUser") {
3144 bpOverlay.ignoring[e.target.dataset.authId] = e.target.dataset.displayName;
3145 var toMute = jQ(".Author-" + e.target.dataset.authId.replace(/:/g, "_"));
3146 for (var i = 0; i < toMute.length; i++) {
3147 toMute[i].style.opacity = .4;
3148 }
3149 updateMuted();
3150 return;
3151 }
3152 else if (e.target.className === "UnmuteUser") {
3153 delete bpOverlay.ignoring[e.target.dataset.authId];
3154 updateMuted();
3155 return;
3156 }
3157 }
3158 });
3159 };
3160
3161 // Code that needs to be run when the bot activates.
3162 var firstRunProcs = function() {
3163 // The entire styleshee for the bot is wrapper up in this long string.
3164 // Probably a better way of doing this
3165 // Lol. TFW web-console css is hard
3166 var style = document.createElement('style');
3167 style.appendChild(document.createTextNode('.headerButtonDiv { display: -webkit-box; display: -moz-box; display: -webkit-flex; display: -ms-flexbox; display: box; display: flex; opacity: 0.3; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=30)"; filter: alpha(opacity=30);} .headerButtonDiv:hover { opacity: 1; -ms-filter: none; filter: none;} button.headerButton { border: none; background: none; cursor: pointer; opacity: 0.5; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=50)"; filter: alpha(opacity=50); display: -webkit-box; display: -moz-box; display: -webkit-flex; display: -ms-flexbox; display: box; display: flex;} button.headerButton:hover { opacity: 0.8; -ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=80)"; filter: alpha(opacity=80);} button.headerButton:active { opacity: 1; -ms-filter: none; filter: none;} .infoTableDiv::-webkit-scrollbar { width: 15px; height: 15px; } .infoTableDiv::-webkit-scrollbar-button { height: 0px; width: 0px; } .infoTableDiv::-webkit-scrollbar-track { background-color: rgba(0,0,0,0.05); } .infoTableDiv::-webkit-scrollbar-thumb { background-color: rgba(255,255,255,0.1); border: 3px solid transparent; -webkit-border-radius: 6px; border-radius: 6px; -webkit-background-clip: content; -moz-background-clip: content; background-clip: content-box; } .infoTableDiv::-webkit-scrollbar-thumb:hover { background-color: rgba(255,255,255,0.15); } .infoTableDiv::-webkit-scrollbar-corner { background-color: rgba(255,255,255,0.1); }#overlaySettingsTab{text-align:left;overflow-y:auto}#overlaySettingsTab h2{padding:.5em .5em 0;opacity:.5;-ms-filter:"alpha(Opacity=50)";filter:alpha(opacity=50)}#overlaySettingsTab h3{padding:.5em .5em 0;opacity:.5;-ms-filter:"alpha(Opacity=50)";filter:alpha(opacity=50)}#overlaySettingsTab table{width:100%;padding:.5em}#overlaySettingsTab table tr td:nth-child(1){width:40%}#overlaySettingsTab table tr td:nth-child(2){width:60%}#overlaySettingsTab table button:not(.UnbanUser),#overlaySettingsTab table input,#overlaySettingsTab table select,#overlaySettingsTab table textarea{width:100%;background:#444;border:none;padding:.25em;color:#fff;font:inherit}#overlaySettingsTab table textarea{resize:vertical;min-height:3em}#overlaySettingsTab table ul{list-style:none}#overlaySettingsTab .BpOS-User .UserRole_hubAdministrator:before{content:\'[★]\';cursor:default;color:#c63}#overlaySettingsTab .BpOS-User .UserRole_host:before{content:\'★\';cursor:default;color:#dc8}#overlaySettingsTab .BpOS-User .UserRole_administrator:before{content:\'☆\';cursor:default;color:#dc8}#overlaySettingsTab .BpOS-User .UserRole_moderator:before{content:\'â—\';cursor:default;color:#346192}#overlaySettingsTab .Actions button{border:none;background:0 0;cursor:pointer;margin:0 .25em;outline:0;font-weight:400;font-size:smaller;opacity:.8;-ms-filter:"alpha(Opacity=80)";filter:alpha(opacity=80)}#overlaySettingsTab .Actions button.BanUser{color:#a00}#overlaySettingsTab .Actions button.ModUser,#overlaySettingsTab .Actions button.UnmodUser{color:#2a3} .Actions button.MuteUser{color:#ddd} .Actions button.UnmuteUser{color:#eee} #overlaySettingsTab .Actions button:hover{opacity:1;-ms-filter:none;filter:none}#overlaySettingsTab .Actions button:active{background:rgba(255,0,0,.2)}#overlaySettingsTab input{-moz-user-select:text;-webkit-user-select:text;-ms-user-select:text}#ChatLog .highlighted{background: rgba(255, 0, 0, 0.05);}#ChatLog .highlighted:hover {background: rgba(255, 0, 0, 0.1);} input[type=text], textarea { -webkit-user-select: text; -moz-user-select: text; -ms-user-select: text; user-select: text; } .emoticon { display: inline-block; margin: -5px 0; padding-top: 5px; vertical-align: baseline !important; } .playerRow td:not(:first-child) { white-space: nowrap; overflow: hidden; }'));
3168 document.getElementsByTagName('head')[0].appendChild(style);
3169
3170 // Load the hideDead on/off images
3171 var hideDeadOn = document.createElement("img");
3172 hideDeadOn.width = 15;
3173 hideDeadOn.height = 15;
3174 hideDeadOn.src = bpImgUrls.hideDeadOn;
3175 bpOverlayImgs.hideDeadOn = hideDeadOn;
3176
3177 var hideDeadOff = document.createElement("img");
3178 hideDeadOff.width = 15;
3179 hideDeadOff.height = 15;
3180 hideDeadOff.src = bpImgUrls.hideDeadOff;
3181 bpOverlayImgs.hideDeadOff = hideDeadOff;
3182
3183 //AutoScrollButton made with makeHeaderButton Function
3184 //See usage in the declaration-meow for makeHeaderButton
3185 makeHeaderButton(bpOverlayImgs.on,
3186 bpOverlayImgs.off,
3187 "chatDownButton",
3188 tran.t("chatDownButtonTitle"),
3189 true, //Because we want the onImg displayed at creation
3190 //Then the function within this function
3191 function() {
3192 // Flip the autoScroll property
3193 bpOverlay.autoScroll = !bpOverlay.autoScroll;
3194
3195 if (bpOverlay.autoScroll) {
3196 // if autoScroll is true, remove the off image and add the on image...
3197 document.getElementById("chatDownButton").removeChild(bpOverlayImgs.off);
3198 document.getElementById("chatDownButton").appendChild(bpOverlayImgs.on);
3199 document.getElementById("chatDownButton").dataset.state = true;
3200 } else {
3201 // ...and vice versa if autoScroll is false
3202 document.getElementById("chatDownButton").removeChild(bpOverlayImgs.on);
3203 document.getElementById("chatDownButton").appendChild(bpOverlayImgs.off);
3204 document.getElementById("chatDownButton").dataset.state = false;
3205 }
3206 }
3207 );
3208
3209 //autoFocus button made
3210 makeHeaderButton(bpOverlayImgs.autoFocusOn,
3211 bpOverlayImgs.autoFocusOff,
3212 "autoFocusButton",
3213 tran.t("autoFocusButtonTitle"),
3214 true,
3215 function() {
3216 // Flip the autoFocus property
3217 bpOverlay.autoFocus = !bpOverlay.autoFocus;
3218
3219 if (bpOverlay.autoFocus) {
3220 // You must know the drill by now
3221 document.getElementById("autoFocusButton").removeChild(bpOverlayImgs.autoFocusOff);
3222 document.getElementById("autoFocusButton").appendChild(bpOverlayImgs.autoFocusOn);
3223 document.getElementById("autoFocusButton").dataset.state = true;
3224 } else {
3225 // I'm not even going to bother
3226 document.getElementById("autoFocusButton").removeChild(bpOverlayImgs.autoFocusOn);
3227 document.getElementById("autoFocusButton").appendChild(bpOverlayImgs.autoFocusOff);
3228 document.getElementById("autoFocusButton").dataset.state = false;
3229 }
3230 }
3231 );
3232
3233 //dragOn button made
3234 makeHeaderButton(bpOverlayImgs.dragOn,
3235 bpOverlayImgs.dragOff,
3236 "dragButton",
3237 tran.t("dragButtonTitle"),
3238 false,
3239 function() {
3240 //We don't want the button to react if the neither of the boxes have been created
3241
3242 // Well, because generateActorConditions checks bpOverlay.dragOn, I don't think it matters anymore
3243 // So I'm just going to override it :P (no offense)
3244 // This is in preparation for saving settings across bombparty sessions
3245 // Because if the user was to prefer the drag box, that should be the default
3246 // Which means it bpOverlay.dragOn needs to be true from the get-go
3247 if (bpOverlay.boxHasBeenCreated || bpOverlay.dragBoxHasBeenCreated) {
3248 bpOverlay.dragonDrop = !bpOverlay.dragonDrop; //Flippety flop the boolean be bop
3249
3250 var button = document.getElementById("dragButton");
3251 var sideBar = document.getElementById("Sidebar");
3252 var infoBox = document.getElementById("infoBox");
3253 var dragOnDrop = document.getElementById("dragonDrop");
3254
3255 //In short, this is a switch between docked and dragon mode. Ugly and intuitive
3256 if (bpOverlay.dragonDrop) {
3257
3258 sideBar.removeChild(infoBox);
3259 dragOnDrop.appendChild(infoBox);
3260
3261 button.removeChild(bpOverlayImgs.dragOff);
3262 button.appendChild(bpOverlayImgs.dragOn);
3263 bpOverlay.dragBoxHasBeenCreated = true;
3264 bpOverlay.boxHasBeenCreated = false;
3265 button.dataset.state = true;
3266 } else {
3267
3268 dragOnDrop.removeChild(infoBox);
3269 sideBar.insertBefore(infoBox, sideBar.firstChild);
3270 bpOverlay.dragBoxHasBeenCreated = false;
3271 bpOverlay.boxHasBeenCreated = true;
3272
3273 button.removeChild(bpOverlayImgs.dragOn);
3274 button.appendChild(bpOverlayImgs.dragOff);
3275 button.dataset.state = false;
3276 }
3277 } else {
3278 // The subverting begins
3279 bpOverlay.dragonDrop = !bpOverlay.dragonDrop; //Flippety flop the boolean be bop
3280
3281 var button = document.getElementById("dragButton");
3282
3283 if (bpOverlay.dragonDrop) {
3284
3285 button.removeChild(bpOverlayImgs.dragOff);
3286 button.appendChild(bpOverlayImgs.dragOn);
3287 button.dataset.state = true;
3288
3289 } else {
3290
3291 button.removeChild(bpOverlayImgs.dragOn);
3292 button.appendChild(bpOverlayImgs.dragOff);
3293 button.dataset.state = false;
3294
3295 }
3296
3297 //alert("Didn't find the infoBox.\n\nIf you're running this for the first time and the round hasn't started or if it's the same player's turn from when you started the overlay then this is normal.\n\nYou impatient flap :D");
3298 }
3299 }
3300 );
3301
3302
3303 //Add the fourth button on the settingsTab that hopefully doesn't interfere with the others.
3304 var sideButtons = document.getElementById("SidebarTabButtons");
3305 var sideTabs = document.getElementById("SidebarTabs");
3306
3307 //Make a clone of the settingstab and change it to what we want
3308 var overlaySettingsTab = sideTabs.children[1].cloneNode(true); //clone settings tab
3309 overlaySettingsTab.id="overlaySettingsTab";
3310 overlaySettingsTab.setAttribute("class", "");
3311 overlaySettingsTab.innerHTML=""; //Empty the innerHTML
3312
3313 //Make a clone of a tab button and change it to what we want
3314 var overlaySettingsButton = sideButtons.children[0].cloneNode(true); //clone whatever
3315 overlaySettingsButton.id="overlaySettingsButton";
3316 overlaySettingsButton.setAttribute("class", ""); //We don't want it to start active
3317 overlaySettingsButton.innerHTML="BpOS";
3318 overlaySettingsButton.title=tran.t("overlaySettingsButtonTitle");
3319
3320 //Appendum les shittendum
3321 sideButtons.appendChild(overlaySettingsButton);
3322 sideTabs.appendChild(overlaySettingsTab);
3323
3324 //Define and activate the onclick function for our fourth button
3325 function overlaySettingsFunction() {
3326 for(i=0; i<=2; i++) {
3327 document.getElementById("SidebarTabButtons").children[i].classList.remove("Active"); //Prevent two active classes
3328 }
3329 for(i=0; i<=2; i++) {
3330 document.getElementById("SidebarTabs").children[i].classList.remove("Active"); //Prevent two active classes
3331 }
3332
3333 document.getElementById("overlaySettingsButton").setAttribute("class", "Active");
3334 document.getElementById("overlaySettingsTab").setAttribute("class", "Active");
3335
3336 }
3337 overlaySettingsButton.onclick=overlaySettingsFunction;
3338
3339
3340 //We only want this once (I believe) so this is outside of a function
3341 //Generate the overlay section and append it to the SettingsTab
3342 var bpOverlayH2 = document.createElement("H2");
3343
3344 //We add an id for the jq to recognize the object
3345 bpOverlayH2.id="bpOverlayH2";
3346 bpOverlayH2.onclick=function() {
3347 jQ('#overlaySettingsTableWrapper').slideToggle('slow');
3348 };
3349
3350
3351 //Since we can't require any styles nor anything (cross browser), we're just going to quickly write
3352 //a mouseover and mouseout hacks so that this is nice and responsive
3353 bpOverlayH2.onmouseover=function() {
3354 jQ('#bpOverlayH2').css("text-shadow", "0 0 24px white");
3355 };
3356
3357 bpOverlayH2.onmouseout=function() {
3358 jQ('#bpOverlayH2').css("text-shadow", "0 0 0px black");
3359 };
3360
3361 bpOverlayH2.textContent = tran.t("overlaySettingsText");
3362 var settingsTab = document.getElementById("overlaySettingsTab");
3363 settingsTab.appendChild(bpOverlayH2);
3364
3365 // Moved over the settings tab things to here
3366 // We wrap everything in a div because jquery doesn't handle table smoothly
3367 var sTabTableWrapper = document.createElement("DIV");
3368 // Wrapper starts hidden
3369 sTabTableWrapper.style.display="none";
3370 sTabTableWrapper.id="overlaySettingsTableWrapper";
3371 var sTabTable = document.createElement("TABLE");
3372 sTabTable.id = "overlaySettingsTable";
3373 sTabTableWrapper.appendChild(sTabTable);
3374 settingsTab.appendChild(sTabTableWrapper);
3375
3376 //Notifications h2
3377 var notificationsH2 = document.createElement("H2");
3378 notificationsH2.id = "notificationsH2";
3379 notificationsH2.textContent = tran.t("notificationsText");
3380 notificationsH2.onmouseover=function() {
3381 jQ('#notificationsH2').css("text-shadow", "0 0 24px white");
3382 };
3383
3384 notificationsH2.onmouseout=function() {
3385 jQ('#notificationsH2').css("text-shadow", "0 0 0px black");
3386 };
3387
3388 notificationsH2.onclick=function() {
3389 jQ('#notificationsWrapper').slideToggle('slow');
3390 };
3391
3392 settingsTab.appendChild(notificationsH2);
3393
3394
3395
3396 // Moved over the settings tab things to here
3397 // We wrap everything in a div because jquery doesn't handle table smoothly
3398 var notTabTableWrapper = document.createElement("DIV");
3399 // Wrapper starts hidden
3400 notTabTableWrapper.style.display="none";
3401 notTabTableWrapper.id="notificationsWrapper";
3402 var notTabTable = document.createElement("TABLE");
3403 notTabTable.id = "notificationsTable";
3404 notTabTableWrapper.appendChild(notTabTable);
3405 settingsTab.appendChild(notTabTableWrapper);
3406
3407 //Chat options h2
3408 var chatH2 = document.createElement("H2");
3409 chatH2.id = "chatH2";
3410 chatH2.textContent = tran.t("chatOpText");
3411 chatH2.onmouseover=function() {
3412 jQ('#chatH2').css("text-shadow", "0 0 24px white");
3413 };
3414
3415 chatH2.onmouseout=function() {
3416 jQ('#chatH2').css("text-shadow", "0 0 0px black");
3417 };
3418
3419 chatH2.onclick=function() {
3420 jQ('#chatWrapper').slideToggle('slow');
3421 };
3422
3423 settingsTab.appendChild(chatH2);
3424
3425 // Wrapper starts hidden
3426 var chatTabTableWrapper = document.createElement("DIV");
3427 chatTabTableWrapper.style.display="none";
3428 chatTabTableWrapper.id="chatWrapper";
3429 var chatTabTable = document.createElement("TABLE");
3430 chatTabTable.id = "chatOpTable";
3431 chatTabTableWrapper.appendChild(chatTabTable);
3432 settingsTab.appendChild(chatTabTableWrapper);
3433
3434 //Themes h2
3435 var themeH2 = document.createElement("H2");
3436 themeH2.id = "themeH2";
3437 themeH2.textContent = tran.t("themeH2Text");
3438 themeH2.onmouseover=function() {
3439 jQ('#themeH2').css("text-shadow", "0 0 24px white");
3440 };
3441
3442 themeH2.onmouseout=function() {
3443 jQ('#themeH2').css("text-shadow", "0 0 0px black");
3444 };
3445
3446 themeH2.onclick=function() {
3447 jQ('#themeWrapper').slideToggle('slow');
3448 };
3449
3450 settingsTab.appendChild(themeH2);
3451
3452 // Wrapper starts hidden
3453 var themeTabTableWrapper = document.createElement("DIV");
3454 themeTabTableWrapper.style.display="none";
3455 themeTabTableWrapper.id="themeWrapper";
3456 var themeTabTable = document.createElement("TABLE");
3457 themeTabTable.id = "themeOpTable";
3458 themeTabTableWrapper.appendChild(themeTabTable);
3459 settingsTab.appendChild(themeTabTableWrapper);
3460
3461 //Easter h2
3462 var easterH2 = document.createElement("H2");
3463 easterH2.id = "easterH2";
3464 easterH2.textContent = tran.t("easterText");
3465 easterH2.onmouseover=function() {
3466 jQ('#easterH2').css("text-shadow", "0 0 24px white");
3467 };
3468
3469 easterH2.onmouseout=function() {
3470 jQ('#easterH2').css("text-shadow", "0 0 0px black");
3471 };
3472
3473 easterH2.onclick=function() {
3474 jQ('#easterWrapper').slideToggle('slow');
3475 };
3476
3477 settingsTab.appendChild(easterH2);
3478
3479 // Wrapper starts hidden
3480 var easterTabTableWrapper = document.createElement("DIV");
3481 easterTabTableWrapper.style.display="none";
3482 easterTabTableWrapper.id="easterWrapper";
3483 var easterTabTable = document.createElement("TABLE");
3484 easterTabTable.id = "easterTable";
3485 easterTabTableWrapper.appendChild(easterTabTable);
3486 settingsTab.appendChild(easterTabTableWrapper);
3487
3488
3489
3490
3491 // Might as well have the current players in here
3492 var playerListH2 = document.createElement("H2");
3493 playerListH2.id ="PlayerListH2";
3494 playerListH2.textContent = tran.t("playerListText");
3495 playerListH2.onclick = function() {
3496 jQ('#PlayerListWrapper').slideToggle('slow');
3497 };
3498
3499
3500 playerListH2.onmouseover=function() {
3501 jQ('#PlayerListH2').css("text-shadow", "0 0 24px white");
3502 };
3503
3504 playerListH2.onmouseout=function() {
3505 jQ('#PlayerListH2').css("text-shadow", "0 0 0px black");
3506 };
3507
3508 settingsTab.appendChild(playerListH2);
3509 var playerListWrapper = document.createElement("DIV");
3510 playerListWrapper.id = "PlayerListWrapper";
3511 playerListWrapper.style.display = "none";
3512 playerListWrapper.style.marginLeft = "8px";
3513 settingsTab.appendChild(playerListWrapper)
3514
3515 var playerListDiv = document.createElement("DIV");
3516 playerListDiv.id = "PlayerList";
3517 playerListWrapper.appendChild(playerListDiv);
3518
3519 var ignoringListH3 = document.createElement("H3");
3520 ignoringListH3.id = "IgnoringListH3";
3521 ignoringListH3.textContent = tran.t("ignoringText");
3522 ignoringListH3.onclick = function () {
3523 jQ('#IgnoringListDiv').slideToggle('slow');
3524 };
3525 ignoringListH3.onmouseover=function() {
3526 jQ('#IgnoringListH3').css("text-shadow", "0 0 24px white");
3527 };
3528
3529 ignoringListH3.onmouseout=function() {
3530 jQ('#IgnoringListH3').css("text-shadow", "0 0 0px black");
3531 };
3532 playerListWrapper.appendChild(ignoringListH3);
3533
3534 var ignoringListDiv = document.createElement("DIV");
3535 ignoringListDiv.id = "IgnoringListDiv";
3536 ignoringListDiv.style.display = "none";
3537 ignoringListDiv.marginLeft = "8px";
3538 playerListWrapper.appendChild(ignoringListDiv);
3539 ignoringListDiv.textContent = tran.t("ignoringEmpty");
3540
3541 // A little attribution table
3542 var creditsH2 = document.createElement("H2");
3543 creditsH2.id="creditsH2";
3544 creditsH2.onclick = function() {
3545 jQ('#creditsTableWrapper').slideToggle('slow');
3546 jQ('#creditsTextDiv').slideToggle('slow');
3547 };
3548
3549 creditsH2.onmouseover=function() {
3550 jQ('#creditsH2').css("text-shadow", "0 0 24px white");
3551 };
3552
3553 creditsH2.onmouseout=function() {
3554 jQ('#creditsH2').css("text-shadow", "0 0 0px black");
3555 };
3556 creditsH2.textContent = tran.t("creditsText");
3557 settingsTab.appendChild(creditsH2);
3558
3559 var creditsTable = document.createElement("TABLE");
3560 var creditsTableWrapper = document.createElement("DIV");
3561 creditsTableWrapper.style.display="none";
3562 creditsTableWrapper.id = "creditsTableWrapper";
3563 creditsTable.id = "creditsTable";
3564 creditsTable.innerHTML = "\
3565 <tr><td>" + tran.t("credits1") + "</td><td>MrInanimated</td></tr>\
3566 <tr><td>" + tran.t("credits2") + "</td><td>Skandalabrandur</td></tr>\
3567 <tr><td>" + tran.t("credits3") + "</td><td>Sanc</td></tr>\
3568 <tr><td>" + tran.t("creditsAutolinker") + "</td><td><a href=\"https://github.com/gregjacobs/Autolinker.js\">Autolinker.js</a></td></tr>\
3569 <tr><td>" + tran.t("creditsTwitchEmotes") + "</td><td><a href=\"http://twitchemotes.com/\">twitchemotes.com</a></td></tr>\
3570 <tr><td>" + tran.t("creditsContextMenu") + "</td><td><a href=\"http://medialize.github.io/jQuery-contextMenu/\">Medialize</a></td></tr>";
3571 creditsTableWrapper.appendChild(creditsTable);
3572 settingsTab.appendChild(creditsTableWrapper);
3573
3574 var creditsText = document.createElement("DIV");
3575 creditsText.textContent = tran.t("creditsTextText");
3576 creditsText.style.marginLeft = "8px";
3577 creditsText.id="creditsTextDiv";
3578 creditsText.style.display="none";
3579 settingsTab.appendChild(creditsText);
3580
3581
3582 generateSettingsElement(tran.t("containerSizeName"),
3583 {
3584 compact: tran.t("containerSizeOptions.compact"),
3585 fitToPlayers: tran.t("containerSizeOptions.fitToPlayers")
3586 },
3587 "containerSelect", "overlaySettingsTable",
3588 function () {
3589 //Get the infoTableDiv element and the selector created with the id 'containerSelect'
3590 var infoTableDiv = document.getElementsByClassName("infoTableDiv")[0];
3591 var sTabSelect = document.getElementById("containerSelect");
3592
3593 // Enclosed this in an if in case those were not created
3594 if (infoTableDiv && sTabSelect) {
3595
3596 //Change container.style.maxHeight depending on user choice
3597 if(sTabSelect.value === "compact") {
3598 infoTableDiv.style.maxHeight = "100px";
3599 } else if (sTabSelect.value === "fitToPlayers") {
3600 infoTableDiv.style.maxHeight = "1000px"; //The autoflow whatever takes care of this.
3601
3602 //Prevent flowing out of page
3603 //Let's be lazy and get the window.onresize and run it.
3604 var funky = window.onresize;
3605 funky();
3606 } else {
3607 //Do nothing
3608 }
3609 }
3610
3611 }
3612 );
3613
3614 // Twitch Emote settings
3615 generateSettingsElement(
3616 tran.t("twitchEmotesName"),
3617 {
3618 on: tran.t("twitchEmotesOptions.on"),
3619 off: tran.t("twitchEmotesOptions.off")
3620 },
3621 "twitchEmoteSelect", "chatOpTable",
3622 function () {
3623 var teSelect = document.getElementById("twitchEmoteSelect");
3624
3625 if (teSelect.value === "on") {
3626 bpOverlay.twitchOn = true;
3627 }
3628 else if (teSelect.value === "off") {
3629 bpOverlay.twitchOn = false;
3630 }
3631 else {
3632 }
3633 }
3634 );
3635
3636
3637 //The text adventure setting
3638 generateSettingsElement(
3639 tran.t("textAdventureName"),
3640 {
3641 off: tran.t("textAdventureOptions.off"),
3642 on: tran.t("textAdventureOptions.on")
3643 },
3644 "adventureSetting", "easterTable",
3645 function() {
3646 var sTabSelect = document.getElementById("adventureSetting");
3647 if(sTabSelect.value === "on") {
3648 toggleTextAdventure(true);
3649 } else if(sTabSelect.value === "off") {
3650 toggleTextAdventure(false);
3651 } else {
3652 toggleTextAdventure(false);
3653 }
3654 }
3655 );
3656
3657 //Score setting
3658 generateSettingsElement(
3659 tran.t("scoreName"),
3660 {
3661 off: tran.t("scoreOption.off"),
3662 on: tran.t("scoreOption.on"),
3663 },
3664 "scoreSetting", "easterTable",
3665 function() {
3666 var sTabSelect = document.getElementById("scoreSetting");
3667 if(sTabSelect.value === "on") {
3668 bpOverlay.scoreMode=true;
3669 } else {
3670 bpOverlay.scoreMode=false;
3671 var meowswitch = document.getElementById("LeaderboardTab");
3672 meowswitch.innerHTML=" ";
3673 }
3674 }
3675 );
3676 //Hard modes
3677 generateSettingsElement(
3678 tran.t("hardModesName"),
3679 {
3680 none: tran.t("hardModesOptions.none"),
3681 rev: tran.t("hardModesOptions.rev"),
3682 jqv: tran.t("hardModesOptions.jqv"),
3683 az: tran.t("hardModesOptions.az"),
3684 xz: tran.t("hardModesOptions.xz")
3685 },
3686 "hardModes", "easterTable",
3687 function() {
3688 var sTabSelect = document.getElementById("hardModes");
3689 if(sTabSelect.value === "rev") {
3690 var wordInputBox = document.getElementById("WordInputBox");
3691
3692 var checkLetter = function() {
3693 document.getElementById("WordInputBox").value = document.getElementById("WordInputBox").value.split("").reverse().join("");
3694 }
3695 wordInputBox.onchange=checkLetter;
3696 } else if (sTabSelect.value === "jqv") {
3697 var wordInputBox = document.getElementById("WordInputBox");
3698
3699 var checkLetter = function() {
3700 var inValue = document.getElementById("WordInputBox").value.toLowerCase();
3701 if(!((inValue.indexOf("q") > -1) || (inValue.indexOf("v") > -1) || (inValue.indexOf("j") > -1))) {
3702 document.getElementById("WordInputBox").value = tran.t("jqvText");
3703 }
3704
3705 }
3706 wordInputBox.onchange=checkLetter;
3707 } else if (sTabSelect.value === "az") {
3708 bpOverlay.alphapos=0;
3709 var wordInputBox = document.getElementById("WordInputBox");
3710
3711 var checkLetter = function() {
3712 var inValue = document.getElementById("WordInputBox").value.toLowerCase();
3713 if(!(inValue[0] === bpOverlay.alphabet[bpOverlay.alphapos])) {
3714 document.getElementById("WordInputBox").value=tran.t("azText").replace("{l}", bpOverlay.alphabet[bpOverlay.alphapos]);
3715 }
3716 }
3717 wordInputBox.onchange=checkLetter;
3718 } else if (sTabSelect.value === "xz") {
3719 var wordInputBox = document.getElementById("WordInputBox");
3720
3721 var checkLetter = function() {
3722 var inValue = document.getElementById("WordInputBox").value.toLowerCase();
3723 if(!((inValue.indexOf("x") > -1) || (inValue.indexOf("z") > -1))) {
3724 document.getElementById("WordInputBox").value = tran.t("xzText");
3725 }
3726
3727 }
3728 wordInputBox.onchange=checkLetter;
3729
3730 } else {
3731 document.getElementById("WordInputBox").onchange="";
3732 }
3733 }
3734 );
3735
3736 //Theme element
3737 generateSettingsElement(
3738 tran.t("themeName"),
3739 {
3740 none: tran.t("themeOptions.none"),
3741 "https://raw.githubusercontent.com/MrInanimated/bp-overlay/master/themes/xmas/xmas.json": tran.t("themeOptions.xmas"),
3742 custom: tran.t("themeOptions.custom"),
3743 },
3744 "themeSelect", "themeOpTable",
3745 function () {
3746 var themeSelect = document.getElementById("themeSelect");
3747 if (themeSelect.value === "none") {
3748 loadCustomTheme(false);
3749 document.getElementById("customThemeRow").style.display = "none";
3750 }
3751 else if (themeSelect.value === "custom") {
3752 document.getElementById("customThemeRow").style.display = "";
3753 }
3754 else {
3755 document.getElementById("customThemeRow").style.display = "none";
3756 }
3757
3758 // Handling the URLs have to be done outside this code
3759 // Because it needs access to GM_xmlhttpRequest
3760
3761 });
3762
3763 // Manual settings creation because it's an input and not a select
3764 // It's wrapped in a anonymous function because
3765 // I can't be bothered to make sure the variable names don't conflict
3766 (function () {
3767 var sTabTable = document.getElementById("themeOpTable");
3768 var sTabTr = document.createElement("TR");
3769 sTabTr.id = "customThemeRow";
3770 sTabTr.style.display = "none";
3771 sTabTable.appendChild(sTabTr);
3772 var sTabTd = document.createElement("TD");
3773 sTabTd.innerHTML = tran.t("customThemeName");
3774 sTabTr.appendChild(sTabTd);
3775 var sTabOptionsTd = document.createElement("TD");
3776 sTabTr.appendChild(sTabOptionsTd);
3777 var sTabInput = document.createElement("INPUT");
3778 sTabInput.id = "customThemeInput";
3779 sTabOptionsTd.appendChild(sTabInput);
3780 })();
3781
3782 //Particle settings
3783 generateSettingsElement(
3784 tran.t("particlesName"),
3785 {
3786 high: tran.t("particlesOptions.high"),
3787 low: tran.t("particlesOptions.low"),
3788 off: tran.t("particlesOptions.off")
3789 },
3790 "particleSelect", "themeOpTable",
3791 function () {
3792 var sTabSelect = document.getElementById("particleSelect");
3793 if(sTabSelect.value === "high") {
3794 bpOverlay.particleSpawnRate = "high";
3795 } else if(sTabSelect.value === "low") {
3796 bpOverlay.particleSpawnRate = "low";
3797 } else {
3798 bpOverlay.particleSpawnRate = "none";
3799 }
3800 }
3801 );
3802
3803 //Notifies
3804 generateSettingsElement(
3805 tran.t("notificationsName"),
3806 {
3807 on: tran.t("notificationOptions.on"),
3808 off: tran.t("notificationOptions.off"),
3809 },
3810 "notificationsSelect", "notificationsTable",
3811 function () {
3812 var sTabSelect = document.getElementById("notificationsSelect");
3813 if (sTabSelect.value == "on") {
3814 bpOverlay.notifications = true;
3815 }
3816 else {
3817 bpOverlay.notifications = false;
3818 }
3819 }
3820 );
3821
3822 // This one's a slider!
3823 (function () {
3824 var sTabTable = document.getElementById("notificationsTable");
3825 var sTabTr = document.createElement("TR");
3826 sTabTable.appendChild(sTabTr);
3827 var sTabTd = document.createElement("TD");
3828 sTabTd.innerHTML = tran.t("notificationVolume");
3829 sTabTr.appendChild(sTabTd);
3830 var sTabOptionsTd = document.createElement("TD");
3831 sTabTr.appendChild(sTabOptionsTd);
3832 var sTabInput = document.createElement("INPUT");
3833 sTabInput.id = "notificationVolumeSlider";
3834 sTabInput.type = "range";
3835 sTabInput.min = 0;
3836 sTabInput.max = 100;
3837 sTabInput.value = 100;
3838 sTabInput.addEventListener("change", function (e) {
3839 bpOverlay.notificationSound.volume = sTabInput.value / 100;
3840 bpOverlay.notificationSound.play();
3841 });
3842 sTabInput.title = tran.t("notificationAliasInputTitle");
3843 sTabOptionsTd.appendChild(sTabInput);
3844 })();
3845
3846 // And another, because this one's an input
3847 (function () {
3848 var sTabTable = document.getElementById("notificationsTable");
3849 var sTabTr = document.createElement("TR");
3850 sTabTr.id = "notificationSettingsRow";
3851 sTabTable.appendChild(sTabTr);
3852 var sTabTd = document.createElement("TD");
3853 sTabTd.innerHTML = tran.t("notificationAlias");
3854 sTabTd.title = tran.t("notificationAliasTitle");
3855 sTabTr.appendChild(sTabTd);
3856 var sTabOptionsTd = document.createElement("TD");
3857 sTabTr.appendChild(sTabOptionsTd);
3858 var sTabInput = document.createElement("INPUT");
3859 sTabInput.id = "notificationAliasInput";
3860 sTabInput.addEventListener("change", function (e) {
3861 var n = sTabInput.value.toLowerCase().split(";");
3862 bpOverlay.alias = [];
3863 var i = 0;
3864 for (; i < n.length; i++) {
3865 if (n[i] !== "") {
3866 bpOverlay.alias.push(n[i]);
3867 }
3868 }
3869 });
3870 sTabInput.title = tran.t("notificationAliasInputTitle");
3871 sTabOptionsTd.appendChild(sTabInput);
3872 })();
3873
3874 //Notification meow
3875 generateSettingsElement(
3876 tran.t("endGameNotification"),
3877 {
3878 on: tran.t("endGameNotificationOptions.on"),
3879 off: tran.t("endGameNotificationOptions.off"),
3880 },
3881 "endGameNotificationSelect", "notificationsTable",
3882 function () {
3883 var sTabSelect = document.getElementById("endGameNotificationSelect");
3884 if (sTabSelect.value == "on") {
3885 bpOverlay.endGameNotification = true;
3886 }
3887 else {
3888 bpOverlay.endGameNotification = false;
3889 }
3890 }
3891 );
3892
3893 document.getElementById("endGameNotificationSelect").value = "off";
3894
3895 var alphaColumnStyle = document.createElement("STYLE");
3896 alphaColumnStyle.textContent = ".alphaColumn{display:none;}";
3897 document.head.appendChild(alphaColumnStyle);
3898
3899 //Alpha display
3900 generateSettingsElement(
3901 tran.t("alphaRouletteName"),
3902 {
3903 on: tran.t("alphaRouletteOptions.on"),
3904 off: tran.t("alphaRouletteOptions.off"),
3905 },
3906 "alphaRouletteSelect", "overlaySettingsTable",
3907 function () {
3908 var sTabSelect = document.getElementById("alphaRouletteSelect");
3909 if (sTabSelect.value == "on") {
3910 alphaColumnStyle.textContent = "";
3911 }
3912 else {
3913 alphaColumnStyle.textContent = ".alphaColumn{display:none;}";
3914 }
3915 }
3916 );
3917
3918 var wordsColumnStyle = document.createElement("STYLE");
3919 wordsColumnStyle.textContent = ".wordsColumn{display:none;}";
3920 document.head.appendChild(wordsColumnStyle);
3921
3922 // Word counter display
3923 generateSettingsElement(
3924 tran.t("wordCounterName"),
3925 {
3926 on: tran.t("wordCounterOptions.on"),
3927 off: tran.t("wordCounterOptions.off"),
3928 },
3929 "wordCounterSelect", "overlaySettingsTable",
3930 function () {
3931 var sTabSelect = document.getElementById("wordCounterSelect");
3932 if (sTabSelect.value == "on") {
3933 wordsColumnStyle.textContent = "";
3934 }
3935 else {
3936 wordsColumnStyle.textContent = ".wordsColumn{display:none;}";
3937 }
3938 }
3939 );
3940
3941 // Only add this if speech synthesis is supported by the browser
3942 if (window.speechSynthesis) {
3943 var speechListener = function (e) {
3944 if (bpOverlay.ignoring[e.userAuthId]) return;
3945
3946 var iterator=0;
3947 for(i=200; i<e.text.length; i+= 200) {
3948 for(j=i; j >= iterator; j -= 1) {
3949 if(e.text[j] === " ") {
3950 barkmeow = new SpeechSynthesisUtterance(e.text.substring(iterator, j));
3951 barkmeow.voice = speechSynthesis.getVoices().filter(function(voice) { return voice.name == bpOverlay.speechName; })[0];
3952 barkmeow.onstart = function() { startPing() };
3953 barkmeow.onend = function() { pingSuccess(); };
3954 speechSynthesis.speak(barkmeow);
3955 iterator=j;
3956 break;
3957 } else if(j == iterator) {
3958 barkmeow = new SpeechSynthesisUtterance(e.text.substring(iterator, i));
3959 barkmeow.voice = speechSynthesis.getVoices().filter(function(voice) { return voice.name == bpOverlay.speechName; })[0];
3960 barkmeow.onstart = function() { startPing() };
3961 barkmeow.onend = function() { pingSuccess(); };
3962 iterator=i;
3963 }
3964 }
3965 }
3966 barkmeow = new SpeechSynthesisUtterance(e.text.substring(iterator, e.text.length));
3967 barkmeow.voice = speechSynthesis.getVoices().filter(function(voice) { return voice.name == bpOverlay.speechName; })[0];
3968 barkmeow.onstart = function() { startPing() };
3969 barkmeow.onend = function() { pingSuccess(); };speechSynthesis.speak(barkmeow);
3970 console.log(barkmeow); //fix for onend not being called at the very end. See stackoverflow. Weird
3971 }
3972
3973 //oh boy. Here we go with the speech element
3974 generateSettingsElement(
3975 tran.t("speechName"),
3976 {
3977 off: tran.t("speechOptions.off"),
3978 on: tran.t("speechOptions.on"),
3979 },
3980 "speechSelect", "chatOpTable",
3981 function() {
3982 var sTabSelect = document.getElementById("speechSelect");
3983 if(sTabSelect.value == "on") {
3984 //The name of the ping function that we will reference
3985 var pingu;
3986
3987 //Starts the ping. If a pingSuccess isn't received within 13 seconds (approx time of 31 w's which break this) after starting a chunk,
3988 //then speechSynthesis.cancel() is called. Dirty workaround.
3989 //it throws out a lot of text if an error occurs, but at least it restarts this whole mess for you
3990 var startPing = function() {
3991 pingu = setTimeout(function(){ speechSynthesis.cancel(); }, 13000);
3992 };
3993
3994 //calling this function signifies that a speecherror has not occurred.
3995 var pingSuccess = function() {
3996 clearTimeout(pingu);
3997 };
3998
3999 //the speecher barkmeow
4000 channel.socket.on("chatMessage", speechListener);
4001 } else {
4002 channel.socket.removeListener("chatMessage", speechListener);
4003 }
4004 }
4005 );
4006
4007 generateSettingsElement(
4008 tran.t("voiceSelect"),
4009 {
4010 us: tran.t("voiceOptions.us"),
4011 ukMale: tran.t("voiceOptions.ukMale"),
4012 ukFem: tran.t("voiceOptions.ukFem"),
4013 fran: tran.t("voiceOptions.fran"),
4014 },
4015 "voiceSelect", "chatOpTable",
4016 function () {
4017 var sTabSelect = document.getElementById("voiceSelect");
4018 if (sTabSelect.value == "us") {
4019 bpOverlay.speechName = "Google US English";
4020 }
4021 else if(sTabSelect.value == "ukMale") {
4022 bpOverlay.speechName = "Google UK English Male";
4023 } else if(sTabSelect.value == "ukFem") {
4024 bpOverlay.speechName = "Google UK English Female";
4025 } else {
4026 bpOverlay.speechName = "Google Français";
4027 }
4028 }
4029
4030 );
4031 }
4032
4033 document.getElementById("alphaRouletteSelect").value = "off";
4034 document.getElementById("wordCounterSelect").value = "off";
4035
4036 // Wrap game functions, make the autoscroll/focus buttons.
4037 wrapGameFunctions();
4038
4039 if (channel.data.state === "playing") {
4040 generateActorConditions();
4041 }
4042 }
4043
4044 firstRunProcs();
4045
4046 // Make updateTime fire every second.
4047 setInterval(updateTime, 1000);
4048
4049 // "Update Text"
4050 channel.appendToChat("Info", tran.t("updateText"));
4051
4052 if (bpOverlay.emoteError) {
4053 channel.appendToChat("Info", tran.t("twitchEmoteError"));
4054 }
4055 }
4056 main();
4057 }
4058}
4059
4060var s = document.createElement('script');
4061s.setAttribute("type", "application/javascript");
4062s.textContent = '(' + source + ')();';
4063
4064document.body.appendChild(s);
4065document.body.removeChild(s);
4066
4067// Function to validate the theme object
4068// This won't catch everything, but it should prevent future errors
4069// By making sure all the variables are the right type
4070// This is two hundred lines of inefficiency :D
4071var validateThemeObj = function (themeObj) {
4072 var valid = true;
4073
4074 // Validate images
4075 if (typeof(themeObj.images) === "object") {
4076 for (var i in themeObj.images) {
4077 if (typeof(themeObj.images[i]) !== "object") {
4078 console.log("Error: image " + i + " is not an object.");
4079 valid = false;
4080 }
4081 else {
4082 if (typeof(themeObj.images[i].src) !== "string") {
4083 console.log("Error: invalid src provided in image " + i + ".");
4084 valid = false;
4085 }
4086
4087 if (typeof(themeObj.images[i].xOffset) !== "number" && typeof(themeObj.images[i].xOffset) !== "undefined") {
4088 console.log("Error: invalid xOffset in image " + i + ".");
4089 valid = false;
4090 }
4091
4092 if (typeof(themeObj.images[i].yOffset) !== "number" && typeof(themeObj.images[i].yOffset) !== "undefined") {
4093 console.log("Error: invalid yOffset in image " + i + ".");
4094 valid = false;
4095 }
4096 }
4097 }
4098 }
4099 else if (typeof(themeObj.images) !== "undefined") {
4100 console.log("Error: images is not an object.");
4101 valid = false;
4102 }
4103
4104 // Validate css
4105 if (typeof(themeObj.css) === "object") {
4106 if (themeObj.css.text) {
4107 if (typeof(themeObj.css.text) !== "string") {
4108 console.log("Error: invalid css.text provided");
4109 valid = false;
4110 }
4111 }
4112 else if (themeObj.css.src) {
4113 if (typeof(themeObj.css.src) !== "string") {
4114 console.log("Error: invalid css source provided");
4115 valid = false;
4116 }
4117 }
4118 else {
4119 console.log("Error: css field declared, but no css provided");
4120 valid = false;
4121 }
4122 }
4123 else if (typeof(themeObj.css) !== "undefined") {
4124 console.log("Error: css is not an object.");
4125 valid = false;
4126 }
4127
4128 var minMaxOrNumber = function (value) {
4129 if (typeof(value) === "object") {
4130 if (typeof(value.min) !== "number") {
4131 return false;
4132 }
4133 if (typeof(value.max) !== "number") {
4134 return false;
4135 }
4136 }
4137 else if (typeof(value) !== "number") {
4138 return false;
4139 }
4140 return true;
4141 }
4142
4143 // Validate particles
4144 if (typeof(themeObj.particles) === "object") {
4145 if (Object.prototype.toString.call(themeObj.particles.emitters) === "[object Array]") { // Arrays are weird
4146 for (i = 0; i < themeObj.particles.emitters.length; i++) {
4147 var e = themeObj.particles.emitters[i];
4148 // Oh god there's so much to validate aaaaaaaaa
4149
4150 // validate position
4151 if (typeof(e.position) === "object") {
4152 if (typeof(e.position.x) !== "number") {
4153 console.log("Error: particle.emitters[" + i + "].position.x is not a number");
4154 valid = false;
4155 }
4156
4157 if (typeof(e.position.y) !== "number") {
4158 console.log("Error: particle.emitters[" + i + "].position.y is not a number");
4159 valid = false;
4160 }
4161
4162 if (typeof(e.position.width) !== "undefined" && typeof(e.position.width) !== "number") {
4163 console.log("Error: particle.emitters[" + i + "].position.width is not a number");
4164 valid = false;
4165 }
4166
4167 if (typeof(e.position.height) !== "undefined" && typeof(e.position.height) !== "number") {
4168 console.log("Error: particle.emitters[" + i + "].position.height is not a number");
4169 valid = false;
4170 }
4171 }
4172 else {
4173 console.log("Error: particles.emitters[" + i + "].position is not an object");
4174 valid = false;
4175 }
4176
4177 // Validate velocity
4178 if (typeof(e.velocity) === "object") {
4179 if (!minMaxOrNumber(e.velocity.x)) {
4180 console.log("Error: particles.emitters[" + i + "].velocity.x is invalid");
4181 valid = false;
4182 }
4183 if (!minMaxOrNumber(e.velocity.y)) {
4184 console.log("Error: particles.emitters[" + i + "].velocity.y is invalid");
4185 valid = false;
4186 }
4187 }
4188 else {
4189 console.log("Error: particles.emitters[" + i + "].velocity is not an object");
4190 valid = false;
4191 }
4192
4193 // Validate size
4194 if (!minMaxOrNumber(e.size)) {
4195 console.log("Error: particles.emitters[" + i + "].size is invalid");
4196 valid = false;
4197 }
4198
4199 // Validate rotation
4200 if (!minMaxOrNumber(e.rotation)) {
4201 console.log("Error: particles.emitters[" + i + "].rotation is invalid");
4202 valid = false;
4203 }
4204
4205 // Validate angularVelocity
4206 if (!minMaxOrNumber(e.angularVelocity)) {
4207 console.log("Error: particles.emitters[" + i + "].angularVelocity is invalid");
4208 valid = false;
4209 }
4210
4211 // Validate spawnRate
4212 if (typeof(e.spawnRate) === "object") {
4213 if (typeof(e.spawnRate.high) !== "number") {
4214 console.log("Error: particles.emitters[" + i + "].spawnRate.high is invalid");
4215 valid = false;
4216 }
4217 if (typeof(e.spawnRate.low) !== "number") {
4218 console.log("Error: particles.emitters[" + i + "].spawnRate.low is invalid");
4219 valid = false;
4220 }
4221 }
4222 else {
4223 console.log("Error: particle.emitters[" + i + "].spawnRate is not an object");
4224 valid = false;
4225 }
4226
4227 // Validate gravity
4228 if (typeof(e.gravity) === "object") {
4229 if (!minMaxOrNumber(e.gravity.x)) {
4230 console.log("Error: particles.emitters[" + i + "].gravity.x is invalid");
4231 valid = false;
4232 }
4233 if (!minMaxOrNumber(e.gravity.y)) {
4234 console.log("Error: particles.emitters[" + i + "].gravity.y is invalid");
4235 valid = false;
4236 }
4237 }
4238 else {
4239 console.log("Error: particles.emitters[" + i + "].gravity is not an object");
4240 valid = false;
4241 }
4242
4243 // Effing hell!
4244 }
4245 }
4246 else {
4247 console.log("Error: particles.emitters is not an array");
4248 valid = false;
4249 }
4250
4251 if (Object.prototype.toString.call(themeObj.particles.images) === "[object Array]") {
4252 for (i = 0; i < themeObj.particles.images.length; i++) {
4253 if (typeof(themeObj.particles.images[i]) !== "string") {
4254 console.log("Error: particles.images[" + i + "] is not a string");
4255 valid = false;
4256 }
4257 }
4258 }
4259 }
4260 else if (typeof(themeObj.particles) !== "undefined") {
4261 console.log("Error: particles is not an object");
4262 valid = false;
4263 }
4264
4265 var validateTextStyle = function (textStyle) {
4266 if (textStyle === undefined) {
4267 return true;
4268 }
4269 if (typeof(textStyle) === "object") {
4270 if (typeof(textStyle.color) !== "undefined") {
4271 if (typeof(textStyle.color) !== "string") {
4272 return false;
4273 }
4274 }
4275
4276 if (typeof(textStyle.fontFamily) !== "undefined") {
4277 if (typeof(textStyle.fontFamily) !== "string") {
4278 return false;
4279 }
4280 }
4281
4282 if (typeof(textStyle.fontSize) !== "undefined") {
4283 if (typeof(textStyle.fontSize) !== "number") {
4284 return false;
4285 }
4286 }
4287
4288 if (typeof(textStyle.majorFontSize) !== "undefined") {
4289 if (typeof(textStyle.majorFontSize) !== "number") {
4290 return false;
4291 }
4292 }
4293
4294 if (typeof(textStyle.minorFontSize) !== "undefined") {
4295 if (typeof(textStyle.minorFontSize) !== "number") {
4296 return false;
4297 }
4298 }
4299 }
4300 else if (typeof(textStyle !== "undefined")) {
4301 return false;
4302 }
4303 return true;
4304 }
4305
4306 var ts = ["statusText", "promptText", "wordText", "highlightedText", "bonusLetterText"];
4307
4308 // Validate textStyles
4309 if (typeof(themeObj.textStyles) === "object") {
4310 for (i = 0; i < ts.length; i++) {
4311 if (!validateTextStyle(themeObj.textStyles[ts[i]])) {
4312 console.log("Error: textStyles." + ts[i] + " is invalid or has some invalid parameters.");
4313 valid = false;
4314 }
4315 }
4316 }
4317 else if (typeof(themeObj.textStyles) !== "undefined") {
4318 console.log("Error: textStyles is not an object.");
4319 valid = false;
4320 }
4321
4322 return valid;
4323};
4324
4325var loadAndApplyTheme = function (url) {
4326 GM_xmlhttpRequest({
4327 method: "GET",
4328 url: url,
4329 onload: function (resp) {
4330 try {
4331 var themeObj = json_parse(resp.responseText);
4332 }
4333 catch (e) {
4334 alert("Invalid JSON.");
4335 console.log(e);
4336 return;
4337 }
4338
4339 if (validateThemeObj(themeObj)) {
4340 var s = document.createElement('script');
4341 s.setAttribute("type", "application/javascript");
4342 s.textContent = "loadCustomTheme(false); loadCustomTheme(" + resp.responseText + ")";
4343
4344 document.body.appendChild(s);
4345 document.body.removeChild(s);
4346 }
4347 else {
4348 alert("Invalid theme specified. Check the console for what is invalid.");
4349 }
4350 },
4351 onerror: function (resp) {
4352 alert("Error loading " + url);
4353 },
4354 });
4355};
4356
4357// This code attaches listeners to relevant settings objects to make them persistent
4358// Process to attach to things in the DOM from the sandboxed environment
4359// This needs to happen in this environment because I need access to the GM functions
4360var attachToSettings = function () {
4361 if (!(document.getElementById("containerSelect") &&
4362 document.getElementById("twitchEmoteSelect") &&
4363 document.getElementById("adventureSetting") &&
4364 document.getElementById("themeSelect") &&
4365 document.getElementById("customThemeInput") &&
4366 document.getElementById("notificationsSelect") &&
4367 document.getElementById("notificationVolumeSlider") &&
4368 document.getElementById("notificationAliasInput") &&
4369 document.getElementById("endGameNotificationSelect") &&
4370 document.getElementById("particleSelect") &&
4371 document.getElementById("chatDownButton") &&
4372 document.getElementById("autoFocusButton") &&
4373 document.getElementById("dragButton") &&
4374 document.getElementById("alphaRouletteSelect"))) {
4375 console.log("Cannot attach, trying again in a second");
4376 setTimeout(attachToSettings, 1000);
4377 }
4378 else {
4379 // Whoo look at them variables
4380 var cs = document.getElementById("containerSelect");
4381 var tes = document.getElementById("twitchEmoteSelect");
4382 var as = document.getElementById("adventureSetting");
4383 var ts = document.getElementById("themeSelect");
4384 var cti = document.getElementById("customThemeInput");
4385 var ns = document.getElementById("notificationsSelect");
4386 var nai = document.getElementById("notificationAliasInput");
4387 var nvs = document.getElementById("notificationVolumeSlider");
4388 var egns = document.getElementById("endGameNotificationSelect");
4389 var ps = document.getElementById("particleSelect");
4390 var cdb = document.getElementById("chatDownButton");
4391 var afb = document.getElementById("autoFocusButton");
4392 var db = document.getElementById("dragButton");
4393 var ars = document.getElementById("alphaRouletteSelect");
4394 var wcs = document.getElementById("wordCounterSelect");
4395
4396 var loadAndChangeSelect = function (element, valueName, defaultValue) {
4397 if (GM_getValue(valueName, defaultValue) !== element.value) {
4398 element.value = GM_getValue(valueName, defaultValue);
4399 var event = new Event("change");
4400 element.dispatchEvent(event);
4401 }
4402 }
4403
4404 var loadAndChangeButton = function (element, valueName, defaultValue) {
4405 if (GM_getValue(valueName, defaultValue) !== element.dataset.state) {
4406 var event = new MouseEvent("click");
4407 element.dispatchEvent(event);
4408 }
4409 }
4410
4411 loadAndChangeSelect(cs, "containerState", "compact");
4412 loadAndChangeSelect(tes, "twitchEmoteState", "on");
4413 loadAndChangeSelect(as, "adventureState", "off");
4414 loadAndChangeSelect(ns, "notificationsState", "on");
4415 loadAndChangeSelect(nai, "notificationAlias", ""); // Yay duck typing
4416 loadAndChangeSelect(egns, "endGameNotificationSetting", "off");
4417 loadAndChangeSelect(ars, "alphaRouletteState", "off");
4418 loadAndChangeSelect(wcs, "wordCounterState", "off");
4419 loadAndChangeSelect(ps, "particleState", "high");
4420 loadAndChangeButton(cdb, "chatDownState", "true"); // String booleans because of the way data attributes work in HTML :(
4421 // Trust me, I don't like being stringly typed either
4422 loadAndChangeButton(afb, "autoFocusState", "true");
4423 loadAndChangeButton(db, "dragState", "false");
4424
4425 // Have to handle this one slightly differently
4426 if (GM_getValue("notificationVolume", 100) !== nvs.value) {
4427 nvs.value = GM_getValue("notificationVolume", 100);
4428 var s = document.createElement("script");
4429 s.textContent = "bpOverlay.notificationSound.volume=" + (nvs.value / 100);
4430 document.body.appendChild(s);
4431 document.body.removeChild(s);
4432 };
4433
4434 cs.addEventListener("change", function () {
4435 setTimeout(function () {
4436 GM_setValue("containerState", cs.value);
4437 }, 100);
4438 });
4439
4440 tes.addEventListener("change", function () {
4441 setTimeout(function () {
4442 GM_setValue("twitchEmoteState", tes.value);
4443 }, 100);
4444 });
4445
4446 as.addEventListener("change", function () {
4447 setTimeout(function () {
4448 GM_setValue("adventureState", as.value);
4449 }, 100);
4450 });
4451
4452 ns.addEventListener("change", function () {
4453 setTimeout(function () {
4454 GM_setValue("notificationsState", ns.value);
4455 }, 100);
4456 });
4457
4458 nai.addEventListener("change", function () {
4459 setTimeout(function () {
4460 GM_setValue("notificationAlias", nai.value);
4461 }, 100);
4462 });
4463
4464 nvs.addEventListener("change", function () {
4465 setTimeout(function () {
4466 GM_setValue("notificationVolume", nvs.value);
4467 }, 100);
4468 });
4469
4470 egns.addEventListener("change", function () {
4471 setTimeout(function () {
4472 GM_setValue("endGameNotificationSetting", egns.value);
4473 }, 100);
4474 });
4475
4476 ps.addEventListener("change", function () {
4477 setTimeout(function () {
4478 GM_setValue("particleState", ps.value);
4479 }, 100);
4480 });
4481
4482 ars.addEventListener("change", function () {
4483 setTimeout(function () {
4484 GM_setValue("alphaRouletteState", ars.value);
4485 }, 100);
4486 });
4487
4488 wcs.addEventListener("change", function () {
4489 setTimeout(function () {
4490 GM_setValue("wordCounterState", wcs.value);
4491 }, 100);
4492 });
4493
4494 cdb.addEventListener("click", function () {
4495 setTimeout(function () {
4496 GM_setValue("chatDownState", cdb.dataset.state);
4497 }, 100);
4498 });
4499
4500 afb.addEventListener("click", function () {
4501 setTimeout(function () {
4502 GM_setValue("autoFocusState", afb.dataset.state);
4503 }, 100);
4504 });
4505
4506 db.addEventListener("click", function () {
4507 setTimeout(function () {
4508 GM_setValue("dragState", db.dataset.state);
4509 }, 100);
4510 });
4511
4512 // These ones are special!
4513 ts.addEventListener("change", function () {
4514 if (ts.value !== "none" && ts.value !== "custom") {
4515 loadAndApplyTheme(ts.value);
4516 GM_setValue("theme", ts.value);
4517 GM_setValue("customTheme", "none");
4518 cti.value = "";
4519 }
4520 else if (ts.value === "none") {
4521 GM_setValue("theme", "none");
4522 GM_setValue("customTheme", "none");
4523 }
4524 });
4525
4526 cti.addEventListener("keypress", function (e) {
4527 if (e.keyCode === 13) {
4528 loadAndApplyTheme(cti.value);
4529 GM_setValue("theme", "custom");
4530 GM_setValue("customTheme", cti.value);
4531 }
4532 });
4533
4534 loadAndChangeSelect(ts, "theme", "none");
4535
4536 if (GM_getValue("customTheme", "none") !== "none") {
4537 GM_setValue("theme", "custom");
4538 cti.value = GM_getValue("customTheme", "none");
4539 loadAndApplyTheme(cti.value);
4540 }
4541
4542 }
4543}
4544
4545attachToSettings();