TagPro Userscript Library

Functions that any TagPro script could benefit from

目前為 2019-04-01 提交的版本,檢視 最新版本

此腳本不應該直接安裝,它是一個供其他腳本使用的函式庫。欲使用本函式庫,請在腳本 metadata 寫上: // @require https://update.cn-greasyfork.org/scripts/371240/685493/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://greasyfork.org/users/152992)
  5. // @version 4.6
  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.6
  24. // @license MIT
  25. // ==/UserLibrary==
  26.  
  27.  
  28. var version = 4.6;
  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://greasyfork.org/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. settings: {
  641. addSettings: function({id, title, fields, icon, tooltipText, buttonText}) {
  642.  
  643. var config = arguments[0];
  644.  
  645. 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.";
  646.  
  647. 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)";
  648.  
  649.  
  650. if (arguments.length != 1 || typeof config != 'object')
  651. throw Error("addSettings() takes one object as an argument! Example: addSettings( {id:'MySettings', title:'Hello World'} )");
  652.  
  653. // Create a new GM_config instance
  654. let settings = new GM_configStruct({
  655.  
  656. frame: SettingsFrame,
  657.  
  658. ...config,
  659.  
  660. id: String(config.id) || 'defaultId',
  661.  
  662. events: {
  663. ...(config.events||{}),
  664.  
  665. open: function(){
  666.  
  667. //Remove the default inline style of the GM_config frame
  668. this.frame.setAttribute('style', '');
  669.  
  670. //Apply some TagPro/Bootstrap styles
  671. SettingsFrame.firstChild.classList.add('form-horizontal');
  672. for (let el of SettingsFrame.getElementsByClassName('config_header')) el.classList.add('header-title');
  673. for (let el of SettingsFrame.getElementsByClassName('config_var')) el.classList.add('form-group');
  674. for (let el of SettingsFrame.getElementsByClassName('field_label')) {
  675. el.classList.add('col-xs-4');
  676. el.classList.add('control-label');
  677. }
  678. for (let el of SettingsFrame.getElementsByClassName('radio_label')) el.classList.add('radio');
  679. for (let el of [...SettingsFrame.getElementsByTagName('input'),
  680. ...SettingsFrame.getElementsByTagName('select'),
  681. ...SettingsFrame.getElementsByTagName('textarea')]) {
  682.  
  683. switch (el.type) {
  684. case 'radio':
  685. el.parentElement.classList.add('col-xs-8');
  686. el.parentElement.style.paddingLeft = '30px';
  687. el.nextElementSibling.prepend(el);
  688. continue;
  689. case 'button':
  690. el.classList.add('btn');
  691. el.classList.add('btn-default');
  692. break;
  693. default:
  694. el.classList.add('form-control');
  695.  
  696. }
  697.  
  698. var div = document.createElement('div');
  699. el.parentElement.appendChild(div);
  700. div.appendChild(el);
  701.  
  702. div.classList.add('col-xs-8');
  703. div.classList.add('pull-right');
  704. }
  705.  
  706. // The footer with the buttons:
  707.  
  708. var buttonsHolder = SettingsFrame.firstElementChild.lastElementChild;
  709. buttonsHolder.classList.add('col-sm-12');
  710. buttonsHolder.classList.add('tpul-settings-frame-buttons-holder');
  711.  
  712. // Place the "footer" on top
  713. buttonsHolder.parentElement.insertBefore(buttonsHolder, buttonsHolder.parentElement.firstElementChild);
  714.  
  715. for (var btn of [...buttonsHolder.getElementsByClassName('saveclose_buttons'),
  716. ...buttonsHolder.getElementsByClassName('reset')]) {
  717. btn.classList.add('btn');
  718. btn.classList.add('btn-primary');
  719. }
  720.  
  721. buttonsHolder.innerHTML = '';
  722.  
  723. for (var type of this.buttons || ['ok','cancel','reset']) {
  724. var button = document.createElement('button');
  725. button.className = 'btn btn-primary';
  726. button.settings = settings;
  727. buttonsHolder.appendChild(button);
  728.  
  729. switch(type.toLowerCase()) {
  730. case 'ok':
  731. button.onclick = function(){
  732. if(this.settings.valid()) {this.settings.save(); this.settings.close(); tpul.notify('Options saved!','success');}
  733. else {tpul.notify('Please fix any issues before saving', 'error');}
  734. };
  735. button.innerText = 'Ok';
  736. break;
  737. case 'cancel':
  738. button.onclick = function(){ this.settings.close(); tpul.notify('Options canceled','warning'); };
  739. button.innerText = 'Cancel';
  740. break;
  741. case 'reset':
  742. button.onclick = function(){ this.settings.reset(); tpul.notify('All options are reset to their defaults','');};
  743. button.innerText = 'Reset';
  744. break;
  745. case 'save':
  746. button.onclick = function(){
  747. if(this.settings.valid()) {this.settings.save(); tpul.notify('Options saved!','success');}
  748. else {tpul.notify('Please fix any issues before saving', 'error');}
  749. };
  750. button.innerText = 'Save';
  751. break;
  752. case 'close':
  753. button.onclick = function(){ this.settings.close(); tpul.notify('Options canceled','warning'); };
  754. button.innerText = 'Close';
  755. break;
  756. }
  757. }
  758.  
  759.  
  760. if (this.tabs) {
  761.  
  762. var tablist = document.createElement('ul');
  763. tablist.classList.add('tab-list');
  764. SettingsFrame.firstElementChild.insertBefore(tablist, SettingsFrame.firstElementChild.lastElementChild);
  765.  
  766. var tabcontent = document.createElement('div');
  767. tabcontent.classList.add('tab-content');
  768. SettingsFrame.firstElementChild.insertBefore(tabcontent, SettingsFrame.firstElementChild.lastElementChild);
  769.  
  770. for (let el of [...SettingsFrame.getElementsByClassName('section_header_holder')]) {
  771.  
  772. var header = el.getElementsByClassName('section_header')[0];
  773.  
  774. tablist.innerHTML += '<li data-target="#'+el.id+'">' + header.innerText;
  775.  
  776. tabcontent.appendChild(el);
  777. el.classList.add('tab-pane');
  778.  
  779. el.removeChild(header);
  780. }
  781.  
  782. tablist.firstElementChild.click();
  783.  
  784. } else {
  785. for (let el of SettingsFrame.getElementsByClassName('section_header')) el.classList.add('header-title');
  786. }
  787.  
  788. //Open the settings on our way (animated, blocking scroll of body etc.)
  789. this.frame.style.display = '';
  790. SettingsFrame.scrollTop = SettingsFrame.offsetHeight;
  791. document.body.classList.add('tpul-settings-shown');
  792.  
  793. // Add an arrow, indicating the user to scroll down for more settings
  794. var arrow = document.createElement('img');
  795. arrow.classList.add("tpul-settings-scroll-down-arrow");
  796. arrow.src = "https://raw.githubusercontent.com/wilcooo/TagPro-UserscriptLibrary/master/arrow.png";
  797. this.frame.appendChild(arrow);
  798. arrow.onclick = function(){
  799. arrow.style.opacity = 0;
  800. SettingsFrame.scrollTo(0,SettingsFrame.scrollHeight)
  801. }
  802.  
  803. last_opened = settings;
  804.  
  805. // If the userscript adds an 'open' event as well, run it as well
  806. if (this.events && typeof this.events.open == "function")
  807. this.events.open.call(this,...arguments);
  808. },
  809.  
  810. close: function(){
  811. if(this.isOpen){}//TODO: Check whether unsaved?
  812.  
  813. //close the settings in our way (animated)
  814. this.frame.style.display = '';
  815. document.body.classList.remove('tpul-settings-shown');
  816.  
  817. if (this.events && typeof this.events.close == "function")
  818. this.events.close.call(this,...arguments);
  819. },
  820. }
  821. });
  822.  
  823. // Remove all other default styles of GM_config
  824. delete settings.css.basic;
  825.  
  826. // Create a button using the function below
  827. var button = tpul.settings.addButton({
  828. onclick: ()=>settings.open(),
  829. icon: icon,
  830. tooltipText: tooltipText,
  831. buttonText: buttonText,
  832. });
  833.  
  834. settings.button = button;
  835.  
  836. for (let c in config) if(settings[c] === undefined) settings[c] = config[c];
  837.  
  838. all_settings.push(settings);
  839.  
  840. return settings;
  841.  
  842. },
  843. addButton: function({onclick, icon, tooltipText, buttonText}) {
  844.  
  845. if (!SettingsMenu) {
  846. console.error('TPUL: Could not find a place to add the settings button for '+name);
  847. return null;
  848. }
  849.  
  850. var button = document.createElement('button');
  851. button.className = 'btn tpul-settings-btn';
  852.  
  853. if (icon) {
  854. if (icon.search(/^url\((.*)\)$/) == -1) icon = 'url("'+icon+'")';
  855. button.style.backgroundImage = icon;
  856. button.innerHTML = '&nbsp;';
  857. } else button.innerText = buttonText || '?';
  858.  
  859. var tooltip = document.createElement('span');
  860. tooltip.innerText = tooltipText || "Configure this script's settings" ;
  861. button.appendChild(tooltip);
  862.  
  863. SettingsMenu.appendChild(button);
  864.  
  865. button.addEventListener('click',function(click){
  866. button.blur();
  867. onclick(click);
  868. });
  869.  
  870. return button;
  871. },
  872. get parent() {return SettingsMenu.parentElement;},
  873. set parent(container) {
  874.  
  875. if (container) console.warn('You are repositioning the tpul settings menu. This will affect all settings buttons, not only for your script!');
  876.  
  877. container = container ||
  878. document.getElementById('tpul-settings-container') || // Try to add it to a position pre-defined by another script (such as ModFather)
  879. document.getElementById('userscript-top') || // Try to add it on top of any page on the server
  880. document.getElementById('options'); // Try to add it to the scoreboard in-game
  881.  
  882. if (container) {
  883. container.classList.remove('hidden');
  884. container.appendChild(SettingsMenu);
  885. } else console.error('Couldn\'t find a parent element.');
  886.  
  887. return container;
  888. },
  889. get menu(){ return SettingsMenu; },
  890. set menu(_){ throw "You can't change the TPUL settings menu object. You might mean to change the tpul.settings.parent"; },
  891. },
  892.  
  893. profile: {
  894. getId: function() {
  895.  
  896. if (!tpul_promises.getProfileId) {
  897. tpul_promises.getProfileId = new Promise(function(resolve,reject) {
  898.  
  899. GM_xmlhttpRequest({
  900. method: "GET",
  901. url: "http://"+document.location.hostname+"/",
  902. onload: function(){
  903. var match = this.responseText.match(/profile\/([0-9a-f]+)/i);
  904. if (match) {
  905. profileId = match[1];
  906. resolve(profileId);
  907. } else reject({error:"not logged in"});
  908. },
  909. onerror: ()=> reject({error:"request error", request:this}),
  910. });
  911. });
  912. }
  913.  
  914. return tpul_promises.getProfileId;
  915. },
  916.  
  917. getInfo: function() {
  918.  
  919. if (!tpul_promises.getProfileInfo) {
  920.  
  921. tpul_promises.getProfileInfo = new Promise(function(resolve,reject) {
  922.  
  923. tpul.profile.getId().then( function(id){
  924.  
  925. GM_xmlhttpRequest({
  926. method: "GET",
  927. url: "http://"+document.location.hostname+"/profiles/"+id,
  928. onload: function(r){
  929. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  930.  
  931. var arr;
  932. try{ arr = JSON.parse(r.response); }
  933. catch(e){ reject({error:"/profiles/ responded invalid JSON", request:this}); }
  934.  
  935. if(arr.error) reject(arr);
  936.  
  937. if(Array.isArray( arr ) && arr.length == 1) {
  938. resolve(arr[0]);
  939. }
  940. else reject({error:"unknown error", response:arr, request:this});
  941. },
  942. onerror: ()=> reject({error:"request error", request:this}),
  943. });
  944. });
  945.  
  946. tpul.profile.getId().catch( reject );
  947.  
  948. });
  949. }
  950.  
  951. return tpul_promises.getProfileInfo;
  952. },
  953.  
  954. getPage: function() {
  955.  
  956. if (!tpul_promises.getProfilePage) {
  957.  
  958. tpul_promises.getProfilePage = new Promise(function(resolve,reject) {
  959.  
  960. tpul.profile.getId().then( function(id){
  961.  
  962. GM_xmlhttpRequest({
  963. method: "GET",
  964. url: "http://"+document.location.hostname+"/profile/"+id,
  965. onload: function(r){
  966. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  967. if(r.response.error) reject(r.response);
  968.  
  969. var match,
  970. profile = {
  971. settings: {
  972. allChat: undefined,
  973. teamChat: undefined,
  974. groupChat: undefined,
  975. systemChat: undefined,
  976. tutorialChat: undefined,
  977.  
  978. names: undefined,
  979. degrees: undefined,
  980. matchState: undefined,
  981. performanceInfo: undefined,
  982. spectatorInfo: undefined,
  983.  
  984. stats: undefined,
  985. },
  986.  
  987. flair: [],
  988. };
  989.  
  990. // If the 'settings' div cannot be found, assume to not be logged in.
  991. if( !/<div(?: [^>]*)? id="settings"/i.test(this.responseText) ) return reject({error:"not logged in", request:this});
  992.  
  993. // Get the global settings
  994. // (ball spin, respawn warnings and video settings are NOT stored on the TP server,
  995. // only in a cookie on your device)
  996. for (var setting in profile.settings) {
  997. match = RegExp('<input(?: [^>]*)? id="' +setting+ '"(?: [^>]*)? (checked)?', 'i').exec(this.responseText);
  998. if (match) {
  999. profile.settings[setting] = Boolean(match[1]);
  1000. } else return reject({error:"unknown error", request:this});
  1001. }
  1002.  
  1003. // Get the 'Custom Team Names' setting (the only non-boolean setting)
  1004. /*
  1005. <select id="teamNames" name="teamNames" class="form-control">
  1006. <option value="always" >Always</option>
  1007. <option value="spectating" >When Spectating</option>
  1008. <option value="never" selected>Never</option>
  1009. </select>
  1010. */
  1011.  
  1012. var teamNamesOptions = /<select(?: [^>]*)? id="teamNames"(?: [^>]*)?>((?:\s*?.*?)*?)<\/select>/i.exec(this.responseText);
  1013. if (teamNamesOptions) {
  1014. var teamNamesOpt_rgx = /<option(?: [^>]*)? value="([^>]*)"(?: [^>]*)? (selected)?(?: [^>]*)?>/ig;
  1015. while ( (match = teamNamesOpt_rgx.exec(teamNamesOptions[1])) ){
  1016.  
  1017. if (match[2]) {
  1018. profile.settings.teamNames = match[1];
  1019. break;
  1020. }
  1021. }
  1022. } else return reject({error:"unknown error", request:this});
  1023.  
  1024. // Get both names
  1025. for (var name of ['reservedName','displayedName']) {
  1026. match = RegExp('<input(?: [^>]*)? id="' +name+ '"(?: [^>]*)? value="(.*?)"', 'i').exec(this.responseText);
  1027. if (match) {
  1028. profile[name] = match[1];
  1029. } else return reject({error:"unknown error", request:this});
  1030. }
  1031.  
  1032. // Get your email
  1033. match = /<span(?: [^>]*)? class="hidden-email"(?: [^>]*)?>[^<]*?\b([A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,})\b<\/span>/i.exec(this.responseText);
  1034. if (match) {
  1035. profile.email = match[1];
  1036. } else return reject({error:"unknown error", request:this});
  1037.  
  1038. // Get all flairs, and whether they are available, and which one is selected
  1039. var flair_rgx = /<li class="(.*?)" data-flair="(.*?)">/ig;
  1040. while ( (match = flair_rgx.exec(this.responseText)) ) {
  1041. var i = profile.flair.push({
  1042. id: match[2],
  1043. selected: match[1].includes('selected'),
  1044. available: match[1].includes('flair-available'),
  1045. });
  1046. if (profile.flair[i-1]) profile.selectedFlair = profile.flair[i-1];
  1047. }
  1048.  
  1049. // Remove duplicate flairs (because there are 3 tabs)
  1050. var flair_ids = [];
  1051. profile.flair = profile.flair.filter(flair => !flair_ids.includes(flair.id) && flair_ids.push(flair.id));
  1052.  
  1053. resolve(profile);
  1054.  
  1055. },
  1056.  
  1057. onerror: ()=> reject({error:"request error", request:this}),
  1058. });
  1059. });
  1060.  
  1061. tpul.profile.getId().catch( reject );
  1062. });
  1063. }
  1064.  
  1065. return tpul_promises.getProfilePage;
  1066. },
  1067.  
  1068. getRolling: function() {
  1069.  
  1070. if (!tpul_promises.getProfileRolling) {
  1071.  
  1072. tpul_promises.getProfileRolling = new Promise(function(resolve,reject) {
  1073.  
  1074. tpul.profile.getId().then( function(id){
  1075.  
  1076. GM_xmlhttpRequest({
  1077. method: "GET",
  1078. url: "http://"+document.location.hostname+"/profile_rolling/"+id,
  1079. onload: function(r){
  1080. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  1081. if(r.response.error) reject(r.response);
  1082. if(Array.isArray( r.response )) {
  1083. resolve(r.response);
  1084. }
  1085. else reject({error:"unknown error", request:this});
  1086. },
  1087. onerror: ()=> reject({error:"request error", request:this}),
  1088. });
  1089. });
  1090.  
  1091. tpul.profile.getId().catch( reject );
  1092.  
  1093. });
  1094. }
  1095.  
  1096. return tpul_promises.getProfileRolling;
  1097. },
  1098.  
  1099. getReservedName: function(fallbackTimeout=5e3) {
  1100.  
  1101. /*
  1102.  
  1103. Where to get the Reserved name from?
  1104.  
  1105. - in-game when auth
  1106. - getInfo /profiles/...
  1107. - getPage /profile/...
  1108.  
  1109. Logic:
  1110.  
  1111. 1. if getInfo was called before: use that
  1112. 2. if getPage was called before: use that
  1113. 3. if in-game and auth: get it that way
  1114. 4. call getInfo() to get the name
  1115.  
  1116. */
  1117.  
  1118. if (!tpul_promises.getReservedName) {
  1119.  
  1120. tpul_promises.getReservedName = new Promise(function(resolve,reject) {
  1121.  
  1122. // The fallback: get the reserved name using getInfo()
  1123. var fallback = function(){
  1124.  
  1125. done = true;
  1126.  
  1127. tpul.profile.getInfo().then(function(profileInfo) {
  1128.  
  1129. resolve(profileInfo.reservedName);
  1130. });
  1131.  
  1132. tpul.profile.getInfo().catch( reject );
  1133. };
  1134.  
  1135. if (tpul_promises.getProfileInfo) {
  1136.  
  1137. tpul_promises.getProfileInfo.then(function(profileInfo){
  1138. resolve(profileInfo.reservedName);
  1139. });
  1140.  
  1141. tpul_promises.getProfileInfo.catch( reject );
  1142.  
  1143. } else if (tpul_promises.getProfilePage) {
  1144.  
  1145. tpul_promises.getProfilePage.then(function(profilePage){
  1146. resolve(profilePage.reservedName);
  1147. });
  1148.  
  1149. tpul_promises.getProfilePage.catch( reject );
  1150.  
  1151. } else if (typeof tagpro != 'undefined' && tagpro.ready) {
  1152. tagpro.ready(function(){
  1153.  
  1154. if (tagpro.players) {
  1155. if (tagpro.players[tagpro.playerId]) {
  1156.  
  1157. if (tagpro.players[tagpro.playerId].auth) {
  1158. resolve (tagpro.players[tagpro.playerId].name);
  1159. } else fallback();
  1160.  
  1161. } else {
  1162.  
  1163. tagpro.socket.on('p',function(playerId) {
  1164. if (tagpro.players[tagpro.playerId]) {
  1165.  
  1166. if (tagpro.players[tagpro.playerId].auth) {
  1167. resolve (tagpro.players[tagpro.playerId].name);
  1168. } else fallback();
  1169.  
  1170. }
  1171. });
  1172.  
  1173. }
  1174. } else fallback();
  1175. });
  1176. } else fallback();
  1177.  
  1178. var done = false;
  1179. setTimeout(function(){
  1180. tpul_promises.getReservedName.then(()=>done=true);
  1181. });
  1182.  
  1183. setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );
  1184.  
  1185. });
  1186. }
  1187.  
  1188. return tpul_promises.getReservedName;
  1189. },
  1190.  
  1191. getDisplayedName: function(fallbackTimeout = 5e3) {
  1192.  
  1193. /*
  1194.  
  1195. Where to get the Displayed name from?
  1196.  
  1197. - in-game
  1198. - getProfile /profile/...
  1199.  
  1200. Logic:
  1201.  
  1202. 1. if getPage was called before: use that
  1203. 2. if in-game: get it that way
  1204. 3. call getPage() to get the name
  1205.  
  1206. */
  1207.  
  1208. if (!tpul_promises.getDisplayedName) {
  1209.  
  1210. tpul_promises.getDisplayedName = new Promise(function(resolve,reject) {
  1211.  
  1212. // The fallback: get the displayed name using getPage()
  1213. var fallback = function(){
  1214.  
  1215. done = true;
  1216.  
  1217. tpul.profile.getPage().then(function(profilePage) {
  1218.  
  1219. resolve(profilePage.displayedName);
  1220. });
  1221.  
  1222. tpul.profile.getPage().catch( reject );
  1223. };
  1224.  
  1225. if (tpul_promises.getProfilePage) {
  1226.  
  1227. tpul_promises.getProfilePage.then(function(profilePage){
  1228. resolve(tpul_promises.getProfilePage.displayedName);
  1229. });
  1230.  
  1231. tpul_promises.getProfilePage.catch( reject );
  1232.  
  1233. } else if (typeof tagpro != 'undefined' && tagpro.ready) {
  1234. tagpro.ready(function(){
  1235.  
  1236. if (tagpro.players) {
  1237. if (tagpro.players[tagpro.playerId]) {
  1238.  
  1239. resolve (tagpro.players[tagpro.playerId].name);
  1240.  
  1241. } else {
  1242.  
  1243. tagpro.socket.on('p',function(playerId) {
  1244. if (tagpro.players[tagpro.playerId]) {
  1245.  
  1246. resolve (tagpro.players[tagpro.playerId].name);
  1247.  
  1248. }
  1249. });
  1250.  
  1251. }
  1252. } else fallback();
  1253. });
  1254. } else fallback();
  1255.  
  1256. var done = false;
  1257. setTimeout(function(){
  1258. tpul_promises.getDisplayedName.then(()=>done=true);
  1259. });
  1260.  
  1261. setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );
  1262.  
  1263. });
  1264. }
  1265.  
  1266. return tpul_promises.getDisplayedName;
  1267.  
  1268. },
  1269.  
  1270. getSettings: function(fallbackTimeout = 5e3) {
  1271.  
  1272. /*
  1273.  
  1274. Where to get the settings from?
  1275.  
  1276. - in-game
  1277. - getPage /profile/...
  1278.  
  1279. Logic:
  1280.  
  1281. 1. if in-game: get it that way
  1282. 2. call getPage() to get the settings
  1283.  
  1284. */
  1285.  
  1286. var top_args = arguments;
  1287.  
  1288. if (!tpul_promises.getProfileSettings) {
  1289.  
  1290. tpul_promises.getProfileSettings = new Promise(function(resolve,reject) {
  1291.  
  1292. var fallback = function(){
  1293.  
  1294. done = true;
  1295.  
  1296. tpul.profile.getPage().then(function(profilePage){
  1297. resolve(profilePage.settings);
  1298. });
  1299.  
  1300. tpul.profile.getPage().catch( reject );
  1301. };
  1302.  
  1303. if (top_args[0] && top_args[0].__settings) {
  1304. resolve(top_args[0].__settings);
  1305. } else if (tpul_promises.getProfilePage) {
  1306.  
  1307. tpul_promises.getProfilePage.then(function(profilePage){
  1308. resolve(profilePage.settings);
  1309. });
  1310.  
  1311. tpul_promises.getProfilePage.catch( reject );
  1312.  
  1313. } else if (typeof tagpro != 'undefined' && tagpro.ready) {
  1314. tagpro.ready(function(){
  1315.  
  1316. if (tagpro.socket && tagpro.socket.on) {
  1317.  
  1318. tagpro.socket.on('settings', function(settings) {
  1319. resolve(Object.assign(settings.ui, {stats:settings.stats}));
  1320. });
  1321.  
  1322. } else fallback();
  1323. });
  1324. } else fallback();
  1325.  
  1326. var done = false;
  1327. setTimeout(function(){
  1328. tpul_promises.getProfileSettings.then(()=>done=true);
  1329. });
  1330.  
  1331. setTimeout( function() { if (!done) fallback(); }, fallbackTimeout );
  1332.  
  1333. });
  1334.  
  1335. }
  1336.  
  1337. return tpul_promises.getProfileSettings;
  1338.  
  1339. },
  1340.  
  1341. setSettings: function(newSettings, persistent=true, immediately=false) {
  1342.  
  1343. 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.");
  1344.  
  1345. return new Promise(function(resolve, reject){
  1346.  
  1347. // Step 1: set any local (cookie) settings
  1348. // These don't have to be send to the server, easy!
  1349.  
  1350. if (persistent) {
  1351.  
  1352. for (let setting in newSettings) {
  1353. if (['sound',
  1354. 'music',
  1355. 'volume',
  1356.  
  1357. 'textures',
  1358.  
  1359. 'disableBallSpin',
  1360. 'tileRespawnWarnings',
  1361. 'disableTutorialChat', // This cookie seems to be unused
  1362. // Setting it anyway \(^.^)/
  1363.  
  1364. 'disableParticles',
  1365. 'forceCanvasRenderer',
  1366. 'disableViewportScaling',
  1367. ].includes(setting)) {
  1368.  
  1369. var expires = new Date(Date.now() + 31536e8).toUTCString(); // A century from now (same as TagPro uses)
  1370. document.cookie = setting + '=' + newSettings[setting] + '; expires='+expires+'; path=/; domain=.koalabeast.com';
  1371. }
  1372. }
  1373.  
  1374. // Step 2: send any server-sided settings to the server
  1375.  
  1376. if (['reservedName',
  1377. 'displayedName',
  1378.  
  1379. 'allChat',
  1380. 'teamChat',
  1381. 'groupChat',
  1382. 'systemChat',
  1383. 'tutorialChat',
  1384.  
  1385. 'names',
  1386. 'degrees',
  1387. 'matchState',
  1388. 'performanceInfo',
  1389. 'spectatorInfo',
  1390.  
  1391. 'teamNames',
  1392. 'stats',
  1393. ].some( s => s in newSettings ) ){
  1394.  
  1395. // Call these to let them run in parallel
  1396. tpul.profile.getSettings();
  1397. tpul.profile.getReservedName();
  1398. tpul.profile.getDisplayedName();
  1399.  
  1400. tpul.profile.getSettings().then( function(settings){
  1401. tpul.profile.getReservedName().then( function(reservedName){
  1402. tpul.profile.getDisplayedName().then( function(displayedName){
  1403.  
  1404. console.log(param({reservedName: reservedName, // Your reservedName
  1405. displayedName: displayedName, // Your displayedName
  1406. //...settings, // The current settings
  1407. //...newSettings}));
  1408. }));
  1409. var req = GM_xmlhttpRequest({
  1410. data: param(Object.assign({},
  1411. settings, // The current settings
  1412. {reservedName: reservedName, // Your reservedName
  1413. displayedName: displayedName}, // Your displayedName
  1414. newSettings // Overwrite with the settings that you want to edit.
  1415. )),
  1416. method: "POST",
  1417. headers: {"Content-Type": "application/x-www-form-urlencoded"},
  1418. url: "http://"+document.location.hostname+"/profile/update",
  1419. onload: function(r){
  1420. // 'r' is the response that we get back from the TP server, lets do some error handling with it:
  1421.  
  1422. var arr;
  1423. try{ arr = JSON.parse(r.response); }
  1424. catch(e){ reject({error:"/profile/update responded invalid JSON", request:this}); }
  1425.  
  1426. if(arr.error) reject(arr);
  1427. else if(arr.success) {
  1428. resolve(arr);
  1429. } else reject({error:'unknown error',response: arr, request:this});
  1430. },
  1431. onerror: reject,
  1432. });
  1433.  
  1434. });
  1435. });
  1436. });
  1437.  
  1438. }
  1439.  
  1440. }
  1441.  
  1442. // Step 3: In case we are in-game, let the settings go into effect immediately.
  1443. // To update the reserved name, a refresh is required. TPUL won't do this!
  1444.  
  1445. if (typeof tagpro != 'undefined' && immediately) {
  1446. if (!tagpro.settings) tagpro.settings = {ui:{}};
  1447. if (!tagpro.settings.ui) tagpro.settings.ui = {};
  1448.  
  1449. for (let setting in newSettings) {
  1450. if (['allChat',
  1451. 'teamChat',
  1452. 'groupChat',
  1453. 'systemChat',
  1454. 'tutorialChat',
  1455. ].includes(setting)){
  1456.  
  1457. tagpro.settings.ui[setting] = newSettings[setting];
  1458. }
  1459. }
  1460.  
  1461. if (setting == 'tutorialChat') {
  1462. var tutorialButton = document.getElementById('tutorialButton');
  1463.  
  1464. if (tutorialButton) {
  1465. var action = tutorialButton.innerText === "Enable Tips";
  1466. if (newSettings[setting] == action) tutorialButton.click();
  1467. }
  1468. }
  1469.  
  1470.  
  1471. }
  1472.  
  1473. });
  1474. }
  1475.  
  1476. },
  1477.  
  1478. rollingChat: {
  1479.  
  1480. _init: function initRollingChat(enable = false){
  1481.  
  1482. if (typeof tagpro == undefined) return console.error( "The `tagpro` object does not exist. Is this a no-script match?" )
  1483.  
  1484. // In case you don't want to load the full TPUL library,
  1485. // You can add RollingChat to your own script by copying this function
  1486. // Usage:
  1487. // initRollingChat(true);
  1488.  
  1489. if (!tagpro.rollingChat) {
  1490.  
  1491. tagpro.rollingChat = {
  1492. enabled: false,
  1493. get handler() {
  1494. return function(event) {
  1495.  
  1496. // Return if not enabled
  1497. if (!tagpro.rollingChat.enabled) return;
  1498.  
  1499. // Whether you are releasing instead of pressing the key:
  1500. var releasing = event.type == 'keyup';
  1501.  
  1502. // Check if any modifier keys where held down during a keyDown
  1503. if (!releasing && (event.ctrlKey || event.shiftKey || event.altKey || event.metaKey)) return;
  1504.  
  1505. // The key that is pressed/released (undefined when it is any other key)
  1506. var arrow = ['left','up','right','down'][[37,38,39,40].indexOf(event.keyCode)];
  1507.  
  1508. // Only if the controls are disabled (usually while composing a message)
  1509. // AND the key is indeed an arrow (not undefined)
  1510. if (tagpro.disableControls && arrow) {
  1511.  
  1512. // Prevent the 'default' thing to happen, which is the cursor moving through the message you are typing
  1513. event.preventDefault();
  1514.  
  1515. // Return if already pressed/released
  1516. if (tagpro.players[tagpro.playerId].pressing[arrow] != releasing) return;
  1517.  
  1518. // Send the key press/release to the server!
  1519. tagpro.sendKeyPress(arrow, releasing);
  1520.  
  1521. // Not necesarry, but useful for other scripts to 'hook onto'
  1522. if (!releasing && tagpro.events.keyDown) tagpro.events.keyDown.forEach(f => f.keyDown(arrow));
  1523. if (releasing && tagpro.events.keyUp) tagpro.events.keyUp.forEach(f => f.keyUp(arrow));
  1524. if(tagpro.ping.avg)setTimeout(()=>(tagpro.players[tagpro.playerId][arrow]=!releasing),tagpro.ping.avg/2);
  1525. }
  1526. };
  1527. }
  1528. };
  1529.  
  1530. // intercept all key presses and releases:
  1531. document.addEventListener('keydown', tagpro.rollingChat.handler);
  1532. document.addEventListener('keyup', tagpro.rollingChat.handler);
  1533. }
  1534.  
  1535. if (enable) tagpro.rollingChat.enabled = true;
  1536. },
  1537.  
  1538. get enabled(){
  1539. tpul.rollingChat._init();
  1540. return tagpro.rollingChat.enabled;
  1541. },
  1542.  
  1543. set enabled(e){
  1544. tpul.rollingChat._init( Boolean(e) );
  1545. 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.');
  1546. },
  1547. },
  1548.  
  1549. notify: function(text, type="message", timeout=Math.max(4000, 50*text.length) ){
  1550.  
  1551. // Accepted types: message, success, error, warning
  1552. // ( white green red yellow )
  1553. // For more types, the only thing you need to add is some CSS
  1554.  
  1555. var notification = document.createElement('div');
  1556. notification.className = 'tpul-notification tpul-notification-' + type;
  1557. notification.innerText = text;
  1558. document.body.appendChild(notification);
  1559.  
  1560. // Hide after a while (timeout)
  1561. setTimeout(function(notification){
  1562. if(notification)notification.classList.add('vanish');
  1563. }, timeout, notification);
  1564.  
  1565. // Hide on click
  1566. notification.onclick = function(){ this.classList.add('vanish'); };
  1567.  
  1568. // Clear up the DOM once the notification is vanished
  1569. notification.addEventListener('transitionend',function(){ this.remove(); });
  1570.  
  1571. // Return the element, for scripters to "play" with
  1572. return notification;
  1573.  
  1574. },
  1575.  
  1576. groupcomm: {
  1577.  
  1578. emit: function ( script, command, ...args ) {
  1579. // Example: tpul.groupcomm.emit('gropro', 'desc', 'welcome to my awesome group')
  1580. if (typeof tagpro != undefined && tagpro.group && tagpro.group.socket && tagpro.group.socket.connected) {
  1581.  
  1582. var full_command = "/" + [...arguments].map(a=>(a||"").replace(/([\^\/:;])/g,"^$1")).join("/") + ";";
  1583.  
  1584. tagpro.group.socket.emit( "touch", full_command.substr( 0,12 ) );
  1585. for (var i = 12; i < full_command.length; i += 11) {
  1586. tagpro.group.socket.emit( "touch", ":" + full_command.substr( i,11 ) );
  1587. }
  1588.  
  1589. tagpro.group.socket.emit( "touch", tagpro.group.socket.playerLocation );
  1590. }
  1591. else throw "Not connected to a group";
  1592. },
  1593.  
  1594. oncommand: function oncommand( callback ) {
  1595. if (typeof tagpro != undefined && tagpro.group && tagpro.group.socket && tagpro.group.socket.connected) {
  1596. if (!tpul.groupcomm._active) tpul.groupcomm._init();
  1597. tpul.groupcomm._callbacks.push(callback);
  1598. }
  1599. else throw "Not connected to a group";
  1600. },
  1601.  
  1602. _callbacks: [],
  1603.  
  1604. _commands: {},
  1605.  
  1606. _active: false,
  1607.  
  1608. _init: function (){
  1609.  
  1610. if (tpul.groupcomm._active) return;
  1611.  
  1612. tpul.groupcomm._active = true;
  1613.  
  1614. tagpro.group.socket.on( "member", function(member) {
  1615.  
  1616. function handleCommand(command){
  1617. var args = [...command
  1618. .replace(/\^(.)/g, "$1^")
  1619. .match(/\/(.*);(?=(?:\^\^)*(?!\^))/,1)[1]
  1620. .split(/\/(?=(?:\^\^)*(?!\^))/)
  1621. .map(a=>a.replace(/(.)\^(?=(?:\^\^)*(?!\^))/g, "$1"))
  1622. ];
  1623.  
  1624. for (var c in tpul.groupcomm._callbacks) {
  1625. var callback = tpul.groupcomm._callbacks[c];
  1626. try { callback({
  1627. member: member,
  1628. script: args.shift() || null,
  1629. command: args.shift() || null,
  1630. args: args,
  1631. raw:command } ); }
  1632. catch(e) {
  1633. console.error("Unhandled GroupComm error. Mod makers, handle your errors!", e);
  1634. tpul.groupcomm._callbacks.splice(c,1);
  1635. }
  1636. }
  1637. }
  1638.  
  1639. var raw = member.location,
  1640. commands = tpul.groupcomm._commands;
  1641.  
  1642. if (typeof raw !== "string") return;
  1643.  
  1644. // A full one-line command: / ... ;
  1645. if ( raw.match(/^\/.*[^^];/) ) {
  1646. handleCommand( raw );
  1647. delete commands[member.id];
  1648. }
  1649.  
  1650. // The start of a multi-line command: / ...
  1651. else if ( raw.match(/^\//) ) {
  1652. commands[member.id] = raw;
  1653. }
  1654.  
  1655. // The end of a multi-line command: : ... ;
  1656. else if ( raw.match(/^:.*[^^];/) ) {
  1657. if (!commands[member.id]) throw "Did not receive start of command.";
  1658. var com = commands[member.id] + raw.slice(1);
  1659. handleCommand( com );
  1660. delete commands[member.id];
  1661. }
  1662.  
  1663. // A middle part of a multi-line command: : ...
  1664. else if ( raw.match(/^:/) ) {
  1665. if (!commands[member.id]) throw "Did not receive start of command.";
  1666. commands[member.id] += raw.slice(1);
  1667. }
  1668.  
  1669. // Not a GroupComm command:
  1670. else delete commands[member.id];
  1671. });
  1672. }
  1673. },
  1674.  
  1675. get playerLocation(){
  1676.  
  1677. // This function seems to be pointless, but I'll make
  1678. // sure it'll keep working even when the site architechture changes,
  1679. // so that you don't have to update your script too often :)
  1680. // (for example, when the SWJ got introduced)
  1681.  
  1682. if ( location.pathname.startsWith('/games/find') ) return 'find';
  1683. if ( location.pathname.match(/^\/groups\/[a-z]{8}$/) ) return 'group';
  1684. var path = location.pathname.match(/\w+/);
  1685. if (path) return path[0];
  1686. if ( location.port ) return 'game';
  1687. if ( location.pathname == '/' ) return 'home';
  1688.  
  1689. throw 'Player location unknown';
  1690. },
  1691.  
  1692. /*events: {
  1693. on: function(event, callback) {
  1694.  
  1695. if( !tpul.events._listeners[event] ) tpul.events._listeners[event] = []
  1696. tpul.events._listener.push(callback)
  1697.  
  1698. //if (event in deepEvents) enableDeepEvents();
  1699. },
  1700.  
  1701. on: function(event, callback) {
  1702.  
  1703. if (event == 'register') throw "You can't use 'register' as an event"
  1704.  
  1705. if (typeof tagpro != undefined) {
  1706. if ( !tagpro.events ) tagpro.events = {}
  1707. if ( !tagpro.events[event] ) tagpro.events[event] = []
  1708.  
  1709.  
  1710. if (!tagpro.events[event]) tagpro.events[event] = [];
  1711.  
  1712. var eventFunc = {};
  1713. eventFunc[event] = callback;
  1714. tagpro.events[event].push(eventFunc);
  1715.  
  1716. //if (event in deepEvents) enableDeepEvents();
  1717. },
  1718.  
  1719. emit: function(event, data) {
  1720.  
  1721. if ( tpul.events._listeners[event] ) for (let callback of tpul.events._listeners[
  1722. if (tagpro.events[event]) for (let listener of tagpro.events[event]) {
  1723. try { listener[event](data); }
  1724. catch (e) {
  1725. console.error("Unhandled tagpro.events.on('"+event+"') error. Mod makers, handle your errors!");
  1726. console.error(e);
  1727. console.error( listener[event]);
  1728. }
  1729. }
  1730. },
  1731.  
  1732. _listeners: {}
  1733. }*/
  1734. };
  1735.  
  1736.  
  1737.  
  1738.  
  1739.  
  1740.  
  1741. // =====DOM SECTION=====
  1742.  
  1743.  
  1744.  
  1745. var SettingsMenu = document.getElementById('tpul-settings-menu') || document.createElement('div');
  1746. SettingsMenu.id = 'tpul-settings-menu';
  1747.  
  1748. var SettingsFrame = document.getElementsByClassName('tpul-settings-frame')[0] || document.createElement('div');
  1749. SettingsFrame.className = 'tpul-settings-frame';
  1750. if(tpul.playerLocation == 'game') SettingsFrame.classList.add('in-game');
  1751. document.body.appendChild(SettingsFrame);
  1752.  
  1753.  
  1754. if (!SettingsMenu.parentElement) tpul.settings.parent = null;
  1755.  
  1756. // =====NOITCES MOD=====
  1757.  
  1758.  
  1759.  
  1760.  
  1761.  
  1762. // OPENING AND CLOSING
  1763.  
  1764. SettingsFrame.onclick = function(click) {
  1765.  
  1766. // Close all settings when clicking outside the panel
  1767. if (SettingsFrame == click.target) for (var settings of all_settings) settings.close();
  1768.  
  1769. };
  1770.  
  1771. SettingsFrame.addEventListener('wheel', function(wheel) {
  1772.  
  1773. // Close all settings when scrolling up far enough
  1774. setTimeout(function(){
  1775. if (SettingsFrame.firstElementChild &&
  1776. SettingsFrame.scrollTop + SettingsFrame.offsetHeight <= SettingsFrame.firstElementChild.offsetTop + 20)
  1777. for (var settings of all_settings) settings.close();
  1778. },200);
  1779.  
  1780. // Open when scrolling down (only in game) DOESN'T WORK PROPERLY
  1781. // if (tpul.playerLocation == 'game' && wheel.deltaY > 0 && last_opened && !last_opened.isOpen) last_opened.open();
  1782.  
  1783. if (wheel.deltaY > 0) {
  1784. // Hide the scrolldown arrow TODO
  1785. for (var arrow of document.getElementsByClassName('tpul-settings-scroll-down-arrow')) {
  1786. arrow.style.opacity = 0;
  1787. }
  1788. }
  1789. })
  1790.  
  1791.  
  1792.  
  1793.  
  1794. // Section tabs
  1795.  
  1796. SettingsFrame.addEventListener('click', function(click) {
  1797. var tablist = click.target.parentElement;
  1798. if (tablist.classList.contains('tab-list')) {
  1799.  
  1800. var scrollTop = SettingsFrame.scrollTop;
  1801. console.log(scrollTop);
  1802.  
  1803. for (let li of tablist.getElementsByTagName('li'))
  1804. li.classList.remove('active');
  1805. for (let pane of tablist.parentElement.getElementsByClassName('tab-pane'))
  1806. pane.classList.remove('active');
  1807. click.target.classList.add('active');
  1808. document.querySelector(click.target.dataset.target).classList.add('active');
  1809.  
  1810. SettingsFrame.scrollTop = scrollTop;
  1811. }
  1812. }, true);
  1813.  
  1814.  
  1815.  
  1816. // Get settings from socket:
  1817.  
  1818. if (typeof tagpro != undefined && tagpro.ready) {
  1819. tagpro.ready(function(){
  1820. if (tagpro.socket && tagpro.socket.on) {
  1821. tagpro.socket.on('settings', function(settings) {
  1822. // Don't try to tamper with this, or copy this in your own script.
  1823. // It will affect all scripts using TPUL.
  1824. tpul.profile.getSettings( {__settings:Object.assign(settings.ui, {stats: settings.stats})} );
  1825. });
  1826. }
  1827. });
  1828. }
  1829.  
  1830. // Some helper function(s)
  1831.  
  1832. function param(o){
  1833.  
  1834. return Object.keys(o).map(function(k) {
  1835. return encodeURIComponent(k) + '=' + encodeURIComponent(o[k.replace(' ','+')]);
  1836. }).join('&').replace(/%20/g, '+');
  1837. }
  1838.  
  1839.  
  1840.  
  1841.  
  1842. // =====NOITCES CIGOL=====
  1843.  
  1844.  
  1845.  
  1846.  
  1847.  
  1848. if (typeof tpul_promises == 'undefined') {
  1849. try{
  1850. window.tpul_promises = {};
  1851. unsafeWindow.tpul_promises = window.tpul_promises;
  1852. }catch(e){}
  1853. }
  1854.  
  1855. var tpul_common = {}
  1856.  
  1857. if (typeof tpul_common == 'undefined') {
  1858. if (window) window.tpul_common = tpul_common
  1859. if (unsafeWindow) unsaeWindow.tpul_common = tpul_common
  1860. }
  1861.  
  1862.  
  1863. // If running independently (not @required by another script)
  1864. // only good for modders or while debugging
  1865. if (typeof GM_info == undefined || GM_info.script.name == 'TagPro Userscript Library') {
  1866. if (typeof tagpro != undefined) tagpro.tpul = tpul;
  1867. if (window) window.tpul = tpul;
  1868. if (unsafeWindow) unsafeWindow.tpul = tpul;
  1869. }
  1870.  
  1871. return tpul;
  1872. })();