Dreadcast Development Kit

Development kit to ease Dreadcast scripts integration.

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        Dreadcast Development Kit
// @namespace   Dreadcast
// @match       https://www.dreadcast.net/Main
// @version     1.1.8
// @author      Pelagia/Isilin
// @description Development kit to ease Dreadcast scripts integration.
// @license     https://github.com/Isilin/dreadcast-scripts?tab=GPL-3.0-1-ov-file
// @grant       GM_xmlhttpRequest
// @grant       GM_addStyle
// @grant       GM_setValue
// @grant       GM_getValue
// @grant       GM_deleteValue
// @grant       GM_listValues
// @connect     docs.google.com
// @connect     googleusercontent.com
// @connect     sheets.googleapis.com
// @connect     raw.githubusercontent.com
// ==/UserScript==

// ===== JQuery utilities =====

$.fn.insertAt = function (index, element) {
  var lastIndex = this.children().size();
  if (index < 0) {
    index = Math.max(0, lastIndex + 1 + index);
  }
  this.append(element);
  if (index < lastIndex) {
    this.children().eq(index).before(this.children().last());
  }
  return this;
};

// ===== Lib =====

const Util = {
  guard: (condition, message) => {
    if (!condition) throw new Error(message);
    return;
  },

  deprecate: (name, replacement) => {
    console.warn(
      name +
        ': this function has been deprecated and should not be used anymore.' +
        (replacement && replacement !== ''
          ? 'Prefer: ' + replacement + '.'
          : ''),
    );
  },

  isArray: (o, optional = false) =>
    $.type(o) === 'array' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isString: (o, optional = false) =>
    $.type(o) === 'string' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isBoolean: (o, optional = false) =>
    $.type(o) === 'boolean' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isNumber: (o, optional = false) =>
    $.type(o) === 'number' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isFunction: (o, optional = false) =>
    $.type(o) === 'function' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isDate: (o, optional = false) =>
    $.type(o) === 'date' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isError: (o, optional = false) =>
    $.type(o) === 'error' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isRegex: (o, optional = false) =>
    $.type(o) === 'regexp' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isObject: (o, optional = false) =>
    $.type(o) === 'object' ||
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')),

  isColor: (o, optional = false) => {
    if (optional && ($.type(o) === 'undefined' || $.type(o) === 'null'))
      return true;
    else {
      const colors = ['rouge', 'bleu', 'vert', 'jaune'];
      return (
        $.type(o) === 'string' &&
        (colors.includes(o) ||
          o.match(/^[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{4}|[0-9a-f]{3}$/gi))
      );
    }
  },

  isJQuery: (o, optional = false) =>
    (optional && ($.type(o) === 'undefined' || $.type(o) === 'null')) ||
    o instanceof $,

  guardArray: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isArray(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be an array.`,
    ),

  guardString: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isString(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be a string.`,
    ),

  guardBoolean: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isBoolean(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be a boolean.`,
    ),

  guardNumber: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isNumber(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be a number.`,
    ),

  guardFunction: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isFunction(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be a a function.`,
    ),

  guardDate: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isDate(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be a date.`,
    ),

  guardError: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isError(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be an error.`,
    ),

  guardRegex: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isRegex(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be a regex.`,
    ),

  guardObject: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isObject(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be an object.`,
    ),

  guardColor: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isColor(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be a color.`,
    ),

  guardJQuery: (context, name, parameter, optional = false) =>
    Util.guard(
      Util.isJQuery(parameter, optional),
      `${context}: '${name}' ${
        optional ? 'optional ' : ''
      }parameter should be a jQuery element.`,
    ),

  isGame: () => window.location.href.includes('https://www.dreadcast.net/Main'),

  isForum: () =>
    window.location.href.includes('https://www.dreadcast.net/Forum') ||
    window.location.href.includes('https://www.dreadcast.net/FAQ'),

  isEDC: () => window.location.href.includes('https://www.dreadcast.net/EDC'),

  isWiki: () => window.location.href.includes('http://wiki.dreadcast.eu/wiki'),

  getContext: () => {
    return Util.isGame()
      ? 'game'
      : Util.isForum()
      ? 'forum'
      : Util.isEDC()
      ? 'edc'
      : 'wiki';
  },
};

// ===== Overwrite DC functions =====

if (Util.isGame() && MenuChat.prototype.originalSend === undefined) {
  MenuChat.prototype.originalSend = MenuChat.prototype.send;
  MenuChat.prototype.sendCallbacks = [];
  MenuChat.prototype.afterSendCallbacks = [];
  MenuChat.prototype.send = function () {
    const $nextFn = () => true;
    const $abortFn = () => false;
    const $message = $('#chatForm .text_chat').val();
    const $res = this.sendCallbacks.every((callback) =>
      callback($message, $nextFn, $abortFn),
    );
    if (!$res) {
      throw new Error('MenuChat.prototype.send: Error on sending message.');
    }

    this.originalSend();

    this.afterSendCallbacks.every((callback) => callback($message));
  };
  MenuChat.prototype.onSend = (callback) => {
    Util.guard(
      Util.isGame(),
      'MenuChat.prototype.onSend: this function should be called in Game only.',
    );
    MenuChat.prototype.sendCallbacks.push(callback);
  };
  MenuChat.prototype.onAfterSend = (callback) => {
    Util.guard(
      Util.isGame(),
      'MenuChat.prototype.onSend: this function should be called in Game only.',
    );
    MenuChat.prototype.afterSendCallbacks.push(callback);
  };
}

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

const DC = {};

DC.LocalMemory = {
  init: (label, defaultValue) => {
    const $currentVal = GM_getValue(label);
    if ($currentVal === undefined) {
      GM_setValue(label, defaultValue);
      return defaultValue;
    } else {
      return $currentVal;
    }
  },

  set: (label, value) => GM_setValue(label, value),

  get: (label) => GM_getValue(label),

  delete: (label) => GM_deleteValue(label),

  list: () => GM_listValues(),
};

DC.Style = {
  apply: (css) => {
    Util.guardString('DC.Style.apply', 'css', css);

    if (typeof GM_addStyle !== 'undefined') {
      GM_addStyle(css);
    } else {
      let $styleNode = document.createElement('style');
      $styleNode.appendChild(document.createTextNode(css));
      (document.querySelector('head') || document.documentElement).appendChild(
        $styleNode,
      );
    }
  },
};

DC.TopMenu = {
  get: () => {
    Util.guard(
      Util.isGame(),
      'MenuChat.prototype.onSend: this function should be called in Game only.',
    );
    return $('.menus');
  },

  add: (element, index = 0) => {
    Util.guard(
      Util.isGame(),
      'MenuChat.prototype.onSend: this function should be called in Game only.',
    );
    Util.guardJQuery('DC.TopMenu.add', 'element', element);
    Util.guardNumber('DC.TopMenu.add', 'index', index);

    const $dom = DC.TopMenu.get();
    if (index === 0) {
      $dom.prepend(element);
    } else {
      $dom.insertAt(index, element);
    }
  },
};

DC.UI = {
  Separator: () => $('<li class="separator" />'),

  Menu: (label, fn) => {
    Util.guardString('DC.UI.Menu', 'label', label);
    Util.guardFunction('DC.UI.Menu', 'fn', fn);

    return $(`<li id="${label}" class="couleur5">${label}</li>`).bind(
      'click',
      fn,
    );
  },

  SubMenu: (label, fn, separatorBefore = false) => {
    Util.guardString('DC.UI.SubMenu', 'label', label);
    Util.guardFunction('DC.UI.SubMenu', 'fn', fn);
    Util.guardBoolean('DC.UI.SubMenu', 'separatorBefore', separatorBefore);

    return $(
      `<li class="link couleur2 ${
        separatorBefore ? 'separator' : ''
      }">${label}</li>`,
    ).bind('click', fn);
  },

  DropMenu: (label, submenu) => {
    Util.guardString('DC.UI.DropMenu', 'label', label);
    Util.guardArray('DC.UI.DropMenu', 'submenu', submenu);

    const $label = label + '▾';

    const $list = $('<ul></ul>');
    submenu.forEach(($submenu) => {
      $($list).append($submenu);
    });

    return $(
      `<li id="${label}" class="parametres couleur5 right hover" onclick="$(this).find('ul').slideDown();">${$label}</li>`,
    ).append($list);
  },

  addSubMenuTo: (name, element, index = 0) => {
    Util.guard(
      Util.isGame(),
      'MenuChat.prototype.onSend: this function should be called in Game only.',
    );
    Util.guardString('DC.UI.addSubMenuTo', 'name', name);
    Util.guardJQuery('DC.UI.addSubMenuTo', 'element', element);
    Util.guardNumber('DC.UI.addSubMenuTo', 'index', index);

    const $menu = $(`.menus li:contains("${name}") ul`);

    if (index === 0) {
      $menu.prepend(element);
    } else {
      $menu.insertAt(index, element);
    }
  },

  TextButton: (id, label, fn) => {
    Util.guardString('DC.UI.TextButton', 'id', id);
    Util.guardString('DC.UI.TextButton', 'label', label);
    Util.guardFunction('DC.UI.TextButton', 'fn', fn);

    return $(`<div id="${id}" class="btnTxt">${label}</div>`).bind('click', fn);
  },

  Button: (id, label, fn) => {
    Util.guardString('DC.UI.Button', 'id', id);
    Util.guardString('DC.UI.Button', 'label', label);
    Util.guardFunction('DC.UI.Button', 'fn', fn);

    return $(
      `<div id="${id}" class="btn add link infoAide"><div class="gridCenter">${label}</div></div>`,
    ).bind('click', fn);
  },

  ColorPicker: (id, value, fn) => {
    Util.guardString('DC.UI.ColorPicker', 'id', id);
    Util.guardString('DC.UI.ColorPicker', 'value', value);
    Util.guardFunction('DC.UI.ColorPicker', 'fn', fn);

    return $(`<input id="${id}" value="${value}"`).bind('click', (e) =>
      fn(e.target.value),
    );
  },

  Tooltip: (text, content) => {
    Util.guardString('DC.UI.Tooltip', 'text', text);
    Util.guardJQuery('DC.UI.Tooltip', 'content', content);

    DC.Style.apply(`
        .tooltip {
          position: relative;
          display: inline-block;
        }
        .tooltip .tooltiptext {
          visibility: hidden;
          background-color: rgba(24,24,24,0.95);
          color: #fff;
          text-align: center;
          padding: 5px;
          border-radius: 6px;
          position: absolute;
          z-index: 1;
          font-size: 1rem;
        }
        .tooltip:hover .tooltiptext {
          visibility: visible;
        }
      `);

    return $(`<div class="tooltip">
        <span class="tooltiptext">${text}</span>
        </div>`).prepend(content);
  },

  Checkbox: (id, defaultEnable, onAfterClick) => {
    Util.guardString('DC.UI.Checkbox', 'id', id);
    Util.guardBoolean('DC.UI.Checkbox', 'defaultEnable', defaultEnable);
    Util.guardFunction('DC.UI.Checkbox', 'onAfterClick', onAfterClick);

    DC.Style.apply(`
        .dc_ui_checkbox {
          cursor: pointer;
          width: 30px;
          height: 18px;
          background: url(../../../images/fr/design/boutons/b_0.png) 0 0 no-repeat;
        }

        .dc_ui_checkbox_on {
          background: url(../../../images/fr/design/boutons/b_1.png) 0 0 no-repeat;
        }
      `);

    return $(
      `<div id="${id}" class="dc_ui_checkbox ${
        defaultEnable ? 'dc_ui_checkbox_on' : ''
      }" />`,
    ).bind('click', () => {
      $(`#${id}`).toggleClass('dc_ui_checkbox_on');
      onAfterClick?.($(`#${id}`).hasClass('dc_ui_checkbox_on'));
    });
  },

  PopUp: (id, title, content) => {
    Util.guard(
      Util.isGame(),
      'MenuChat.prototype.onSend: this function should be called in Game only.',
    );
    Util.guardString('DC.UI.PopUp', 'id', id);
    Util.guardString('DC.UI.PopUp', 'title', title);
    Util.guardJQuery('DC.UI.PopUp', 'content', content);

    $('#loader').fadeIn('fast');

    const html = `
        <div id="${id}" class="dataBox"  onClick="engine.switchDataBox(this)" style="display: block; z-index: 5; left: 764px; top: 16px;">
          <relative>
            <div class="head" ondblclick="$('#${id}').toggleClass('reduced');">
            <div title="Fermer la fenêtre (Q)" class="info1 link close transition3s" onClick="engine.closeDataBox($(this).parent().parent().parent().attr('id'));" alt="$('${id}').removeClass('active')">
              <i class="fas fa-times"></i>
            </div>
            <div title="Reduire/Agrandir la fenêtre" class="info1 link reduce transition3s" onClick="$('#${id}').toggleClass('reduced');">
              <span>-</span>
            </div>
            <div class="title">${title}</div>
          </div>
          <div class="dbloader"></div>
          <div class="content" style="max-width: 800px; max-height: 600px; overflow-y: auto; overflow-x: hidden;">
          </div>
        </relative>
      </div>`;

    engine.displayDataBox(html);
    $(`#${id} .content`).append(content);

    $('#loader').hide();
  },

  SideMenu: (id, label, content) => {
    Util.guardString('DC.UI.SideMenu', 'id', id);
    Util.guardString('DC.UI.SideMenu', 'label', label);
    Util.guardJQuery('DC.UI.SideMenu', 'content', content);

    const $idContainer = id + '_container';
    const $idButton = id + '_button';
    const $idContent = id + '_content';

    if ($('div#zone_sidemenu').length === 0) {
      $('body').append('<div id="zone_sidemenu"></div>');
    }
    $('#zone_sidemenu').append(
      `<div id="${$idContainer}" class="sidemenu_container"></div>`,
    );

    $(`#${$idContainer}`).append(
      DC.UI.TextButton(
        $idButton,
        '<i class="fas fa-chevron-left"></i>' + label,
        () => {
          const isOpen = $(`#${$idButton}`).html().includes('fa-chevron-right');
          if (isOpen) {
            $(`#${$idButton}`)
              .empty()
              .append('<i class="fas fa-chevron-left"></i>' + label);
            $(`#${$idContent}`).css('display', 'none');
          } else {
            $(`#${$idButton}`)
              .empty()
              .append('<i class="fas fa-chevron-right"></i>' + label);
            $(`#${$idContent}`).css('display', 'block');
          }
        },
      ),
    );

    $(`#${$idContainer}`).append(
      `<div id="${$idContent}" class="sidemenu_content"></div>`,
    );
    $(`#${$idContent}`).append(content);

    DC.Style.apply(`
        #zone_sidemenu {
          display: flex;
          flex-direction: column;
          position: absolute;
          right: 0px;
          top: 80px;
          z-index: 999999;
        }

        .sidemenu_container {
          display: flex;
        }

        .sidemenu_container > .btnTxt:first-child {
          margin: 0 auto;
          min-width: 100px;
          max-width: 100px;
          font-size: 1rem;
          padding: 1%;
          display: grid;
          height: 100%;
          box-sizing: border-box;
          grid-template-columns: 10% 1fr;
          align-items: center;
          text-transform: uppercase;
          font-family: Arial !important;
          line-height: normal !important;
        }

        .sidemenu_container .btnTxt:hover {
          background: #0b9bcb;
          color: #fff;
        }

        .sidemenu_content {
          background-color: #000;
          color: #fff !important;
          box-shadow: 0 0 15px -5px inset #a2e4fc !important;
          padding: 10px;
          width: 200px;
          display: none;
        }
      `);
  },
};

DC.Network = {
  fetch: (args) => {
    Util.guardObject('DC.Network.fetch', 'args', args);

    return new Promise((resolve, reject) => {
      GM_xmlhttpRequest(
        Object.assign({}, args, {
          onload: (e) => resolve(e.response),
          onerror: reject,
          ontimeout: reject,
        }),
      );
    });
  },

  loadSpreadsheet: async (sheetId, tabName, range, apiKey, onLoad) => {
    Util.guardString('DC.Network.loadSpreadsheet', 'sheetId', sheetId);
    Util.guardString('DC.Network.loadSpreadsheet', 'tabName', tabName);
    Util.guardString('DC.Network.loadSpreadsheet', 'range', range);
    Util.guardString('DC.Network.loadSpreadsheet', 'apiKey', apiKey);
    Util.guardFunction('DC.Network.loadSpreadsheet', 'onLoad', onLoad);

    const urlGoogleSheetDatabase = `https://sheets.googleapis.com/v4/spreadsheets/${sheetId}/values/${tabName}!${range}?key=${apiKey}`;
    const result = await DC.Network.fetch({
      method: 'GET',
      url: urlGoogleSheetDatabase,
      headers: {
        'Content-Type': 'application/json',
      },
      responseType: 'json',
    });
    onLoad(result.values);
  },

  loadScript: async (url, onAfterLoad) => {
    Util.guardString('DC.Network.loadScript', 'url', url);
    Util.guardFunction(
      'DC.Network.loadScript',
      'onAfterLoad',
      onAfterLoad,
      true,
    );

    // TODO we should check that url is from a valid and secure source.
    const result = await DC.Network.fetch({
      method: 'GET',
      url,
      headers: {
        'Content-Type': 'text/javascript',
      },
    });
    // TODO we have to secure more this call
    eval(result);

    onAfterLoad?.();
  },

  loadJson: async (url) => {
    Util.guardString('DC.Network.loadJson', 'url', url);

    const result = await DC.Network.fetch({
      method: 'GET',
      url,
      headers: {
        'Content-Type': 'application/json',
      },
      responseType: 'json',
    });
    return result;
  },
};

DC.Chat = {
  sendMessage: (message) => {
    Util.guard(
      Util.isGame(),
      'MenuChat.prototype.onSend: this function should be called in Game only.',
    );
    Util.guardString('DC.Chat.sendMessage', 'message', message);

    $('#chatForm .text_chat').val(message);
    $('#chatForm .text_valider').click();
  },

  t: (message, decoration) => {
    Util.guardString('DC.Chat.t', 'message', message);
    Util.guardObject('DC.Chat.t', 'decoration', decoration);
    Util.guardBoolean('DC.Chat.t', 'decoration.bold', decoration.bold, true);
    Util.guardBoolean(
      'DC.Chat.t',
      'decoration.italic',
      decoration.italic,
      true,
    );
    Util.guardColor('DC.Chat.t', 'decoration.color', decoration.color, true);

    var prefix = '';
    var suffix = '';

    if (decoration.bold) {
      prefix += '[b]';
      suffix += '[b]';
    }

    if (decoration.italic) {
      prefix += '[i]';
      suffix = '[/i]' + suffix;
    }

    if (decoration.color && decoration.color !== '') {
      prefix += '[c=' + decoration.color + ']';
      suffix = '[/c]' + suffix;
    }

    return prefix + message + suffix;
  },

  addCommand: (label, fn) => {
    Util.guard(
      Util.isGame(),
      'MenuChat.prototype.onSend: this function should be called in Game only.',
    );
    Util.guardString('DC.Chat.addCommand', 'label', label);
    Util.guardFunction('DC.Chat.addCommand', 'fn', fn);

    nav.getChat().onSend((message, next, abort) => {
      const forbiden = ['me', 'y', 'ye', 'yme', 'w', 'we', 'wme', 'roll', ''];

      const labelUsed = message.split(' ')[0].substr(1);
      if (
        message[0] !== '/' ||
        labelUsed !== label ||
        forbiden.includes(labelUsed)
      ) {
        return next();
      }

      const content = message.substr(labelUsed.length + 1);

      if (fn(labelUsed, content)) {
        return next();
      } else {
        return abort();
      }
    });
  },
};

DC.Deck = {
  checkSkill: (info) => {
    Util.guard(
      Util.isGame(),
      'DC.Deck.checkSkill: this function should be called in Game only.',
    );
    Util.guardNumber('DC.Deck.checkSkill', 'info', info);

    return info <= $('.stat_6_entier').first().html();
  },

  write: (node, deckId) => {
    Util.guard(
      Util.isGame(),
      'DC.Deck.write: this function should be called in Game only.',
    );
    Util.guardJQuery('DC.Deck.write', 'node', node);
    Util.guardString('DC.Deck.write', 'deckId', deckId);

    const mode =
      $(`#${deckId} .zone_ecrit div:last-child`).attr('class') ===
      'ligne_resultat_fixed';

    if (mode) {
      $(`#${deckId} .zone_ecrit>.ligne_resultat_fixed:last-child`).append(node);
    } else {
      $('<div class="ligne_resultat_fixed" />')
        .append(node)
        .appendTo($(`#${deckId} .zone_ecrit`));
    }
  },

  createCommand: (info, command, fn, helpFn, help) => {
    Util.guard(
      Util.isGame(),
      'DC.Deck.createCommand: this function should be called in Game only.',
    );
    Util.guardNumber('DC.Deck.createCommand', 'info', info);
    Util.guardString('DC.Deck.createCommand', 'command', command);
    Util.guardFunction('DC.Deck.createCommand', 'fn', fn);
    Util.guardFunction('DC.Deck.createCommand', 'helpFn', helpFn);
    Util.guardString('DC.Deck.createCommand', 'help', help);

    $(document).ajaxComplete(function (event, xhr, settings) {
      // Handle custom deck command
      if (/Command/.test(settings.url)) {
        var deckId = 'db_deck_' + settings.data.match(/[0-9]*$/)[0];
        var lastCommand = $(`#${deckId} .ligne_ecrite_fixed input`)
          .last()
          .val();

        // Handle Date command

        if (new RegExp(`^${command}`, 'gi').test(lastCommand)) {
          if (DC.Deck.checkSkill(info)) {
            fn(lastCommand, deckId);
          } else {
            DC.Deck.write(
              $(
                '<span>Votre niveau en informatique est trop faible pour réussir cette commande</span>',
                deckId,
              ),
            );
          }
        }
        // Handle help Date command
        else if (new RegExp(`^help ${command}`, 'gi').test(lastCommand)) {
          helpFn(deckId);
        }
        // Handle help Date command
        else if (/^help$/gi.test(lastCommand)) {
          DC.Deck.write($(`<br />${help}`), deckId);
        }
      }
    });
  },
};