hwm_armory_framework

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

Dieses Skript sollte nicht direkt installiert werden. Es handelt sich hier um eine Bibliothek für andere Skripte, welche über folgenden Befehl in den Metadaten eines Skriptes eingebunden wird // @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> */