Pendoria+

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/vue@2.5.13/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);

}());