您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Improve Pendoria with visual enhancements and statistics
// ==UserScript== // @name Pendoria+ // @description Improve Pendoria with visual enhancements and statistics // @namespace http://pendoria.net/ // @version 0.6.2 // @author Michael Owens (Xikeon) // @match *://pendoria.net/game* // @match *://www.pendoria.net/game* // @grant none // @require https://unpkg.com/[email protected]/dist/vue.min.js // ==/UserScript== (function () { function __$styleInject(css, returnValue) { if (typeof document === 'undefined') { return returnValue; } css = css || ''; var head = document.head || document.getElementsByTagName('head')[0]; var style = document.createElement('style'); style.type = 'text/css'; head.appendChild(style); if (style.styleSheet){ style.styleSheet.cssText = css; } else { style.appendChild(document.createTextNode(css)); } return returnValue; } function __$strToBlobUri(str, mime, isBinary) {try {return window.URL.createObjectURL(new Blob([Uint8Array.from(str.split('').map(function(c) {return c.charCodeAt(0)}))], {type: mime}));} catch (e) {return "data:" + mime + (isBinary ? ";base64," : ",") + str;}} let debug = false; function log() { if (!debug) { return } console.log('[PendoriaPlus]', ...arguments); } function capitalize([first, ...rest]) { if (!first) { return '' } return first.toUpperCase() + rest.join('') } function guessType (setting) { let type = 'string'; if (setting.type) { return setting.type } if ('options' in setting) { return 'select' } if ('constraint' in setting) { if ('min' in setting.constraint || 'max' in setting.constraint) { return 'number' } } if ('default' in setting && typeof setting.default === 'boolean') { return 'checkbox' } return type } function secondsToString (sec_num) { let hours = Math.floor(sec_num / 3600); let minutes = Math.floor((sec_num - (hours * 3600)) / 60); let seconds = sec_num - (hours * 3600) - (minutes * 60); let str = ''; if (hours > 0) { if (hours < 10) hours = '0' + hours; str += hours + 'h'; } if (hours > 0 || minutes > 0) { if (minutes < 10) minutes = '0' + minutes; str += minutes + 'm'; } if (seconds < 10) seconds = '0' + seconds; str += seconds + 's'; return str } function ModuleSetting (setting) { const defaults = { type: guessType(setting), value: ('value' in setting ? setting.value : setting.default), toString () { return this.value || this.default } }; let obj = Object.assign(defaults, setting); let oldValue = JSON.parse(JSON.stringify(obj.value)); if ('onChange' in obj) { setInterval(() => { if (obj.value !== oldValue) { oldValue = JSON.parse(JSON.stringify(obj.value)); obj.onChange(obj.value); } }, 10); } return obj } /*export class ModuleSetting { constructor (options) { const defaults = { type: this.guessType(options) } this.options = Object.assign({}, defaults, options) } get value() { return this.options.value } set value(value) { if (this.options.type === 'number') { this.options.value = parseInt(value, 10) return } this.options.value = value } guessType (options) { let type = 'string' if (options.type) { return options.type } if ('constraint' in options) { if ('min' in options.constraint || 'max' in options.constraint) { return 'number' } } if ('default' in options && typeof options.default === 'boolean') { return 'checkbox' } return type } toString () { return this.options.value } }*/ /** * Hook onto jQuery.ajaxComplete and be able to attach events to ajax calls */ let AJAX_CALLBACKS = {}; var AjaxCallback = { init () { $(document).ajaxComplete((e, xhr, settings) => { let foundPath = Object.keys(AJAX_CALLBACKS).find(path => settings.url.match(path)); if (!foundPath) { return } AJAX_CALLBACKS[foundPath].forEach(cb => { cb(e, xhr, settings); }); }); }, on (path, fn) { if (!fn) { return } AJAX_CALLBACKS[path] = AJAX_CALLBACKS[path] || []; AJAX_CALLBACKS[path].push(fn); }, off (path, fn) { if (!fn) { return } let cbs = AJAX_CALLBACKS[path]; if (!cbs) { return } let i = cbs.findIndex(currentCallback => currentCallback === fn); if (i > -1) { AJAX_CALLBACKS[path].splice(i, 1); log('[AjaxCallback]', 'after splice', AJAX_CALLBACKS[path]); } } }; // Needs a rewrite let log$1 = (...args) => console.log(...args); let $$1 = window.jQuery; let inDepTarget = null; let dotFromObject = function (obj, dotNotation) { return dotNotation.split('.').reduce((o,i) => { if (!(i in o)) { return '' } return o[i] }, obj) }; let updateDotFromObject = function (obj, dotNotation, value) { var dots = dotNotation.split('.'); var parentDataObject = dots.reduce((o, i, ci) => { if (ci === dots.length - 1) { return o } return o[i] }, obj); var lastdot = dots[dots.length - 1]; parentDataObject[lastdot] = value; }; let $template = function (input, ...values) { // compile html with tmp holders for nested nodes let strings = input; let html = ''; let nodes = []; let varFilters = {}; let lastuid = -1; if (typeof strings === String) { strings = strings.split('\n'); } for (var i in strings) { html += strings[i]; let value = values[i]; if (value) { if (value instanceof Node || value instanceof jQuery) { html += `<div id="jQTpl-tmp-node-${i}"></div>`; nodes.push(i); } else { html += value; } } } // find variables html = html.replace(/{{(.*?)}}/g, function (m, key) { lastuid += 1; // save and strip filters let [cleanKey, ...filters] = key.split('|'); if (filters) { varFilters[cleanKey] = varFilters[cleanKey] || {}; varFilters[cleanKey][lastuid] = filters; } return `<span class="jQTpl-tmp-var-${cleanKey}" data-uid="${lastuid}"></span>` }); // return function that will start the app return function ($data, $root) { let $dataFilters = {}; if (typeof $data === 'object' && typeof $data._onChange !== 'function') { $dataFilters = $data.filters || {}; $data = $data.scope; } $data = $data || null; $root = $root || $$1('#app'); // add watcher for changes let $dataEls = {}; let changeListeners = {}; let showListeners = {}; let applyFilters = function (value, datakey, uid) { let filters = varFilters[datakey][uid]; if (Array.isArray(filters) && filters.length) { value = filters.reduce((acc, cur) => $dataFilters[cur](acc), value); } return value }; if ($data) { $data._onChange((key, newValue, oldValue) => { // Update textNodes if (key in $dataEls) { $dataEls[key].forEach(node => { node.el.nodeValue = applyFilters(newValue, key, node.uid); }); } // 2-way binding: from data to view if (key in changeListeners) { changeListeners[key].forEach(node => node.val(newValue)); } // show/hide elements if (key in showListeners) { showListeners[key].forEach(node => node[0].toggle(node[1](newValue))); } }); } let $doc = $$1(html); nodes.forEach((i) => { $doc.find(`#jQTpl-tmp-node-${i}`).replaceWith(values[i]); }); $doc.find(`[class^=jQTpl-tmp-var-]`).each((k, el) => { let datakey = el.className.split('-').pop(); let value = dotFromObject($data, datakey); let uid = el.getAttribute('data-uid'); value = applyFilters(value, datakey, uid); let t = document.createTextNode(value); el.replaceWith(t); $dataEls[datakey] = $dataEls[datakey] || []; $dataEls[datakey].push({ el: t, uid: uid }); }); let updatingRelatedRadios = false; $doc.tplModel = function (selector, model) { let cleanSelector = selector.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); // log('[jQTpl]', 'cleanSelector', cleanSelector) let tplModelRegex = new RegExp('tplModel[\\s]*\\([\\s]*([\'"`])' + cleanSelector + '\\1[\s]*,[\s]*(.+?)\\)', 'g'); let tplModelCall = tplModelRegex.exec(arguments.callee.caller.toString()); if (!tplModelCall || tplModelCall.length < 3) { return this; } var dots = tplModelCall[2].split('.'); dots.shift(); var dataDotNotation = dots.join('.'); log$1('[jQTpl]', 'get', dataDotNotation, 'in', $data); var value = dotFromObject($data, dataDotNotation); var $el = this.find(selector); // var $dataEl = $dataEls[dataDotNotation].el $el .val(value) .on('input change paste deselect', (e) => { if (e.target.type.includes('select') && e.type === 'input') { // Select boxes trigger input & event, ignore one of them return } if (updatingRelatedRadios) { $$1(e.target).prop('checked', false); return } let newValue = e.target.value; if (e.target.type === 'radio' && !updatingRelatedRadios) { // update other radios with same name let name = $el.attr('name'); updatingRelatedRadios = true; // $(`[type="radio"][jq-model="${dataDotNotation}"]`).not(e.target).change() updatingRelatedRadios = false; newValue = $$1(e.target).prop('checked', true).val(); } if (e.target.type === 'checkbox') { newValue = e.target.checked; } updateDotFromObject($data, dataDotNotation, newValue); }); // this doesn't work for dot notation of course if (['radio', 'checkbox'].includes($el[0].type)) { $el.prop('checked', value); } else { $el.val(value); } changeListeners[dataDotNotation] = changeListeners[dataDotNotation] || []; changeListeners[dataDotNotation].push($el); return this }; $doc.tplShow = function (selector, model, validator) { if (!validator) { validator = (value) => value; } if (typeof model === 'undefined') { model = selector; selector = null; } let tplShowCall; if (selector) { let cleanSelector = selector.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&'); let tplShowRegex = new RegExp('tplShow[\\s]*\\([\\s]*([\'"`])' + cleanSelector + '\\1[\\s]*,[\\s]*(.+)[\\s]*\\)', 'g'); tplShowCall = tplShowRegex.exec(arguments.callee.caller.toString()); } else { let tplShowRegex = new RegExp('tplShow[\\s]*\\([\\s]*(.+)[\\s]*\\)', 'g'); tplShowCall = tplShowRegex.exec(arguments.callee.caller.toString()); } if (!tplShowCall || tplShowCall.length < 3) { return this; } let lastParams = tplShowCall[2].split(','); let m = lastParams.shift(); let dots = m.split('.'); dots.shift(); let dataDotNotation = dots.join('.'); let $el = (selector ? this.find(selector) : this); showListeners[dataDotNotation] = showListeners[dataDotNotation] || []; showListeners[dataDotNotation].push([$el, validator, model]); $el.toggle(validator(model)); return this }; $root.append($doc); return $doc } }; let observable = function (obj) { let listeners = []; const handler = function (root) { root = root || ''; if (root) root += '.'; let deps = {}; return { set(target, key, value, receiver) { // extend proxify to appended nested object if(({}).toString.call(value) === "[object Object]") { value = deepApply(key, value); } let oldValue = target[key]; target[key] = value; listeners.forEach(cb => cb(`${root}${key}`, target[key], oldValue)); if (key in deps && deps[key]) { deps[key].forEach(changeFunc => { setTimeout(changeFunc); }); } return Reflect.set(target, key, value, receiver) }, get(target, key, receiver) { if (key === 'toJSON') { return function() { return target; } } if(!(key in target)) { target[key] = null; // new Proxy(null, handler()) } if (inDepTarget) { deps[key] = deps[key] || []; if (deps[key].indexOf(inDepTarget) == -1) { deps[key].push(inDepTarget); } } return Reflect.get(target, key, receiver) }, deleteProperty(target, key) { delete target[key]; }, has: function(target, prop) { if (prop === '_onChange') { return false } return prop in target } } }; let deepApply = function (property, data) { var proxy = new Proxy({}, handler(property)); var props = Object.keys(data); var size = props.length; for (var i = 0; i < size; i++) { property = props[i]; proxy[property] = data[property]; } return proxy }; Object.defineProperty(obj, '_onChange', { configurable: false, writable: false, enumerable: false, // hide it from for..in value: function (cb) { listeners.push(cb); //console.log('_onChange registered') } }); Object.keys(obj).forEach(k => { let v = obj[k]; if(({}).toString.call(v) === "[object Object]") { v = deepApply(k, v); } obj[k] = v; }); let p = new Proxy(obj || {}, handler()); Object.keys(obj).forEach(key => { if (typeof obj[key] !== 'function') { return } let f = obj[key]; //.bind(p) let value; let onDependencyUpdated = function () { let oldValue = value; value = f(p); listeners.forEach(cb => cb(key, value, oldValue)); }; Object.defineProperty(p, key, { get: function () { inDepTarget = onDependencyUpdated; value = f(p); inDepTarget = null; return value } }); }); return p }; /** * Color resources red/green based on if you hit the goal (Guild Buildings & Scraptown) */ var Global = { settings: { enabled: ModuleSetting({ label: 'Enabled', default: true, }), colorCodingEnabled: ModuleSetting({ label: 'Color-code resources remaining (scraptown, guild buildings)', default: true, onChange (value) { if (this.settings.enabled.value) { this.toggleColorCoding(value); } } }), }, init () { // do I need this? if (!PendoriaPlus.isInitialized) { console.error('Module VisualResourceStatus loaded before PendoriaPlus was initialized!'); return } log('[VisualResourceStatus]', 'init'); this.ajaxGuildBuildings = this.ajaxGuildBuildings.bind(this); this.ajaxScraptownDetails = this.ajaxScraptownDetails.bind(this); this.ranEnable = false; if (this.settings.enabled.value) { this.enable(); } }, enable () { if (this.ranEnable) { return } this.ranEnable = true; if (this.settings.colorCodingEnabled.value) { this.setEventHandlers(); } }, disable () { if (!this.ranEnable) { return } this.ranEnable = false; this.removeEventHandlers(); }, setEventHandlers () { AjaxCallback.on('/guild/buildings', this.ajaxGuildBuildings); AjaxCallback.on('/scraptown/details/*', this.ajaxScraptownDetails); }, removeEventHandlers () { AjaxCallback.off('/guild/buildings', this.ajaxGuildBuildings); AjaxCallback.off('/scraptown/details/*', this.ajaxScraptownDetails); }, toggleColorCoding (value) { if (typeof value == 'undefined') { value = !this.settings.colorCodingEnabled; } value = !!value; this[value ? 'setEventHandlers' : 'removeEventHandlers'](); }, coloredElement (from, to) { let goalReached = from >= to; let $statSpan = $('<span>'); return $statSpan .text(from.toLocaleString()) .css('color', (goalReached ? 'rgb(29, 166, 87)' : 'red')) }, ajaxGuildBuildings () { $('.guild-overview .guild-section:nth-child(2) table tr').each((k, el) => { let $el = $(el); let $columns = $(el).find('td'); // check if building is activated if ($columns.length < 2) { return } let $statsCol = $columns.eq(1); let statsText = $statsCol.text(); // check if row has stats (x / x,xxx) if (!statsText.includes('/')) { return } let [from, to] = statsText.split(' / ').map(str => +str.replace(/[,.]/g, '')); let $statSpan = this.coloredElement(from, to); $statsCol.text('').append( $statSpan, ' / ' + to.toLocaleString() ); }); }, // Phat arrow to keep scope ajaxScraptownDetails () { $('.display-item span:eq(1) div:not(.dotted)').each((k, el) => { let $el = $(el); let [from, _, to, type] = $el.text().split(' '); from = +from.replace(/[,.]/g, ''); to = +to.replace(/[,.]/g, ''); let $statSpan = this.coloredElement(from, to); $el.text('').append( $statSpan, ' / ' + to.toLocaleString() + ' ' + type ); }); } }; __$styleInject("\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n.pp_settings_module_header {\n border-bottom: 1px solid white;\n}\n",undefined); var settingsView = {render: function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{attrs:{"id":"pp_settings"}},[_c('ul',{staticClass:"nav nav-tabs"},[_c('li',{class:{active: _vm.tab == 'settings'}},[_c('a',{on:{"click":function($event){_vm.tab = 'settings';}}},[_vm._v("Settings")])]),_vm._v(" "),_c('li',{class:{active: _vm.tab == 'about'}},[_c('a',{on:{"click":function($event){_vm.tab = 'about';}}},[_vm._v("About Pendoria+")])])]),_vm._v(" "),_c('div',{staticClass:"tab-game-content"},[(_vm.tab == 'settings')?_c('div',[_vm._l((_vm.modulesWithSettings),function(module,name){return _c('div',[_c('h2',{staticClass:"pp_settings_module_header",on:{"click":function($event){_vm.toggleModuleSettings(name);}}},[('enabled' in module.settings)?_c('input',{directives:[{name:"model",rawName:"v-model",value:(module.settings.enabled.value),expression:"module.settings.enabled.value"}],attrs:{"type":"checkbox"},domProps:{"checked":Array.isArray(module.settings.enabled.value)?_vm._i(module.settings.enabled.value,null)>-1:(module.settings.enabled.value)},on:{"click":function($event){_vm.toggleModule(name, $event);},"change":function($event){var $$a=module.settings.enabled.value,$$el=$event.target,$$c=$$el.checked?(true):(false);if(Array.isArray($$a)){var $$v=null,$$i=_vm._i($$a,$$v);if($$el.checked){$$i<0&&(module.settings.enabled.value=$$a.concat([$$v]));}else{$$i>-1&&(module.settings.enabled.value=$$a.slice(0,$$i).concat($$a.slice($$i+1)));}}else{_vm.$set(module.settings.enabled, "value", $$c);}}}}):_vm._e(),_vm._v(" "+_vm._s(name)+" ")]),_vm._v(" "),_c('div',{directives:[{name:"show",rawName:"v-show",value:(_vm.modulesOpened.includes(name) || true),expression:"modulesOpened.includes(name) || true"}]},_vm._l((module.settings),function(value,setting){return (setting != 'enabled')?_c('div',{staticStyle:{"min-height":"31px"}},[_c('label',{attrs:{"for":setting}},[_vm._v(" "+_vm._s(value.label)+" ")]),_vm._v(" "),(value.type == 'checkbox')?_c('input',{directives:[{name:"model",rawName:"v-model",value:(value.value),expression:"value.value"}],attrs:{"id":setting,"type":"checkbox"},domProps:{"checked":Array.isArray(value.value)?_vm._i(value.value,null)>-1:(value.value)},on:{"change":function($event){var $$a=value.value,$$el=$event.target,$$c=$$el.checked?(true):(false);if(Array.isArray($$a)){var $$v=null,$$i=_vm._i($$a,$$v);if($$el.checked){$$i<0&&(value.value=$$a.concat([$$v]));}else{$$i>-1&&(value.value=$$a.slice(0,$$i).concat($$a.slice($$i+1)));}}else{_vm.$set(value, "value", $$c);}}}}):_vm._e(),_vm._v(" "),(value.type == 'number')?_c('div',{staticStyle:{"display":"inline-block","width":"40%"}},[_c('input',{directives:[{name:"model",rawName:"v-model.number",value:(value.value),expression:"value.value",modifiers:{"number":true}}],attrs:{"id":setting,"type":"range","min":value.constraint.min,"max":value.constraint.max},domProps:{"value":(value.value)},on:{"__r":function($event){_vm.$set(value, "value", _vm._n($event.target.value));},"blur":function($event){_vm.$forceUpdate();}}}),_vm._v(" "+_vm._s(value.value)+" ")]):_vm._e(),_vm._v(" "),(value.type == 'select')?_c('select',{directives:[{name:"model",rawName:"v-model",value:(value.value),expression:"value.value"}],attrs:{"id":setting},on:{"change":function($event){var $$selectedVal = Array.prototype.filter.call($event.target.options,function(o){return o.selected}).map(function(o){var val = "_value" in o ? o._value : o.value;return val}); _vm.$set(value, "value", $event.target.multiple ? $$selectedVal : $$selectedVal[0]);}}},_vm._l((value.options),function(option){return _c('option',{domProps:{"value":option}},[_vm._v(" "+_vm._s(_vm._f("capitalize")(option))+" ")])})):_vm._e(),_vm._v(" "),(setting === 'sound')?_c('button',{on:{"click":_vm.playSound}},[_vm._v("►")]):_vm._e()]):_vm._e()}))])}),_vm._v(" "),_c('button',{on:{"click":_vm.saveSettings}},[_vm._v("Save settings")])],2):_vm._e(),_vm._v(" "),(_vm.tab == 'about')?_c('div',[_vm._m(0),_vm._v(" "),_c('p',[_vm._v(" Pendoria+ is a combination of visual improvements and enhancements to the overal Pendoria experience. Created by Xikeon. ")])]):_vm._e()])])},staticRenderFns: [function(){var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('p',[_vm._v(" Thanks for using "),_c('strong',[_vm._v("Pendoria+")]),_vm._v("! ")])}], filters: {capitalize}, data () { return { modulesOpened: [] } }, computed: { modulesWithSettings () { return Object.keys(this.modules) .filter(k => 'settings' in this.modules[k] && Object.keys(this.modules[k].settings).length > 0) .reduce((res, k) => (res[k] = this.modules[k], res), {}) } }, methods: { toggleModule (name, value) { // Note: this couldn't be a deep watcher due to Vue internally giving a stack overflow if (value instanceof MouseEvent) { value = value.target.checked; } if (value) { setTimeout(() => { this.modules[name].enable(); }, 50); } else { this.$nextTick(() => { this.modules[name].disable(); }); } }, toggleModuleSettings (name) { return const idx = this.modulesOpened.indexOf(name); if (idx !== -1) { this.modulesOpened.splice(idx, 1); } else { this.modulesOpened.push(name); } }, saveSettings () { ModuleManager.saveSettings(); } } }; __$styleInject("#pp_settings label {\r\n width: 50%;\r\n max-width: 40%;\r\n /*height: 20px;*/\r\n}\r\n\r\n#pp_settings input[type=\"range\"] {\r\n display: inline-block;\r\n width: 75%;\r\n margin-right: 5px;\r\n}",undefined); const defaultScope = { test_version: '0.1', modules: {}, tab: 'settings', text: '', radio: '', checkbox: '', select: '', }; // TODO: load from localstorage? let scope$1 = defaultScope; var Settings = { methods: { playSound () { log('[Settings]', 'test sound'); StatsPanel.playSound(); } }, init () { log('[Settings]', 'init'); this.addModules(); this.addMenuItem(); }, addModules () { let modules = ModuleManager.get(); Object.keys(modules).forEach(name => { log('[Settings]', 'add module', name); scope$1.modules[name] = modules[name]; }); }, addMenuItem () { let $item = $('<li><a href="#" id="pendoriaplus-button"> <i class="fa fa-wrench"> </i>Pendoria+</a></li>'); $item.find('a').on('click', this.open.bind(this)); // $("#gameframe-menu ul li:first-child").before($item) $("#menu ul li:last-child").before($item); }, open () { if (this.vm) { this.vm.$destroy(); this.vm = null; setTimeout(this.open.bind(this), 1); // give Vue time to destroy return } this.$wrapper = $('<div id="pendoriaplus_settings"></div>'); $('#gameframe-battle').hide(); $('#gameframe-content').show().html(this.$wrapper); this.initView(); }, initView () { const ViewCtor = Vue.extend(settingsView); this.vm = new ViewCtor({ el: this.$wrapper[0], // template: settingsView, data: scope$1, methods: this.methods, // methods: methods }); }, }; __$styleInject("#pendoriaplus_stats {\r\n position: relative;\r\n background: rgba(0, 0, 0, .8);\r\n color: #fff;\r\n margin-bottom: 20px;\r\n}\r\n\r\n.pendoriaplus_stats_content {\r\n padding: 15px 15px 15px 15px;\r\n}\r\n\r\n.pendoriaplus_stats_content label {\r\n margin: 0;\r\n}",undefined); var appView = "<div>\r\n <div class=\"frame frame-vertical-left\"></div>\r\n <div class=\"frame frame-vertical-right\"></div>\r\n <div class=\"frame frame-horizontal-top\"></div>\r\n <div class=\"frame frame-horizontal-bottom\"></div>\r\n <div class=\"frame frame-top-left\"></div>\r\n <div class=\"frame frame-top-right\"></div>\r\n <div class=\"frame frame-bottom-right\"></div>\r\n <div class=\"frame frame-bottom-left\"></div>\r\n <div class=\"pendoriaplus_stats_content\">\r\n <div><a href=\"#\" class=\"pendoriaplus_reset_stats\">Reset</a></div>\r\n\r\n <div>\r\n <label>Actions:</label>\r\n {{stats.actions|toLocaleString}}\r\n </div>\r\n <div>\r\n <label>Exp gained:</label>\r\n {{stats.exp|toLocaleString}}\r\n </div>\r\n\r\n <div id=\"pendoriaplus_stats_battle\">\r\n <div class=\"pendoriaplus_ts_only\">\r\n <label>Win / Loss:</label>\r\n {{stats.wins|toLocaleString}} / {{stats.losses|toLocaleString}} ({{winPercentage}}%)\r\n </div>\r\n <div class=\"pendoriaplus_ts_only\">\r\n <label>Gold gained:</label>\r\n {{stats.gold|toLocaleString}}\r\n </div>\r\n <div class=\"pendoriaplus_ts_only\">\r\n <label>Gold p/h:</label>\r\n {{goldPerHour|toLocaleString}}\r\n </div>\r\n </div>\r\n\r\n <div id=\"pendoriaplus_stats_ts\">\r\n <div class=\"pendoriaplus_ts_only\">\r\n <label>Quint procs:</label>\r\n {{stats.quints|toLocaleString}} ({{quintsPercentage}}%)\r\n </div>\r\n <div class=\"pendoriaplus_ts_only\">\r\n <label>{{skill|ucfirst}} gained:</label>\r\n {{stats.resources|toLocaleString}}\r\n </div>\r\n <div class=\"pendoriaplus_ts_only\">\r\n <label>{{skill|ucfirst}}/h:</label>\r\n {{resourcesPerHour|toLocaleString}}\r\n </div>\r\n </div>\r\n </div>\r\n</div>"; let sounds = { plucky: 'https://notificationsounds.com/soundfiles/1728efbda81692282ba642aafd57be3a/file-sounds-1101-plucky.wav', openEnded: 'https://notificationsounds.com/soundfiles/8eefcfdf5990e441f0fb6f3fad709e21/file-sounds-1100-open-ended.wav', ping: 'https://notificationsounds.com/soundfiles/4e4b5fbbbb602b6d35bea8460aa8f8e5/file-sounds-1096-light.wav', }; function perHour(total, actions) { let hours = (actions * 6) / 3600; return Math.round(total / hours) } let scope$$1 = observable({ type: 'battle', skill: '', stats: { actions: 0, exp: 0, wins: 0, losses: 0, gold: 0, quints: 0, resources: 1, }, resetStatsDate: +new Date(), winPercentage (data) { const totalBattles = data.stats.wins + data.stats.losses; if (totalBattles === 0) { return 0 } return (100 / totalBattles * data.stats.wins).toFixed(2) }, quintsPercentage (data) { if (data.stats.actions === 0 || data.stats.quints === 0) { return 0 } return (100 / data.stats.actions * data.stats.quints).toFixed(2) }, resourcesPerHour (data) { // let passedHours = Math.abs(new Date(data.resetStatsDate) - new Date()) / 36e5 // return Math.round(data.stats.resources / passedHours); if (data.stats.actions === 0) { return 0 } return perHour(data.stats.resources, data.stats.actions) }, goldPerHour (data) { // let passedHours = Math.abs(new Date(data.resetStatsDate) - new Date()) / 36e5 // return Math.round(data.stats.gold / passedHours); if (data.stats.gold === 0) { return 0 } return perHour(data.stats.gold, data.stats.actions) }, }); let filters = { toLocaleString: (value) => value.toLocaleString(), ucfirst: capitalize }; var StatsPanel = { settings: { enabled: ModuleSetting({ label: 'Enabled', default: true, }), timeToLevel: ModuleSetting({ label: 'Show estimated time to next level', default: true, onChange (value) { if (!this.settings.enabled.value) { return } this.$nextLevel.toggle(value); } }), panelEnabled: ModuleSetting({ label: 'Show panel under mini profile', default: true, onChange (value) { if (!this.settings.enabled.value) { return } this[value ? 'initPanel' : 'removePanel'](); }, }), lowActionSoundEnabled: ModuleSetting({ label: 'Low action sound enabled', default: true, }), lowActions: ModuleSetting({ label: 'Low action sound at actions remaining', default: 50, constraint: { min: 1, max: 100, }, }), lowActionRepeat: ModuleSetting({ label: 'Repeat sound every 6 seconds', default: true, }), sound: ModuleSetting({ label: 'Low action sound', default: 'plucky', options: Object.keys(sounds) }), volume: ModuleSetting({ label: 'Low action sound volume', default: 50, constraint: { min: 0, max: 100, }, }), }, init () { log('[StatsPanel]', 'init'); log('[StatsPanel]', 'sounds', sounds); this.ranEnable = false; this.soundTimeout = null; this.$nextLevel = $('<div style="text-align: center; margin-top: 3px;"></div>').insertAfter('#exp'); this.onTradeskillData = this.onTradeskillData.bind(this); this.onBattleData = this.onBattleData.bind(this); if (this.settings.enabled.value) { this.enable(); } }, enable () { if (this.ranEnable) { return } this.ranEnable = true; this.bindSocketMessages(); this.initPanel(); this.setSound(this.settings.sound.value); this.setVolume(this.settings.volume.value); this.$nextLevel.toggle(this.settings.timeToLevel.value); }, disable () { if (!this.ranEnable) { return } this.ranEnable = false; this.unbindSocketMessages(); this.removePanel(); this.removeSound(); this.$nextLevel.toggle(false); }, bindSocketMessages () { socket.on('tradeskill data', this.onTradeskillData); socket.on('battle data', this.onBattleData); }, unbindSocketMessages () { socket.off('tradeskill data', this.onTradeskillData); socket.off('battle data', this.onBattleData); }, setSound (name) { this.audio = new Audio(sounds[name]); this.audio.setAttribute('name', name); }, setVolume (volume) { if (volume < 0 || volume > 100) { return } this.audio.volume = volume / 100; }, playSound() { if (!this.audio || !this.settings.lowActionSoundEnabled.value) { return } if (this.audio.getAttribute('name') != this.settings.sound.value) { this.setSound(this.settings.sound.value); } if (this.audio.volume !== this.settings.volume.value / 100) { this.setVolume(this.settings.volume.value); } this.audio.play(); }, removeSound () { this.audio = null; }, initPanel () { if (this.$wrapper || !this.settings.panelEnabled.value) { return } this.$wrapper = $('<div id="pendoriaplus_stats"></div>').insertAfter($('#profile')); let app = $template(appView); this.$app = app({scope: scope$$1, filters}, this.$wrapper); this.initPanelBindings(); }, removePanel () { if (!this.$wrapper) { return } this.$wrapper.remove(); this.$wrapper = null; this.$app = null; }, initPanelBindings () { this.$app.find('.pendoriaplus_reset_stats').on('click', e => { e.preventDefault(); Object.keys(scope$$1.stats).forEach(k => { scope$$1.stats[k] = 0; }); scope$$1.resetStatsDate = +new Date(); }); this.$app // .tplShow('> div', this.settings.panelEnabled.value) .tplShow('#pendoriaplus_stats_battle', scope$$1.type, type => type === 'battle') .tplShow('#pendoriaplus_stats_ts', scope$$1.type, type => type === 'tradeskill'); }, checkActionsRemaining (data) { if (this.soundTimeout) { clearTimeout(this.soundTimeout); this.soundTimeout = null; } // TODO: improve check so if it skips a number magically, it will still play? if (data.actionsRemaining === this.settings.lowActions.value || (this.settings.lowActionRepeat.value && data.actionsRemaining <= this.settings.lowActions.value)) { this.playSound(); } if (data.actionsRemaining <= 0) { this.soundTimeout = setTimeout(() => { this.checkActionsRemaining(data); }, 6000); } }, onTradeskillData (data) { if (scope$$1.type !== 'tradeskill') { this.resetStats(); } this.checkActionsRemaining(data); this.calculateTimeToLevel(data.expToLevel, data.exp, data.gainedExp); scope$$1.type = 'tradeskill'; scope$$1.skill = data.skill; scope$$1.stats.actions += 1; if (data.quintProc) { scope$$1.stats.quints += 1; } if (data.gainedAmount) { scope$$1.stats.resources += data.gainedAmount; scope$$1.stats.exp += data.gainedExp; } }, onBattleData (data) { if (scope$$1.type !== 'battle') { this.resetStats(); } this.checkActionsRemaining(data); this.calculateTimeToLevel(data.expToLevel, data.exp, data.gainedexp); scope$$1.type = 'battle'; scope$$1.stats.actions += 1; if (data.victory) { scope$$1.stats.wins++; } else { scope$$1.stats.losses++; } if (data.gainedgold) { scope$$1.stats.gold += data.gainedgold; scope$$1.stats.exp += data.gainedexp; } }, calculateTimeToLevel (max, totalGained, gained) { // TODO: maybe use saved stats, to be more accurate for battlers who can e.g. win 90% if (!this.settings.timeToLevel.value) { return } let timeToLevel = secondsToString(Math.round(((max - totalGained) / gained) * 6)); if (!gained) { timeToLevel = '?'; } setTimeout(() => { this.$nextLevel.text(`Next level: ${timeToLevel}`); }, 10); }, resetStats () { Object.keys(scope$$1.stats).forEach(stat => scope$$1.stats[stat] = 0); } }; __$styleInject("#chat_wrapper {\r\n position: absolute;\r\n bottom: 0;\r\n left: 50%;\r\n transform: translateX(-50%);\r\n padding: 0 20px;\r\n height: 30%;\r\n width: 100%;\r\n max-width: 1100px;\r\n pointer-events: none;\r\n}\r\n\r\n#chat_wrapper #chat {\r\n position: relative;\r\n margin: 0 0 0 auto;\r\n height: 100%;\r\n width: calc(80%);\r\n pointer-events: all;\r\n}\r\n\r\n/* Not so much an aside anymore, is it */\r\n#chat.with-tabs #chat-content {\r\n width: 100%;\r\n margin: 0;\r\n}\r\n\r\n#chat.with-tabs .wrapper {\r\n display: grid;\r\n grid-template-rows: min-content auto;\r\n}\r\n\r\n#chat.with-tabs aside {\r\n float: none;\r\n overflow: hidden;\r\n height: auto;\r\n width: 100%;\r\n padding: 10px 0px 0 0;\r\n}\r\n\r\n#chat.with-tabs aside ul {\r\n float: left;\r\n margin: 0;\r\n}\r\n\r\n#chat.with-tabs aside li {\r\n display: inline-block;\r\n margin-right: 10px;\r\n}\r\n",undefined); var Chat = { // ranEnable: false, settings: { enabled: ModuleSetting({ label: 'Enabled', default: true, }), size: ModuleSetting({ label: 'Chat size & position', default: 'content', options: ['default', 'content', 'side-by-side'], onChange (value) { this.setSize(value); } }), tabs: ModuleSetting({ label: 'Channels as tabs', default: true, onChange (value) { this.toggleTabs(value); }, }) }, init () { this.ranEnable = false; this.$wrapper = $('<div id="chat_wrapper"></div>'); this.$chat = $(document).find('#chat'); this.isDragging = false; this.onMouseMove = this.onMouseMove.bind(this); $(document).on('mouseup', () => { this.isDragging = false; }); $(document).on('mousemove', this.onMouseMove); this.$chat.find('#dragable').on('mousedown', (e) => { if (this.settings.size.value === 'content') { e.stopPropagation(); window.isDragging = false; this.isDragging = true; } }); if (this.settings.enabled) { this.enable(); } }, enable () { if (this.ranEnable) { return } this.ranEnable = true; this.setSize(); this.toggleTabs(this.settings.tabs.value); }, disable () { if (!this.ranEnable) { return } this.ranEnable = false; this.setSize('default'); this.toggleTabs(false); }, setSize (value) { if (typeof value === 'undefined') { value = this.settings.size.value; } if (value === 'content') { this.$chat.wrap(this.$wrapper); this.$chat .prepend('<div class="frame frame-vertical-left"></div>') .prepend('<div class="frame frame-vertical-right"></div>'); } else { this.$chat.unwrap(); this.$chat.find('.frame-vertical-left, .frame-vertical-right').remove(); } $('body')[value === 'side-by-side' ? 'addClass' : 'removeClass']('pp_chat_side_by_side'); }, onMouseMove (e) { if (!this.isDragging) { return } if (e && e.target.tagName !== 'input') { e.stopPropagation(); } var height = (1 - (e.clientY / $(window).height())) * 100; if(height > 85) { height = '85%'; } else if (height < 30) { height = '30%'; } else { height = height + '%'; } $('#chat_wrapper').css('height', height); }, toggleTabs (value) { if (typeof value === 'undefined') { value = !this.settings.tabs.value; } value = !!value; this.$chat[value ? 'addClass' : 'removeClass']('with-tabs'); }, }; __$styleInject("#pp_timeRemaining {\r\n color: #fff;\r\n margin-right: 10px;\r\n}",undefined); var Quests = { settings: { enabled: ModuleSetting({ label: 'Enabled', default: true, }), showTimeRemaning: ModuleSetting({ label: 'Show est. time remaining', default: true, }), }, init () { log('[StatsPanel]', 'init'); this.ranEnable = false; this.$timeRemaining = null; this.onSocketData = this.onSocketData.bind(this); if (this.settings.enabled.value) { this.enable(); } }, enable () { if (this.ranEnable) { return } this.ranEnable = true; this.bindSocketMessages(); if (this.settings.showTimeRemaning.value) { this.initTimeRemaning(); } }, disable () { if (!this.ranEnable) { return } this.ranEnable = false; this.unbindSocketMessages(); this.removeTimeRemaining(); }, initTimeRemaning () { this.$timeRemaining = $('<span id="pp_timeRemaining"></span>').insertAfter('#quest_prog'); }, removeTimeRemaining () { if (!this.$timeRemaining) { return } this.$timeRemaining.remove(); this.$timeRemaining = null; }, bindSocketMessages () { socket.on('tradeskill data', this.onSocketData); socket.on('battle data', this.onSocketData); }, unbindSocketMessages () { socket.off('tradeskill data', this.onSocketData); socket.off('battle data', this.onSocketData); }, onSocketData (data) { if (data.quest_status !== 2) { this.$timeRemaining.text(''); return } this.setRemainingTime(data.quest_count); }, setRemainingTime (actions) { const sec_num = actions * 6; const str = secondsToString(sec_num); log('[Quests]', 'setRemainingTime', actions, str); this.$timeRemaining.text(str ? `(${str})` : ''); }, }; const modules = { AjaxCallback, StatsPanel, Chat, Quests, Global, Settings, }; window.pp_modules = modules; var ModuleManager = { init () { log('[ModuleManager]', 'init'); }, saveSettings () { log('[ModuleManager]', 'saveSettings'); let settings = {}; Object.keys(modules).forEach(moduleName => { if ('settings' in modules[moduleName]) { settings[moduleName] = {}; let moduleSettings = modules[moduleName].settings; Object.keys(moduleSettings).forEach(settingName => { settings[moduleName][settingName] = moduleSettings[settingName].value; }); } }); localStorage.setItem('PendoriaPlus', JSON.stringify(settings)); }, loadSettings () { let settings = localStorage.getItem('PendoriaPlus'); if (settings) { log('[ModuleManager]', 'Loading settings'); try { settings = JSON.parse(settings); log('[ModuleManager]', settings); Object.keys(settings).forEach(moduleName => { const moduleSettings = settings[moduleName]; Object.keys(moduleSettings).forEach(settingName => { modules[moduleName].settings[settingName].value = moduleSettings[settingName]; }); }); } catch (e) { log('[ModuleManager]', 'Error loading settings:', e.toString()); } } }, get (name = '') { if (!name) { return modules } if (!(name in modules)) { return null } return modules[name] }, add (name, module) { if (!name) { log('[ModuleManager]', 'tried to add empty module'); return } if (!module || typeof module !== 'object') { log('[ModuleManager]', 'tried to add invalid module:', name); return } modules[name] = module; } }; __$styleInject("#gameframe-menu {\r\n width: 80%;\r\n}\r\n\r\n.pp_chat_side_by_side #content\r\n{\r\n right: auto;\r\n width: 60%;\r\n height: 90%;\r\n}\r\n\r\n.pp_chat_side_by_side #chat\r\n{\r\n left: auto;\r\n width: 42%;\r\n height: 94%;\r\n top: 60px;\r\n}\r\n\r\n@media only screen and (max-width: 980px) {\r\n #gameframe-menu li a {\r\n font-size: 12px;\r\n }\r\n}\r\n",undefined); var PendoriaPlus = { isInitialized: false, init () { log('Initializing'); this.isInitialized = true; this.initModules(); }, initModules () { ModuleManager.loadSettings(); Object.values(ModuleManager.get()) .forEach(module => { if ('settings' in module) { Object.keys(module.settings) .forEach(key => { let setting = module.settings[key]; if (setting.onChange) { setting.onChange = setting.onChange.bind(module); } }); } module.init(); }); }, getSocket () { return window.socket } }; (function ($) { if (typeof $ === 'undefined') { console.error('PendoriaPlus could not load, jQuery was not found. Is the website even working?'); return } $(function () { if (!window.hasPendoriaPlus) { window.hasPendoriaPlus = true; PendoriaPlus.init(); } }); })(jQuery); }());