Greasy Fork is available in English.

hwm_armory_framework

Хелпер для других скриптов склада

Acest script nu ar trebui instalat direct. Aceasta este o bibliotecă pentru alte scripturi care este inclusă prin directiva meta a // @require https://update.greasyfork.org/scripts/457946/1239339/hwm_armory_framework.js

// ==UserScript==
// @name          hwm_armory_framework
// @namespace     https://github.com/bonArt0/hwm_scripts
// @version       1.5.1
// @description   Helper for other hwm_armory scripts
// @author        bonArt
// @license       GPL-3.0-only
// @icon          https://dcdn.heroeswm.ru/i/btns/job_fl_btn_warehouse.png
// @match         https://*.heroeswm.ru/sklad_info.php?*
// @match         https://178.248.235.15/sklad_info.php?*
// @match         https://www.lordswm.com/sklad_info.php?*
// @match         https://my.lordswm.com/sklad_info.php?*
// @supportURL    https://www.heroeswm.ru/sms-create.php?mailto_id=117282
// ==/UserScript==

/**
 * @enum
 */
const FrameworkClassNames = {
    ARMORY_BOX: 'afw_armory_box',
    OVERVIEW_BOX: 'afw_armory_overview_box',
    INFO_BOX: 'afw_armory_info_box',
    ACCOUNT_BOX: 'afw_armory_account_box',
    ONLINE_TOGGLE_BOX: 'afw_armory_online_toggle_box',
    MANAGEMENT_TOGGLE_BOX: 'afw_armory_management_toggle_box',
    SECTORS_BOX: 'afw_armory_sectors_box',
    MANAGEMENT_BOX: 'afw_armory_management_box',
    MANAGEMENT_HEADER_BOX: 'afw_armory_management_header_box',
    MANAGEMENT_HEADER_PUTS_BOX: 'afw_armory_management_header_puts_box',
    MANAGEMENT_HEADER_BALANCE_BOX: 'afw_armory_management_header_balance_box',
    MANAGEMENT_HEADER_BATTLES_BOX: 'afw_armory_management_header_battles_box',
    MANAGEMENT_HEADER_SMITHS_BOX: 'afw_armory_management_header_smiths_box',
    MANAGEMENT_HEADER_CAPACITY_BOX: 'afw_armory_management_header_capacity_box',
    MANAGEMENT_BODY_BOX: 'afw_armory_management_body_box',
    MANAGEMENT_BODY_PUTS_BOX: 'afw_armory_management_body_puts_box',
    MANAGEMENT_BODY_BALANCE_BOX: 'afw_armory_management_body_balance_box',
    MANAGEMENT_BODY_BATTLES_BOX: 'afw_armory_management_body_battles_box',
    MANAGEMENT_BODY_SMITHS_BOX: 'afw_armory_management_body_smiths_box',
    MANAGEMENT_BODY_CAPACITY_BOX: 'afw_armory_management_body_capacity_box',
    TAKES_BOX: 'afw_armory_takes_box',
    TAKES_REPAIRS_HEADER_BOX: 'afw_armory_takes_repairs_header_box',
    TAKES_REPAIRS_BODY_BOX: 'afw_armory_takes_repairs_body_box',
    TAKES_LEASES_HEADER_BOX: 'afw_armory_takes_leases_header_box',
    TAKES_LEASES_BODY_BOX: 'afw_armory_takes_leases_body_box',
    TABS_BOX: 'afw_armory_tabs_box',
    DESCRIPTION_BOX: 'afw_armory_description_box',
    DESCRIPTION_FORM_BOX: 'afw_armory_description_form',
    ARTS_BOX: 'afw_armory_arts_box',
    ARTS_LIST_BOX: 'afw_armory_arts_list_box',
    ARTS_ROW_BOX: 'afw_armory_arts_row_box',
    ARTS_HEADER_BOX: 'afw_armory_arts_header_box',
    ARTS_FOOTER_BOX: 'afw_armory_arts_footer_box',
};

/**
 * @enum
 */
const ArmoryTab = {
    TAB_DESCRIPTION: 0,
    TAB_WEAPON: 1,
    TAB_ARMOR: 2,
    TAB_JEWELRY: 3,
    TAB_BACKPACK: 4,
    TAB_SETS: 5,
    TAB_ON_LEASE: 6,
    TAB_UNAVAILABLE: 7,
};

/**
 * @type {ArmoryFramework} _ArmoryFrameworkInstance
 * @private
 */
let _ArmoryFrameworkInstance;

/* <editor-fold desc="errors"> */

class AlreadyInitializedError extends Error {
    constructor() {
        super();

        this.message = 'Framework already initialized';
    }
}

class UnsupportedFeatureError extends Error {
    constructor(feature) {
        super();

        this.message = `Framework doesn't support ${feature} yet`;
    }
}

class FrameworkError extends Error {
    /**
     * @type {object}
     * @public
     */
    context = {};

    constructor(message, context) {
        super(message);

        this.context = context;
    }
}

class LayoutError extends FrameworkError {
    constructor(context) {
        super(`Something happen with game layout`, context);
    }
}

class BoxMissedException extends Error {}

/* </editor-fold> */

class ArmoryFramework {
    /**
     * @type {boolean}
     * @private
     */
    _initialized = false;

    /**
     * @type {boolean}
     * @private
     */
    _isManagementMode;

    /**
     * @type {ArmoryTab}
     */
    activeTab = ArmoryTab.TAB_DESCRIPTION;

    /**
     * @type {ArmoryBox}
     * @public
     * @readonly
     */
    armory;

    /**
     * @return {ArmoryFramework|null}
     */
    static init() {
        if (!_ArmoryFrameworkInstance || !_ArmoryFrameworkInstance?._initialized) {
            console.info('Armory Framework initialization started');

            try {
                _ArmoryFrameworkInstance = new ArmoryFramework();
            } catch (e) {
                if (e instanceof UnsupportedFeatureError) {
                    console.warn(e.message);
                    return null;
                }

                if (e instanceof AlreadyInitializedError) {
                    console.info(e.message);
                    return _ArmoryFrameworkInstance;
                }

                console.error('Something happen while framework initializing', e.context);
                throw e;
            }

            console.info('Armory Framework initialized');
        }

        return _ArmoryFrameworkInstance;
    }

    /**
     * @private use ArmoryFramework.init()
     */
    constructor() {
        this._initFramework();
    }

    /**
     * @throws {Error} on any init failure
     * @private
     */
    _initFramework() {
        if (!this.isManagementMode()) {
            throw new UnsupportedFeatureError('non-management mode');
        }

        if (this._initialized) {
            throw new AlreadyInitializedError();
        }

        this.activeTab = this._findActiveTab();
        this.armory = new ArmoryBox(this._findInitialAnchor(), this.activeTab);
        this._initialized = true;
    }

    /**
     * @returns {boolean}
     */
    isManagementMode() {
        if (this._isManagementMode === undefined) {
            this._isManagementMode = document.body.innerHTML.search('sklad_rc_on=0') > -1;
        }

        return this._isManagementMode;
    }

    /**
     * @returns {HTMLImageElement}
     * @throws {LayoutError} on init failure
     * @private
     */
    _findInitialAnchor() {
        const initialAnchor = document.getElementsByClassName('rs').item(0);
        if (initialAnchor && initialAnchor.tagName === 'IMG') {
            return initialAnchor;
        }

        throw new LayoutError({element: this, anchor: initialAnchor});
    }

    /**
     * @returns {ArmoryTab}
     * @private
     */
    _findActiveTab() {
        const params = new URLSearchParams(window.location.search);

        if (!params.has('cat') || +params.get('cat') < 0) {
            return ArmoryTab.TAB_DESCRIPTION;
        }

        if (Number.isNaN(params.get('cat'))) {
            return ArmoryTab.TAB_WEAPON;
        }

        switch (+params.get('cat')) {
            case 0:
                return ArmoryTab.TAB_WEAPON;
            case 1:
                return ArmoryTab.TAB_ARMOR;
            case 2:
                return ArmoryTab.TAB_JEWELRY;
            case 3:
                return ArmoryTab.TAB_SETS;
            case 4:
                return ArmoryTab.TAB_ON_LEASE;
            case 5:
                return ArmoryTab.TAB_UNAVAILABLE;
            case 6:
            default:
                return ArmoryTab.TAB_BACKPACK;
        }
    }

    /* <editor-fold desc="common"> */

    /**
     * @param {string} name
     * @param {string} innerHTML
     * @returns {HTMLLabelElement}
     */
    buildCheckboxLabel(name, innerHTML) {
        const checkbox = document.createElement('input');
        checkbox.type = 'checkbox';
        checkbox.name = name;

        const label = document.createElement('label');
        label.append(checkbox);
        label.append(innerHTML);

        return label;
    }

    /* </editor-fold> */
}

/**
 * @abstract
 */
class Box {
    /**
     * @type {HTMLElement}
     * @public
     * @readonly
     */
    box;

    /**
     * @param {HTMLElement} anchor
     * @throws {LayoutError} on init failure
     */
    constructor(anchor) {
        this._initBox(anchor);
    }

    /**
     * @throws {LayoutError} on init failure
     * @protected
     */
    _initBox(anchor) {
        const box = this._findBox(anchor);

        if (box?.tagName === this._getBoxTag()) {
            box.classList.add(this._getBoxClassName());
            this.box = box;
            return;
        }

        throw new LayoutError({element: this, box: box, anchor: anchor});
    }

    /**
     * @return {HTMLElement}
     * @abstract
     * @protected
     */
    _findBox(anchor) {}

    /**
     * @todo replace name
     * @return {string}
     * @abstract
     * @protected
     */
    _getBoxTag() {}

    /**
     * @return {string}
     * @abstract
     * @protected
     */
    _getBoxClassName() {}
}

/**
 * @abstract
 */
class TableSectionBasedBox extends Box {
    _getBoxTag() {
        return 'TBODY';
    }
}

/**
 * @abstract
 */
class TableRowBasedBox extends Box {
    _getBoxTag() {
        return 'TR';
    }
}

/**
 * @abstract
 */
class TableCellBasedBox extends Box {
    _getBoxTag() {
        return 'TD';
    }
}

/**
 * @abstract
 */
class FormBasedBox extends Box {
    _getBoxTag() {
        return 'FORM';
    }
}

class ArmoryBox extends TableCellBasedBox {
    /**
     * @type {OverviewBox}
     * @public
     * @readonly
     */
    overview;

    /**
     * @type {ManagementBox}
     * @public
     * @readonly
     */
    management;

    /**
     * @type {TakesBox|null}
     * @public
     * @readonly
     */
    takes;

    /**
     * @type {TabsBox}
     * @public
     * @readonly
     */
    tabs;

    /**
     * @type {DescriptionContainer|null}
     * @public
     * @readonly
     */
    description;

    /**
     * @type {ArtsBox|null}
     * @public
     * @readonly
     */
    arts;

    /**
     * @param {HTMLElement} anchor
     * @param {ArmoryTab} activeTab
     * @throws {Error} on init failure
     */
    constructor(anchor, activeTab) {
        super(anchor);

        const box = this.box;

        this.overview = new OverviewBox(box);
        this.management = new ManagementBox(box);

        try {
            this.takes = new TakesBox(box);
        } catch (e) {
            if (!(e instanceof BoxMissedException)) {
                throw e;
            }
            this.takes = null;
        }

        this.tabs = new TabsBox(box);

        switch (activeTab) {
            case ArmoryTab.TAB_DESCRIPTION:
                this.description = new DescriptionContainer(box);
                this.arts = null;
                break;
            case ArmoryTab.TAB_ON_LEASE:
            case ArmoryTab.TAB_WEAPON:
            case ArmoryTab.TAB_ARMOR:
            case ArmoryTab.TAB_JEWELRY:
            case ArmoryTab.TAB_BACKPACK:
            case ArmoryTab.TAB_SETS:
            case ArmoryTab.TAB_UNAVAILABLE:
                this.description = null;
                this.arts = new ArtsBox(box, activeTab);
        }
    }

    /**
     * @param {HTMLTableCellElement} anchor
     * @return {HTMLTableElement}
     */
    _findBox(anchor) {
        return anchor
            ?.parentElement // td#0, armory account clear part
            ?.parentElement // tr#0
            ?.parentElement // tbody#0
            ?.parentElement // table#0
            ?.parentElement // td#0, armory account
            ?.parentElement // tr#0
            ?.parentElement // tbody#0
            ?.parentElement // table#0
            ?.parentElement // td#1
            ?.parentElement // tr#0
            ?.parentElement // tbody#0, armory overview
            ?.parentElement // table#0
            ?.parentElement; // td#0, armory box
    }

    _getBoxClassName() {
        return FrameworkClassNames.ARMORY_BOX;
    }
}

/* <editor-fold desc="armory overview"> */

class OverviewBox extends TableSectionBasedBox {
    /**
     * @type {OverviewInfoBox}
     * @public
     * @readonly
     */
    info;

    /**
     * @type {OverviewAccountBox}
     * @public
     * @readonly
     */
    account;

    /**
     * @type {OverviewOnlineToggleBox}
     * @public
     * @readonly
     */
    onlineToggle;

    /**
     * @type {OverviewManagementToggleBox}
     * @public
     * @readonly
     */
    managementToggle;

    /**
     * @type {OverviewSectorsBox}
     * @public
     * @readonly
     */
    sectors;

    constructor(anchor) {
        super(anchor);

        const box = this.box;

        this.info = new OverviewInfoBox(box);
        this.account = new OverviewAccountBox(box);
        this.onlineToggle = new OverviewOnlineToggleBox(box);
        this.managementToggle = new OverviewManagementToggleBox(box);
        this.sectors = new OverviewSectorsBox(box);
    }

    /**
     * @param {HTMLTableCellElement} anchor
     * @return {HTMLTableElement}
     */
    _findBox(anchor) {
        return anchor
            ?.children.item(0) // table
            ?.children.item(0); // tbody
    }

    _getBoxClassName() {
        return FrameworkClassNames.OVERVIEW_BOX;
    }
}

class OverviewInfoBox extends TableCellBasedBox {
    /**
     * @returns {number}
     */
    getCurrentCapacity() {
        return +this.box.innerHTML.match(/<b>(\d+)<\/b> из \d+/)?.at(1);
    }

    /**
     * @returns {number}
     */
    getTotalCapacity() {
        return +this.box.innerHTML.match(/<b>\d+<\/b> из (\d+)/).at(1);
    }

    /**
     * @param {HTMLTableSectionElement} anchor
     * @return {HTMLTableCellElement}
     */
    _findBox(anchor) {
        return anchor
            ?.children.item(0) // tr
            ?.children.item(0); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.INFO_BOX;
    }
}

class OverviewAccountBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableSectionElement} anchor
     * @return {HTMLTableElement}
     */
    _findBox(anchor) {
        return anchor
            ?.children.item(0) // tr
            ?.children.item(1); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.ACCOUNT_BOX;
    }
}

class OverviewOnlineToggleBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableSectionElement} anchor
     * @return {HTMLTableCellElement}
     */
    _findBox(anchor) {
        return anchor
            ?.children.item(0) // tr
            ?.children.item(2); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.ONLINE_TOGGLE_BOX;
    }
}

class OverviewManagementToggleBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableSectionElement} anchor
     * @return {HTMLTableCellElement}
     */
    _findBox(anchor) {
        return anchor
            ?.children.item(0) // tr
            ?.children.item(3); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_TOGGLE_BOX;
    }
}

class OverviewSectorsBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableSectionElement} anchor
     * @return {HTMLTableCellElement}
     */
    _findBox(anchor) {
        return anchor
            ?.children.item(1) // tr
            ?.children.item(0); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.SECTORS_BOX;
    }
}

/* </editor-fold> */

/* <editor-fold desc="armory management"> */

class ManagementBox extends TableSectionBasedBox {
    /**
     * @type {ManagementHeaderBox}
     * @public
     * @readonly
     */
    header;

    /**
     * @type {ManagementBodyBox}
     * @public
     * @readonly
     */
    body;

    constructor(anchor) {
        super(anchor);

        const box = this.box;

        this.header = new ManagementHeaderBox(box);
        this.body = new ManagementBodyBox(box);
    }

    /**
     * @param {HTMLTableCellElement} anchor
     * @return {HTMLTableElement}
     */
    _findBox(anchor) {
        return anchor
            .children.item(1) // table
            .children.item(0); // tbody
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_BOX;
    }
}

class ManagementHeaderBox extends TableRowBasedBox {
    /**
     * @type {ManagementHeaderPutsBox}
     * @public
     * @readonly
     */
    puts;

    /**
     * @type {ManagementHeaderBalanceBox}
     * @public
     * @readonly
     */
    balance;

    /**
     * @type {ManagementHeaderBattlesBox}
     * @public
     * @readonly
     */
    battles;

    /**
     * @type {ManagementHeaderSmithsBox}
     * @public
     * @readonly
     */
    smiths;

    /**
     * @type {ManagementHeaderCapacityBox}
     * @public
     * @readonly
     */
    capacity;

    constructor(anchor) {
        super(anchor);

        this.puts = new ManagementHeaderPutsBox(this.box);
        this.balance = new ManagementHeaderBalanceBox(this.box);
        this.battles = new ManagementHeaderBattlesBox(this.box);
        this.smiths = new ManagementHeaderSmithsBox(this.box);
        this.capacity = new ManagementHeaderCapacityBox(this.box)
    }

    /**
     * @param {HTMLTableSectionElement} anchor
     * @return {HTMLTableRowElement}
     */
    _findBox(anchor) {
        return anchor
            ?.children.item(0); // tr
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_HEADER_BOX;
    }
}

class ManagementHeaderPutsBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(0); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_HEADER_PUTS_BOX;
    }
}

class ManagementHeaderBalanceBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(1); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_HEADER_BALANCE_BOX;
    }
}

class ManagementHeaderBattlesBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(2); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_HEADER_BATTLES_BOX;
    }
}

class ManagementHeaderSmithsBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(3); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_HEADER_SMITHS_BOX;
    }
}

class ManagementHeaderCapacityBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(4); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_HEADER_CAPACITY_BOX;
    }
}

class ManagementBodyBox extends TableRowBasedBox {
    /**
     * @type {ManagementBodyPutsBox}
     * @public
     * @readonly
     */
    puts;

    /**
     * @type {ManagementBodyBalanceBox}
     * @public
     * @readonly
     */
    balance;

    /**
     * @type {ManagementBodyBattlesBox}
     * @public
     * @readonly
     */
    battles;

    /**
     * @type {ManagementBodySmithsBox}
     * @public
     * @readonly
     */
    smiths;

    /**
     * @type {ManagementBodyCapacityBox}
     * @public
     * @readonly
     */
    capacity;

    constructor(anchor) {
        super(anchor);

        this.puts = new ManagementBodyPutsBox(this.box);
        this.balance = new ManagementBodyBalanceBox(this.box);
        this.battles = new ManagementBodyBattlesBox(this.box);
        this.smiths = new ManagementBodySmithsBox(this.box);
        this.capacity = new ManagementBodyCapacityBox(this.box);
    }

    /**
     * @param {HTMLTableSectionElement} anchor
     * @return {HTMLTableRowElement}
     */
    _findBox(anchor) {
        return anchor
            .children.item(1); // tr
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_BODY_BOX;
    }
}

class ManagementBodyPutsBox extends FormBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(0) // td
            .children.item(0); // form
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_BODY_PUTS_BOX;
    }

    isDisabled() {
        return this.box.innerHTML.search('нет доступа') > -1;
    }

    hideForm() {
        this.box.style.display = 'none';
    }

    /**
     * @return {(number|string)[][]}
     */
    getArtsList() {
        const artsPutOptions= this.box.elements[2].options;
        const arts = Array.from(artsPutOptions).map((option) => [+option.value, option.innerHTML]);
        arts.shift(); // remove first "0" element
        return arts;
    }

    /**
     * @return {number}
     */
    getArmoryId() {
        const id = this.box.children.item(0)?.value;

        if (id === undefined) {
            throw new FrameworkError('Armory id not found', this);
        }

        return +id;
    }

    /**
     * @returns {string}
     */
    getArtsPutSign() {
        const sign = this.box.children.item(1)?.value;

        if (sign === undefined) {
            throw new FrameworkError('Armory puts sign not found', this);
        }

        return sign + '';
    }
}

class ManagementBodyBalanceBox extends FormBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(1) // td
            .children.item(0); // form
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_BODY_BALANCE_BOX;
    }
}

class ManagementBodyBattlesBox extends FormBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(2) // td
            .children.item(0); // form
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_BODY_BATTLES_BOX;
    }
}

class ManagementBodySmithsBox extends FormBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(3) // td
            .children.item(0); // form
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_BODY_SMITHS_BOX;
    }
}

class ManagementBodyCapacityBox extends FormBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableCellElement}
     * @private
     */
    _findBox(anchor) {
        return anchor
            .children.item(4) // td
            .children.item(0); // form
    }

    _getBoxClassName() {
        return FrameworkClassNames.MANAGEMENT_BODY_CAPACITY_BOX;
    }
}

/* </editor-fold> */

/* <editor-fold desc="armory takes"> */

/**
 * @todo group repairs and leases
 */
class TakesBox extends TableSectionBasedBox {
    /**
     * @type {TakesRepairsContainer|null}
     * @public
     * @readonly
     */
    repairs;

    /**
     * @type {TakesLeasesContainer|null}
     * @public
     * @readonly
     */
    leases;

    /**
     * @param {HTMLElement} anchor
     * @throws {Error} on init failure
     * @throws {BoxMissedException} on empty box
     */
    constructor(anchor) {
        // BoxMissedException
        super(anchor);

        const box = this.box;
        if (box.children.length === 4) {
            // box has both repairs and leases row pairs
            this.repairs = new TakesRepairsContainer(box);
            this.leases = new TakesLeasesContainer(box, false);
        } else if (box.innerHTML.search('action=repair') > -1) {
            // "Repair" button exists, so box has only repairs row pair
            this.repairs = new TakesRepairsContainer(box);
            this.leases = null;
        } else {
            // box has only leases row pair
            this.repairs = null;
            this.leases = new TakesLeasesContainer(box, true);
        }
    }

    /**
     * @param {HTMLTableCellElement} anchor
     * @return {HTMLTableElement}
     * @throws {BoxMissedException}
     */
    _findBox(anchor) {
        const box = anchor
            .children.item(2); // table

        if (box.children.length > 0) {
            return box.children.item(0); // tbody
        }

        // no arts in lease or smith
        throw new BoxMissedException;
    }

    _getBoxClassName() {
        return FrameworkClassNames.TAKES_BOX;
    }
}

class TakesRepairsContainer {
    /**
     * @type {TakesRepairsHeaderBox}
     * @public
     * @readonly
     */
    header;

    /**
     * @type {TakesRepairsBodyBox}
     * @public
     * @readonly
     */
    body;

    /**
     * @param {HTMLElement} anchor
     * @throws {Error} on init failure
     */
    constructor(anchor) {
        this.header = new TakesRepairsHeaderBox(anchor, 0);
        this.body = new TakesRepairsBodyBox(anchor, 1);
    }
}

class TakesLeasesContainer {
    /**
     * @type {TakesLeasesHeaderBox}
     * @public
     * @readonly
     */
    header;

    /**
     * @type {TakesLeasesBodyBox}
     * @public
     * @readonly
     */
    body;

    /**
     * @param {HTMLElement} anchor
     * @param {boolean} only
     * @throws {Error} on init failure
     */
    constructor(anchor, only) {
        this.header = new TakesLeasesHeaderBox(anchor, only ? 0 : 2);
        this.body = new TakesLeasesBodyBox(anchor, only ? 1 : 3);
    }
}

/**
 * @abstract
 */
class TakesRowBox extends TableCellBasedBox {
    /**
     * BEWARE OF MAGIC!
     * For variadic TR id usage
     * @todo refactor this
     * @type {number}
     * @private
     */
    static _currentRowId;

    /**
     * @param {HTMLElement} anchor
     * @param {number} trId
     */
    constructor(anchor, trId) {
        TakesRowBox._currentRowId = trId;
        super(anchor);
    }

    /**
     * @param {HTMLTableSectionElement} anchor
     * @return {HTMLTableRowElement}
     */
    _findBox(anchor) {
        return anchor
            .children.item(TakesRowBox._currentRowId) // tr
            .children.item(0); // td
    }
}

class TakesRepairsHeaderBox extends TakesRowBox {
    _getBoxClassName() {
        return FrameworkClassNames.TAKES_REPAIRS_HEADER_BOX;
    }
}

class TakesRepairsBodyBox extends TakesRowBox {
    _getBoxClassName() {
        return FrameworkClassNames.TAKES_REPAIRS_BODY_BOX;
    }
}

class TakesLeasesHeaderBox extends TakesRowBox {
    _getBoxClassName() {
        return FrameworkClassNames.TAKES_LEASES_HEADER_BOX;
    }
}

class TakesLeasesBodyBox extends TakesRowBox {
    _getBoxClassName() {
        return FrameworkClassNames.TAKES_LEASES_BODY_BOX;
    }
}

/* </editor-fold> */

/* <editor-fold desc="armory tabs"> */

class TabsBox extends TableRowBasedBox {
    /**
     * @param {HTMLTableCellElement} anchor
     * @return {HTMLTableElement}
     */
    _findBox(anchor) {
        return anchor
            .children.item(3) // table
            .children.item(0) // tbody
            .children.item(0); // tr
    }

    _getBoxClassName() {
        return FrameworkClassNames.TABS_BOX;
    }
}

/* </editor-fold> */

/* <editor-fold desc="armory description"> */

class DescriptionContainer {
    /**
     * @type {DescriptionBox}
     * @public
     * @readonly
     */
    text;

    /**
     * @type {DescriptionFormBox}
     * @public
     * @readonly
     */
    form;

    /**
     * @param {HTMLElement} anchor
     * @throws {Error} on init failure
     */
    constructor(anchor) {
        this.text = new DescriptionBox(anchor);
        this.form = new DescriptionFormBox(anchor);
    }
}

class DescriptionBox extends TableCellBasedBox {
    /**
     * @param {HTMLTableCellElement} anchor
     * @return {HTMLTableElement}
     */
    _findBox(anchor) {
        return anchor
            .children.item(4) // table
            .children.item(0) // tbody
            .children.item(0) // tr
            .children.item(0); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.DESCRIPTION_BOX;
    }
}

class DescriptionFormBox extends FormBasedBox {
    /**
     * @param {HTMLTableCellElement} anchor
     * @return {HTMLFormElement}
     */
    _findBox(anchor) {
        return anchor
            .children.item(5); // form
    }

    _getBoxClassName() {
        return FrameworkClassNames.DESCRIPTION_FORM_BOX;
    }
}

/* </editor-fold> */

/* <editor-fold desc="armory arts"> */

class ArtsBox extends TableCellBasedBox {
    /**
     * @type {ArtsListBox|null}
     * @public
     * @readonly
     */
    list;

    /**
     * @param {HTMLTableCellElement} anchor
     * @param {ArmoryTab} activeTab
     */
    constructor(anchor, activeTab) {
        super(anchor);

        try {
            this.list = new ArtsListBox(this.box, activeTab);
        } catch (e) {
            if (!(e instanceof BoxMissedException)) {
                throw e;
            }
            this.list = null;
        }
    }

    /**
     * @param {HTMLTableCellElement} anchor
     * @return {HTMLTableElement}
     */
    _findBox(anchor) {
        return anchor
            .children.item(4) // table
            .children.item(0) // tbody
            .children.item(0) // tr
            .children.item(0); // td
    }

    _getBoxClassName() {
        return FrameworkClassNames.ARTS_BOX;
    }
}

class ArtsListBox extends TableSectionBasedBox {
    /**
     * @type {ArtsHeaderBox}
     * @public
     * @readonly
     */
    header;

    /**
     * @type {ArtsFooterBox|null}
     * @public
     * @readonly
     */
    footer;

    /**
     * @type {ArtsRowBox[]}
     * @public
     * @readonly
     */
    rows;

    /**
     * @param {HTMLTableCellElement} anchor
     * @param {ArmoryTab} activeTab
     * @throws {BoxMissedException}
     */
    constructor(anchor, activeTab) {
        // BoxMissedException
        super(anchor);

        const rows = Array.from(this.box.children)
            .filter(ArtsListBox._filterTagsCallback.bind({tag: 'TR'}));

        this.header = new ArtsHeaderBox(rows.shift());
        this.footer = activeTab === ArmoryTab.TAB_UNAVAILABLE ? new ArtsFooterBox(rows.pop()) : null;
        this.rows = rows.map((row) => new ArtsRowBox(row));
    }

    /**
     * @param {HTMLTableCellElement} anchor
     * @return {HTMLTableElement}
     * @throws {BoxMissedException}
     */
    _findBox(anchor) {
        const listContents = Array.from(anchor.children)
            .filter(ArtsListBox._filterTagsCallback.bind({tag: 'TABLE'}))
        if (listContents.length > 0) {
            return listContents
                .shift() // table
                .children.item(0); // tbody
        }

        // no arts in list
        throw new BoxMissedException;
    }

    _getBoxClassName() {
        return FrameworkClassNames.ARTS_LIST_BOX;
    }

    static _filterTagsCallback(row) {
        return row.tagName === this.tag; // there is a <script> here besides correct tags
    }
}

class ArtsRowBox extends TableRowBasedBox {
    /**
     * @param {HTMLTableRowElement} anchor
     * @return {HTMLTableRowElement}
     */
    _findBox(anchor) {
        return anchor;
    }

    _getBoxClassName() {
        return FrameworkClassNames.ARTS_ROW_BOX;
    }
}

class ArtsHeaderBox extends ArtsRowBox {
    _getBoxClassName() {
        return FrameworkClassNames.ARTS_HEADER_BOX;
    }
}

class ArtsFooterBox extends ArtsRowBox {
    _getBoxClassName() {
        return FrameworkClassNames.ARTS_FOOTER_BOX;
    }
}

/* </editor-fold> */