// ==UserScript==
// @name hwm_build_manager
// @author Chie (http://www.heroeswm.ru/pl_info.php?id=645888)
// @icon https://s.gravatar.com/avatar/5fd0059ad34d082dfbd50cfdeb9aab6a
// @description Менеджер билдов для HWM
// @namespace https://github.com/betula/hwm_build_manager
// @homepageURL https://github.com/betula/hwm_build_manager
// @include http://*heroeswm.ru/*
// @include http://178.248.235.15/*
// @include http://*lordswm.com/*
// @version 1.0.2
// @grant none
// @downloadURL https://update.greasyfork.icu/scripts/30768/hwm_build_manager.user.js
// @updateURL https://update.greasyfork.icu/scripts/30768/hwm_build_manager.meta.js
// ==/UserScript==
// Build Manager 1.0.2
class ServiceContainer {
constructor() {
this.instances = {};
}
_service(ctor) {
let { name } = ctor;
if (!this.instances[name]) {
this.instances[name] = new ctor(this);
}
return this.instances[name];
}
get manager() { return this._service(ManagerService) }
get fraction() { return this._service(FractionService) }
get inventory() { return this._service(InventoryService) }
get attribute() { return this._service(AttributeService) }
get army() { return this._service(ArmyService) }
get skill() { return this._service(SkillService) }
get current() { return this._service(CurrentService) }
get change() { return this._service(ChangeService) }
get import() { return this._service(ImportService) }
}
class ManagerService {
constructor(services) {
this.services = services;
this._storage = new LocalStorageArrayDriver('BM_MANAGER');
this.items = [];
this._restore();
}
createNew() {
let nextNumber = 1;
for (let item of this.items) {
let match = item.name.match(/^Новый билд (\d+)$/);
if (match) {
nextNumber = Math.max(nextNumber, (parseInt(match[1]) || 0) + 1);
}
}
let item = {
id: uniqid(),
name: `Новый билд ${nextNumber}`,
fraction: this.services.fraction.default.id,
inventory: this.services.inventory.default.id,
attribute: this.services.attribute.default,
army: this.services.army.default,
skill: this.services.skill.default
}
this.items.push(item);
this._store();
return item;
}
remove(item) {
let { founded, index } = this._searchByItem(item);
if (!founded) return null;
let items = this.items;
items = items.slice(0, index).concat( items.slice(index + 1) );
this.items = items;
this._store();
if (index === items.length) {
return items[index - 1]
}
return items[index];
}
duplicate(item) {
let { founded } = this._searchByItem(item);
if (!founded) return null;
let duplicate = deepCopy(item);
duplicate.id = uniqid();
duplicate.name += ' копия';
this.items.push(duplicate);
this._store()
return duplicate;
}
update(updatedItem) {
let { founded, index } = this._searchById(updatedItem.id);
if (!founded) return null;
this.items[index] = updatedItem;
this._store();
this.services.current.mayBeOnlyNameUpdated(updatedItem);
return updatedItem;
}
searchEquals(item) {
let { founded, index } = this._searchById(item.id);
if (founded) {
if (deepEquals(item, this.items[index])) {
return {
founded,
index
}
}
}
return {
founded: false
}
}
serialize() {
return JSON.stringify(this.items);
}
unserialize(value) {
const INVALID = 'invalid';
const Checker = {
string(value) {
if (typeof value !== 'string' || value.length === 0) throw INVALID;
},
number(value) {
if (typeof value !== 'number') throw INVALID;
},
array(value) {
if (!Array.isArray(value)) throw INVALID;
},
object(value) {
if (!value || typeof value !== 'object') throw INVALID;
},
keys(value, keys) {
if (!keys.every({}.hasOwnProperty.bind(value))) throw INVALID;
for (let key of Object.keys(value)) {
Checker.enum(key, keys);
}
},
enum(value, values) {
if (Array.isArray(value)) {
for (let once of value) {
Checker.enum(once, values);
}
} else {
if (Array.isArray(values)) {
if (values.indexOf(value) === -1) throw INVALID;
} else {
if (!values.hasOwnProperty(value)) throw INVALID;
}
}
},
length(value, len) {
if (value.length !== len) throw INVALID;
}
};
let items = [];
try {
items = JSON.parse(value);
Checker.array(items);
for (let item of items) {
Checker.object(item);
Checker.keys(item, [ 'id', 'name', 'fraction', 'inventory', 'attribute', 'army', 'skill' ]);
let { id, name, fraction, inventory, attribute, army, skill } = item;
Checker.string(id);
Checker.string(name);
Checker.enum(fraction, this.services.fraction.map);
Checker.enum(inventory, this.services.inventory.map);
Checker.object(attribute);
Checker.keys(attribute, this.services.attribute.list);
Object.values(attribute).forEach(Checker.number);
Checker.array(army);
Checker.length(army, 7);
army.forEach(Checker.number);
Checker.array(skill);
Checker.enum(skill, this.services.skill.map);
}
} catch(e) {
return false;
}
this.items = items;
this._store();
return true;
}
_searchById(id) {
const items = this.items;
let founded = false;
let index;
for (index = 0; index < items.length; index++) {
if (items[index].id === id) {
founded = true;
break;
}
}
return founded ? { founded, index } : { founded: false };
}
_searchByItem(item) {
let index = this.items.indexOf(item);
return index != -1
? { founded: true, index }
: { founded: false };
}
_restore() {
this.items = this._storage.fetch();
}
_store() {
this._storage.put(this.items);
}
}
class CurrentService {
constructor(services) {
this.services = services;
this._storage = new LocalStorageDriver('BM_CURRENT');
this._restore();
}
get item() {
return this._item;
}
mayBeOnlyNameUpdated(updatedItem) {
let current = this._item;
if (!current) return;
if (current.id !== updatedItem.id) return;
let clone = deepCopy(current);
clone.name = updatedItem.name;
if (!deepEquals(clone, updatedItem)) return;
current.name = updatedItem.name;
this._store();
}
isExpired() {
if (!this._item) return false;
return !this.services.manager.searchEquals(this._item).founded;
}
change(item, force) {
item = deepCopy(item);
const change = () => {
if (!item) return item;
if (force || !this._item) {
return this.services.change.force(item);
}
return this.services.change.diff(this._item, item);
};
return this.changeQueue = (this.changeQueue || Promise.resolve())
.then(change, change)
.then((item) => {
this._update(item);
return item;
}, (e) => {
this._update();
return Promise.reject(e);
});
}
equals(item) {
return deepEquals(this._item, item);
}
_update(item = null) {
this._item = item;
this._store();
}
_restore() {
this._item = this._storage.fetch();
}
_store() {
this._storage.put(this._item);
}
}
class ChangeService {
constructor(services) {
this.services = services;
this.cache = {};
}
force(to) {
const promise = Promise.resolve()
.then(() => this._fraction(to.fraction))
.then(() => this._skill(to.skill))
.then(() => this._army(to.army))
.then(() => this._inventory(to.inventory))
.then(() => this._reset())
.then(() => this._attribute(to.attribute))
.then(() => to);
return PromiseMDecorator(promise);
}
diff(from, to) {
const fractionChanged = from.fraction !== to.fraction;
const skillChanged = fractionChanged || !deepEquals(from.skill, to.skill);
const armyChanged = fractionChanged || !deepEquals(from.army, to.army);
const inventoryChanged = from.inventory !== to.inventory;
const attributeChanged = !deepEquals(from.attribute, to.attribute);
let serial = Promise.resolve();
if (fractionChanged) {
serial = serial.then(() => this._fraction(to.fraction, from.fraction))
}
if (skillChanged) {
serial = serial.then(() => this._skill(to.skill))
}
if (armyChanged) {
serial = serial.then(() => this._army(to.army))
}
if (inventoryChanged) {
serial = serial.then(() => this._inventory(to.inventory))
}
if (attributeChanged) {
serial = serial
.then(() => this._reset())
.then(() => this._attribute(to.attribute))
}
serial = serial.then(() => to);
return PromiseMDecorator(serial);
}
_fraction(to, from) {
to = this.services.fraction.map[to];
let prepare = Promise.resolve();
if (from) {
from = this.services.fraction.map[from];
} else {
prepare = this.services.import.getFraction().then((id) => {
from = this.services.fraction.map[id];
})
}
const change = (data) => {
return httpPlainRequest('FORM', '/castle.php', data)
}
return prepare.then(() => {
if (from.fract !== to.fract) {
let promise = change({ fract: to.fract });
if (to.classid !== '0') {
return promise.then(() => {
return change({ classid: to.classid })
});
}
return promise
}
if (from.classid !== to.classid) {
return change({ classid: to.classid })
}
})
}
_skill(list) {
let data = {
rand: Math.random(),
setstats: 1,
}
for (let i = 0; i < list.length; i++) {
data[`param${i}`] = list[i];
}
return httpPlainRequest('FORM', '/skillwheel.php', data);
}
_army(list) {
let data = {
rand: Math.random(),
}
for (let i = 0; i < list.length; i++) {
data[`countv${i+1}`] = list[i];
}
return httpPlainRequest('FORM', '/army_apply.php', data);
}
_inventory(id) {
let struct = this.services.inventory.map[id];
let data = {
r: Date.now() + String(Math.random()).slice(2)
};
data[struct.type] = struct.value;
return httpPlainRequest('GET', '/inventory.php', data);
}
_reset() {
let resetLinkPromise = Promise.resolve(this.cache.resetLink);
if (!this.cache.resetLink) {
resetLinkPromise = httpPlainRequest('GET', '/home.php').then((html) => {
let m = html.match(/shop\.php\?b=reset_tube&reset=2&sign=[0-9a-f]+/);
if (!m) return null;
return this.cache.resetLink = '/' + m[0];
})
}
return resetLinkPromise.then((url) => (url ? httpPlainRequest('GET', url) : null));
}
_attribute(obj) {
const getTotal = () => {
return httpPlainRequest('GET', '/home.php').then((html) => {
let m = html.match(/href="home\.php\?increase_all=knowledge"(?:.|\n)*?(\d+)\<\/td/);
if (!m) return null;
return parseInt(m[1]) || null;
});
}
const increase = (name, count) => {
let serial = Promise.resolve();
for (let i = 0; i < count; i++) {
serial = serial.then(() => httpPlainRequest('GET', `/home.php?increase=${name}`));
}
return serial;
}
const increaseAll = (name) => {
return httpPlainRequest('GET', `/home.php?increase_all=${name}`);
}
const distribute = (total) => {
total = total || 0;
let list = [];
for (let name of Object.keys(obj)) {
list.push({ name, value: obj[name] })
}
list.sort((a, b) => a.value - b.value);
let serial = Promise.resolve();
let used = 0;
list.slice(0, -1).forEach(({ name, value }) => {
if (used >= total) return;
if (value === 0) return;
let v = Math.min(total - used, value);
used += value;
serial = serial.then(() => increase(name, v));
});
if (total > used) {
let { name, value } = list[ list.length - 1 ];
if (value > 0) {
if (value < total - used) {
serial = serial.then(() => increase(name, value));
} else {
serial = serial.then(() => increaseAll(name));
}
}
}
return serial;
}
return getTotal().then(distribute);
}
}
class ImportService {
constructor(services) {
this.services = services;
}
getInventoryNamesIfAvailable() {
if (location.pathname.match(/^\/inventory\.php/)) {
let list = [];
let nodes = document.querySelectorAll('a[href*="inventory.php?all_on"]');
for (let node of nodes) {
let [ _, type, value ] = node.getAttribute('href').match(/(all_on)=(\d)/);
let name = node.innerText;
list.push({
type,
value,
name
});
}
return list;
}
}
getArmy() {
return httpPlainRequest('GET', '/army.php').then((html) => {
let m = html.match(/\ {
let dict = {};
for (let { fract, classid } of this.services.fraction.list) {
if (!dict[fract]) {
dict[fract] = {};
}
dict[fract][classid] = true;
}
const extractFract = () => {
let m = html.match(/\