Greasy Fork is available in English.

HWM_roulette_upd

Пропатченная рулетка

// ==UserScript==
// @name            HWM_roulette_upd
// @namespace       Мифист
// @author          Мифист
// @version         1.2
// @description     Пропатченная рулетка
// @match           https://www.heroeswm.ru/roulette.php*
// @match           https://*.lordswm.com/roulette.php*
// @run-at          document-end
// @grant           none
// @license         MIT
// @noframes
// ==/UserScript==

(function(view) {
  'use strict';

  clearTimeout(view.Timer);

  if (!Element.prototype.scrollIntoViewIfNeeded) {
    Element.prototype.scrollIntoViewIfNeeded = Element.prototype.scrollIntoView;
  }

  // =========================

  const DEV_ID = '5781303';
  const PATH = '/roulette.php';
  const MODULE_NAME = 'HWM_roulette_upd';
  const SYMBOL = Symbol.for(`__${DEV_ID}__`);
  const modules = view[SYMBOL] || (view[SYMBOL] = {});
  modules[MODULE_NAME] = '1.0';

  const {
    $, $$,
    fetch, clamp, wait, memoize,
    attempt, parseNode, getElemIndex
  } = (document[`__utils_${DEV_ID}`] || (() => {
    const $ = (selector, ctx = document) => ctx.querySelector(selector);
    const $$ = (selector, ctx = document) => [...ctx.querySelectorAll(selector)];
    const getElemIndex = elem => [...elem.parentNode.children].indexOf(elem);
    const clamp = (min, val, max = Infinity) => Math.max(min, Math.min(val, max));
    const wait = (sec = 1) => new Promise((resolve) => setTimeout(resolve, 1e3 * sec));

    function attempt(that, callback, thisArg) {
      if (thisArg == null) thisArg = that;
      return that ? callback.call(thisArg, that) : null;
    }

    function parseNode(html, callback) {
      let elem = document.createElement('div');
      elem.innerHTML = html;
      elem = elem.firstElementChild.cloneNode(true);
      callback && callback.call(elem, elem);
      return elem;
    }

    function memoize(fn) {
      const cache = new Map();
      return (x) => cache.get(x) || cache.set(x, fn(x)).get(x);
    }

    function fetch({ url, method = 'GET', type = 'document', body = null }) {
      return new Promise((resolve, reject) => {
        const xhr = new XMLHttpRequest();
        xhr.open(method, url);
        xhr.responseType = type;

        xhr.onload = () => {
          const {status} = xhr;
          if (status === 200) return resolve(xhr.response);

          const er = new Error(`Error with status ${status}`);
          er.status = status;
          reject(er);
        };

        xhr.onerror = () => {
          const {status} = xhr;
          const er = new Error(`HTTP error with status ${status}`);
          er.status = status;
          reject(er);
        };

        xhr.send(body);
      });
    }

    fetch.get = url => fetch({ url });
    fetch.post = (url, data) => fetch({ url, method: 'POST', body: data });

    return {
      $, $$,
      fetch, clamp, wait, memoize,
      attempt, parseNode, getElemIndex
    };
  })());

  const formatNum = (num) => num.toLocaleString('en');
  const parseNum = (num) => String(num).replaceAll(',', '') >> 0;
  const activeElem = (elem) => elem.classList.add('__active');
  const inactiveElem = (elem) => elem.classList.remove('__active');
  const reduceBets = (bets, key = 'value') => bets.reduce((a, b) => a + b[key], 0);

  // =========================

  let locked = false;
  const RED_NUMBERS = [1, 3, 5, 7, 9, 12, 14, 16, 18, 19, 21, 23, 25, 27, 30, 32, 34, 36];
  const [MIN, MAX, CASH] = getMinMaxCash();

  function getMinMaxCash(context = document) {
    const script = context.forms[0].previousElementSibling;
    const {text} = script;
    const reg = /bet\.value [><] (\d+)/g;
    const [cash, min] = [...text.matchAll(reg)].map(match => +match[1]);
    const max = +text.match(/maxsum = (\d+)/)[1];
    return [min, max, cash];
  }

  function checkMoney(value) {
    if (roulCash < value) return !!roulAlert.notEnoughMoney();
    if (roulBets.sum + value > roulBets.MAX) return !!roulAlert.outOfRange();
    return true;
  }

  function hwmBetToObject(elem) {
    const id = elem.lastElementChild.textContent;
    const value = parseNum(elem.firstElementChild.textContent);
    return { id, value };
  }

  function renderApp() {
    return /*html*/`
      <main id="container">
        ${templates.Tools()}
        ${templates.Roul()}
        ${templates.Wheel()}
        ${templates.Details()}
        ${templates.Bets()}
        ${templates.Games()}
        ${templates.Density()}
        ${templates.Alert()}
        <div id="roul-tip"></div>
        <div id="roul-mark">
          <div>roulette</div>
          by <a href="/pl_info.php?id=${DEV_ID}">Мифист</a>
        </div>
      </main>
    `;
  }

  const templates = {
    Games() {
      return /*html*/`
        <section id="roul-games" class="roul-box">
          <div class="roul-games__info roul-box">
            <div class="roul-games__bets roul-box-body ui-scroll"></div>
            <footer class="roul-games__footer roul-box-footer"></footer>
          </div>
          <div class="roul-games__content roul-box-body ui-scroll">
            <div class="roul-games__body"></div>
            <b id="load_prev_games" title="Загрузить последние 24 игры">+</b>
          </div>
        </section>
      `;
    },

    Density(context = document) {
      const table = $$('table.wb', context).pop().firstElementChild;
      const allSum = table.lastElementChild.textContent.split(/\s/)[0];

      const items = [...table.children].slice(1, -1).map(row => {
        const id = row.lastElementChild.textContent;
        const val = row.firstElementChild.textContent;
        return /*html*/`
          <div class="roul-box-row">
            <span class="roul-bet-key">${roulID.get(id)}</span>
            <span class="roul-bet-value">${val}</span>
          </div>
        `;
      });

      return /*html*/`
        <section id="roul-density" class="roul-box">
          <header class="roul-box-header">
            Плотность ставок
            <button id="roul-density__upd">&orarr;</button>
          </header>
          <div class="roul-box-body ui-scroll">${items.join('')}</div>
          <footer class="roul-box-footer">
            Всего:
            <span class="roul-sum">${allSum}</span>
          </footer>
        </section>
      `;
    },

    Details() {
      return /*html*/`
        <section id="roul-details" class="roul-box">
          <div id="roul-cash">Баланс: <span class="roul-sum">${formatNum(CASH)}</span></div>
          <div id="roul-minbet">Минимальная ставка: <span class="roul-sum">${formatNum(MIN)}</span></div>
          <div id="roul-maxbet">Макс. сумма ставок: <span class="roul-sum">${formatNum(MAX)}</span></div>
          <div>До спина: <span id="roul-timer">00:00</span></div>
        </section>
      `;
    },

    Bets() {
      return /*html*/`
        <section id="roul-bets" class="roul-box">
          <header class="roul-bets__tabs">
            <div class="roul-bets__tab __active">Ставки (стол)</div>
            <div class="roul-bets__tab">Ставки (принято) <span id="roul-bets__count">0 / 0</span></div>
          </header>
          <div class="roul-box">
            <div class="roul-bets__body roul-box-body ui-scroll"></div>
            <footer class="roul-bets__footer roul-box-footer">
              <div class="roul-bets__apply">
                <div class="roul-bets__action" data-action="cancel">&times;</div>
                <div class="roul-bets__action" data-action="accept">&check;</div>
              </div>
              Всего: <span class="roul-sum">0</span>
            </footer>
          </div>
          <div class="roul-box" hidden>
            <div class="roul-bets__body roul-box-body ui-scroll"></div>
            <footer class="roul-bets__footer roul-box-footer">Всего: <span class="roul-sum">0</span></footer>
          </div>
        </section>
      `;
    },

    Alert() {
      return /*html*/`
        <div id="roul-alert" data-action="hide">
          <div id="roul-alert__inner">
            <div id="roul-alert__content"></div>
            <button id="roul-alert__ok" data-action="hide">OK</button>
          </div>
        </div>
      `;
    },

    Wheel() {
      const numbers = '0-28-9-26-30-11-7-20-32-17-5-22-34-15-3-24-36-13-1-00-27-10-25-29-12-8-19-31-18-6-21-33-16-4-23-35-14-2'.split('-');
      const angle = 360 / numbers.length / 360;
      const rotate = ind => `style="--turn: ${+(angle * ind).toFixed(3)}turn"`;
      const getSegment = (num, ind) => {
        return /*html*/`<div class="wheel__segment" data-num="${num}" ${rotate(ind)}></div>`;
      };

      return /*html*/`
        <section id="wheel">
          <div id="wheel__main">
            <div id="wheel__segments">${numbers.map(getSegment).join('')}</div>
            <div id="wheel__rotor">
              ${'<div class="wheel__line"></div>'.repeat(4)}
              <div id="wheel__turret"></div>
            </div>
          </div>
          <div id="wheel__ball"></div>
          <div id="wheel__info">
            <div id="wheel__number"></div>
            <div id="wheel__score">Выигрыш: </div>
          </div>
        </section>
      `;
    },

    Tools() {
      function getCoin(val) {
        return /*html*/`<b class="roul-coin" data-coin="${val}" data-action="setMultiplier"></b>`;
      }

      const coins = [5, 25, 50, 100, 250, 500, '1e3', 'MIN', 'MAX'];

      return /*html*/`
        <section id="roul-tools">
          <div class="roul-tools__item">${coins.map(getCoin).join('')}</div>
          <div class="roul-tools__item">
            <button class="roul-tools__btn" data-action="rebet" title="Повторить последние ставки">&larr;</button>
          </div>
        </section>
      `.replace('" data-coin="100', ' __active" data-coin="100');
    },

    Roul() {
      return /*html*/`
        <section id="roulette">
          ${this.RoulInside()}
          ${this.RoulOutside()}
          ${this.Chips()}
        </section>
      `;
    },

    RoulInside() {
      const getColor = id => RED_NUMBERS.includes(id) ? 'red' : 'black';
      const getZero = id => getCell(id).replace('black', 'zero');

      function getCell(id) {
        const className = `roul-area roul-cell roul-num roul-${getColor(id)}`;
        return /*html*/`
          <div class="${className}" data-id="Straight up ${id}">
            <b class="roul-value">${id}</b>
            <b class="roul-fish"></b>
          </div>
        `;
      }

      function getColumn(order) {
        return getCell('&').replace('num roul-black', 'x3 roul-column')
          .replace('Straight up &', `${order} Column`)
          .replace('&', '2 to 1');
      }

      const coloredCellsHTML = [...Array(12)].map((x, i) => {
        const start = i * 3 + 1;
        const cells = [start + 2, start + 1, start].map(getCell);
        return /*html*/`<div class="roulette__col">${cells.join('')}</div>`;
      }).join('');

      const zerosHTML = ['00', '0'].map(getZero).join('');
      const columnsHTML = ['3rd', '2nd', '1st'].map(getColumn).join('');

      return /*html*/`
        <div class="roulette__section">
          <div class="roulette__col">${zerosHTML}</div>
          ${coloredCellsHTML}
          <div class="roulette__col">${columnsHTML}</div>
        </div>
      `;
    },

    RoulOutside() {
      const defaultClasses = ['area', 'outside', 'x3'].map(x => `roul-${x}`);

      function getDozen(order) {
        const classes = [...defaultClasses, 'roul-dozen'];
        return /*html*/`
          <div class="${classes.join(' ')}" data-id="${order} Dozen">
            <b class="roul-value">${order[0]}<sup>${order.slice(1)}</sup> 12</b>
            <b class="roul-fish"></b>
          </div>
        `;
      }

      function getSection(id) {
        const value = id === '1-18 Half' ? '1 to 18' : id === '19-36 Half' ? '19 to 36' : id;
        const classes = [...defaultClasses];
        if (/R|B/.test(id[0])) classes.push(`roul-${id.toLowerCase()}`);

        return /*html*/`
          <div class="${classes.join(' ')}" data-id="${id}">
            <b class="roul-value">${value}</b>
            <b class="roul-fish"></b>
          </div>
        `;
      }

      const dozens = ['1st', '2nd', '3rd'];
      const others = ['1-18 Half', 'EVEN', 'RED', 'BLACK', 'ODD', '19-36 Half'];

      return /*html*/`
        <div class="roulette__section">
          ${dozens.map(getDozen).join('')}
          ${others.map(getSection).join('')}
        </div>
      `;
    },

    Chips() {
      function chip(id, num = 0, mod = 'n') {
        const className = `roul-chip roul-chip${num} roul-chip-${mod}`;
        return /*html*/`<b class="${className}" data-id="${id}"></b>`;
      }

      function getSplits(start) {
        return chip(`Split ${start}, ${start + 3}`, 4)
          + chip(`Split ${start - 1}, ${start + 2}`, 6)
          + chip(`Split ${start - 2}, ${start + 1}`, 8)
          + chip(`Split ${start - 1}, ${start}`, 5, 'h')
          + chip(`Split ${start - 2}, ${start - 1}`, 7, 'h');
      }

      function getCorners(start) {
        const nums1 = [start - 1, start, start + 2, start + 3].join(', ');
        const nums2 = [start - 2, start - 1, start + 1, start + 2].join(', ');
        return chip(`Corner ${nums1}`, 5) + chip(`Corner ${nums2}`, 7);
      }

      function getStreetAndSixline(start) {
        return chip(`Street ${start - 2}-${start}`, 9, 'h')
          + chip(`Sixline ${start - 2}-${start + 3}`, 9);
      }

      const colsHTML = [...Array(11)].map((x, i) => {
        const start = i * 3 + 3;
        return /*html*/`
          <div class="roul-chips__col">
            ${getSplits(start)}
            ${getCorners(start)}
            ${getStreetAndSixline(start)}
          </div>
        `;
      }).join('');

      return /*html*/`
        <div id="roul-chips">
          <div class="roul-chips__col">
            ${chip('Split 3, 00', 4)}
            ${chip('Numbers 2, 00, 3', 5)}
            ${chip('Split 2, 00', 0, 'x').replace('>', ' style="top: 4.5em;">')}
            ${chip('Split 0, 2', 0, 'x').replace('>', ' style="top: 6.3em;">')}
            ${chip('Split 0, 1', 8)}
            ${chip('Numbers 0, 1, 2', 7)}
            ${chip('Numbers 0, 00, 1, 2, 3', 9)}
            ${chip('Split 0, 00', 0, 'h').replace('>', ' style="top: 50%;">')}
            ${chip('Numbers 0, 00, 2').replace('>', ' style="top: 50%;">')}
          </div>
          ${colsHTML}
          <div class="roul-chips__col">
            ${chip('Split 35, 36', 5, 'h')}
            ${chip('Split 34, 35', 7, 'h')}
            ${chip('Street 34-36', 9, 'h')}
            ${chip('Red Snake', 9)}
          </div>
        </div>
      `;
    },
  };

  const roulID = {
    get(id) { return this[id] || id; },
    'Numbers 0, 00, 1, 2, 3': 'Top Line',
    'Numbers 0, 1, 2': 'Basket 1',
    'Numbers 0, 00, 2': 'Basket 2',
    'Numbers 2, 00, 3': 'Basket 3',
    'Split 0, 00': 'Row',
    'Split 2, 00': 'Split 00, 2',
    'Split 3, 00': 'Split 00, 3',
    '1-18 Half': '1 to 18',
    '19-36 Half': '19 to 36',
  };

  roulID.keys = Object.keys(roulID).slice(1);

  // =========================

  const logError = {
    create: (status, msg) => Object.assign(new Error(msg), { status }),
    disconnect(er, data) {
      const text = {
        '-1': 'Произошла деавторизация; рулетка не доступна',
        0:  'Потеряно соединение с интернетом',
      }[er.status] || 'Что-то пошло не так :)';

      this.show(er, text);
      data && this.handleData(data);
    },
    show({message}, userText) {
      console.error(message);
      roulAlert.show(/*html*/`
        <div id="roul-log">
          <p>${message}</p>
          <p>${userText}</p>
        </div>
      `);
    },
    handleData(data) {
      const log = $('#roul-log');
      const link = parseNode(`<a href="#">${data.text}</a>`);
      log.appendChild(link);
      link.onclick = (e) => {
        link.onclick = null;
        e.preventDefault();
        data.callback();
      };
    }
  }

  // =============== [[ FORM ]]

  const formData = ((form) => {
    !form.parlay_dec && setParlayDec(document.body);

    function setParlayDec(ctx) {
      const gameId = ctx.innerHTML.match(/inforoul\.php\?id=(\d+)/)[1];
      const inputHTML = `<input name="parlay_dec" value="${+gameId + 1}">`;
      form.appendChild(parseNode(inputHTML));
    }

    function isEquals(bet1, bet2) {
      const a = [bet1.id, bet1.value].toString();
      const b = [bet2.id, bet2.value].toString();
      return a === b;
    }

    function betToPostData(bet) {
      const data = new FormData(form);
      data.set('bettype', bet.id);
      data.set('bet', bet.value);
      return data;
    }

    function sendRedSnake(self, snakeBet) {
      if (!snakeBet.initialized) {
        snakeBet.initialized = true;
        const keys = '9, 12|16, 19|27, 30|1|5|14|23|32|34'.split('|');
        snakeBet.keys = keys.map((x, i) => {
          return i < 3 ? `Split ${x}` : `Straight up ${x}`;
        });
      }

      const betVal = snakeBet.value / 12 >> 0;
      const bets = snakeBet.keys.map((id) => {
        const value = betVal * (1 + +id.startsWith('Sp'));
        return { id, value, count: 0, onLoadEnd: Function.prototype };
      });

      return new Promise(function next(resolve, reject) {
        if (locked) bets.splice(0);

        if (!bets.length) {
          snakeBet.onLoadEnd(snakeBet.keys.length ? 2 : 1);
          return resolve();
        }

        const bet = bets.pop();
        return self.send(bet).then(status => {
          if (status === 1) {
            snakeBet.keys.splice(snakeBet.keys.indexOf(bet.id), 1);
          } else if (!bet.count++) bets.push(bet);

          return wait(0.2).then(() => next(resolve, reject));
        }).catch(reject);
      });
    }

    return {
      get seconds() {
        return form.minutes.value * 60 + +form.seconds.value;
      },
      get restSeconds() {
        return clamp(0, 300 - this.seconds);
      },
      get gameId() {
        return form.parlay_dec.value;
      },
      async send(bet) {
        if (bet.id === 'Red Snake') return await sendRedSnake(this, bet);

        const doc = await fetch.post(form.action, betToPostData(bet))

        if (!doc.URL.includes(PATH)) {
          throw logError.create(-1, 'Authorization error');
        }

        const table = $('table.wb:nth-child(2)', doc);
        const test = isEquals(bet, hwmBetToObject(table.rows[1]));
        const status = test ? 1 : 2;
        bet.onLoadEnd(status);
        return status;
      },
      update(newForm) {
        form = document.importNode(newForm, true);
        !form.parlay_dec && setParlayDec(newForm.closest('body'));
      }
    };
  })(document.forms[0].cloneNode(true));

  // =========================

  const initialBets = [...$('table.wb:nth-child(2)').rows]
    .slice(1, -1)
    .map(hwmBetToObject);

  const container = parseNode(renderApp());

  $$('link', document.head).forEach((link) => {
    const reg = /(sweetalert|top_basic)\.css/;
    return reg.test(link.href) && link.remove();
  });

  parseNode('<style id="roul-CSS"></style>', function() {
    this.append(/*css*/`
      @keyframes roulWin {
        0% {filter: brightness(1);}
        100% {filter: brightness(1.1);}
      }

      @keyframes roulSpin {
        100% {transform: rotate(1turn);}
      }

      /* === GLOBAL === */

        :root {
          font-size: 10px;
          overflow-y: visible;
        }
        body.txt {
          overflow-y: visible;
        }
        main *,
        ::before,
        ::after {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }
        [hidden] {
          display: none !important;
        }

      /* === COMMON === */

        .ui-scroll {
          overflow-x: hidden;
          overflow-y: auto;
        }
        .ui-scroll::-webkit-scrollbar {
          width: 6px;
          background-color: #eee;
        }
        .ui-scroll::-webkit-scrollbar-thumb {
          background-color: #ccc;
        }
        .ui-scroll::-webkit-scrollbar-thumb:hover {
          background-color: #aaa;
        }
        .ui-scroll::-webkit-scrollbar-thumb:active {
          background-color: gray;
        }

        .roul-bet-key {
          padding: 0 0.5em;
          border-right: var(--border);
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          width: 11.5em;
        }
        .roul-bet-value {
          padding: 0 0.5em;
          text-align: center;
          color: brown;
          flex: 1;
        }
        .roul-box {
          background-color: var(--bg);
          box-shadow: var(--shadow);
          overflow: hidden;
        }
        .roul-box-body {
          background-color: inherit;
        }
        .roul-box-body:empty {
          display: flex;
          justify-content: center;
          align-items: center;
        }
        .roul-box-body:empty::after {
          content: "No bets";
          color: gray;
        }
        .roul-box-row {
          line-height: 1.8;
          display: flex;
          background-color: inherit;
          box-shadow: var(--shadow);
        }
        .roul-box-row:nth-child(odd) {
          background-color: #efebe4;
        }
        .roul-box-header,
        .roul-box-footer {
          line-height: 2;
          position: relative;
          padding: 0 0.5em;
          background-color: #e9e0c6;
          box-shadow: var(--shadow);
          overflow: hidden;
        }
        .roul-box-footer {
          margin-top: -1px;
          text-align: right;
          background-color: #d9e4d2;
        }
        .roul-box-body:empty + footer {
          visibility: hidden;
        }
        .roul-sum {
          color: brown;
        }

      /* === CONTAINER === */

        #container {
          --bg: #e9e4dc;
          --shadow: 0 0 0 1px #aaa;
          --border: 1px solid #aaa;
          --highlight-color-1: #d7c4a4;
          --highlight-color-2: #dfd6c8;
          --highlight-color-3: #eaebb9;
          --selected-bet: linear-gradient(to right, var(--highlight-color-3), transparent);

          font-family: Arial, Helvetica, sans-serif;
          font-size: 1.6rem;
          max-width: 102rem;
          min-width: 64rem;
          position: relative;
          margin: 1rem auto 0;
          color: #333;
          user-select: none;
        }
        #container.__locked > section {
          filter: grayscale(.3);
          pointer-events: none;
        }

      /* === TOOLS === */

        #roul-tools {
          width: 100%;
          max-width: 84rem;
          display: flex;
          justify-content: space-between;
          column-gap: 3em;
          position: absolute;
          left: calc(50% - 42rem);
          top: 1rem;
        }
        #container.__locked > #roul-tools {
          filter: grayscale(1);
        }
        .roul-tools__item {
          display: flex;
          column-gap: 0.2em;
        }

        .roul-coin,
        .roul-tools__btn {
          font-size: 1em;
          width: 3em;
          height: 3em;
          display: inline-block;
          position: relative;
          border-radius: 50%;
          cursor: pointer;
        }
        .roul-coin {
          display: flex;
          justify-content: center;
          align-items: center;
          color: #172533;
          background-color: #333;
          box-shadow: inset 0 0 0 0.5rem currentColor, inset 0 0 0 0.6rem #cfcf89;
          opacity: .7;
        }
        .roul-coin:nth-child(1) {
          color: #88a06d;
        }
        .roul-coin:nth-child(2) {
          color: #a89632;
        }
        .roul-coin:nth-child(3) {
          color: #bc6d0b;
        }
        .roul-coin:nth-child(4) {
          color: #8d2525;
        }
        .roul-coin:nth-child(5) {
          color: #62258d;
        }
        .roul-coin:nth-child(6) {
          color: #0044a5;
        }
        .roul-coin:nth-child(7) {
          color: #115b68;
        }
        .roul-coin:hover {
          transform: scale(1.2);
          z-index: 2;
        }
        .roul-coin.__active {
          opacity: 1;
          pointer-events: none;
        }
        .roul-coin::before {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          border: .2em dashed #eee;
          border-radius: inherit;
          box-shadow: 0 0 0 1px #555, 0 0 4px 1px #111;
        }
        .roul-coin::after {
          content: attr(data-coin);
          font-family: Consolas, monospace;
          font-size: .95em;
          color: #eee;
        }

        .roul-tools__btn {
          background-color: #f3e9d1;
          border: 2px solid #999;
          border-color: #999 #666 #666 #999;
        }
        .roul-tools__btn:hover,
        .roul-tools__btn:focus {
          background-color: #eadec0;
        }
        .roul-tools__btn:active {
          transform: scale(.9);
        }
        .roul-tools__btn::after {
          content: "";
          position: absolute;
          top: 2px; right: 2px; bottom: 2px; left: 2px;
          border: 1px dashed #999;
          border-radius: inherit;
        }

      /* === ROULETTE === */

        #roulette {
          font-size: 2rem;
          width: 42em;
          display: flex;
          flex-direction: column;
          align-items: center;
          position: absolute;
          left: calc(50% - 42rem);
          top: 9.5rem;
          color: #8191a2;
          pointer-events: none;
        }
        #roulette::before {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          margin: -1em;
          background-color: var(--bg);
          border: 2px solid #afa18e;
          border-top-left-radius: 1.5em;
        }
        .roulette__section {
          display: flex;
          flex-wrap: wrap;
        }
        .roulette__section:first-child {
          height: 10.8em;
          position: relative;
          z-index: 2;
        }
        .roulette__section:nth-child(2) {
          width: calc(100% - 6em);
          height: 8em;
        }
        .roulette__col {
          display: flex;
          flex-direction: column;
        }
        .roulette__col:nth-last-child(2) {
          position: relative;
          z-index: 2;
        }
        .roul-area {
          position: relative;
          background-color: var(--bg);
          box-shadow: 0 0 0 1px #8d8d8d;
          cursor: pointer;
          pointer-events: auto;
        }
        .__target {
          background-color: #dfd6c8;
        }
        .roul-area:hover {
          background-color: #d7c4a4;
        }
        .roul-x3.__target,
        .roul-x3:hover {
          color: #eadecf;
          background-color: #c4a97e;
        }
        .roul-cell {
          flex: 1;
          width: 3em;
          display: flex;
          justify-content: center;
          align-items: center;
        }
        .roul-black {
          color: #222 !important;
        }
        .roul-red {
          color: #b43b3b !important;
        }

        #winner {
          background-color: var(--highlight-color-3);
          animation: roulWin .3s alternate 4;
        }

        .roul-value {
          pointer-events: none;
        }
        .roul-column > .roul-value {
          transform: rotate(-90deg);
        }

        /* outside */

        .roul-outside {
          width: 6em;
          display: flex;
          justify-content: center;
          align-items: center;
        }
        .roul-dozen {
          width: 12em;
        }
        .roul-outside > .roul-value {
          font-size: 1.1em;
        }
        .roul-dozen > .roul-value {
          font-size: 1.5em;
        }

        /* chips */

        #roul-chips {
          height: 10.8em;
          display: flex;
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
        }
        .roul-chips__col {
          width: 3em;
          height: 100%;
          position: relative;
        }
        .roul-chip {
          width: 0.8em;
          height: 0.8em;
          position: absolute;
          right: -0.4em;
          margin-top: -0.4em;
          background-color: transparent !important;
          pointer-events: auto;
          cursor: pointer;
          z-index: 4;
        }
        .roul-chip-h {
          width: auto;
          left: 0; right: 0;
        }
        .roul-chip4 {
          top: 0;
        }
        .roul-chip5, .roul-chip6 {
          top: 33.333%;
        }
        .roul-chip7, .roul-chip8 {
          top: 66.666%;
        }
        .roul-chip9 {
          top: 100%;
          z-index: 5;
        }
        .roul-chip4, .roul-chip6, .roul-chip8 {
          height: 3.6em;
          margin-top: 0;
        }
        .roul-chip[data-id^="Cor"],
        .roul-chip[data-id^="Six"] {
          z-index: 6;
        }
        .roul-chip-x {
          height: 1em;
          margin-top: -0.5em;
        }
        .roul-chips__col:first-child > .roul-chip {
          z-index: 7;
        }
        .roul-fish,
        .roul-chip::after {
          --size: 1.5em;
          font-size: 0.8em;
          width: var(--size);
          height: var(--size);
          display: flex;
          justify-content: center;
          align-items: center;
          position: absolute;
          top: calc(50% - var(--size) / 2);
          left: calc(50% - var(--size) / 2);
          color: #eee;
          background-color: #1c405f;
          border: .2em dashed;
          border-radius: 50%;
          box-shadow: 0 0 0 1px #333, 0 0 3px 1px #111;
          pointer-events: none;
          opacity: 0;
          z-index: 4;
        }
        .roul-fish::after,
        .roul-chip::after {
          content: "$";
          font-weight: normal;
        }
        .roul-area:hover > .roul-fish,
        .__target > .roul-fish,
        .roul-chip:hover::after,
        .roul-chip.__target::after {
          opacity: 1;
        }

        /* zero */

        .roul-zero {
          color: #55a867;
          border-top-left-radius: 3rem;
        }
        .roul-zero:last-child {
          border-top-left-radius: 0;
          border-bottom-left-radius: 3rem;
        }

        /* disable */

        #container.__locked .roul-area,
        #container.__locked .roul-chip {
          pointer-events: none;
        }

      /* === WHEEL === */

        #wheel {
          --diameter: 26rem;
          --radius: calc(var(--diameter) / 2);
          width: var(--diameter);
          height: var(--diameter);
          position: absolute;
          left: -23rem;
          top: -8rem;
          text-align: center;
          color: #eee;
          border-radius: 50%;
          filter: drop-shadow(0 0 2px #333);
          pointer-events: none;
          z-index: 100;
        }
        #wheel::before {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          margin: -9%;
          border-radius: inherit;
          background-color: #6b432f;
          background-image: radial-gradient(50% 50%, #5d2e18 91%, #271a07 92%, #6b432f 93%);
          box-shadow: inset -2px -2px 4px #fff9;
        }
        #wheel.__spining {
          transition: transform 2.05s linear;
        }
        #wheel.__spinending {
          transition: transform 5.1s cubic-bezier(.35, .9, .7, 1);
        }
        #wheel__main {
          --turn: 0;
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          border-radius: inherit;
          transition: inherit;
          transform: rotate(calc(1turn * var(--turn)));
        }

        /* segments */

        #wheel__segments {
          height: 100%;
          position: relative;
        }
        #wheel__segments::before,
        #wheel__segments::after {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          border-radius: 50%;
        }
        #wheel__segments::before {
          margin: -4px;
          border: 1px solid #8b7b74;
          box-shadow: inset 0 0 0 6px #bfb997, inset 0 0 0 8px #e3e0ca;
          z-index: 2;
        }
        #wheel__segments::after {
          margin: 12%;
          background-color: rgba(10, 10, 10, .5);
          border: 1px solid #e6cc9a;
          box-shadow: inset 0 0 0 1px #e4d6bb;
        }
        .wheel__segment {
          --size: calc(var(--diameter) / 12);
          --size2: calc(var(--size) / 2);
          width: var(--size);
          height: 50%;
          position: absolute;
          left: calc(50% - var(--size2));
          top: 0;
          color: #222;
          border: 0 solid transparent;
          border-width: var(--radius) var(--size2) 0;
          border-top-color: currentColor;
          transform: rotate(var(--turn));
          transform-origin: bottom;
        }
        .wheel__segment:nth-child(odd) {
          color: #ba3535;
        }
        .wheel__segment:nth-child(1),
        .wheel__segment:nth-child(20) {
          color: #43914b;
        }
        .wheel__segment.__active {
          color: #5f9ea0;
        }
        .wheel__segment::after {
          content: attr(data-num);
          font-family: Arial, Helvetica, sans-serif;
          font-size: var(--size2);
          width: inherit;
          position: absolute;
          left: -1em;
          top: calc(-1 * var(--radius) + 1em);
          color: #eee;
        }

        /* rotor */

        #wheel__rotor {
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          margin: 28%;
          background-color: #633d2b;
          background-image: radial-gradient(circle, #6b4838 50%, #1e0c03);
          border: 1px solid #e6cc9a;
          border-radius: inherit;
          box-shadow: inset 0 0 0 1px #eee0c6;
          z-index: 2;
        }
        .wheel__line {
          height: 1px;
          position: absolute;
          left: 0.2em;
          right: 0.2em;
          top: 0.2em;
          bottom: 0.2em;
          margin: auto;
          background-color: black;
          box-shadow: 0 0 2px gray;
        }
        .wheel__line:nth-child(2) {
          transform: rotate(90deg);
        }
        .wheel__line:nth-child(3) {
          transform: rotate(45deg);
        }
        .wheel__line:nth-child(4) {
          transform: rotate(135deg);
        }
        #wheel__turret {
          width: 40%;
          height: 40%;
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          margin: auto;
          background-image: radial-gradient(50% 50%, #ebd5ab 40%, #9c8a63 60%, #e9d09e 80%, #998a6c 90%);
          box-shadow: 0 0 .4rem 1px #383838;
          border-radius: 50%;
        }
        #wheel__turret::before,
        #wheel__turret::after {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          margin: 14%;
          border: 0.3rem dashed #b49f82;
          border-radius: inherit;
        }
        #wheel__turret::after {
          margin: 38%;
          background-color: #e2d9c5;
          border: 1px solid #b2a89a;
          box-shadow: inset 0 0 2px #666;
        }

        /* ball */

        #wheel__ball {
          --turn: 0;
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          margin: 4%;
          transform: rotate(calc(-1turn * var(--turn)));
          transition: margin .6s ease-out;
          z-index: 5;
        }
        .__spining > #wheel__ball {
          transition: inherit;
          will-change: transform;
        }
        .__result #wheel__ball {
          margin: 20%;
          transition: margin .4s ease-out;
        }
        #wheel__ball::after {
          --size: calc(var(--radius) / 9);
          content: "";
          width: var(--size);
          height: var(--size);
          display: inline-block;
          background-color: white;
          border-radius: 50%;
          box-shadow: inset 1px 1px 3px 2px #999;
          filter: drop-shadow(2px 3px 4px black);
        }

        /* info */

        #wheel__info {
          font-size: calc(var(--radius) / 10);
          display: flex;
          flex-direction: column;
          align-items: center;
          justify-content: center;
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          margin: 30%;
          border-radius: inherit;
          opacity: 0;
          visibility: hidden;
          transition: opacity .5s, visibility .5s;
        }
        .__result #wheel__info {
          opacity: 1;
          visibility: visible;
        }
        #wheel__number {
          font-size: 3.5em;
        }
        #wheel__score {
          font-size: 1.2em;
          line-height: 1.4;
        }
        #wheel__score::after {
          content: attr(data-score);
          display: block;
          color: #e3c59d;
        }

      /* === MINI WHEEL === */

        #wheel-wrap {
          width: 9rem;
          height: 6rem;
          position: absolute;
          left: calc(100% - 3rem);
          top: 0;
          background-color: var(--bg);
          outline: 1px solid black;
          overflow: hidden;
          visibility: hidden;
        }
        #wheel.mini-wheel {
          --diameter: 30rem;
          left: -4rem;
          top: 0.5rem;
          filter: none;
          transform: rotate(-30deg);
        }

      /* === DETAILS === */

        @keyframes roulTimerBlink {
          from {
            transform: scale(1);
            filter: opacity(1);
          }
          to {
            transform: scale(1.1);
            filter: opacity(0.6);
          }
        }
        #roul-details {
          font-family: Consolas, monospace;
          line-height: 1.3em;
          position: absolute;
          left: -22rem;
          top: 22rem;
          padding: 0.3em 0.6em;
        }
        #roul-cash {
          color: #416e90;
        }
        #roul-maxbet {
          margin-bottom: 0.4rem;
          padding-bottom: 0.4rem;
          border-bottom: 1px solid #aaa;
        }
        #roul-timer {
          display: inline-block;
          color: #59908e;
        }
        #roul-timer.__blink {
          color: #ca0000;
          animation: roulTimerBlink .5s linear infinite alternate;
        }

      /* === BETS === */

        #roul-bets {
          width: 34rem;
          position: absolute;
          left: -22rem;
          top: 34rem;
        }
        .roul-bets__tabs {
          line-height: 2;
          display: flex;
          position: relative;
        }
        .roul-bets__tab {
          flex: 1 auto;
          padding: 0 0.5em;
          color: gray;
          background-color: #dcd7c8;
          box-shadow: var(--shadow);
          cursor: pointer;
        }
        .roul-bets__tab:hover {
          background-color: #e3ddca;
        }
        .roul-bets__tab.__active {
          color: inherit;
          background-color: #e9e0c6;
          pointer-events: none;
        }
        #roul-bets__count {
          font-family: Consolas, monospace;
          font-size: 0.8em;
          min-width: 4.3em;
          float: right;
          text-align: right;
          pointer-events: none;
        }

        .roul-bets__body {
          height: 17.3rem;
        }

        .roul-bets__apply {
          display: flex;
          float: left;
          position: relative;
          left: -0.5em;
        }
        .roul-bets__action {
          --d: 0deg;
          --s: 30%;
          --l: 90%;
          width: 2em;
          text-align: center;
          background-color: #eee;
          background-color: hsl(var(--d, 180deg), var(--s), var(--l));
          box-shadow: var(--shadow);
          cursor: pointer;
        }
        .roul-bets__action[data-action="accept"] {
          --d: 150deg;
        }
        .roul-bets__action:hover {
          --s: 40%;
          --l: 85%;
        }

        .roul-bet.__active {
          background-image: var(--selected-bet);
        }
        .roul-bet.__win {
          background-image: var(--selected-bet);
          animation: roulWin .3s alternate 4;
        }

        .roul-bet__action {
          width: 2em;
          text-align: center;
          background-color: #eee;
          box-shadow: var(--shadow);
          cursor: pointer;
        }
        .roul-bet__action:hover {
          background-color: #fff;
        }
        .roul-bet__action[data-action="remove"] {
          color: brown;
        }

        .roul-bet__status {
          width: 2em;
          position: relative;
          text-align: center;
        }
        [data-status="0"] > .roul-bet__status::after {
          content: "";
          width: 0.9em;
          height: 0.9em;
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          margin: auto;
          border: 2px solid royalblue;
          border-right-color: transparent;
          border-radius: 50%;
          animation: roulSpin .75s linear infinite;
        }
        [data-status="1"] > .roul-bet__status::after {
          content: "✓";
          color: green;
        }
        [data-status="2"] > .roul-bet__status::after {
          content: "✗";
          display: block;
          color: brown;
          cursor: pointer;
        }

      /* === GAMES === */

        #roul-games {
          --size: 3rem;
          height: calc(var(--size) * 8 - 1px);
          position: absolute;
          left: calc(100% - 3rem);
          top: 8rem;
          overflow: visible;
          pointer-events: auto !important;
          z-index: 100;
        }
        #roul-games::before,
        #roul-games::after {
          content: "";
          width: 1px;
          position: absolute;
          left: var(--size);
          top: 0;
          bottom: 0;
          background-color: #ccc;
        }
        #roul-games::after {
          left: calc(var(--size) * 2);
        }
        .roul-games__content {
          height: 100%;
        }
        .roul-games__content::before {
          content: "";
          width: 4px;
          position: absolute;
          right: 100%;
          top: 0;
          bottom: 0;
        }
        .roul-games__body {
          width: calc(var(--size) * 3);
          min-height: 100%;
          overflow: hidden;
          background-color: inherit;
        }
        .roul-game {
          font: inherit;
          width: var(--size);
          height: var(--size);
          line-height: var(--size);
          position: relative;
          float: left;
          text-align: center;
          text-decoration: none;
          color: #222;
          background-color: inherit;
          outline: 1px solid #ccc;
          z-index: 2;
        }
        .roul-game:empty {
          pointer-events: none;
        }
        .roul-game:nth-child(3n+2) {
          color: #4aab55;
        }
        .roul-game:nth-child(3n) {
          color: #b20000;
        }
        .roul-game.__active {
          background-color: #eadbae;
        }
        .roul-game.__loading {
          color: transparent;
        }
        .roul-game.__loading::after {
          content: "";
          position: absolute;
          left: 0;
          right: 0;
          top: 0;
          bottom: 0;
          margin: 8px;
          border: 2px solid royalblue;
          border-right-color: transparent;
          border-radius: 50%;
          animation: roulSpin .75s linear infinite;
        }

        /* info */

        .roul-games__info {
          width: max-content;
          min-width: 24rem;
          height: 100%;
          position: absolute;
          right: calc(100% + 4px);
          top: 0;
          opacity: .97;
          visibility: hidden;
        }
        .roul-games__info.__shown {
          visibility: visible;
        }
        .roul-games__info::before {
          content: "";
          width: 4px;
          position: absolute;
          left: 100%;
          top: 0;
          bottom: 0;
        }
        .roul-games__bets {
          height: calc(100% - var(--size) + 1px);
        }
        .roul-game-bet {
          line-height: var(--size);
        }
        .roul-game-bet.__selected {
          background-image: var(--selected-bet);
        }
        .roul-game-bet__value {
          color: inherit;
        }
        .roul-game-bet__value::before {
          content: attr(data-bet);
          color: brown;
        }
        .roul-game-bet__value::after {
          content: attr(data-prize);
          color: green;
        }

        .roul-games__footer {
          padding: 0;
          padding-left: 3rem;
          text-align: left;
        }
        .roul-games__footer > .roul-game-bet {
          background-color: inherit;
          pointer-events: none;
        }
        #roul-game-repeat {
          width: 2.9rem;
          line-height: 3rem;
          position: absolute;
          left: 0;
          top: 0;
          text-align: center;
          background-color: white;
          cursor: pointer;
        }

        #load_prev_games {
          font-weight: normal;
          width: 2rem;
          line-height: 2rem;
          position: absolute;
          right: .4rem; bottom: .4rem;
          text-align: center;
          background-color: white;
          box-shadow: 0 0 0 1px #999;
          cursor: pointer;
        }

      /* === DENSITY === */

        #roul-density {
          min-width: 20rem;
          position: absolute;
          left: calc(100% - 12rem);
          top: 34rem;
          pointer-events: auto !important;
          z-index: 2;
        }
        #roul-density > header {
          display: flex;
          align-items: center;
          justify-content: space-between;
          column-gap: 0.5em;
        }
        #roul-density__upd {
          width: 2rem;
          height: 2rem;
          margin-left: 0.6em;
          background-color: white;
          border: 1px solid #555;
          border-radius: 50%;
          cursor: pointer;
        }
        #roul-density__upd.__loading {
          animation: roulSpin .4s linear infinite;
        }
        #roul-density > div {
          height: 26rem;
        }

      /* === TIP === */

        #roul-tip {
          line-height: 3.4rem;
          position: fixed;
          left: 0; top: 0;
          padding: 0 1rem;
          background-color: #eee;
          border: var(--border);
          box-shadow: 0 0 1px #777;
          overflow: hidden;
          pointer-events: none;
          opacity: .95;
          z-index: 102;
        }
        #roul-tip:empty {
          display: none;
        }
        #roul-tip::after {
          content: attr(data-value);
          display: inline-block;
          margin-left: 1rem;
          padding-left: 1rem;
          border-left: inherit;
          color: brown;
        }

      /* === MARK === */

        #roul-mark {
          line-height: 1.4;
          position: absolute;
          left: 50%;
          top: 51rem;
          margin-right: -50%;
          transform: translateX(-50%);
          text-align: center;
          pointer-events: none;
          opacity: 0.3;
        }
        #roul-mark:hover {
          opacity: 1;
          transition: opacity .2s .2s;
        }
        #roul-mark > div {
          font-family: serif;
          font-size: 2.2em;
          color: #ffeab6;
          letter-spacing: 0.15em;
          text-shadow: 0 0 2px black, 0 0 2px black;
          text-transform: uppercase;
        }
        #roul-mark > a {
          font: inherit;
          color: #824242;
          pointer-events: auto;
          text-decoration: none;
        }
        #roul-mark > a:hover {
          text-decoration: underline;
        }

      /* === ALERT === */

        #roul-alert {
          font-size: 1.1em;
          position: fixed;
          top: 0; right: 0; bottom: 0; left: 0;
          background-color: rgba(127, 127, 127, .7);
          opacity: 0;
          visibility: hidden;
          pointer-events: none;
          transition: opacity 0.25s, visibility 0.25s;
          z-index: 105;
        }
        #roul-alert.__shown {
          opacity: 1;
          visibility: visible;
          pointer-events: auto;
        }
        #roul-alert__inner {
          max-width: 32em;
          margin: auto;
          padding: 1em;
          color: #eee;
          background-color: #444;
          border: 1px solid gray;
          border-top: none;
          box-shadow: 0 0 6px #666;
          overflow: hidden;
          transform: perspective(50em) rotateX(-40deg);
          transform-origin: 50% 0;
          transition: transform 0.15s ease-out;
          cursor: default;
        }
        #roul-alert.__shown #roul-alert__inner {
          transform: perspective(50em) rotateX(0);
        }
        #roul-alert__content {
          line-height: 1.5;
          padding: 1em;
          margin-bottom: 1em;
          text-align: center;
          background-color: #555;
          border: 1px dashed gray;
        }
        #roul-alert__ok {
          float: right;
          padding: .4em 2em;
          text-align: center;
          color: #eee;
          background-color: #8a766f;
          border: 1px solid #bbb;
          outline: 1px solid #444;
          outline-offset: -3px;
          cursor: pointer;
        }
        #roul-alert__ok:hover {
          background-color: #9e8c86;
        }
        #roul-alert__ok:focus {
          border-color: #dabe99;
        }

        #roul-log {
          text-align: justify;
        }
        #roul-log > p:first-child {
          color: lightcoral;
        }
        #roul-log > a {
          display: inline-block;
          color: inherit;
          margin-top: 1em;
        }

      @media screen and (max-width: 1480px) {
        #roul-tools {
          font-size: 0.9em;
          width: auto;
          padding: 0 0.5em;
          left: 0;
        }
        #roulette {
          font-size: 1.7rem;
          top: 26em;
          left: -0.7em;
          transform: rotate(90deg);
          transform-origin: top;
        }
        .roul-num > .roul-value {
          transform: rotate(-90deg);
        }
        #wheel {
          left: 42rem;
          top: 10rem;
        }
        #wheel-wrap {
          left: 74rem;
          top: 3rem;
        }
        #roul-details {
          left: 38rem;
          top: 42rem;
        }
        #roul-bets {
          left: 38rem;
          top: 54rem;
        }
        #roul-games {
          left: 74rem;
          top: 11rem;
        }
        #roul-density {
          left: 74rem;
          top: 45.3rem;
        }
        #roul-mark {
          left: calc(80rem + 14vw);
          top: 2rem;
        }
      }

      @media screen and (max-width: 1160px) {
        #roul-mark {
          left: 79rem;
          top: 36.3rem;
        }
      }
    `);

    container.prepend(this);
    $('body > center').replaceWith(container);
  });

  // =============== [[ CASH ]]

  const roulCash = ((target) => {
    let cash = CASH;
    const goldNode = modules.HWM_new_header
      ? attempt($('.header-res'), el => el.lastChild)
      : ($('#ResourceAmount') || $('#top_res_table td:nth-child(2)'));

    return {
      get value() {
        return cash - roulBets.sum;
      },
      set value(val) {
        cash = val;
        this.update(val);
      },
      update(val) {
        const value = formatNum(val || this.value);
        target.textContent = value;
        if (goldNode) goldNode.textContent = value;
      },
      valueOf() {
        return this.value;
      }
    };
  })($('#roul-cash > span'));

  // =============== [[ DENSITY ]]

  const roulDensity = ((target) => {
    const {children} = target;
    const button = children[0].firstElementChild;
    let isLoading = false;

    return {
      __init__() {
        button.addEventListener('click', this.update);
      },
      clear() {
        children[1].innerHTML = '';
      },
      update() {
        if (isLoading) return;

        isLoading = true;
        button.classList.add('__loading');

        fetch.get(PATH).then((doc) => {
          if (!doc.URL.includes(PATH)) {
            throw logError.create(-1, 'Authorization error');
          }

          const elems = [...parseNode(templates.Density(doc)).children];
          children[1].replaceWith(elems[1]);
          children[2].replaceWith(elems[2]);

          isLoading = false;
          button.classList.remove('__loading');
        }).catch(handleError);

        function handleError(er) {
          isLoading = false;
          button.classList.remove('__loading');
          return logError.disconnect(er);
        }
      }
    };
  })($('#roul-density'));

  // =============== [[ BETS ]]

  const roulBets = ((target) => {
    const tabElems = [...target.firstElementChild.children];
    const contElems = [...target.children].slice(1);
    const bodyElems = contElems.map(el => el.firstElementChild);
    const sumElems = contElems.map(el => el.lastElementChild.lastElementChild);

    const bets1 = [];
    const bets2 = [];

    const getBets = ind => !ind ? bets1 : bets2;

    let tabIndex = 0;

    class Bet {
      constructor(id, value) {
        createBet.call(this, 0, {id, value});
      }
      static create({id, value}) {
        return parseNode(/*html*/`
          <div class="roul-bet roul-box-row">
            <div class="roul-bet__action" data-action="remove">&times;</div>
            <div class="roul-bet__action" data-action="multiply">&times;2</div>
            <div class="roul-bet__key roul-bet-key">${roulID.get(id)}</div>
            <div class="roul-bet__value roul-bet-value">${formatNum(value)}</div>
          </div>
        `);
      }
      static get activeBet() {
        return $('.__active', bodyElems[0]);
      }
      refresh(val) {
        this.value += val;
        this.target.lastElementChild.textContent = formatNum(this.value);
        roulCash.update();
        updateBetsSum();
      }
      remove() {
        this.target.remove();
        roulAPI.unselect(this.id);
        bets1.splice(bets1.indexOf(this), 1);
        roulCash.update();
        updateBetsSum();
      }
      multiply(meta) {
        if (meta) {
          const min = this.id === 'Red Snake' ? roulBets.MIN * 12 : roulBets.MIN;
          if (~~(this.value / 2) < min) return this.remove();
        } else if (!checkMoney(this.value)) return;

        this.refresh(meta ? -this.value / 2 >> 0 : this.value);
        this.select();
      }
      select() {
        attempt(Bet.activeBet, inactiveElem);
        activeElem(this.target);
        this.target.scrollIntoViewIfNeeded(false);
      }
    }

    class Bet2 {
      constructor(id, value, status = 0) {
        createBet.call(this, 1, {id, value, status});
      }
      static create({id, value, status}) {
        return parseNode(/*html*/`
          <div class="roul-bet roul-bet2 roul-box-row" data-status="${status}">
            <div class="roul-bet__status"></div>
            <div class="roul-bet__key roul-bet-key">${roulID.get(id)}</div>
            <div class="roul-bet__value roul-bet-value">${formatNum(value)}</div>
          </div>
        `);
      }
      win() {
        this.target.classList.add('__win');
        this.target.scrollIntoViewIfNeeded(false);
      }
      submit() {
        this.target.dataset.status = this.status = 0;
        return formData.send(this);
      }
      onLoadEnd(status) {
        this.target.dataset.status = this.status = status;
        recount();
      }
    }

    function createBet(ind, data) {
      Object.assign(this, data);
      this.target = [Bet, Bet2][ind].create(data);
      getBets(ind).push(this);
      bodyElems[ind].prepend(this.target);
    }

    function clearBets(ind = 0) {
      getBets(ind).splice(0);
      bodyElems[ind].innerHTML = '';
      sumElems[ind].textContent = '0';
    }

    function updateBetsSum(ind = 0) {
      const sum = reduceBets(getBets(ind));
      sumElems[ind].textContent = formatNum(sum);
    }

    function recount() {
      const {completed} = this.data;
      const countElem = tabElems[1].firstElementChild;
      countElem.textContent = `${completed.length} / ${bets2.length}`;
    }

    return {
      __init__() {
        recount = recount.bind(this);

        target.addEventListener('click', (e) => {
          e.stopPropagation();
          if (locked) return;

          const trg = e.target;

          if (trg.matches('.roul-bets__tab')) {
            this.switchTab(getElemIndex(trg));
            return;
          }

          if (trg.matches('.roul-bets__action')) {
            this[trg.dataset.action]();
            return;
          }

          if (trg.matches('.roul-bet__action')) {
            const bet = this.find('target', trg.parentNode);
            bet[trg.dataset.action](e.ctrlKey);
            return;
          }

          if (trg.matches('[data-status="2"] > .roul-bet__status')) {
            const bet = this.find('target', trg.parentNode, bets2);
            return bet.submit().catch(er => {
              bet.onLoadEnd(2);
              return logError.disconnect(er);
            });
          }
        });

        if (!initialBets.length) return;

        initialBets.forEach((bet) => new Bet2(bet.id, bet.value, 1));
        this.switchTab(1);
        updateBetsSum(1);
        recount();
      },
      MIN,
      MAX,
      lastBets: [],
      get data() {
        return {
          get completed() {
            return bets2.filter((bet) => bet.status === 1);
          },
          get failed() {
            return bets2.filter((bet) => bet.status === 2);
          }
        };
      },
      get sum() {
        return reduceBets(bets1) + reduceBets(bets2);
      },
      get multiplier() {
        const multiplier = roulTools.activeCoinValue;
        const value = multiplier === 'MAX'
          ? this.MAX - this.sum
          : multiplier === 'MIN'
          ? this.MIN
          : +multiplier;

        return clamp(1, value, roulCash);
      },
      switchTab(ind) {
        if (tabIndex === ind) return;

        tabIndex = ind;
        inactiveElem(tabElems[ind ^ 1]);
        activeElem(tabElems[ind]);
        contElems[0].hidden = !!ind;
        contElems[1].hidden = !ind;
      },
      find(key, value, bets = bets1) {
        return bets.find(bet => bet[key] === value);
      },
      add() {
        const bet = new Bet(...arguments);
        bet.refresh(0);
        return bet;
      },
      cancel() {
        this.clearTableBets();
        roulCash.update();
      },
      accept() {
        const bets = bets1.map(bet => new Bet2(bet.id, bet.value));
        const snakeIndex = bets.findIndex(bet => bet.id === 'Red Snake');
        snakeIndex > 0 && bets.unshift(bets.splice(snakeIndex, 1)[0]);

        this.clearTableBets();
        this.switchTab(1);
        updateBetsSum(1);

        return this.acceptSequentially(bets).catch((er) => {
          bets.splice(0).forEach((bet) => bet.onLoadEnd(2));
          logError.disconnect(er);
          return er;
        });
      },
      acceptSequentially(bets) {
        return new Promise(function next(resolve, reject) {
          if (locked) bets.splice(0).forEach((bet) => bet.onLoadEnd(2));
          if (!bets.length) return resolve(roulBets.data.failed);

          const bet = bets.pop();
          return bet.submit()
            .then(() => wait(0.2))
            .then(() => next(resolve, reject))
            .catch((er) => {
              bet.onLoadEnd(2);
              return reject(er);
            });
        });
      },
      clearTableBets() {
        clearBets(0);
        roulAPI.clear();
      },
      clear() {
        clearBets(1);
        this.clearTableBets();
        recount();
      },
      updateMinMax(MIN, MAX) {
        Object.assign(this, { MIN, MAX })
        $('#roul-minbet').lastChild.textContent = formatNum(MIN);
        $('#roul-maxbet').lastChild.textContent = formatNum(MAX);
      }
    };
  })($('#roul-bets'));

  function mergeBets(bets, callback) {
    const data = new Map;

    bets.forEach((bet) => {
      const bet2 = data.get(bet.id);
      if (!bet2) return data.set(bet.id, bet);

      bet2.value += bet.value;
      callback && callback(bet2, bet);
    });

    return [...data.values()];
  }

  function rebet(bets) {
    const sum = bets.reduce((a, b) => a + b.value, 0);
    if (!(sum && checkMoney(sum))) return false;

    roulBets.switchTab(0);

    let lastBet = null;

    bets.forEach((bet, i) => {
      lastBet = roulBets.find('id', bet.id);
      if (lastBet) return lastBet.refresh(bet.value);

      roulAPI.select(bet.id);
      lastBet = roulBets.add(bet.id, bet.value);
    });

    lastBet.select();
    return true;
  }

  // =============== [[ GAMES ]]

  const roulGames = ((target) => {
    let activeGame = null;
    const infoElem = target.firstElementChild;
    const bodyElem = target.lastElementChild.firstElementChild;
    const [betsElem, footer] = infoElem.children;

    footer.appendChild(parseNode(getBetTemplate({
      id: 'Итого',
      value: 0,
      prize: 0,
    })));

    const allSumElem = footer.firstElementChild.lastElementChild;

    const toggleInfo = infoElem.classList.toggle.bind(
      infoElem.classList,
      '__shown'
    );

    const getDefaultGameData = elem => ({
      gameId: elem.search.slice(4),
      number: elem.textContent,
      totalBet: 0,
      totalPrize: 0
    });

    function updateSum(game) {
      allSumElem.dataset.bet = formatNum(game.totalBet);
      allSumElem.dataset.prize = formatNum(game.totalPrize);
    }

    function getBetTemplate(bet) {
      const value = formatNum(bet.value);
      const prize = formatNum(bet.prize);
      const selected = bet.prize ? ' __selected' : '';

      return /*html*/`
        <div class="roul-game-bet roul-box-row${selected}">
          <div class="roul-game-bet__key roul-bet-key">${roulID.get(bet.id)}</div>
          <div class="roul-game-bet__value roul-bet-value" data-bet="${value}" data-prize="${prize}"> / </div>
        </div>
      `;
    }

    function hwmGameToObject(table, data) {
      const bets = [];
      const rows = table.children;

      for (let i = 1; i < 30; i++) {
        const row = rows[i];

        if (i === 1) {
          if (row.textContent.startsWith('Все')) break;
          i++;
          continue;
        }

        if (row.childElementCount === 3) {
          data.totalBet = parseNum(row.firstElementChild.textContent);
          data.totalPrize = parseNum(row.lastElementChild.textContent);
          break;
        }

        const nodes = row.children;
        const id = nodes[2].textContent;
        const value = parseNum(nodes[0].textContent);
        const prize = parseNum(nodes[3].textContent);
        bets.push({ id, value, prize });
      }

      data.bets = !bets.length ? null : mergeBets(bets, (bet1, bet2) => {
        bet1.prize += bet2.prize;
      });

      return data;
    }

    return {
      __init__() {
        let isLoading = false;

        target.addEventListener('mouseleave', this.hide.bind(this));

        bodyElem.addEventListener('mouseover', (e) => {
          e.stopPropagation();

          const trg = e.target;
          trg.matches('.roul-game[href]') && this.showGame(trg);
        });

        bodyElem.nextElementSibling.addEventListener('click', (e) => {
          const button = e.currentTarget;
          if (isLoading) return;

          isLoading = true;
          const add = ([gameId, number]) => this.add({gameId, number});

          this.loadPrevGames().then((games) => {
            button.remove();
            bodyElem.innerHTML = '';
            games.reverse().forEach(add);
          }).catch((er) => {
            isLoading = false;
            logError.disconnect(er);
          });
        });

        const repeatButton = parseNode(
          '<div id="roul-game-repeat" title="Повторить ставки">&larr;</div>'
        );
        footer.prepend(repeatButton);

        repeatButton.addEventListener('click', () => {
          if (!activeGame || locked) return;
          attempt(activeGame.__gameData.bets, rebet);
        });
      },
      get lastGame() {
        return attempt($('a[href]', bodyElem), elem => {
          return elem.__gameData || getDefaultGameData(elem);
        });
      },
      hide() {
        attempt(activeGame, inactiveElem);
        activeGame = null;
        toggleInfo(false);
      },
      add(data) {
        const {gameId, number} = data;
        const create = parseNode.bind(null, '<a class="roul-game"></a>');
        const elems = [...Array(3)].map(create);
        const elem = elems[RED_NUMBERS.includes(+number) ? 2 : !!+number ^ 1];

        elem.textContent = number;
        elem.href = `/inforoul.php?id=${gameId}`;
        elem.target = '_blank';

        bodyElem.parentNode.scrollTop = 0;
        bodyElem.prepend(...elems);

        if (data.hasOwnProperty('bets')) elem.__gameData = data;
      },
      async showGame(elem) {
        attempt(activeGame, inactiveElem);
        activeGame = elem;

        const gameData = elem.__gameData
          || await this.loadGame(elem).catch(er => er);

        if (gameData instanceof Error) {
          elem.classList.remove('__loading');
          logError.disconnect(gameData);
          return;
        }

        if (elem !== activeGame) return;

        if (gameData.bets) {
          betsElem.innerHTML = gameData.bets.map(getBetTemplate).join('');
          updateSum(gameData);
        } else betsElem.innerHTML = '';

        activeElem(elem);
        toggleInfo(true);
      },
      async loadGame(elem) {
        elem.classList.add('__loading');

        const data = getDefaultGameData(elem);
        const doc = await fetch.get(`/inforoul.php?id=${data.gameId}`);
        const table = $('table.wbwhite > tbody', doc);

        elem.classList.remove('__loading');
        return (elem.__gameData = hwmGameToObject(table, data));
      },
      async loadPrevGames() {
        const doc = await fetch.get('/allroul.php');

        return $$('table.wb > tbody > tr', doc).slice(1).map(row => {
          const gameId = row.firstElementChild.firstElementChild.search.slice(4);
          const number = row.lastElementChild.textContent.trim();
          return [gameId, number];
        });
      }
    };
  })($('#roul-games'));

  // =============== [[ ROULETTE ]]

  const roulAPI = ((target, data) => {
    const targets = document.getElementsByClassName('__target');
    const styles = [0, 0].map(() => document.createElement('style'));
    container.firstElementChild.after(...styles);

    const getID = el => el.dataset.id;
    const getCSS = ind => `{background-color: var(--highlight-color-${ind})}`;

    const getHighlightedCSS = memoize(id => {
      return getSelectors(id).join(',') + getCSS(1);
    });

    function getSelectors(id) {
      const items = data[id].items;
      return items.map(item => `[data-id="${getID(item)}"]`);
    }

    function reselectTargetItems() {
      const selectrors = Array.from(targets, el => getSelectors(getID(el)));
      const css = !selectrors.length ? '' : selectrors.join(',') + getCSS(2);
      styles[0].textContent = css;
    }

    return {
      target,
      data,
      find: (id) => data[id],
      highlight(id) {
        styles[1].textContent = getHighlightedCSS(id);
      },
      unhighlight() {
        styles[1].textContent = '';
      },
      select(id) {
        data[id].target.classList.add('__target');
        reselectTargetItems();
      },
      unselect(id) {
        data[id].target.classList.remove('__target');
        reselectTargetItems();
      },
      clear() {
        styles[0].textContent = styles[1].textContent = '';
        [...targets].forEach(el => el.classList.remove('__target'));
      }
    };
  })($('#roulette'), {});

  const roulette = ((target, that) => {
    const elems = $$('.roul-area');

    ['00', '0'].forEach(num => setData(elems.shift()));

    function setData(target, isOutside = false) {
      const {id} = target.dataset;
      const factor = id[0] === 'S' ? 36 : /Col|Doz/.test(id) ? 3 : 2;
      const dispatcher = isOutside ? outsideData[id] : (() => [target]);
      that[id] = createData(id, factor, target, dispatcher);
    }

    function createData(id, factor, target, dispatcher) {
      return {
        id,
        factor,
        target,
        get items() {
          return dispatcher();
        }
      };
    }

    function dispatch(callback) {
      let items = null;
      return () => items || (items = callback());
    }

    const insideElems = elems.slice(0, 36);
    const outsideElems = elems.slice(36);

    const boundSlice = (...values) => elems.slice.bind(insideElems, ...values);
    const boundFilter = fn => elems.filter.bind(insideElems, fn);

    const not = fn => (...args) => !fn(...args);
    const orderIsEqual = (div, rem) => (x, i) => i % div === rem;
    const isEven = orderIsEqual(2, 1);
    const isRed = el => RED_NUMBERS.includes(+el.dataset.id.slice(12));

    const outsideData = {
      '1st Column': dispatch(boundFilter(orderIsEqual(3, 2))),
      '2nd Column': dispatch(boundFilter(orderIsEqual(3, 1))),
      '3rd Column': dispatch(boundFilter(orderIsEqual(3, 0))),
      '1st Dozen': dispatch(boundSlice(0, 12)),
      '2nd Dozen': dispatch(boundSlice(12, 24)),
      '3rd Dozen': dispatch(boundSlice(24)),
      '1-18 Half': dispatch(boundSlice(0, 18)),
      'EVEN': dispatch(boundFilter(isEven)),
      'RED': dispatch(boundFilter(isRed)),
      'BLACK': dispatch(boundFilter(not(isRed))),
      'ODD': dispatch(boundFilter(not(isEven))),
      '19-36 Half': dispatch(boundSlice(18))
    };

    roulAPI.__init__ = function() {
      insideElems.forEach(elem => setData(elem));
      outsideElems.forEach(elem => setData(elem, true));
      initChips(this.data);

      target.addEventListener('mouseover', (e) => {
        e.stopPropagation();
        const trg = e.target;
        const {id} = trg.dataset;
        id && this.highlight(id);

        if (id && trg.matches('.__target')) setElemMoveHandler(id, trg);
        else roulTip.hide();
      });

      target.addEventListener('mouseleave', (e) => {
        e.stopPropagation();
        roulTip.hide();
        this.unhighlight();
      });

      target.addEventListener('click', (e) => {
        e.stopPropagation();
        if (locked) return;

        const elem = e.target.closest('[data-id]');
        if (!elem) return;

        const meta = e.ctrlKey;
        const {id} = elem.dataset;
        const {multiplier} = roulBets;
        const isMaxCoin = roulTools.activeCoinValue === 'MAX';
        const isSnake = id === 'Red Snake';
        const minSnakeBet = roulBets.MIN * 12;
        const bet = roulBets.find('id', id);

        if (!bet) {
          if (meta) return;

          const value = isSnake
            ? clamp(minSnakeBet, multiplier * (isMaxCoin ? 1 : 12))
            : clamp(roulBets.MIN, multiplier);

          if (!checkMoney(value)) return;

          roulBets.add(id, value).select();
          return after.call(this);
        }

        if (!testBet.call(this, isSnake ? minSnakeBet : roulBets.MIN)) return;

        bet.refresh(meta ? -multiplier : multiplier);
        bet.select();
        after.call(this);

        function testBet(min) {
          if (!meta) return checkMoney(multiplier);
          if (!(isMaxCoin || bet.value - multiplier < min)) return true;

          bet.remove();
          roulTip.hide();
          this.unselect(id);
        }

        function after() {
          this.select(id);
          roulBets.switchTab(0);
          setElemMoveHandler(id, elem);
          elem.onmousemove(e);
        }
      });

      function setElemMoveHandler(id, elem) {
        elem.onmousemove = (e) => {
          elem.matches('.__target') && roulTip.show(e, id);
        };
        elem.onmouseleave = () => {
          elem.onmouseleave = elem.onmousemove = null;
        };
      }
    };

    return that;
  })(roulAPI.target, roulAPI.data);

  function initChips(that) {
    const factors = {
      Split: 18,
      Street: 12,
      Numbers: 12,
      Corner: 9,
      Sixline: 6
    };

    function getItems(nums) {
      return nums.map(num => that[`Straight up ${num}`].target);
    }

    function add(target, id, factor, nums) {
      let items = null;

      that[id] = {
        id,
        factor,
        target,
        get items() {
          return items || (items = getItems(nums));
        }
      };
    }

    function streetline(target, id, factor, start) {
      const nums = [...Array(factor === 6 ? 6 : 3)].map((x, i) => start - i);
      return add(target, id, factor, nums);
    }

    const chips = $$('.roul-chip');
    add(chips.pop(), 'Red Snake', 3, [1, 5, 9, 12, 14, 16, 19, 23, 27, 30, 32, 34]);

    chips.forEach((chip) => {
      const {id} = chip.dataset;
      const key = id.split(' ', 1)[0];
      const nums = id.match(/\d+/g);

      switch (key) {
        case 'Street':
        case 'Sixline':
          return streetline(chip, id, factors[key], +nums.pop());
        case 'Split':
        case 'Corner':
        case 'Numbers':
          return add(chip, id, nums.length === 5 ? 7 : factors[key], nums);
      }
    });
  }

  // =============== [[ TIP ]]

  const roulTip = ((target) => {
    const htmlElem = document.documentElement;
    const getClientWidth = () => htmlElem.clientWidth;

    return {
      move(x, y) {
        target.style.transform = `translate(${~~x}px, ${~~y}px)`;
      },
      show(e, id) {
        target.textContent = roulID.get(id);
        target.dataset.value = formatNum(roulBets.find('id', id).value);

        const maxX = getClientWidth() - target.offsetWidth - 5;
        const maxY = view.innerHeight - target.offsetHeight;
        const x = Math.min(e.clientX + 10, maxX);
        const y = Math.min(e.clientY + 10, maxY);

        this.move(x, y);
      },
      hide() {
        if (this.shown) target.textContent = '';
      },
      get shown() {
        return !!target.offsetWidth;
      }
    };
  })($('#roul-tip'));

  // =============== [[ WHEEL ]]

  const roulWheel = ((target) => {
    let isLoading = false;
    let isNumDefined = false;
    let prevSegment = null;

    const segments = [...$('#wheel__segments').children];
    const altSegments = segments.map((el) => el.cloneNode());
    const wheelClasses = target.classList;
    const wheelMain = target.firstElementChild;
    const wheelStyle = wheelMain.style;
    const ballStyle = wheelMain.nextElementSibling.style;
    const infoElem = target.lastElementChild;

    const fix3 = (num) => +num.toFixed(3);
    const getTurn = (style) => parseFloat(style.getPropertyValue('--turn') || '0');
    const setTurn = (style, turn) => style.setProperty('--turn', fix3(turn));
    const recalcTurn = (style, plus) => setTurn(style, plus + getTurn(style));
    const resetTurnToMin = (style) => setTurn(style, getTurn(style) % 1);

    const getSegment = memoize(number => {
      const index = segments.findIndex(el => el.dataset.num === number);
      const [r, g, b] = ['#ba3535', '#43914b', '#222'];
      return {
        number,
        color: !+number ? g : RED_NUMBERS.includes(+number) ? r : b,
        turn: getTurn(segments[index].style),
        segment: segments[index],
        altSegment: altSegments[index]
      };
    });

    const wrapper = parseNode(/*html*/`
      <div id="wheel-wrap">
        <div id="wheel" class="mini-wheel">
          <div id="wheel__main">
            <div id="wheel__segments"></div>
          </div>
        </div>
      </div>
    `);

    const miniWheel = wrapper.firstElementChild;
    const miniWheelStyle = miniWheel.firstElementChild.style;

    miniWheel.firstElementChild.firstElementChild.append(...altSegments);
    target.after(wrapper);

    function calcBallTurn() {
      const turn = getTurn(wheelStyle);
      const val = ~~turn + fix3(Math.ceil(turn) - turn);
      setTurn(ballStyle, val - prevSegment.turn);
    }

    function waitForResult(endTime) {
      if (isNumDefined) return stoping();

      [wheelStyle, ballStyle].forEach((style) => recalcTurn(style, 2));
      !(isLoading || endTime - Date.now() > 0) && defineNumber();
      setTimeout(waitForResult, 2e3, endTime);
    }

    async function defineNumber() {
      isLoading = true;
      const {gameId} = formData;
      const doc = await fetch.get(`/inforoul.php?id=${gameId}`).catch(er => er);

      if (doc instanceof Error) {
        return logError.disconnect(doc, {
          text: 'Попробовать снова (закончить раунд)?',
          callback() {
            roulAlert.hide();
            defineNumber();
          }
        });
      }

      const elem = $('td.wbwhite > div:last-child', doc);
      if (!elem) return (isLoading = false);

      isNumDefined = true;
      const number = elem.textContent.split(' ').pop();
      prevSegment = getSegment(number);
      console.log(`${gameId}: ${number}`);
    }

    function stoping() {
      wheelClasses.add('__spinending');
      miniWheel.classList.add('__spinending');

      recalcTurn(wheelStyle, 2 + Math.random() * 0.2);
      calcBallTurn();
      setTurn(miniWheelStyle, 2 - prevSegment.turn);

      wrapper.style.visibility = 'visible';
      wait(5).then(roulWheel.endSpin);
    }

    function spinAfter(game) {
      const scoreElem = infoElem.lastElementChild;
      scoreElem.hidden = !game.bets;
      scoreElem.dataset.score = formatNum(game.totalPrize);

      roulGames.add(game);
      roulCash.value += game.totalPrize;
      wait(3.5).then(next);

      function next() {
        return loadNextGame().then(startGame).catch(er => {
          logError.disconnect(er, {
            text: 'Попробовать снова (начать следующую игру)?',
            callback() {
              roulAlert.hide();
              next();
            }
          });
        });
      }
    }

    return {
      onGameStart() {
        wrapper.style.visibility = '';
        setTurn(miniWheelStyle, 0);
        [prevSegment.segment, prevSegment.altSegment].forEach(inactiveElem);
      },
      startSpin(endTime) {
        locked = true;
        isNumDefined = isLoading = false;
        document.title = 'Roulette | spining . . .';

        container.classList.add('__locked');
        wheelClasses.add('__spining');

        roulBets.clearTableBets();
        roulBets.switchTab(1);

        setTimeout(waitForResult, 50, endTime);
      },
      endSpin() {
        wheelClasses.remove('__spining', '__spinending');
        miniWheel.classList.remove('__spinending');
        [wheelStyle, ballStyle].forEach(resetTurnToMin);
        [prevSegment.segment, prevSegment.altSegment].forEach(activeElem);

        document.title = `Roulette | Number ${prevSegment.number}`;
        infoElem.firstElementChild.textContent = prevSegment.number;
        infoElem.style.backgroundColor = prevSegment.color;

        container.classList.add('__result');
        endGame(prevSegment.number, spinAfter);
      }
    };
  })($('#wheel'));

  // =============== [[ ALERT ]]

  const roulAlert = ((target) => {
    const body = target.firstElementChild.firstElementChild;
    const button = body.nextElementSibling;

    function onCloseHandler({key}) {
      return key === 'Escape' && this.hide();
    }

    return {
      __init__() {
        onCloseHandler = onCloseHandler.bind(this);

        target.addEventListener('click', (e) => {
          e.stopPropagation();
          const trg = e.target;
          const {action} = trg.dataset;
          action && this[action]();
        });
      },
      show(html) {
        body.innerHTML = html;
        target.classList.add('__shown');
        document.addEventListener('keydown', onCloseHandler);
        setTimeout(button.focus.bind(button), 50);
      },
      hide() {
        target.classList.remove('__shown');
        document.removeEventListener('keydown', onCloseHandler);
      },
      notEnoughMoney() {
        this.show('Недостаточно денег!');
      },
      outOfRange() {
        const sum = `<b style="color:gold">${formatNum(roulBets.MAX)}</b>`;
        this.show(`Сумма ставок не может превышать ${sum}`);
      }
    };
  })($('#roul-alert'));

  // =============== [[ TOOLS ]]

  const roulTools = ((target) => {
    let activeCoinValue = '100';

    return {
      __init__() {
        target.addEventListener('click', (e) => {
          e.stopPropagation();
          if (locked) return;

          const trg = e.target;
          const {action} = trg.dataset;
          this[action] && this[action](trg);
        });
      },
      get activeCoin() {
        return $('.roul-coin.__active', target);
      },
      get activeCoinValue() {
        return activeCoinValue;
      },
      setMultiplier(coin) {
        attempt(this.activeCoin, inactiveElem);
        activeElem(coin);
        activeCoinValue = coin.dataset.coin;
      },
      rebet() {
        return rebet(roulBets.lastBets);
      }
    };
  })($('#roul-tools'));

  // =============== [[ TIMER ]]

  const roulTimer = createTimer($('#roul-timer'), {
    onTick(timer) {
      document.title = `Roulette | ${timer}`;
      timer.value <= 10 && timer.target.classList.add('__blink');
    },
    onStop(timer) {
      timer.target.classList.remove('__blink');
      roulWheel.startSpin(timer.endTime);
    }
  });

  function createTimer(target, props) {
    const format = (num) => num > 9 ? num : `0${num}`;
    const getValue = (sec) => Math.max(0, sec - 10);

    let playState = false;
    let startTime = 0;
    let endTime = 0;
    let timeVal = 0;
    let timerId = 0;

    function stringify(value) {
      const min = format(value / 60 >> 0);
      const sec = format(value % 60);
      return `${min}:${sec}`;
    }

    function clear() {
      playState = false;
      clearTimeout(timerId);
    }

    function ticking() {
      timeVal = getValue((endTime - Date.now()) / 1e3 >> 0);
      target.textContent = stringify(timeVal);
      props.onTick(that);
      timerId = setTimeout(ticking, 1e3);

      if (!timeVal) {
        clear();
        props.onStop(that);
      }
    }

    function resetTimeStamps() {
      startTime = Date.now();
      endTime = startTime + formData.restSeconds * 1e3;
    }

    const that = {
      target,
      get value() {
        return timeVal;
      },
      get endTime() {
        return endTime;
      },
      start() {
        if (playState) return;
        if (!startTime) resetTimeStamps();

        playState = true;
        ticking();
      },
      pause() {
        return playState && clear();
      },
      reset() {
        resetTimeStamps();
        this.pause();
        this.start();
      },
      toString() {
        return stringify(this.value);
      }
    };

    return that;
  }

  // =========================

  function endGame(number, callback) {
    const numElem = roulette[`Straight up ${number}`].target;
    numElem.id = 'winner';

    const bets = mergeBets(roulBets.data.completed);

    const game = {
      number,
      gameId: formData.gameId,
      totalBet: reduceBets(bets),
      totalPrize: 0,
      bets: null
    };

    if (!bets.length) return callback(game);

    game.bets = bets.map((bet) => ({
      id: bet.id,
      value: bet.value,
      prize: ~~attempt(roulette[bet.id], data => {
        const test = data.items.includes(numElem);
        test && bet.win();
        return test ? bet.value * data.factor : 0;
      })
    }));

    game.totalPrize = reduceBets(game.bets, 'prize');
    callback(game);
  }

  function startGame() {
    locked = false;

    attempt(roulGames.lastGame.bets, bets => (roulBets.lastBets = [...bets]));
    attempt($('#winner'), el => el.removeAttribute('id'));
    container.classList.remove('__result', '__locked');

    roulBets.clear();
    roulDensity.clear();
    roulTimer.reset();
    roulWheel.onGameStart();
  }

  function loadNextGame() {
    return fetch.get(PATH).then((doc) => {
      if (!doc.URL.includes(PATH)) {
        throw logError.create(-1, 'Authorization error');
      }

      const [min, max, cash] = getMinMaxCash(doc);
      roulBets.MAX !== max && roulBets.updateMinMax(min, max);
      roulCash.value = cash;
      formData.update(doc.forms[0]);
    });
  }

  // =========================

  container.addEventListener('mouseover', (e) => {
    e.stopPropagation();

    const trg = e.target;
    if (!trg.matches('.roul-bet-key')) return;

    const key = trg.textContent;
    const id = roulID.keys.find(k => roulID[k] === key) || key;
    const roulElem = roulette[id].target;

    roulAPI.highlight(id);

    trg.onmouseleave = () => {
      trg.onmouseleave = null;
      roulAPI.unhighlight();
    };
  });

  document.addEventListener('keydown', (e) => {
    if (locked || !/Numpad[1-9]/.test(e.code)) return;
    e.preventDefault();
    roulTools.setMultiplier($(`.roul-coin:nth-child(${e.code.slice(6)})`));
  });

  // Чтобы не забивать асинхронный стек и память
  // глупым тяжеловесным админским кодом
  if (!modules.HWM_new_header) stopHeartWhenNeeded();

  function stopHeartWhenNeeded() {
    if (!view.hasOwnProperty('top_line_draw_canvas_heart')) return;

    const target = $('#health_amount');
    const nativeSetTimeout = view.setTimeout;
    const hwmHeartBeatFnName = 'run_top_line_heart_timer';

    view.setTimeout = function(handler, delay, ...args) {
      if (!handler) return nativeSetTimeout(null);
      if (handler.name === hwmHeartBeatFnName) return heartBeat(...arguments);
      return nativeSetTimeout(handler, delay, ...args);
    };

    function heartBeat() {
      if (target.textContent === '100') {
        view.setTimeout = nativeSetTimeout;
        return nativeSetTimeout(null);
      } else return nativeSetTimeout(...arguments);
    }
  }

  // =========================

  roulTools.__init__();
  roulBets.__init__();
  roulGames.__init__();
  roulDensity.__init__();
  roulAlert.__init__();
  roulAPI.__init__();

  roulTimer.start();
})(document.defaultView);