Torn Custom Race Presets (Legacy-safe)

Faster custom races with presets (ES5 + TM Legacy compatible)

// ==UserScript==
// @name         Torn Custom Race Presets (Legacy-safe)
// @namespace    https://greasyfork.org/en/scripts/393632-torn-custom-race-presets
// @version      0.2.4
// @description  Faster custom races with presets (ES5 + TM Legacy compatible)
// @author       Cryosis7 [926640] + tweaks
// @match        *://www.torn.com/loader.php?sid=racing*
// @match        *://www.torn.com/page.php?sid=racing*
// @include      *://www.torn.com/*sid=racing*
// @grant        none
// @run-at       document-end
// @license MIT
// ==/UserScript==

/**
 * Edit presets as you wish. Any field you omit will be left as-is in the form.
 */
var presets = [
  { name: "Docks 100Laps",       maxDrivers: 2, trackName: "Docks",       numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Industrial 100Laps",  maxDrivers: 2, trackName: "Industrial",  numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Uptown 100Laps",      maxDrivers: 2, trackName: "Uptown",      numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Withdrawal 100Laps",  maxDrivers: 2, trackName: "Withdrawal",  numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Underdog 100Laps",    maxDrivers: 2, trackName: "Underdog",    numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Parkland 100Laps",    maxDrivers: 2, trackName: "Parkland",    numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Commerce 100Laps",    maxDrivers: 2, trackName: "Commerce",    numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Two Islands 100Laps", maxDrivers: 2, trackName: "Two Islands", numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Vector 100Laps",      maxDrivers: 2, trackName: "Vector",      numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Mudpit 100Laps",      maxDrivers: 2, trackName: "Mudpit",      numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Hammerhead 100Laps",  maxDrivers: 2, trackName: "Hammerhead",  numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Sewage 100Laps",      maxDrivers: 2, trackName: "Sewage",      numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Meltdown 100Laps",    maxDrivers: 2, trackName: "Meltdown",    numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Speedway 100Laps",    maxDrivers: 2, trackName: "Speedway",    numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Stone Park 100Laps",  maxDrivers: 2, trackName: "Stone Park",  numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" },
  { name: "Convict 100Laps",     maxDrivers: 2, trackName: "Convict",     numberOfLaps: 100, upgradesAllowed: true,  betAmount: 0, waitTime: 0, password: "" }
];

(function () {
  'use strict';

  // Use page jQuery; TM Legacy can isolate $ unless @grant none is used
  var $ = window.jQuery || (typeof unsafeWindow !== 'undefined' ? unsafeWindow.jQuery : null);

  // Simple waiter (polling) — robust on Legacy
  function waitFor(testFn, cb, interval, timeout) {
    var iv = interval || 100, to = timeout || 20000, t = 0;
    var id = setInterval(function () {
      if (testFn()) { clearInterval(id); cb(); }
      t += iv;
      if (t >= to) clearInterval(id);
    }, iv);
  }

  function toTitleCase(str) {
    str = String(str || '');
    var parts = str.toLowerCase().split(' ');
    for (var i = 0; i < parts.length; i++) {
      var s = parts[i];
      parts[i] = s ? s.charAt(0).toUpperCase() + s.substring(1) : s;
    }
    return parts.join(' ');
  }

  function scrubPresets() {
    for (var i = 0; i < presets.length; i++) {
      var x = presets[i];
      if (x.name && x.name.length > 25) x.name = x.name.substring(0, 25);
      if (x.maxDrivers != null) x.maxDrivers = x.maxDrivers > 100 ? 100 : (x.maxDrivers < 2 ? 2 : x.maxDrivers);
      if (x.trackName != null) x.trackName = toTitleCase(x.trackName);
      if (x.numberOfLaps != null) x.numberOfLaps = x.numberOfLaps > 100 ? 100 : (x.numberOfLaps < 1 ? 1 : x.numberOfLaps);
      if (x.upgradesAllowed != null) x.upgradesAllowed = !!x.upgradesAllowed;
      if (x.betAmount != null) x.betAmount = x.betAmount > 10000000 ? 10000000 : (x.betAmount < 0 ? 0 : x.betAmount);
      if (x.waitTime != null) x.waitTime = x.waitTime > 2880 ? 2880 : (x.waitTime < 0 ? 0 : x.waitTime); // allow 0
      if (x.password && x.password.length > 25) x.password = x.password.substring(0, 25);
    }
  }

  function setSelectByVisibleText(selectSelector, visibleText) {
    var $sel = $(selectSelector);
    if ($sel.length === 0) return;
    var target = String(visibleText || '').toLowerCase();
    var $opt = $sel.find('option').filter(function () {
      return $(this).text().replace(/^\s+|\s+$/g, '').toLowerCase() === target;
    }).first();
    if ($opt.length) {
      $sel.val($opt.val()).trigger('change');
      try { if ($sel.selectmenu) { $sel.selectmenu('refresh'); } } catch (e) {}
    }
  }

  function fillPreset(index) {
    var race = presets[index];
    if (!race) return;

    if ("name" in race) $('.race-wrap div.input-wrap input').val(race.name).trigger('input').trigger('change');
    if ("maxDrivers" in race) $('.drivers-max-wrap div.input-wrap input').val(race.maxDrivers).trigger('input').trigger('change');
    if ("numberOfLaps" in race) $('.laps-wrap > .input-wrap > input').val(race.numberOfLaps).trigger('input').trigger('change');
    if ("betAmount" in race) $('.bet-wrap > .input-wrap > input').val(race.betAmount).trigger('input').trigger('change');
    if ("waitTime" in race) $('.time-wrap > .input-wrap > input').val(race.waitTime).trigger('input').trigger('change');
    if ("password" in race) $('.password-wrap > .input-wrap > input').val(race.password).trigger('input').trigger('change');

    if ("trackName" in race) setSelectByVisibleText('#select-racing-track', race.trackName);
    if ("upgradesAllowed" in race) setSelectByVisibleText('#select-allow-upgrades', race.upgradesAllowed ? "Allow upgrades" : "Stock cars only");
  }

  function drawPresetBar() {
    var $container = $('#racingAdditionalContainer > .form-custom-wrap');
    if ($container.length === 0) return;

    // Remove an existing bar to avoid duplicates
    $('#race-presets-bar').remove();

    // Build buttons with string concatenation (no template literals)
    var btns = '';
    for (var i = 0; i < presets.length; i++) {
      var el = presets[i];
      var label = (el && el.name) ? el.name : ("Preset " + (i + 1));
      btns += '<button class="torn-btn preset-btn" data-index="' + i + '" style="margin:0 10px 10px 0">' + label + '</button>';
    }

    var html = ''
      + '<div id="race-presets-bar" class="filter-container m-top10">'
      + '  <div class="title-gray top-round">Race Presets</div>'
      + '  <div class="cont-gray p10 bottom-round">' + btns + '</div>'
      + '</div>';

    $container.before(html);

    // Click handlers
    $('#race-presets-bar .preset-btn').each(function () {
      var idx = parseInt(this.getAttribute('data-index'), 10);
      this.onclick = function () { fillPreset(idx); };
    });
  }

  function init() {
    // ensure we really have page jQuery
    if (!window.jQuery) return; // give up if page has no jQuery
    scrubPresets();
    drawPresetBar();

    // Re-add bar when racing form (re)loads via AJAX
    try {
      // Use page's jQuery ajaxComplete — works better than document
      $(document).ajaxComplete(function (e, xhr, settings) {
        try {
          var url = settings && settings.url ? settings.url : '';
          if (url.indexOf('section=createCustomRace') >= 0) {
            scrubPresets();
            drawPresetBar();
          }
        } catch (ignore) {}
      });
    } catch (ignore2) {}

    // Poll as a fallback (Legacy-safe)
    setInterval(function () {
      if ($('#racingAdditionalContainer .form-custom-wrap').length && !$('#race-presets-bar').length) {
        scrubPresets();
        drawPresetBar();
      }
    }, 1500);
  }

  // Wait for jQuery and the racing form to exist
  waitFor(function () {
    return (window.jQuery || (typeof unsafeWindow !== 'undefined' && unsafeWindow.jQuery)) &&
           document.getElementById('racingAdditionalContainer');
  }, init, 100, 25000);
})();