TagPro Userscript Library

Functions that any TagPro script could benefit from

This script should not be not be installed directly. It is a library for other scripts to include with the meta directive // @require https://update.greatest.deepsurf.us/scripts/371240/727770/TagPro%20Userscript%20Library.js

  1. // ==UserScript==
  2. // @name TagPro Userscript Library
  3. // @description Functions that any TagPro script could benefit from
  4. // @author Ko </u/Wilcooo> (https://greatest.deepsurf.us/users/152992)
  5. // @version 4.13
  6. // @license MIT
  7. // @match *://*.koalabeast.com/*
  8. // @match *://*.jukejuice.com/*
  9. // @match *://*.newcompte.fr/*
  10. // @downloadURL https://raw.githubusercontent.com/wilcooo/TagPro-UserscriptLibrary/master/tpul.lib.js
  11. // @supportURL https://www.reddit.com/message/compose/?to=Wilcooo
  12. // @grant GM_setValue
  13. // @grant GM_getValue
  14. // @grant GM_xmlhttpRequest
  15. // @connect koalabeast.com
  16. // ==/UserScript==
  17.  
  18.  
  19.  
  20. // ==UserLibrary==
  21. // @name TagPro Userscript Library
  22. // @description Functions that any TagPro script could benefit from
  23. // @version 4.12
  24. // @license MIT
  25. // ==/UserLibrary==
  26.  
  27.  
  28. var version = 4.12;
  29. console.log('Loading TPUL (TagPro Userscript Library) version '+version);
  30.  
  31.  
  32.  
  33. ////////////////////////////////////////////////////////////////
  34. ////////////////////////////////////////////////////////////////
  35. ////////////////////////////////////////////////////////////////
  36.  
  37. // To use this library, include these 5 lines in your userscripts' metadata block:
  38.  
  39. // @require https://greatest.deepsurf.us/scripts/371240/code/TagPro%20Userscript%20Library.js
  40. // @grant GM_setValue
  41. // @grant GM_getValue
  42. // @grant GM_xmlhttpRequest
  43. // @connect koalabeast.com
  44.  
  45. ////////////////////////////////////////////////////////////////
  46. ////////////////////////////////////////////////////////////////
  47. ////////////////////////////////////////////////////////////////
  48.  
  49.  
  50.  
  51.  
  52.  
  53.  
  54. /* TODO
  55.  
  56. compatibility with SWJ (done I think)
  57.  
  58. Option to change the layout of the settings
  59.  
  60. Notify "options cancld" when scrolling away
  61.  
  62. margin beneath buttons on scoreboard
  63.  
  64. option to disable notifications.
  65.  
  66. ESC cancels, option to Save when canceld (scroll away, esc)
  67.  
  68. */
  69.  
  70.  
  71.  
  72.  
  73.  
  74.  
  75.  
  76. var GM_configStruct = (function(){
  77.  
  78.  
  79. ////////////////////////////////////////////////////////////////
  80. // START OF ORIGINAL GM_CONFIG //
  81. ////////////////////////////////////////////////////////////////
  82.  
  83. /*
  84. Copyright 2009+, GM_config Contributors (https://github.com/sizzlemctwizzle/GM_config)
  85.  
  86. GM_config Contributors:
  87. Mike Medley <medleymind@gmail.com>
  88. Joe Simmons
  89. Izzy Soft
  90. Marti Martz
  91.  
  92. GM_config is distributed under the terms of the GNU Lesser General Public License.
  93.  
  94. GM_config is free software: you can redistribute it and/or modify
  95. it under the terms of the GNU Lesser General Public License as published by
  96. the Free Software Foundation, either version 3 of the License, or
  97. (at your option) any later version.
  98.  
  99. This program is distributed in the hope that it will be useful,
  100. but WITHOUT ANY WARRANTY; without even the implied warranty of
  101. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  102. GNU Lesser General Public License for more details.
  103.  
  104. You should have received a copy of the GNU Lesser General Public License
  105. along with this program. If not, see <http://www.gnu.org/licenses/>.
  106. */
  107.  
  108. function GM_configStruct(){if(arguments.length){GM_configInit(this,arguments);this.onInit()}}
  109. function GM_configInit(config,args){if(typeof config.fields=="undefined"){config.fields={};config.onInit=config.onInit||function(){};config.onOpen=config.onOpen||function(){};config.onSave=config.onSave||function(){};config.onClose=config.onClose||function(){};config.onReset=config.onReset||function(){};config.isOpen=false;config.title="User Script Settings";config.css={basic:["#GM_config * { font-family: arial,tahoma,myriad pro,sans-serif; }","#GM_config { background: #FFF; }","#GM_config input[type='radio'] { margin-right: 8px; }",
  110. "#GM_config .indent40 { margin-left: 40%; }","#GM_config .field_label { font-size: 12px; font-weight: bold; margin-right: 6px; }","#GM_config .radio_label { font-size: 12px; }","#GM_config .block { display: block; }","#GM_config .saveclose_buttons { margin: 16px 10px 10px; padding: 2px 12px; }","#GM_config .reset, #GM_config .reset a,"+" #GM_config_buttons_holder { color: #000; text-align: right; }","#GM_config .config_header { font-size: 20pt; margin: 0; }","#GM_config .config_desc, #GM_config .section_desc, #GM_config .reset { font-size: 9pt; }",
  111. "#GM_config .center { text-align: center; }","#GM_config .section_header_holder { margin-top: 8px; }","#GM_config .config_var { margin: 0 0 4px; }","#GM_config .section_header { background: #414141; border: 1px solid #000; color: #FFF;"," font-size: 13pt; margin: 0; }","#GM_config .section_desc { background: #EFEFEF; border: 1px solid #CCC; color: #575757;"+" font-size: 9pt; margin: 0 0 6px; }"].join("\n")+"\n",basicPrefix:"GM_config",stylish:""}}if(args.length==1&&typeof args[0].id=="string"&&typeof args[0].appendChild!=
  112. "function")var settings=args[0];else{var settings={};for(var i=0,l=args.length,arg;i<l;++i){arg=args[i];if(typeof arg.appendChild=="function"){settings.frame=arg;continue}switch(typeof arg){case "object":for(var j in arg){if(typeof arg[j]!="function"){settings.fields=arg;break}if(!settings.events)settings.events={};settings.events[j]=arg[j]}break;case "function":settings.events={onOpen:arg};break;case "string":if(/\w+\s*\{\s*\w+\s*:\s*\w+[\s|\S]*\}/.test(arg))settings.css=arg;else settings.title=
  113. arg;break}}}if(settings.id)config.id=settings.id;else if(typeof config.id=="undefined")config.id="GM_config";if(settings.title)config.title=settings.title;if(settings.css)config.css.stylish=settings.css;if(settings.frame)config.frame=settings.frame;if(settings.events){var events=settings.events;for(var e in events)config["on"+e.charAt(0).toUpperCase()+e.slice(1)]=events[e]}if(settings.fields){var stored=config.read(),fields=settings.fields,customTypes=settings.types||{},configId=config.id;for(var id in fields){var field=
  114. fields[id];if(field)config.fields[id]=new GM_configField(field,stored[id],id,customTypes[field.type],configId);else if(config.fields[id])delete config.fields[id]}}if(config.id!=config.css.basicPrefix){config.css.basic=config.css.basic.replace(new RegExp("#"+config.css.basicPrefix,"gm"),"#"+config.id);config.css.basicPrefix=config.id}}
  115. GM_configStruct.prototype={init:function(){GM_configInit(this,arguments);this.onInit()},open:function(){var match=document.getElementById(this.id);if(match&&(match.tagName=="IFRAME"||match.childNodes.length>0))return;var config=this;function buildConfigWin(body,head){var create=config.create,fields=config.fields,configId=config.id,bodyWrapper=create("div",{id:configId+"_wrapper"});head.appendChild(create("style",{type:"text/css",textContent:config.css.basic+config.css.stylish}));bodyWrapper.appendChild(create("div",
  116. {id:configId+"_header",className:"config_header block center"},config.title));var section=bodyWrapper,secNum=0;for(var id in fields){var field=fields[id],settings=field.settings;if(settings.section){section=bodyWrapper.appendChild(create("div",{className:"section_header_holder",id:configId+"_section_"+secNum}));if(Object.prototype.toString.call(settings.section)!=="[object Array]")settings.section=[settings.section];if(settings.section[0])section.appendChild(create("div",{className:"section_header center",
  117. id:configId+"_section_header_"+secNum},settings.section[0]));if(settings.section[1])section.appendChild(create("p",{className:"section_desc center",id:configId+"_section_desc_"+secNum},settings.section[1]));++secNum}section.appendChild(field.wrapper=field.toNode())}bodyWrapper.appendChild(create("div",{id:configId+"_buttons_holder"},create("button",{id:configId+"_saveBtn",textContent:"Save",title:"Save settings",className:"saveclose_buttons",onclick:function(){config.save()}}),create("button",{id:configId+
  118. "_closeBtn",textContent:"Close",title:"Close window",className:"saveclose_buttons",onclick:function(){config.close()}}),create("div",{className:"reset_holder block"},create("a",{id:configId+"_resetLink",textContent:"Reset to defaults",href:"#",title:"Reset fields to default values",className:"reset",onclick:function(e){e.preventDefault();config.reset()}}))));body.appendChild(bodyWrapper);config.center();window.addEventListener("resize",config.center,false);config.onOpen(config.frame.contentDocument||
  119. config.frame.ownerDocument,config.frame.contentWindow||window,config.frame);window.addEventListener("beforeunload",function(){config.close()},false);config.frame.style.display="block";config.isOpen=true}var defaultStyle="bottom: auto; border: 1px solid #000; display: none; height: 75%;"+" left: 0; margin: 0; max-height: 95%; max-width: 95%; opacity: 0;"+" overflow: auto; padding: 0; position: fixed; right: auto; top: 0;"+" width: 75%; z-index: 9999;";if(this.frame){this.frame.id=this.id;this.frame.setAttribute("style",
  120. defaultStyle);buildConfigWin(this.frame,this.frame.ownerDocument.getElementsByTagName("head")[0])}else{document.body.appendChild(this.frame=this.create("iframe",{id:this.id,style:defaultStyle}));this.frame.src="about:blank";this.frame.addEventListener("load",function(e){var frame=config.frame;var body=frame.contentDocument.getElementsByTagName("body")[0];body.id=config.id;buildConfigWin(body,frame.contentDocument.getElementsByTagName("head")[0])},false)}},save:function(){var forgotten=this.write();
  121. this.onSave(forgotten)},close:function(){if(this.frame.contentDocument){this.remove(this.frame);this.frame=null}else{this.frame.innerHTML="";this.frame.style.display="none"}var fields=this.fields;for(var id in fields){var field=fields[id];field.wrapper=null;field.node=null}this.onClose();this.isOpen=false},set:function(name,val){this.fields[name].value=val;if(this.fields[name].node)this.fields[name].reload()},get:function(name,getLive){var field=this.fields[name],fieldVal=null;if(getLive&&field.node)fieldVal=
  122. field.toValue();return fieldVal!=null?fieldVal:field.value},write:function(store,obj){if(!obj){var values={},forgotten={},fields=this.fields;for(var id in fields){var field=fields[id];var value=field.toValue();if(field.save)if(value!=null){values[id]=value;field.value=value}else values[id]=field.value;else forgotten[id]=value}}try{this.setValue(store||this.id,this.stringify(obj||values))}catch(e){this.log("GM_config failed to save settings!")}return forgotten},read:function(store){try{var rval=this.parser(this.getValue(store||
  123. this.id,"{}"))}catch(e){this.log("GM_config failed to read saved settings!");var rval={}}return rval},reset:function(){var fields=this.fields;for(var id in fields)fields[id].reset();this.onReset()},create:function(){switch(arguments.length){case 1:var A=document.createTextNode(arguments[0]);break;default:var A=document.createElement(arguments[0]),B=arguments[1];for(var b in B)if(b.indexOf("on")==0)A.addEventListener(b.substring(2),B[b],false);else if(",style,accesskey,id,name,src,href,which,for".indexOf(","+
  124. b.toLowerCase())!=-1)A.setAttribute(b,B[b]);else A[b]=B[b];if(typeof arguments[2]=="string")A.innerHTML=arguments[2];else for(var i=2,len=arguments.length;i<len;++i)A.appendChild(arguments[i])}return A},center:function(){var node=this.frame;if(!node)return;var style=node.style,beforeOpacity=style.opacity;if(style.display=="none")style.opacity="0";style.display="";style.top=Math.floor(window.innerHeight/2-node.offsetHeight/2)+"px";style.left=Math.floor(window.innerWidth/2-node.offsetWidth/2)+"px";
  125. style.opacity="1"},remove:function(el){if(el&&el.parentNode)el.parentNode.removeChild(el)}};
  126. (function(){var isGM=typeof GM_getValue!="undefined"&&typeof GM_getValue("a","b")!="undefined",setValue,getValue,stringify,parser;if(!isGM){setValue=function(name,value){return localStorage.setItem(name,value)};getValue=function(name,def){var s=localStorage.getItem(name);return s==null?def:s};stringify=JSON.stringify;parser=JSON.parse}else{setValue=GM_setValue;getValue=GM_getValue;stringify=typeof JSON=="undefined"?function(obj){return obj.toSource()}:JSON.stringify;parser=typeof JSON=="undefined"?
  127. function(jsonData){return(new Function("return "+jsonData+";"))()}:JSON.parse}GM_configStruct.prototype.isGM=isGM;GM_configStruct.prototype.setValue=setValue;GM_configStruct.prototype.getValue=getValue;GM_configStruct.prototype.stringify=stringify;GM_configStruct.prototype.parser=parser;GM_configStruct.prototype.log=window.console?console.log:isGM&&typeof GM_log!="undefined"?GM_log:window.opera?opera.postError:function(){}})();
  128. function GM_configDefaultValue(type,options){var value;if(type.indexOf("unsigned ")==0)type=type.substring(9);switch(type){case "radio":case "select":value=options[0];break;case "checkbox":value=false;break;case "int":case "integer":case "float":case "number":value=0;break;default:value=""}return value}
  129. function GM_configField(settings,stored,id,customType,configId){this.settings=settings;this.id=id;this.configId=configId;this.node=null;this.wrapper=null;this.save=typeof settings.save=="undefined"?true:settings.save;if(settings.type=="button")this.save=false;this["default"]=typeof settings["default"]=="undefined"?customType?customType["default"]:GM_configDefaultValue(settings.type,settings.options):settings["default"];this.value=typeof stored=="undefined"?this["default"]:stored;if(customType){this.toNode=
  130. customType.toNode;this.toValue=customType.toValue;this.reset=customType.reset}}
  131. GM_configField.prototype={create:GM_configStruct.prototype.create,toNode:function(){var field=this.settings,value=this.value,options=field.options,type=field.type,id=this.id,configId=this.configId,labelPos=field.labelPos,create=this.create;function addLabel(pos,labelEl,parentNode,beforeEl){if(!beforeEl)beforeEl=parentNode.firstChild;switch(pos){case "right":case "below":if(pos=="below")parentNode.appendChild(create("br",{}));parentNode.appendChild(labelEl);break;default:if(pos=="above")parentNode.insertBefore(create("br",
  132. {}),beforeEl);parentNode.insertBefore(labelEl,beforeEl)}}var retNode=create("div",{className:"config_var",id:configId+"_"+id+"_var",title:field.title||""}),firstProp;for(var i in field){firstProp=i;break}var label=field.label&&type!="button"?create("label",{id:configId+"_"+id+"_field_label","for":configId+"_field_"+id,className:"field_label"},field.label):null;switch(type){case "textarea":retNode.appendChild(this.node=create("textarea",{innerHTML:value,id:configId+"_field_"+id,className:"block",cols:field.cols?
  133. field.cols:20,rows:field.rows?field.rows:2}));break;case "radio":var wrap=create("div",{id:configId+"_field_"+id});this.node=wrap;for(var i=0,len=options.length;i<len;++i){var radLabel=create("label",{className:"radio_label"},options[i]);var rad=wrap.appendChild(create("input",{value:options[i],type:"radio",name:id,checked:options[i]==value}));var radLabelPos=labelPos&&(labelPos=="left"||labelPos=="right")?labelPos:firstProp=="options"?"left":"right";addLabel(radLabelPos,radLabel,wrap,rad)}retNode.appendChild(wrap);
  134. break;case "select":var wrap=create("select",{id:configId+"_field_"+id});this.node=wrap;for(var i=0,len=options.length;i<len;++i){var option=options[i];wrap.appendChild(create("option",{value:option,selected:option==value},option))}retNode.appendChild(wrap);break;default:var props={id:configId+"_field_"+id,type:type,value:type=="button"?field.label:value};switch(type){case "checkbox":props.checked=value;break;case "button":props.size=field.size?field.size:25;if(field.script)field.click=field.script;
  135. if(field.click)props.onclick=field.click;break;case "hidden":break;default:props.type="text";props.size=field.size?field.size:25}retNode.appendChild(this.node=create("input",props))}if(label){if(!labelPos)labelPos=firstProp=="label"||type=="radio"?"left":"right";addLabel(labelPos,label,retNode)}return retNode},toValue:function(){var node=this.node,field=this.settings,type=field.type,unsigned=false,rval=null;if(!node)return rval;if(type.indexOf("unsigned ")==0){type=type.substring(9);unsigned=true}switch(type){case "checkbox":rval=
  136. node.checked;break;case "select":rval=node[node.selectedIndex].value;break;case "radio":var radios=node.getElementsByTagName("input");for(var i=0,len=radios.length;i<len;++i)if(radios[i].checked)rval=radios[i].value;break;case "button":break;case "int":case "integer":case "float":case "number":var num=Number(node.value);var warn='Field labeled "'+field.label+'" expects a'+(unsigned?" positive ":"n ")+"integer value";if(isNaN(num)||type.substr(0,3)=="int"&&Math.ceil(num)!=Math.floor(num)||unsigned&&
  137. num<0){alert(warn+".");return null}if(!this._checkNumberRange(num,warn))return null;rval=num;break;default:rval=node.value;break}return rval},reset:function(){var node=this.node,field=this.settings,type=field.type;if(!node)return;switch(type){case "checkbox":node.checked=this["default"];break;case "select":for(var i=0,len=node.options.length;i<len;++i)if(node.options[i].textContent==this["default"])node.selectedIndex=i;break;case "radio":var radios=node.getElementsByTagName("input");for(var i=0,len=
  138. radios.length;i<len;++i)if(radios[i].value==this["default"])radios[i].checked=true;break;case "button":break;default:node.value=this["default"];break}},remove:function(el){GM_configStruct.prototype.remove(el||this.wrapper);this.wrapper=null;this.node=null},reload:function(){var wrapper=this.wrapper;if(wrapper){var fieldParent=wrapper.parentNode;fieldParent.insertBefore(this.wrapper=this.toNode(),wrapper);this.remove(wrapper)}},_checkNumberRange:function(num,warn){var field=this.settings;if(typeof field.min==
  139. "number"&&num<field.min){alert(warn+" greater than or equal to "+field.min+".");return null}if(typeof field.max=="number"&&num>field.max){alert(warn+" less than or equal to "+field.max+".");return null}return true}};var GM_config=new GM_configStruct;
  140.  
  141.  
  142. ////////////////////////////////////////////////////////////////
  143. // END OF ORIGINAL GM_CONFIG //
  144. ////////////////////////////////////////////////////////////////
  145.  
  146.  
  147.  
  148.  
  149. // I'm going to edit GM_config slightly.
  150. // Mostly to get rid of the 'alerts' when something is wrong.
  151. // (alerts pause the window, which causes you to disconnect from a game)
  152.  
  153.  
  154. // This function will return true when no errors were found.
  155.  
  156. GM_configStruct.prototype.valid = function() {
  157.  
  158.  
  159. for (var id in this.fields) {
  160.  
  161. var node = this.fields[id].node;
  162.  
  163. if (node.validity && !node.validity.valid) return false;
  164.  
  165. /*
  166. var field = this.fields[id],
  167. type = field.settings.type,
  168. unsigned = false;
  169.  
  170. if (type.indexOf('unsigned ') == 0) {
  171. type = type.substring(9);
  172. unsigned = true;
  173. }
  174.  
  175. if (['int','integer','float','number'].includes(type)) {
  176.  
  177. var num = Number(field.node.value);
  178.  
  179. var warn = 'Field labeled "' + field.label + '" expects a' +
  180. (unsigned ? ' positive ' : 'n ') + 'integer value';
  181.  
  182. if (isNaN(num) ||
  183. (type.substr(0, 3) == 'int' && Math.ceil(num) != Math.floor(num)) ||
  184. (unsigned && num < 0)) {
  185. // Add a few ways for scripters to know that there is an error
  186. field.error = true;
  187. field.wrapper.classList.add('error');
  188. correct = false;
  189. }
  190.  
  191. else if (typeof field.settings.min == "number" && num < field.settings.min) {
  192. // Add a few ways for scripters to know that there is an error
  193. field.error = true;
  194. field.wrapper.classList.add('error');
  195. correct = false;
  196. }
  197.  
  198. else if (typeof field.settings.max == "number" && num > field.settings.max) {
  199. // Add a few ways for scripters to know that there is an error
  200. field.error = true;
  201. field.wrapper.classList.add('error');
  202. correct = false;
  203. }
  204.  
  205. else {
  206. // Add a few ways for scripters to know that there is NO error
  207. field.error = false;
  208. field.wrapper.classList.remove('error');
  209. }
  210. }*/
  211. }
  212.  
  213. return true;
  214. };
  215.  
  216.  
  217. // Change the field prototype
  218.  
  219. var org_toNode = GM_configField.prototype.toNode;
  220.  
  221. GM_configField.prototype.toNode = function(){
  222.  
  223. var retNode = org_toNode.apply(this, ...arguments);
  224.  
  225. var unsigned = false,
  226. type = this.settings.type;
  227.  
  228. if (type.indexOf('unsigned ') === 0) {
  229. type = type.substring(9);
  230. unsigned = true;
  231. }
  232.  
  233. if (this.node.validity) {
  234. // Validity checks will work for ANY input, not only numbers.
  235. // For example, if you want a text field to have at least 3 characters,
  236. // manually set the 'minLength' tag to 3 and the rest will be done
  237. // automagically.
  238.  
  239. // Immediately show a validity report while typing / clicking
  240. this.node.addEventListener('input', this.node.reportValidity);
  241. this.node.addEventListener('click', this.node.reportValidity);
  242.  
  243. // The autocomplete covers the validity report (at least in Chrome)
  244. this.node.autocomplete = 'off';
  245. }
  246.  
  247. if (['int','integer','float','number'].includes(type)) {
  248.  
  249. // By default, GM_config makes most inputs a text field, even numbers.
  250. // Lets fix that, to be able to check min and max values better.
  251.  
  252. this.node.type = 'number';
  253.  
  254. if (this.settings.min) this.node.min = this.settings.min;
  255. if (this.settings.max) this.node.max = this.settings.max;
  256.  
  257. // unsigned means non-negative
  258. if (unsigned) this.node.min = Math.max(0,this.settings.min);
  259.  
  260. // integers are only whole numbers
  261. if (type.substr(0, 3) == 'int') this.node.step = 1;
  262. }
  263.  
  264. if (!['radio','select','checkbox','button','hidden'].includes(type)) {
  265. // Disable TagPro's controls when typing inside a field you can type in
  266. // You can set tpul.rollingChat.enable = true to make the Arrow keys move your ball, even when typing text.
  267. this.node.addEventListener('focus', function(){typeof tagpro != 'undefined' && (tagpro.disableControls = true)});
  268. this.node.addEventListener('blur', function(){typeof tagpro != 'undefined' && (tagpro.disableControls = false)});
  269. }
  270.  
  271. return retNode;
  272. };
  273.  
  274.  
  275.  
  276. return GM_configStruct;
  277. })();
  278.  
  279.  
  280.  
  281.  
  282.  
  283.  
  284.  
  285.  
  286. var tpul = (function(){
  287.  
  288.  
  289.  
  290.  
  291. // =====STYLE SECTION=====
  292.  
  293.  
  294.  
  295. // Create our own stylesheet to define the styles in:
  296.  
  297. var style = document.getElementById('tpul-style') || document.createElement('style');
  298. document.head.appendChild(style);
  299. style.id = 'tpul-style';
  300.  
  301. // Remove all existing rules of any previous TPUL version.
  302.  
  303. var styleSheet = style.sheet;
  304. Array.from(styleSheet.cssRules).forEach(rule => styleSheet.deleteRule(rule));
  305.  
  306. // THE SETTINGS MENU BUTTONS
  307.  
  308. // Container for settings buttons
  309. styleSheet.insertRule(` #tpul-settings-menu {
  310. text-align: center;
  311. margin: 0 10%;
  312. }`);
  313.  
  314. // A settings button
  315. styleSheet.insertRule(` .tpul-settings-btn {
  316. position: relative;
  317. width: 64px;
  318. height: 64px;
  319. padding: 10px;
  320. margin: 20px;
  321. background-size: contain !important;
  322. background-origin: content-box !important;
  323. background-repeat: no-repeat !important;
  324. outline: none;
  325. }`);
  326.  
  327. // Blue line around button when focussed
  328. styleSheet.insertRule(` .tpul-settings-btn:focus::after {
  329. content: "";
  330. position: absolute;
  331. width: 100%;
  332. height: 100%;
  333. border: 2px solid Highlight;
  334. top: 0;
  335. left: 0;
  336. }`);
  337.  
  338. // Tooltip of button
  339. styleSheet.insertRule(` .tpul-settings-btn span {
  340. position: absolute;
  341. z-index: 1;
  342. border-radius: 10px;
  343. margin-top: 10px;
  344. padding: 10px;
  345. background: #0E8AE0;
  346. border: 1px solid #095C96;
  347. box-shadow: 0 3px #095C96;
  348. font-size: small;
  349. top: 100%;
  350. left: 50%;
  351. transform: translateX(-50%);
  352. width: max-content;
  353. min-width: 64px;
  354. max-width: 128px;
  355. overflow-wrap: break-word;
  356. word-wrap: break-word;
  357. pointer-events: none;
  358. opacity: 0;
  359. transition: opacity .3s;
  360. }`);
  361.  
  362.  
  363. // Arrow of tooltip
  364. styleSheet.insertRule(` .tpul-settings-btn span::after {
  365. content: "";
  366. position: absolute;
  367. left: 50%;
  368. bottom: 100%;
  369. margin-left: -20px;
  370. border-width: 20px;
  371. border-style: solid;
  372. border-color: transparent transparent #0E8AE0 transparent;
  373. }`);
  374.  
  375. // Show tooltip when hovering/focussing
  376. styleSheet.insertRule(`.tpul-settings-btn:hover span, .tpul-settings-btn:focus span {
  377. opacity: 1;
  378. }`);
  379.  
  380.  
  381.  
  382. // THE SETTINGS PANEL
  383.  
  384. // The frame (gray, spans full page)
  385. styleSheet.insertRule(` .tpul-settings-frame {
  386. position: fixed;
  387. z-index: 1;
  388. left: 0;
  389. top: 0;
  390. width: 100%;
  391. height: 100%;
  392. overflow: auto;
  393. background-color: rgba(0,0,0,0.4);
  394. scroll-behavior: smooth;
  395.  
  396. transition: opacity .5s;
  397. opacity: 0;
  398. pointer-events: none;
  399. }`);
  400.  
  401. // The frame when shown
  402. styleSheet.insertRule(`.tpul-settings-shown .tpul-settings-frame {
  403. opacity: 1;
  404. pointer-events: auto;
  405. }`);
  406.  
  407. // The settings window itself
  408. styleSheet.insertRule(` .tpul-settings-frame > div {
  409. width: 80%;
  410. max-width: 800px;
  411. margin: auto;
  412. margin-bottom: 10%;
  413.  
  414. position: relative;
  415. padding: 20px;
  416.  
  417. border: 1px solid #888;
  418. border-radius: 15px;
  419. background: #353535;
  420.  
  421. font-size: 16px;
  422.  
  423. top: 200%;
  424. transition: top .5s;
  425. }`);
  426.  
  427. styleSheet.insertRule(`.tpul-settings-shown .tpul-settings-frame > div { top: 120%; }`);
  428. // In a game we want to have an 80% gap to be able to keep playing.
  429. styleSheet.insertRule(`.tpul-settings-shown .tpul-settings-frame.in-game > div { top: 180%; }`);
  430.  
  431.  
  432. styleSheet.insertRule(`.tpul-settings-frame .config_header {
  433. font-size: 2em;
  434. font-weight: bold;
  435. }`);
  436.  
  437. styleSheet.insertRule(`.tpul-settings-frame .section_header {
  438. font-size: 1.5em;
  439. font-weight: bold;
  440. }`);
  441.  
  442. styleSheet.insertRule(`.tpul-settings-frame .config_var {
  443. }`);
  444.  
  445.  
  446. // ERRORS in fields:
  447. styleSheet.insertRule(`.tpul-settings-frame .config_var input:invalid {
  448. box-shadow: inset 0 0 10px rgba(255,0,0,1), 0 0 10px rgba(255, 0, 0, 1);
  449. }`);
  450.  
  451. /*styleSheet.insertRule(`.tpul-settings-frame .config_var.error:before {
  452. content: attr(data-min) ' - ' attr(data-max);
  453. display: block;
  454. text-align: right;
  455. margin: 5px 20px;
  456. color: #FFA9A2;
  457. font-style: italic;
  458. }`);*/
  459.  
  460. styleSheet.insertRule(`.tpul-settings-frame .field_label {
  461. font-weight: bold;
  462. }`);
  463. styleSheet.insertRule(`.tpul-settings-frame .form-control {
  464. background: #212121;
  465. border-color: #5f5f5f;
  466. }`);
  467. styleSheet.insertRule(`.tpul-settings-frame .form-control[type="checkbox"] {
  468. width: auto;
  469. }`);
  470. styleSheet.insertRule(`.tpul-settings-frame .btn-default {
  471. border-color: #888888;
  472. }`);
  473. styleSheet.insertRule(`.tpul-settings-frame textarea.form-control {
  474. resize: vertical;
  475. }`);
  476.  
  477. styleSheet.insertRule(`.tpul-settings-frame .btn-primary {
  478. margin-left: 10px;
  479. }`);
  480.  
  481.  
  482.  
  483. styleSheet.insertRule(`.tpul-settings-frame .tab-list {
  484. border-bottom-color: #888888;
  485. }`);
  486. styleSheet.insertRule(`.tpul-settings-frame .tab-list li {
  487. cursor: pointer;
  488. color: #8BC34A;
  489. font-size: 1.5em;
  490. }`);
  491. styleSheet.insertRule(`.tpul-settings-frame .tab-list li:hover {
  492. color: #689F38;
  493. }`);
  494. styleSheet.insertRule(`.tpul-settings-frame .tab-list li.active {
  495. border-color: #888888;
  496. border-bottom-color: transparent;
  497. background-color: #353535;
  498. }`);
  499.  
  500.  
  501.  
  502. // save/close/etc buttons
  503.  
  504. styleSheet.insertRule(`.tpul-settings-frame-buttons-holder {
  505. height: 0;
  506. text-align: right;
  507. }`);
  508.  
  509. styleSheet.insertRule(`.tpul-settings-frame-buttons-holder button {
  510. padding: 4px .5em;
  511. }`);
  512.  
  513.  
  514. styleSheet.insertRule(`@keyframes bounce {
  515. 0%, 20% {transform: translate(-50px,50%)scale(.06)}
  516. 10% {transform: translate(-50px,55%)scale(.06)}
  517. }`);
  518.  
  519. styleSheet.insertRule(`.tpul-settings-scroll-down-arrow {
  520. position: fixed;
  521. width: 80%;
  522. max-width: 800px;
  523. left: 50%;
  524. transform: translate(-50px,50%)scale(.06);
  525. bottom: 30px;
  526. z-index: 1;
  527. transition: opacity .5s;
  528. animation-name: bounce;
  529. animation-delay: 1s;
  530. animation-duration: 3s;
  531. animation-iteration-count: infinite;
  532. animation-direction: alternate;
  533. transition: opacity .5s;
  534. cursor: pointer;
  535. }`);
  536.  
  537. styleSheet.insertRule(`
  538. @media screen and (max-width: 1000) {
  539. .tpul-settings-scroll-down-arrow {
  540. right: calc(10% + 30px);
  541. width: 5px;
  542. }
  543. }`);
  544.  
  545.  
  546. /*
  547.  
  548. //Bad design notice:
  549.  
  550. styleSheet.insertRule(` .tpul-settings-frame > div::after {
  551. content: "Sorry for the bad design, I'm working on it!";
  552. font-style: italic;
  553. color: gray;
  554. }`);
  555.  
  556. */
  557.  
  558. // Stop the body from scrolling when the settings panel is shown
  559. styleSheet.insertRule(`body.tpul-settings-shown {
  560. overflow:hidden !important;
  561. }`);
  562.  
  563.  
  564.  
  565.  
  566. // Notifications
  567. styleSheet.insertRule(` .tpul-notification-success {
  568. border-color: #8BC34A;
  569. background: #4C6D25;
  570. color: black;
  571. }`);
  572.  
  573. styleSheet.insertRule(` .tpul-notification-error {
  574. border-color: #BD0E0B;
  575. background: #6B2121;
  576. color: #FFA9A2;
  577. }`);
  578.  
  579. styleSheet.insertRule(` .tpul-notification-warning {
  580. border-color: Olive;
  581. background: DarkKhaki;
  582. color: black;
  583. }`);
  584.  
  585. styleSheet.insertRule(` .tpul-notification {
  586. position: fixed;
  587. bottom: 0px;
  588.  
  589. padding: 10px;
  590.  
  591. width: 100%;
  592.  
  593. text-align: center;
  594.  
  595. cursor: pointer;
  596. z-index: 2;
  597.  
  598. border-top: 1px solid #404040;
  599. background: #353535;
  600. color: #fff;
  601.  
  602. animation: slideUp 1s;
  603. transform: translateY(0);
  604. transition: transform 1s;
  605. }`);
  606.  
  607. styleSheet.insertRule(` .tpul-notification.vanish {
  608. transform: translateY(100%);
  609. }`);
  610.  
  611. styleSheet.insertRule(` @keyframes slideUp {
  612. 0% { transform: translateY(100%); }
  613. 100% { transform: translateY(0%); }
  614. }`);
  615.  
  616.  
  617.  
  618. // =====NOITCES ELYTS=====
  619.  
  620.  
  621.  
  622.  
  623.  
  624. // =====LOGIC SECTION=====
  625.  
  626.  
  627.  
  628. var GM_storage = typeof GM_setValue === 'function' && typeof GM_getValue === 'function',
  629. all_settings = [],
  630. profileId = null,
  631. last_opened = null,
  632. rollingChatEnabled = false;
  633.  
  634.  
  635. // THE TPUL OBJECT!!
  636.  
  637. var tpul = {
  638. get version(){return version},
  639.  
  640. get noscript(){return typeof tagpro != 'object'},
  641.  
  642. settings: {
  643. addSettings: function({id, title, fields, icon, tooltipText, buttonText}) {
  644.  
  645. var config = arguments[0];
  646.  
  647. if (config.allowLocal && !id && !GM_storage) throw "TPUL: A unique id is required, because localStorage will be used! By the way; it is better to @grant GM_getValue and GM_setValue and set 'allowLocal:false' to use private storage instead.";
  648.  
  649. if (!config.allowLocal && !GM_storage) throw "TPUL: Please @grant GM_setValue and GM_getValue in your userscripts metadata (recommended) or use 'allowLocal:true' (not recommended)";
  650.  
  651.  
  652. if (arguments.length != 1 || typeof config != 'object')
  653. throw Error("addSettings() takes one object as an argument! Example: addSettings( {id:'MySettings', title:'Hello World'} )");
  654.  
  655. // Create a new GM_config instance
  656. let settings = new GM_configStruct({
  657.  
  658. frame: SettingsFrame,
  659.  
  660. ...config,
  661.  
  662. id: String(config.id) || 'defaultId',
  663.  
  664. events: {
  665. ...(config.events||{}),
  666.  
  667. open: function(){
  668.  
  669. //Remove the default inline style of the GM_config frame
  670. this.frame.setAttribute('style', '');
  671.  
  672. //Apply some TagPro/Bootstrap styles
  673. SettingsFrame.firstChild.classList.add('form-horizontal');
  674. for (let el of SettingsFrame.getElementsByClassName('config_header')) el.classList.add('header-title');
  675. for (let el of SettingsFrame.getElementsByClassName('config_var')) el.classList.add('form-group');
  676. for (let el of SettingsFrame.getElementsByClassName('field_label')) {
  677. el.classList.add('col-xs-4');
  678. el.classList.add('control-label');
  679. }
  680. for (let el of SettingsFrame.getElementsByClassName('radio_label')) el.classList.add('radio');
  681. for (let el of [...SettingsFrame.getElementsByTagName('input'),
  682. ...SettingsFrame.getElementsByTagName('select'),
  683. ...SettingsFrame.getElementsByTagName('textarea')]) {
  684.  
  685. switch (el.type) {
  686. case 'radio':
  687. el.parentElement.classList.add('col-xs-8');
  688. el.parentElement.style.paddingLeft = '30px';
  689. el.nextElementSibling.prepend(el);
  690. continue;
  691. case 'button':
  692. el.classList.add('btn');
  693. el.classList.add('btn-default');
  694. break;
  695. default:
  696. el.classList.add('form-control');
  697.  
  698. }
  699.  
  700. var div = document.createElement('div');
  701. el.parentElement.appendChild(div);
  702. div.appendChild(el);
  703.  
  704. div.classList.add('col-xs-8');
  705. div.classList.add('pull-right');
  706. }
  707.  
  708. // The footer with the buttons:
  709.  
  710. var buttonsHolder = SettingsFrame.firstElementChild.lastElementChild;
  711. buttonsHolder.classList.add('col-sm-12');
  712. buttonsHolder.classList.add('tpul-settings-frame-buttons-holder');
  713.  
  714. // Place the "footer" on top
  715. buttonsHolder.parentElement.insertBefore(buttonsHolder, buttonsHolder.parentElement.firstElementChild);
  716.  
  717. for (var btn of [...buttonsHolder.getElementsByClassName('saveclose_buttons'),
  718. ...buttonsHolder.getElementsByClassName('reset')]) {
  719. btn.classList.add('btn');
  720. btn.classList.add('btn-primary');
  721. }
  722.  
  723. buttonsHolder.innerHTML = '';
  724.  
  725. for (var type of this.buttons || ['ok','cancel','reset']) {
  726. var button = document.createElement('button');
  727. button.className = 'btn btn-primary';
  728. button.settings = settings;
  729. buttonsHolder.appendChild(button);
  730.  
  731. switch(type.toLowerCase()) {
  732. case 'ok':
  733. button.onclick = function(){
  734. if(this.settings.valid()) {this.settings.save(); this.settings.close(); tpul.notify('Options saved!','success');}
  735. else {tpul.notify('Please fix any issues before saving', 'error');}
  736. };
  737. button.innerText = 'Ok';
  738. break;
  739. case 'cancel':
  740. button.onclick = function(){ this.settings.close(); tpul.notify('Options canceled','warning'); };
  741. button.innerText = 'Cancel';
  742. break;
  743. case 'reset':
  744. button.onclick = function(){ this.settings.reset(); tpul.notify('All options are reset to their defaults','');};
  745. button.innerText = 'Reset';
  746. break;
  747. case 'save':
  748. button.onclick = function(){
  749. if(this.settings.valid()) {this.settings.save(); tpul.notify('Options saved!','success');}
  750. else {tpul.notify('Please fix any issues before saving', 'error');}
  751. };
  752. button.innerText = 'Save';
  753. break;
  754. case 'close':
  755. button.onclick = function(){ this.settings.close(); tpul.notify('Options canceled','warning'); };
  756. button.innerText = 'Close';
  757. break;
  758. }
  759. }
  760.  
  761.  
  762. if (this.tabs) {
  763.  
  764. var tablist = document.createElement('ul');
  765. tablist.classList.add('tab-list');
  766. SettingsFrame.firstElementChild.insertBefore(tablist, SettingsFrame.firstElementChild.lastElementChild);
  767.  
  768. var tabcontent = document.createElement('div');
  769. tabcontent.classList.add('tab-content');
  770. SettingsFrame.firstElementChild.insertBefore(tabcontent, SettingsFrame.firstElementChild.lastElementChild);
  771.  
  772. for (let el of [...SettingsFrame.getElementsByClassName('section_header_holder')]) {
  773.  
  774. var header = el.getElementsByClassName('section_header')[0];
  775.  
  776. tablist.innerHTML += '<li data-target="#'+el.id+'">' + header.innerText;
  777.  
  778. tabcontent.appendChild(el);
  779. el.classList.add('tab-pane');
  780.  
  781. el.removeChild(header);
  782. }
  783.  
  784. tablist.firstElementChild.click();
  785.  
  786. } else {
  787. for (let el of SettingsFrame.getElementsByClassName('section_header')) el.classList.add('header-title');
  788. }
  789.  
  790. //Open the settings on our way (animated, blocking scroll of body etc.)
  791. this.frame.style.display = '';
  792. SettingsFrame.scrollTop = SettingsFrame.offsetHeight;
  793. document.body.classList.add('tpul-settings-shown');
  794.  
  795. // Add an arrow, indicating the user to scroll down for more settings
  796. var arrow = document.createElement('img');
  797. arrow.classList.add("tpul-settings-scroll-down-arrow");
  798. arrow.src = "https://raw.githubusercontent.com/wilcooo/TagPro-UserscriptLibrary/master/arrow.png";
  799. this.frame.appendChild(arrow);
  800. arrow.onclick = function(){
  801. arrow.style.opacity = 0;
  802. SettingsFrame.scrollTo(0,SettingsFrame.scrollHeight)
  803. }
  804.  
  805. last_opened = settings;
  806.  
  807. // If the userscript adds an 'open' event as well, run it as well
  808. if (this.events && typeof this.events.open == "function")
  809. this.events.open.call(this,...arguments);
  810. },
  811.  
  812. close: function(){
  813. if(this.isOpen){}//TODO: Check whether unsaved?
  814.  
  815. //close the settings in our way (animated)
  816. this.frame.style.display = '';
  817. document.body.classList.remove('tpul-settings-shown');
  818.  
  819. if (this.events && typeof this.events.close == "function")
  820. this.events.close.call(this,...arguments);
  821. },
  822. }
  823. });
  824.  
  825. // Remove all other default styles of GM_config
  826. delete settings.css.basic;
  827.  
  828. // Create a button using the function below
  829. var button = tpul.settings.addButton({
  830. onclick: ()=>settings.open(),
  831. icon: icon,
  832. tooltipText: tooltipText,
  833. buttonText: buttonText,
  834. });
  835.  
  836. settings.button = button;
  837.  
  838. for (let c in config) if(settings[c] === undefined) settings[c] = config[c];
  839.  
  840. all_settings.push(settings);
  841.  
  842. return settings;
  843.  
  844. },
  845. addButton: function({onclick, icon, tooltipText, buttonText}) {
  846.  
  847. if (!SettingsMenu) {
  848. console.error('TPUL: Could not find a place to add the settings button for '+name);
  849. return null;
  850. }
  851.  
  852. var button = document.createElement('button');
  853. button.className = 'btn tpul-settings-btn';
  854.  
  855. if (icon) {
  856. if (icon.search(/^url\((.*)\)$/) == -1) icon = 'url("'+icon+'")';
  857. button.style.backgroundImage = icon;
  858. button.innerHTML = '&nbsp;';
  859. } else button.innerText = buttonText || '?';
  860.  
  861. var tooltip = document.createElement('span');
  862. tooltip.innerText = tooltipText || "Configure this script's settings" ;
  863. button.appendChild(tooltip);
  864.  
  865. SettingsMenu.appendChild(button);
  866.  
  867. button.addEventListener('click',function(click){
  868. button.blur();
  869. onclick(click);
  870. });
  871.  
  872. return button;
  873. },
  874. get parent() {return SettingsMenu.parentElement;},
  875. set parent(container) {
  876.  
  877. if (container) console.warn('You are repositioning the tpul settings menu. This will affect all settings buttons, not only for your script!');
  878.  
  879. container = container ||
  880. document.getElementById('tpul-settings-container') || // Try to add it to a position pre-defined by another script (such as ModFather)
  881. document.getElementById('userscript-top') || // Try to add it on top of any page on the server
  882. document.getElementById('options'); // Try to add it to the scoreboard in-game
  883.  
  884. if (container) {
  885. container.classList.remove('hidden');
  886. container.appendChild(SettingsMenu);
  887. } else console.error('Couldn\'t find a parent element.');
  888.  
  889. return container;
  890. },
  891. get menu(){ return SettingsMenu; },
  892. set menu(_){ throw "You can't change the TPUL settings menu object. You might mean to change the tpul.settings.parent"; },
  893. },
  894.  
  895. profile: {
  896. getId: function() {
  897.  
  898. if (!tpul_promises.getProfileId) {
  899. tpul_promises.getProfileId = new Promise(function(resolve,reject) {
  900.  
  901. GM_xmlhttpRequest({
  902. method: "GET",
  903. url: "http://"+document.location.hostname+"/",
  904. onload: function(){
  905. var match = this.responseText.match(/profile\/([0-9a-f]+)/i);
  906. if (match) {
  907. profileId = match[1];
  908. resolve(profileId);
  909. } else reject({error:"not logged in"});
  910. },
  911. onerror: ()=> reject({error:"request error", request:this}),
  912. });
  913. });
  914. }
  915.  
  916. return tpul_promises.getProfileId;
  917. },
  918.  
  919. getInfo: function() {
  920.  
  921. if (!tpul_promises.getProfileInfo) {
  922.  
  923. tpul_promises.getProfileInfo = new Promise(function(resolve,reject) {
  924.  
  925. tpul.profile.getId().then( function(id){
  926.  
  927. GM_xmlhttpRequest({
  928. method: "GET",
  929. url: "http://"+document.location.hostname+"/profiles/"+id,
  930. onload: function(r){
  931. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  932.  
  933. var arr;
  934. try{ arr = JSON.parse(r.response); }
  935. catch(e){ reject({error:"/profiles/ responded invalid JSON", request:this}); }
  936.  
  937. if(arr.error) reject(arr);
  938.  
  939. if(Array.isArray( arr ) && arr.length == 1) {
  940. resolve(arr[0]);
  941. }
  942. else reject({error:"unknown error", response:arr, request:this});
  943. },
  944. onerror: ()=> reject({error:"request error", request:this}),
  945. });
  946. });
  947.  
  948. tpul.profile.getId().catch( reject );
  949.  
  950. });
  951. }
  952.  
  953. return tpul_promises.getProfileInfo;
  954. },
  955.  
  956. getPage: function() {
  957.  
  958. if (!tpul_promises.getProfilePage) {
  959.  
  960. tpul_promises.getProfilePage = new Promise(function(resolve,reject) {
  961.  
  962. tpul.profile.getId().then( function(id){
  963.  
  964. GM_xmlhttpRequest({
  965. method: "GET",
  966. url: "http://"+document.location.hostname+"/profile/"+id,
  967. onload: function(r){
  968. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  969. if(r.response.error) reject(r.response);
  970.  
  971. var match,
  972. profile = {
  973. settings: {
  974. allChat: undefined,
  975. teamChat: undefined,
  976. groupChat: undefined,
  977. systemChat: undefined,
  978. tutorialChat: undefined,
  979.  
  980. names: undefined,
  981. degrees: undefined,
  982. matchState: undefined,
  983. performanceInfo: undefined,
  984. spectatorInfo: undefined,
  985.  
  986. stats: undefined,
  987. },
  988.  
  989. flair: [],
  990. };
  991.  
  992. // If the 'settings' div cannot be found, assume to not be logged in.
  993. if( !/<div(?: [^>]*)? id="settings"/i.test(this.responseText) ) return reject({error:"not logged in", request:this});
  994.  
  995. // Get the global settings
  996. // (ball spin, respawn warnings and video settings are NOT stored on the TP server,
  997. // only in a cookie on your device)
  998. for (var setting in profile.settings) {
  999. match = RegExp('<input(?: [^>]*)? id="' +setting+ '"(?: [^>]*)? (checked)?', 'i').exec(this.responseText);
  1000. if (match) {
  1001. profile.settings[setting] = Boolean(match[1]);
  1002. } else return reject({error:"unknown error", request:this});
  1003. }
  1004.  
  1005. // Get the 'Custom Team Names' setting (the only non-boolean setting)
  1006. /*
  1007. <select id="teamNames" name="teamNames" class="form-control">
  1008. <option value="always" >Always</option>
  1009. <option value="spectating" >When Spectating</option>
  1010. <option value="never" selected>Never</option>
  1011. </select>
  1012. */
  1013.  
  1014. var teamNamesOptions = /<select(?: [^>]*)? id="teamNames"(?: [^>]*)?>((?:\s*?.*?)*?)<\/select>/i.exec(this.responseText);
  1015. if (teamNamesOptions) {
  1016. var teamNamesOpt_rgx = /<option(?: [^>]*)? value="([^>]*)"(?: [^>]*)? (selected)?(?: [^>]*)?>/ig;
  1017. while ( (match = teamNamesOpt_rgx.exec(teamNamesOptions[1])) ){
  1018.  
  1019. if (match[2]) {
  1020. profile.settings.teamNames = match[1];
  1021. break;
  1022. }
  1023. }
  1024. } else return reject({error:"unknown error", request:this});
  1025.  
  1026. // Get both names
  1027. for (var name of ['reservedName','displayedName']) {
  1028. match = RegExp('<input(?: [^>]*)? id="' +name+ '"(?: [^>]*)? value="(.*?)"', 'i').exec(this.responseText);
  1029. if (match) {
  1030. profile[name] = match[1];
  1031. } else return reject({error:"unknown error", request:this});
  1032. }
  1033.  
  1034. // Get your email
  1035. match = /<span(?: [^>]*)? class="hidden-email"(?: [^>]*)?>[^<]*?\b([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})\b<\/span>/i.exec(this.responseText);
  1036. if (match) {
  1037. profile.email = match[1];
  1038. } else return reject({error:"unknown error", request:this});
  1039.  
  1040. // Get all flairs, and whether they are available, and which one is selected
  1041. var flair_rgx = /<li class="(.*?)" data-flair="(.*?)">/ig;
  1042. while ( (match = flair_rgx.exec(this.responseText)) ) {
  1043. var i = profile.flair.push({
  1044. id: match[2],
  1045. selected: match[1].includes('selected'),
  1046. available: match[1].includes('flair-available'),
  1047. });
  1048. if (profile.flair[i-1]) profile.selectedFlair = profile.flair[i-1];
  1049. }
  1050.  
  1051. // Remove duplicate flairs (because there are 3 tabs)
  1052. var flair_ids = [];
  1053. profile.flair = profile.flair.filter(flair => !flair_ids.includes(flair.id) && flair_ids.push(flair.id));
  1054.  
  1055. resolve(profile);
  1056.  
  1057. },
  1058.  
  1059. onerror: ()=> reject({error:"request error", request:this}),
  1060. });
  1061. });
  1062.  
  1063. tpul.profile.getId().catch( reject );
  1064. });
  1065. }
  1066.  
  1067. return tpul_promises.getProfilePage;
  1068. },
  1069.  
  1070. getRolling: function() {
  1071.  
  1072. if (!tpul_promises.getProfileRolling) {
  1073.  
  1074. tpul_promises.getProfileRolling = new Promise(function(resolve,reject) {
  1075.  
  1076. tpul.profile.getId().then( function(id){
  1077.  
  1078. GM_xmlhttpRequest({
  1079. method: "GET",
  1080. url: "http://"+document.location.hostname+"/profile_rolling/"+id,
  1081. onload: function(r){
  1082. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  1083. if(r.response.error) reject(r.response);
  1084. if(Array.isArray( r.response )) {
  1085. resolve(r.response);
  1086. }
  1087. else reject({error:"unknown error", request:this});
  1088. },
  1089. onerror: ()=> reject({error:"request error", request:this}),
  1090. });
  1091. });
  1092.  
  1093. tpul.profile.getId().catch( reject );
  1094.  
  1095. });
  1096. }
  1097.  
  1098. return tpul_promises.getProfileRolling;
  1099. },
  1100.  
  1101. getReservedName: function(fallbackTimeout=5e3) {
  1102.  
  1103. /*
  1104.  
  1105. Where to get the Reserved name from?
  1106.  
  1107. - in-game when auth
  1108. - getInfo /profiles/...
  1109. - getPage /profile/...
  1110.  
  1111. Logic:
  1112.  
  1113. 1. if getInfo was called before: use that
  1114. 2. if getPage was called before: use that
  1115. 3. if in-game and auth: get it that way
  1116. 4. call getInfo() to get the name
  1117.  
  1118. */
  1119.  
  1120. if (!tpul_promises.getReservedName) {
  1121.  
  1122. tpul_promises.getReservedName = new Promise(function(resolve,reject) {
  1123.  
  1124. // The fallback: get the reserved name using getInfo()
  1125. var fallback = function(){
  1126.  
  1127. done = true;
  1128.  
  1129. tpul.profile.getInfo().then(function(profileInfo) {
  1130.  
  1131. resolve(profileInfo.reservedName);
  1132. });
  1133.  
  1134. tpul.profile.getInfo().catch( reject );
  1135. };
  1136.  
  1137. if (tpul_promises.getProfileInfo) {
  1138.  
  1139. tpul_promises.getProfileInfo.then(function(profileInfo){
  1140. resolve(profileInfo.reservedName);
  1141. });
  1142.  
  1143. tpul_promises.getProfileInfo.catch( reject );
  1144.  
  1145. } else if (tpul_promises.getProfilePage) {
  1146.  
  1147. tpul_promises.getProfilePage.then(function(profilePage){
  1148. resolve(profilePage.reservedName);
  1149. });
  1150.  
  1151. tpul_promises.getProfilePage.catch( reject );
  1152.  
  1153. } else if (typeof tagpro != 'undefined' && tagpro.ready) {
  1154. tagpro.ready(function(){
  1155.  
  1156. if (tagpro.players) {
  1157. if (tagpro.players[tagpro.playerId]) {
  1158.  
  1159. if (tagpro.players[tagpro.playerId].auth) {
  1160. resolve (tagpro.players[tagpro.playerId].name);
  1161. } else fallback();
  1162.  
  1163. } else {
  1164.  
  1165. tagpro.socket.on('p',function(playerId) {
  1166. if (tagpro.players[tagpro.playerId]) {
  1167.  
  1168. if (tagpro.players[tagpro.playerId].auth) {
  1169. resolve (tagpro.players[tagpro.playerId].name);
  1170. } else fallback();
  1171.  
  1172. }
  1173. });
  1174.  
  1175. }
  1176. } else fallback();
  1177. });
  1178. } else fallback();
  1179.  
  1180. var done = false;
  1181. setTimeout(function(){
  1182. tpul_promises.getReservedName.then(()=>done=true);
  1183. });
  1184.  
  1185. setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );
  1186.  
  1187. });
  1188. }
  1189.  
  1190. return tpul_promises.getReservedName;
  1191. },
  1192.  
  1193. getDisplayedName: function(fallbackTimeout = 5e3) {
  1194.  
  1195. /*
  1196.  
  1197. Where to get the Displayed name from?
  1198.  
  1199. - in-game
  1200. - getProfile /profile/...
  1201.  
  1202. Logic:
  1203.  
  1204. 1. if getPage was called before: use that
  1205. 2. if in-game: get it that way
  1206. 3. call getPage() to get the name
  1207.  
  1208. */
  1209.  
  1210. if (!tpul_promises.getDisplayedName) {
  1211.  
  1212. tpul_promises.getDisplayedName = new Promise(function(resolve,reject) {
  1213.  
  1214. // The fallback: get the displayed name using getPage()
  1215. var fallback = function(){
  1216.  
  1217. done = true;
  1218.  
  1219. tpul.profile.getPage().then(function(profilePage) {
  1220.  
  1221. resolve(profilePage.displayedName);
  1222. });
  1223.  
  1224. tpul.profile.getPage().catch( reject );
  1225. };
  1226.  
  1227. if (tpul_promises.getProfilePage) {
  1228.  
  1229. tpul_promises.getProfilePage.then(function(profilePage){
  1230. resolve(tpul_promises.getProfilePage.displayedName);
  1231. });
  1232.  
  1233. tpul_promises.getProfilePage.catch( reject );
  1234.  
  1235. } else if (typeof tagpro != 'undefined' && tagpro.ready) {
  1236. tagpro.ready(function(){
  1237.  
  1238. if (tagpro.players) {
  1239. if (tagpro.players[tagpro.playerId]) {
  1240.  
  1241. resolve (tagpro.players[tagpro.playerId].name);
  1242.  
  1243. } else {
  1244.  
  1245. tagpro.socket.on('p',function(playerId) {
  1246. if (tagpro.players[tagpro.playerId]) {
  1247.  
  1248. resolve (tagpro.players[tagpro.playerId].name);
  1249.  
  1250. }
  1251. });
  1252.  
  1253. }
  1254. } else fallback();
  1255. });
  1256. } else fallback();
  1257.  
  1258. var done = false;
  1259. setTimeout(function(){
  1260. tpul_promises.getDisplayedName.then(()=>done=true);
  1261. });
  1262.  
  1263. setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );
  1264.  
  1265. });
  1266. }
  1267.  
  1268. return tpul_promises.getDisplayedName;
  1269.  
  1270. },
  1271.  
  1272. getSettings: function(fallbackTimeout = 5e3) {
  1273.  
  1274. /*
  1275.  
  1276. Where to get the settings from?
  1277.  
  1278. - in-game
  1279. - getPage /profile/...
  1280.  
  1281. Logic:
  1282.  
  1283. 1. if in-game: get it that way
  1284. 2. call getPage() to get the settings
  1285.  
  1286. */
  1287.  
  1288. var top_args = arguments;
  1289.  
  1290. if (!tpul_promises.getProfileSettings) {
  1291.  
  1292. tpul_promises.getProfileSettings = new Promise(function(resolve,reject) {
  1293.  
  1294. var fallback = function(){
  1295.  
  1296. done = true;
  1297.  
  1298. tpul.profile.getPage().then(function(profilePage){
  1299. resolve(profilePage.settings);
  1300. });
  1301.  
  1302. tpul.profile.getPage().catch( reject );
  1303. };
  1304.  
  1305. if (top_args[0] && top_args[0].__settings) {
  1306. resolve(top_args[0].__settings);
  1307. } else if (tpul_promises.getProfilePage) {
  1308.  
  1309. tpul_promises.getProfilePage.then(function(profilePage){
  1310. resolve(profilePage.settings);
  1311. });
  1312.  
  1313. tpul_promises.getProfilePage.catch( reject );
  1314.  
  1315. } else if (typeof tagpro != 'undefined' && tagpro.ready) {
  1316. tagpro.ready(function(){
  1317.  
  1318. if (tagpro.socket && tagpro.socket.on) {
  1319.  
  1320. tagpro.socket.on('settings', function(settings) {
  1321. resolve(Object.assign(settings.ui, {stats:settings.stats}));
  1322. });
  1323.  
  1324. } else fallback();
  1325. });
  1326. } else fallback();
  1327.  
  1328. var done = false;
  1329. setTimeout(function(){
  1330. tpul_promises.getProfileSettings.then(()=>done=true);
  1331. });
  1332.  
  1333. setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );
  1334.  
  1335. });
  1336.  
  1337. }
  1338.  
  1339. return tpul_promises.getProfileSettings;
  1340.  
  1341. },
  1342.  
  1343. setSettings: function(newSettings, persistent=true, immediately=false) {
  1344.  
  1345. if (immediately) console.warn("Most settings will NOT take effect immediately, I might add this functionality in the future. Only chat settings work at the moment.");
  1346.  
  1347. return new Promise(function(resolve, reject){
  1348.  
  1349. // Step 1: set any local (cookie) settings
  1350. // These don't have to be send to the server, easy!
  1351.  
  1352. if (persistent) {
  1353.  
  1354. for (let setting in newSettings) {
  1355. if (['sound',
  1356. 'music',
  1357. 'volume',
  1358.  
  1359. 'textures',
  1360.  
  1361. 'disableBallSpin',
  1362. 'tileRespawnWarnings',
  1363. 'disableTutorialChat', // This cookie seems to be unused
  1364. // Setting it anyway \(^.^)/
  1365.  
  1366. 'disableParticles',
  1367. 'forceCanvasRenderer',
  1368. 'disableViewportScaling',
  1369. ].includes(setting)) {
  1370.  
  1371. var expires = new Date(Date.now() + 31536e8).toUTCString(); // A century from now (same as TagPro uses)
  1372. document.cookie = setting + '=' + newSettings[setting] + '; expires='+expires+'; path=/; domain=.koalabeast.com';
  1373. }
  1374. }
  1375.  
  1376. // Step 2: send any server-sided settings to the server
  1377.  
  1378. if (['reservedName',
  1379. 'displayedName',
  1380.  
  1381. 'allChat',
  1382. 'teamChat',
  1383. 'groupChat',
  1384. 'systemChat',
  1385. 'tutorialChat',
  1386.  
  1387. 'names',
  1388. 'degrees',
  1389. 'matchState',
  1390. 'performanceInfo',
  1391. 'spectatorInfo',
  1392.  
  1393. 'teamNames',
  1394. 'stats',
  1395. ].some( s => s in newSettings ) ){
  1396.  
  1397. // Call these to let them run in parallel
  1398. tpul.profile.getSettings();
  1399. tpul.profile.getReservedName();
  1400. tpul.profile.getDisplayedName();
  1401.  
  1402. tpul.profile.getSettings().then( function(settings){
  1403. tpul.profile.getReservedName().then( function(reservedName){
  1404. tpul.profile.getDisplayedName().then( function(displayedName){
  1405.  
  1406. console.log(param({reservedName: reservedName, // Your reservedName
  1407. displayedName: displayedName, // Your displayedName
  1408. //...settings, // The current settings
  1409. //...newSettings}));
  1410. }));
  1411. var req = GM_xmlhttpRequest({
  1412. data: param(Object.assign({},
  1413. settings, // The current settings
  1414. {reservedName: reservedName, // Your reservedName
  1415. displayedName: displayedName}, // Your displayedName
  1416. newSettings // Overwrite with the settings that you want to edit.
  1417. )),
  1418. method: "POST",
  1419. headers: {"Content-Type": "application/x-www-form-urlencoded"},
  1420. url: "http://"+document.location.hostname+"/profile/update",
  1421. onload: function(r){
  1422. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  1423.  
  1424. var arr;
  1425. try{ arr = JSON.parse(r.response); }
  1426. catch(e){ reject({error:"/profile/update responded invalid JSON", request:this}); }
  1427.  
  1428. if(arr.error) reject(arr);
  1429. else if(arr.success) {
  1430. resolve(arr);
  1431. } else reject({error:'unknown error',response: arr, request:this});
  1432. },
  1433. onerror: reject,
  1434. });
  1435.  
  1436. });
  1437. });
  1438. });
  1439.  
  1440. }
  1441.  
  1442. }
  1443.  
  1444. // Step 3: In case we are in-game, let the settings go into effect immediately.
  1445. // To update the reserved name, a refresh is required. TPUL won't do this!
  1446.  
  1447. if (typeof tagpro != 'undefined' && immediately) {
  1448. if (!tagpro.settings) tagpro.settings = {ui:{}};
  1449. if (!tagpro.settings.ui) tagpro.settings.ui = {};
  1450.  
  1451. for (let setting in newSettings) {
  1452. if (['allChat',
  1453. 'teamChat',
  1454. 'groupChat',
  1455. 'systemChat',
  1456. 'tutorialChat',
  1457. ].includes(setting)){
  1458.  
  1459. tagpro.settings.ui[setting] = newSettings[setting];
  1460. }
  1461. }
  1462.  
  1463. if (setting == 'tutorialChat') {
  1464. var tutorialButton = document.getElementById('tutorialButton');
  1465.  
  1466. if (tutorialButton) {
  1467. var action = tutorialButton.innerText === "Enable Tips";
  1468. if (newSettings[setting] == action) tutorialButton.click();
  1469. }
  1470. }
  1471.  
  1472.  
  1473. }
  1474.  
  1475. });
  1476. }
  1477.  
  1478. },
  1479.  
  1480. rollingChat: {
  1481.  
  1482. _init: function initRollingChat(enable = false){
  1483.  
  1484. if (typeof tagpro == 'undefined') return console.error( "The `tagpro` object does not exist. Is this a no-script match?" )
  1485.  
  1486. // In case you don't want to load the full TPUL library,
  1487. // You can add RollingChat to your own script by copying this function
  1488. // Usage:
  1489. // initRollingChat(true);
  1490.  
  1491. if (!tagpro.rollingChat) {
  1492.  
  1493. tagpro.rollingChat = {
  1494. enabled: false,
  1495. get handler() {
  1496. return function(event) {
  1497.  
  1498. // Return if not enabled
  1499. if (!tagpro.rollingChat.enabled) return;
  1500.  
  1501. // Whether you are releasing instead of pressing the key:
  1502. var releasing = event.type == 'keyup';
  1503.  
  1504. // Check if any modifier keys where held down during a keyDown
  1505. if (!releasing && (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey)) return;
  1506.  
  1507. // The key that is pressed/released (undefined when it is any other key)
  1508. var arrow = ['left','up','right','down'][[37,38,39,40].indexOf(event.keyCode)];
  1509.  
  1510. // Only if the controls are disabled (usually while composing a message)
  1511. // AND the key is indeed an arrow (not undefined)
  1512. if (tagpro.disableControls && arrow) {
  1513.  
  1514. // Prevent the 'default' thing to happen, which is the cursor moving through the message you are typing
  1515. event.preventDefault();
  1516.  
  1517. // Return if already pressed/released
  1518. if (tagpro.players[tagpro.playerId].pressing[arrow] != releasing) return;
  1519.  
  1520. // Send the key press/release to the server!
  1521. tagpro.sendKeyPress(arrow, releasing);
  1522.  
  1523. // Not necesarry, but useful for other scripts to 'hook onto'
  1524. if (!releasing && tagpro.events.keyDown) tagpro.events.keyDown.forEach(f => f.keyDown(arrow));
  1525. if (releasing && tagpro.events.keyUp) tagpro.events.keyUp.forEach(f => f.keyUp(arrow));
  1526. if(tagpro.ping.avg)setTimeout(()=>(tagpro.players[tagpro.playerId][arrow]=!releasing),tagpro.ping.avg/2);
  1527. }
  1528. };
  1529. }
  1530. };
  1531.  
  1532. // intercept all key presses and releases:
  1533. document.addEventListener('keydown', tagpro.rollingChat.handler);
  1534. document.addEventListener('keyup', tagpro.rollingChat.handler);
  1535. }
  1536.  
  1537. if (enable) tagpro.rollingChat.enabled = true;
  1538. },
  1539.  
  1540. get enabled(){
  1541. tpul.rollingChat._init();
  1542. return tagpro.rollingChat.enabled;
  1543. },
  1544.  
  1545. set enabled(e){
  1546. tpul.rollingChat._init( Boolean(e) );
  1547. if (!e) console.warn('Disabling Rolling Chat! This will disable Rolling Chat for all scripts, not only yours! Please enable it again asap to not get users confused.');
  1548. },
  1549. },
  1550.  
  1551. notify: function(text, type="message", timeout=Math.max(4000, 50*text.length) ){
  1552.  
  1553. // Accepted types: message, success, error, warning
  1554. // ( white green red yellow )
  1555. // For more types, the only thing you need to add is some CSS
  1556.  
  1557. var notification = document.createElement('div');
  1558. notification.className = 'tpul-notification tpul-notification-' + type;
  1559. notification.innerText = text;
  1560. document.body.appendChild(notification);
  1561.  
  1562. // Hide after a while (timeout)
  1563. setTimeout(function(notification){
  1564. if(notification)notification.classList.add('vanish');
  1565. }, timeout, notification);
  1566.  
  1567. // Hide on click
  1568. notification.onclick = function(){ this.classList.add('vanish'); };
  1569.  
  1570. // Clear up the DOM once the notification is vanished
  1571. notification.addEventListener('transitionend',function(){ this.remove(); });
  1572.  
  1573. // Return the element, for scripters to "play" with
  1574. return notification;
  1575.  
  1576. },
  1577.  
  1578. groupcomm: {
  1579.  
  1580. emit: function ( script, command, ...args ) {
  1581. // Example: tpul.groupcomm.emit('gropro', 'desc', 'welcome to my awesome group')
  1582. if (typeof tagpro != 'undefined' && tagpro.group && tagpro.group.socket) {
  1583.  
  1584. var full_command = "/" + [...arguments].map(a=>(a||"").replace(/([\^\/:;])/g,"^$1")).join("/") + ";";
  1585.  
  1586. tagpro.group.socket.emit( "touch", full_command.substr( 0,12 ) );
  1587. for (var i = 12; i < full_command.length; i += 11) {
  1588. tagpro.group.socket.emit( "touch", ":" + full_command.substr( i,11 ) );
  1589. }
  1590.  
  1591. tagpro.group.socket.emit( "touch", tagpro.group.socket.playerLocation );
  1592. }
  1593. else throw "Not connected to a group";
  1594. },
  1595.  
  1596. oncommand: function oncommand( callback ) {
  1597. if (typeof tagpro != 'undefined' && tagpro.group && tagpro.group.socket) {
  1598. if (!tpul.groupcomm._active) tpul.groupcomm._init();
  1599. tpul.groupcomm._callbacks.push(callback);
  1600. }
  1601. else throw "Not connected to a group";
  1602. },
  1603.  
  1604. _callbacks: [],
  1605.  
  1606. _commands: {},
  1607.  
  1608. _active: false,
  1609.  
  1610. _init: function (){
  1611.  
  1612. if (tpul.groupcomm._active) return;
  1613.  
  1614. tpul.groupcomm._active = true;
  1615.  
  1616. tagpro.group.socket.on( "member", function(member) {
  1617.  
  1618. function handleCommand(command){
  1619. var args = [...command
  1620. .replace(/\^(.)/g, "$1^")
  1621. .match(/\/(.*);(?=(?:\^\^)*(?!\^))/,1)[1]
  1622. .split(/\/(?=(?:\^\^)*(?!\^))/)
  1623. .map(a=>a.replace(/(.)\^(?=(?:\^\^)*(?!\^))/g, "$1"))
  1624. ];
  1625.  
  1626. for (var c in tpul.groupcomm._callbacks) {
  1627. var callback = tpul.groupcomm._callbacks[c];
  1628. try { callback({
  1629. member: member,
  1630. script: args.shift() || null,
  1631. command: args.shift() || null,
  1632. args: args,
  1633. raw:command } ); }
  1634. catch(e) {
  1635. console.error("Unhandled GroupComm error. Mod makers, handle your errors!", e);
  1636. tpul.groupcomm._callbacks.splice(c,1);
  1637. }
  1638. }
  1639. }
  1640.  
  1641. var raw = member.location,
  1642. commands = tpul.groupcomm._commands;
  1643.  
  1644. if (typeof raw !== "string") return;
  1645.  
  1646. // A full one-line command: / ... ;
  1647. if ( raw.match(/^\/.*[^^];/) ) {
  1648. handleCommand( raw );
  1649. delete commands[member.id];
  1650. }
  1651.  
  1652. // The start of a multi-line command: / ...
  1653. else if ( raw.match(/^\//) ) {
  1654. commands[member.id] = raw;
  1655. }
  1656.  
  1657. // The end of a multi-line command: : ... ;
  1658. else if ( raw.match(/^:.*[^^];/) ) {
  1659. if (!commands[member.id]) throw "Did not receive start of command.";
  1660. var com = commands[member.id] + raw.slice(1);
  1661. handleCommand( com );
  1662. delete commands[member.id];
  1663. }
  1664.  
  1665. // A middle part of a multi-line command: : ...
  1666. else if ( raw.match(/^:/) ) {
  1667. if (!commands[member.id]) throw "Did not receive start of command.";
  1668. commands[member.id] += raw.slice(1);
  1669. }
  1670.  
  1671. // Not a GroupComm command:
  1672. else delete commands[member.id];
  1673. });
  1674. }
  1675. },
  1676.  
  1677. chat: {
  1678.  
  1679. emit: function(message, type='all'){
  1680. // type: all/team/group/mod
  1681.  
  1682. if (tpul.playerLocation != 'game') {
  1683. console.error( "TPUL wasn't able to send this chat, as we haven't joined a game", message )
  1684. throw "TPUL: can't send a chat when not in a game";
  1685. }
  1686.  
  1687. if (!tpul.noscript && tagpro.socket) {
  1688. // Method 1: Emit a message using the socket.
  1689. // Preferable since it has the most chance
  1690. // to work with other scripts.
  1691.  
  1692. if (type == 'group') tagpro.group && tagpro.group.socket.emit('chat', message);
  1693.  
  1694. else tagpro.socket.emit('chat',{
  1695. message:message,
  1696. toAll:type!='team',
  1697. asMod:type=='mod',
  1698. });
  1699.  
  1700. } else if (typeof $ == 'function') {
  1701. // Method 2: Send a message "manually" (because no-script)
  1702. // This takes about 15ms on my computer (TagPro's chat function isn't really performant)
  1703. // So expect a frame or two to drop while sending a macro (still better than typing it out yourself though)
  1704. // On top of that, your input will be delayed by the same amount of time.
  1705. // Of course, THESE DELAYS WILL ONLY HAPPEN AT THE MOMENT YOU USE ONE OF YOUR MACROS!
  1706.  
  1707. // The chatbox and name input box
  1708. var chat = document.getElementById('chat'),
  1709. name = document.getElementById('name')
  1710.  
  1711. // Blur (unfocus) the 'name' input, in case you are in the middle of changing your name
  1712. if (name == document.activeElement) name.blur()
  1713.  
  1714. // Close the box in case it is already opened
  1715. if (chat.style.display != 'none' && chat.style.display != '' ) {
  1716. chat.value = ""
  1717. this._handler( { type: 'keydown', keyCode: 13, preventDefault: ()=>0 } )
  1718. }
  1719.  
  1720. // Trick the handler in opening the box
  1721. var keyCode = { team: 84, group: 71, mod: 77, all: 13 }[type]
  1722. this._handler( { type: 'keydown', keyCode: keyCode, preventDefault: ()=>0 } )
  1723.  
  1724. // Type out the message:
  1725. document.getElementById('chat').value = message;
  1726.  
  1727. // Trick the handler in closing the box and sending the message
  1728. this._handler( { type: 'keydown', keyCode: 13, preventDefault: ()=>0 } )
  1729.  
  1730. } else console.error( "TPUL wasn't able to send the chat message", message )
  1731. },
  1732.  
  1733. get _handler(){ // Self-overwriting getter
  1734. // Find the keydown handler that opens/closes the chat box
  1735. delete this._handler;
  1736. return this._handler = $._data( document, "events" ).keydown.find(
  1737. listener => listener.handler.toString().includes('tagpro.keys.chatToTeam')
  1738. ).handler || function(){$(document).trigger(...arguments)};
  1739. },
  1740. },
  1741.  
  1742. get playerLocation(){
  1743.  
  1744. // This function seems to be pointless, but I'll make
  1745. // sure it'll keep working even when the site architechture changes,
  1746. // so that you don't have to update your script too often :)
  1747. // (for example, when the SWJ got introduced)
  1748.  
  1749. if ( location.pathname.startsWith('/games/find') ) return 'find';
  1750. if ( location.pathname.match(/^\/groups\/[a-z]{8}$/) ) return 'group';
  1751. var path = location.pathname.match(/\w+/);
  1752. if (path) return path[0];
  1753. if ( location.port ) return 'game';
  1754. if ( location.pathname == '/' ) return 'home';
  1755.  
  1756. throw 'Player location unknown';
  1757. },
  1758.  
  1759. /*events: {
  1760. on: function(event, callback) {
  1761.  
  1762. if( !tpul.events._listeners[event] ) tpul.events._listeners[event] = []
  1763. tpul.events._listener.push(callback)
  1764.  
  1765. //if (event in deepEvents) enableDeepEvents();
  1766. },
  1767.  
  1768. on: function(event, callback) {
  1769.  
  1770. if (event == 'register') throw "You can't use 'register' as an event"
  1771.  
  1772. if (typeof tagpro != 'undefined') {
  1773. if ( !tagpro.events ) tagpro.events = {}
  1774. if ( !tagpro.events[event] ) tagpro.events[event] = []
  1775.  
  1776.  
  1777. if (!tagpro.events[event]) tagpro.events[event] = [];
  1778.  
  1779. var eventFunc = {};
  1780. eventFunc[event] = callback;
  1781. tagpro.events[event].push(eventFunc);
  1782.  
  1783. //if (event in deepEvents) enableDeepEvents();
  1784. },
  1785.  
  1786. emit: function(event, data) {
  1787.  
  1788. if ( tpul.events._listeners[event] ) for (let callback of tpul.events._listeners[
  1789. if (tagpro.events[event]) for (let listener of tagpro.events[event]) {
  1790. try { listener[event](data); }
  1791. catch (e) {
  1792. console.error("Unhandled tagpro.events.on('"+event+"') error. Mod makers, handle your errors!");
  1793. console.error(e);
  1794. console.error( listener[event]);
  1795. }
  1796. }
  1797. },
  1798.  
  1799. _listeners: {}
  1800. }*/
  1801. };
  1802.  
  1803.  
  1804.  
  1805.  
  1806.  
  1807.  
  1808. // =====DOM SECTION=====
  1809.  
  1810.  
  1811.  
  1812. var SettingsMenu = document.getElementById('tpul-settings-menu') || document.createElement('div');
  1813. SettingsMenu.id = 'tpul-settings-menu';
  1814.  
  1815. var SettingsFrame = document.getElementsByClassName('tpul-settings-frame')[0] || document.createElement('div');
  1816. SettingsFrame.className = 'tpul-settings-frame';
  1817. if(tpul.playerLocation == 'game') SettingsFrame.classList.add('in-game');
  1818. document.body.appendChild(SettingsFrame);
  1819.  
  1820.  
  1821. if (!SettingsMenu.parentElement) tpul.settings.parent = null;
  1822.  
  1823. // =====NOITCES MOD=====
  1824.  
  1825.  
  1826.  
  1827.  
  1828.  
  1829. // OPENING AND CLOSING
  1830.  
  1831. SettingsFrame.onclick = function(click) {
  1832.  
  1833. // Close all settings when clicking outside the panel
  1834. if (SettingsFrame == click.target) for (var settings of all_settings) settings.close();
  1835.  
  1836. };
  1837.  
  1838. SettingsFrame.addEventListener('wheel', function(wheel) {
  1839.  
  1840. // Close all settings when scrolling up far enough
  1841. setTimeout(function(){
  1842. if (SettingsFrame.firstElementChild &&
  1843. SettingsFrame.scrollTop + SettingsFrame.offsetHeight <= SettingsFrame.firstElementChild.offsetTop + 20)
  1844. for (var settings of all_settings) settings.close();
  1845. },200);
  1846.  
  1847. // Open when scrolling down (only in game) DOESN'T WORK PROPERLY
  1848. // if (tpul.playerLocation == 'game' && wheel.deltaY > 0 && last_opened && !last_opened.isOpen) last_opened.open();
  1849.  
  1850. if (wheel.deltaY > 0) {
  1851. // Hide the scrolldown arrow TODO
  1852. for (var arrow of document.getElementsByClassName('tpul-settings-scroll-down-arrow')) {
  1853. arrow.style.opacity = 0;
  1854. }
  1855. }
  1856. })
  1857.  
  1858.  
  1859.  
  1860.  
  1861. // Section tabs
  1862.  
  1863. SettingsFrame.addEventListener('click', function(click) {
  1864. var tablist = click.target.parentElement;
  1865. if (tablist.classList.contains('tab-list')) {
  1866.  
  1867. var scrollTop = SettingsFrame.scrollTop;
  1868. console.log(scrollTop);
  1869.  
  1870. for (let li of tablist.getElementsByTagName('li'))
  1871. li.classList.remove('active');
  1872. for (let pane of tablist.parentElement.getElementsByClassName('tab-pane'))
  1873. pane.classList.remove('active');
  1874. click.target.classList.add('active');
  1875. document.querySelector(click.target.dataset.target).classList.add('active');
  1876.  
  1877. SettingsFrame.scrollTop = scrollTop;
  1878. }
  1879. }, true);
  1880.  
  1881.  
  1882.  
  1883. // Get settings from socket:
  1884.  
  1885. if (typeof tagpro != 'undefined' && tagpro.ready) {
  1886. tagpro.ready(function(){
  1887. if (tagpro.socket && tagpro.socket.on) {
  1888. tagpro.socket.on('settings', function(settings) {
  1889. // Don't try to tamper with this, or copy this in your own script.
  1890. // It will affect all scripts using TPUL.
  1891. tpul.profile.getSettings( {__settings:Object.assign(settings.ui, {stats: settings.stats})} );
  1892. });
  1893. }
  1894. });
  1895. }
  1896.  
  1897. // Some helper function(s)
  1898.  
  1899. function param(o){
  1900.  
  1901. return Object.keys(o).map(function(k) {
  1902. return encodeURIComponent(k) + '=' + encodeURIComponent(o[k.replace(' ','+')]);
  1903. }).join('&').replace(/%20/g, '+');
  1904. }
  1905.  
  1906.  
  1907.  
  1908.  
  1909. // =====NOITCES CIGOL=====
  1910.  
  1911.  
  1912.  
  1913.  
  1914.  
  1915. if (typeof tpul_promises == 'undefined') {
  1916. try{
  1917. window.tpul_promises = {};
  1918. unsafeWindow.tpul_promises = window.tpul_promises;
  1919. }catch(e){}
  1920. }
  1921.  
  1922. var tpul_common = {}
  1923.  
  1924. if (typeof tpul_common == 'undefined') {
  1925. if (window) window.tpul_common = tpul_common
  1926. if (unsafeWindow) unsaeWindow.tpul_common = tpul_common
  1927. }
  1928.  
  1929.  
  1930. // If running independently (not @required by another script)
  1931. // only good for modders or while debugging
  1932. if (typeof GM_info == 'undefined' || GM_info.script.name == 'TagPro Userscript Library') {
  1933. if (typeof tagpro != 'undefined') tagpro.tpul = tpul;
  1934. if (window) window.tpul = tpul;
  1935. if (unsafeWindow) unsafeWindow.tpul = tpul;
  1936. }
  1937.  
  1938. return tpul;
  1939. })();