· 7 months ago · Mar 07, 2025, 04:30 PM
1// ==UserScript==
2// @name Torn PDA Race Config GUI - v3.0.26 - DOM Readiness Polling Fix
3// @version 3.0.26
4// @description PDA GUI to configure Torn racing parameters... - Version 3.0.26 - DOM Readiness Polling Fix
5// @author GNSC4
6// @match https://www.torn.com/loader.php?sid=racing*
7// @grant none
8// @updateURL https://raw.githubusercontent.com/GNSC4/torn-race-config-gui/main/torn-race-config-gui-v3.0.26-DomReadyPollFix.user.js
9// @downloadURL https://raw.githubusercontent.com/GNSC4/torn-race-config-gui/main/torn-race-config-gui-v3.0.26-DomReadyPollFix.user.js
10// @run-at document-end
11// @require https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.29.1/moment.min.js
12// ==/UserScript==
13
14(function() {
15 'use strict';
16
17 let guiInitialized = false;
18 let domCheckAttempts = 0; // Counter for DOM check attempts - v3.0.26
19 const MAX_DOM_CHECK_ATTEMPTS = 100; // Maximum DOM check attempts - v3.0.26
20
21 const style = document.createElement('style');
22 style.textContent = `
23 #tcLogo { pointer-events: none; }
24 .gui-button {
25 color: #ddd;
26 background-color: #555;
27 border: 1px solid #777;
28 border-radius: 3px;
29 padding: 8px 15px;
30 cursor: pointer;
31 margin-top: 5px;
32 margin-right: 5px;
33 transition: background-color 0.3s ease;
34 font-size: 0.9em;
35 display: inline-block;
36 text-decoration: none;
37 }
38
39 .gui-button:hover,
40 .preset-button:hover,
41 .remove-preset:hover,
42 .close-button:hover,
43 #closeGUIButton:hover,
44 #toggleRaceGUIButton:hover,
45 #setNowButton:hover,
46 #quickPresetButtonsContainer > .quick-race-button:hover,
47 div.content-title > h4 > #toggleRaceGUIButton:hover {
48 background-color: #777;
49 }
50
51 div.content-title > h4 > #toggleRaceGUIButton {
52 background-color: #555;
53 border: 1px solid #777;
54 }
55
56
57 #raceConfigGUI {
58 position: fixed;
59 top: 85px;
60 left: 20px;
61 background-color: #222;
62 color: #ddd;
63 border: 1px solid #555;
64 padding: 20px;
65 z-index: 1000;
66 font-family: sans-serif;
67 border-radius: 10px;
68 max-width: 420px;
69 display: none; /* --- GUI STARTS HIDDEN - v3.0.23 --- */
70 }
71
72 #raceConfigGUI h2, #raceConfigGUI h3, #raceConfigGUI h4 {
73 color: #eee;
74 margin-top: 0;
75 margin-bottom: 15px;
76 text-align: center;
77 }
78
79 #raceConfigGUI label {
80 display: block;
81 margin-bottom: 5px;
82 color: #ccc;
83 }
84
85 #raceConfigGUI input[type="text"],
86 #raceConfigGUI input[type="number"],
87 #raceConfigGUI input[type="date"],
88 #raceConfigGUI input[type="time"],
89 #raceConfigGUI select {
90 padding: 9px;
91 margin-bottom: 0px;
92 border: 1px solid #555;
93 background-color: #444;
94 color: #eee !important;
95 border-radius: 7px;
96 width: calc(100% - 24px);
97 }
98
99 #raceConfigGUI input:focus,
100 #raceConfigGUI select:focus {
101 border-color: #888;
102 box-shadow: 0 0 6px rgba(136, 136, 136, 0.5);
103 }
104
105
106 #raceConfigGUI .presets-section {
107 margin-bottom: 20px;
108 padding-bottom: 15px;
109 border-bottom: 1px solid #eee;
110 }
111
112 #raceConfigGUI .presets-section:last-child {
113 border-bottom: 0px solid #eee;
114 }
115
116 #raceConfigGUI .config-section:last-child {
117 border-bottom: 0px solid #eee;
118 }
119
120
121 #raceConfigGUI .config-section h4,
122 #raceConfigGUI .car-select-section h4,
123 #raceConfigGUI .presets-section h4 {
124 border-top: 1px solid #555;
125 padding-top: 12px;
126 font-size: 1.4em;
127 margin-bottom: 18px;
128 }
129
130
131 #raceConfigGUI #createRaceButton {
132 display: inline-block !important;
133 text-align: center !important;
134 white-space: nowrap !important;
135 overflow: visible !important;
136 width: 90% !important;
137 max-width: 250px !important;
138 padding: 10px 15px !important;
139 font-size: 1.1em !important;
140 color: #eee !important;
141 background-color: #555 !important;
142 border: 1px solid #777 !important;
143 }
144
145 #raceConfigGUI #createRaceButton:hover,
146 #raceConfigGUI #closeGUIButton:hover,
147 #raceConfigGUI #setNowButton:hover {
148 background-color: #777;
149 }
150
151 #raceConfigGUI #setNowButton:hover {
152 background-color: #888;
153 }
154
155
156 #raceConfigGUI .preset-button,
157 #raceConfigGUI .remove-preset,
158 #raceConfigGUI .close-button {
159 padding: 10px 15px;
160 margin-top: 5px;
161 margin-right: 5px;
162 border: none;
163 border-radius: 5px;
164 color: #fff;
165 background-color: #666;
166 cursor: pointer;
167 transition: background-color 0.3s ease;
168 font-size: 0.9em;
169 display: inline-block;
170 text-decoration: none;
171 width: 100%;
172 max-width: 100%;
173 box-sizing: border-box;
174 overflow-wrap: break-word;
175 }
176
177
178 #raceConfigGUI .preset-buttons-container {
179 display: flex;
180 flex-wrap: wrap;
181 gap: 8px;
182 margin-bottom: 15px;
183 align-items: flex-start;
184 max-width: calc(100% - 20px);
185 }
186
187 #raceConfigGUI .preset-button-container {
188 display: inline-flex;
189 flex-direction: column;
190 align-items: center;
191 margin-bottom: 20px;
192 text-align: center;
193 position: relative;
194 }
195
196 #raceConfigGUI .presets-section .preset-buttons-container .preset-button:hover {
197 background-color: #777;
198 }
199
200
201 #raceConfigGUI .remove-preset {
202 background-color: #955;
203 color: #eee;
204 padding: 5px 10px;
205 border-radius: 50%;
206 font-size: 0.8em;
207 line-height: 1;
208 display: inline-flex;
209 align-items: center;
210 justify-content: center;
211 width: 20px;
212 height: 20px;
213 text-decoration: none;
214 position: absolute;
215 top: 0px;
216 right: -5px;
217 float: none;
218 }
219
220 #raceConfigGUI .remove-preset:hover {
221 background-color: #c77;
222 }
223
224
225 #raceConfigGUI #closeGUIButton {
226 position: absolute;
227 top: 10px;
228 right: 10px;
229 border-radius: 50%;
230 width: 25px;
231 height: 25px;
232 padding: 0;
233 display: inline-flex;
234 align-items: center;
235 justify-content: center;
236 font-size: 1em;
237 line-height: 1;
238 }
239
240 #raceConfigGUI #statusMessageBox {
241 margin-top: 15px;
242 padding: 10px;
243 border: 1px solid #777;
244 border-radius: 5px;
245 background-color: #333;
246 color: #ddd;
247 text-align: center;
248 font-size: 0.9em;
249 }
250
251 #raceConfigGUI #statusMessageBox.error,
252 #raceConfigGUI #statusMessageBox.success {
253 background-color: #522;
254 border-color: #944;
255 color: #eee;
256 }
257
258 #raceConfigGUI #statusMessageBox.success {
259 background-color: #252;
260 border-color: #494;
261 color: #efe;
262 }
263
264 #raceConfigGUI .api-key-section {
265 margin-bottom: 20px;
266 text-align: center;
267 }
268
269 #raceConfigGUI .config-params-section {
270 display: grid;
271 grid-template-columns: repeat(auto-fit, minmax(100px, 1fr));
272 gap: 15px;
273 margin-bottom: 20px;
274 }
275
276 #raceConfigGUI .config-params-section label {
277 text-align: left;
278 }
279
280 #raceConfigGUI .config-params-section input[type="text"],
281 #raceConfigGUI .config-params-section select,
282 #raceConfigGUI .config-params-section input[type="number"] {
283 width: 100%;
284 }
285
286 #raceConfigGUI .config-params-section .driver-input-container {
287 display: inline-block;
288 width: 49%;
289 margin-right: 1%;
290 margin-bottom: 0px;
291 }
292
293 #raceConfigGUI .config-params-section .driver-input-container:nth-child(even) {
294 margin-right: 0;
295 }
296
297
298 #raceConfigGUI .config-params-section .driver-input-container:last-child {
299 margin-right: 0;
300 }
301
302 #raceConfigGUI .config-params-section .driver-input-container input[type="number"] {
303 width: calc(100% - 22px);
304 }
305
306 #raceConfigGUI .config-section > div {
307 margin-bottom: 12px;
308 display: flex;
309 align-items: center;
310 }
311
312 #raceConfigGUI .config-section label {
313 margin-bottom: 0;
314 margin-right: 10px;
315 width: auto;
316 flex-shrink: 0;
317 text-align: right;
318 min-width: 110px;
319 }
320
321
322 @media (max-width: 768px) {
323 #raceConfigGUI {
324 position: fixed;
325 top: 0;
326 left: 0;
327 width: 95%;
328 max-height: 90%;
329 overflow-y: auto;
330 padding: 15px;
331 margin: 2.5%;
332 border-radius: 15px;
333 }
334
335 #raceConfigGUI h2, #raceConfigGUI h4 {
336 font-size: 1.5em;
337 }
338
339
340 #raceConfigGUI button,
341 #raceConfigGUI #toggleRaceGUIButton,
342 #raceConfigGUI .preset-button,
343 #raceConfigGUI .remove-preset,
344 #raceConfigGUI .gui-button,
345 #raceConfigGUI .close-button {
346 padding: 12px 20px;
347 font-size: 1.1em;
348 margin: 5px 8px 5px 0;
349 }
350
351 #raceConfigGUI input[type="text"],
352 #raceConfigGUI select {
353 padding: 12px;
354 font-size: 1.1em;
355 }
356
357 #raceConfigGUI .config-params-section {
358 grid-template-columns: 1fr;
359 }
360
361 #raceConfigGUI .config-params-section .driver-input-container {
362 display: block;
363 width: 100%;
364 margin-right: 0;
365 }
366
367
368 #raceConfigGUI .car-select-section {
369 flex-direction: column;
370 align-items: stretch;
371 }
372
373 #raceConfigGUI .car-select-section label {
374 margin-right: 0;
375 margin-bottom: 5px;
376 text-align: center;
377 }
378
379 #raceConfigGUI .car-select-section select {
380 margin-right: 0;
381 margin-bottom: 10px;
382 }
383
384 #raceConfigGUI .car-select-section button#updateCarsButton {
385 margin-right: 0;
386 width: 100%;
387 }
388
389 #quickPresetButtonsContainer {
390 text-align: center;
391 max-width: 95%;
392 }
393
394 .quick-race-button {
395 margin: 5px;
396 }
397
398 .preset-button-container {
399 text-align: center;
400 }
401
402
403 body {
404 background-color: #181818;
405 color: #ddd;
406 }
407
408 a {
409 color: #8da9c4;
410 }
411
412 a:hover {
413 color: #b0cddb;
414 }
415
416 div.race-container {
417 background-color: #282828 !important;
418 color: #ddd !important;
419 }
420
421 .race-body, .race-head {
422 background-color: #333 !important;
423 color: #eee !important;
424 }
425
426 .race-list-row {
427 border-bottom: 1px solid #444 !important;
428 }
429
430 .race-details-wrap {
431 background-color: #3a3a3a !important;
432 color: #ddd !important;
433 }
434
435 .race-bet-section {
436 background-color: #444 !important;
437 color: #ddd !important;
438 }
439
440 .race-bet-input {
441 background-color: #555 !important;
442 color: #eee !important;
443 border-color: #666 !important;
444 }
445
446 .race-bet-button {
447 background-color: #666 !important;
448 color: #fff !important;
449 }
450
451 .race-bet-button:hover {
452 background-color: #777 !important;
453 }
454
455 .race-content-section {
456 background-color: #333 !important;
457 color: #eee !important;
458 }
459 `;
460 document.head.appendChild(style);
461
462
463 function createRaceConfigGUI() {
464 let gui = document.createElement('div');
465 gui.id = 'raceConfigGUI';
466 gui.innerHTML = `
467 <div style="text-align: center; margin-bottom: 15px;">
468 <img id="tcLogo" src="https://www.torn.com/images/v2/main/logo-black.svg" alt="Torn City Logo" style="height: 25px; margin-bottom: 10px;">
469 <h2>Race Configuration</h2>
470 </div>
471
472 <div class="api-key-section">
473 <h4>API Key</h4>
474 <input type="text" id="apiKeyInput" placeholder="Enter your API Key">
475 <button id="saveApiKeyButton" class="gui-button">Save API Key</button>
476 </div>
477
478 <div class="config-section">
479 <h4>Race Settings</h4>
480
481 <div><label for="trackSelect">Track</label>
482 <select id="trackSelect">
483 <option value="6">6 - Uptown</option>
484 <option value="1">1 - Industrial</option>
485 <option value="2">2 - Dudek</option>
486 <option value="3">3 - Parkland</option>
487 <option value="4">4 - Dirt Track</option>
488 <option value="5">5 - Speedway</option>
489 </select></div>
490
491 <div><label for="lapsInput">Laps:</label>
492 <input type="number" id="lapsInput" value="100" min="1" max="999"></div>
493
494 <div class="config-params-section">
495 <div class="driver-input-container"><label for="minDriversInput">Min Drivers:</label>
496 <input type="number" id="minDriversInput" value="2" min="2" max="10"></div>
497 <div class="driver-input-container"><label for="maxDriversInput">Max Drivers</label>
498 <input type="number" id="maxDriversInput" value="2" min="2" max="10"></div>
499 </div>
500
501 <div><label for="raceNameInput">Race Name:</label>
502 <input type="text" id="raceNameInput" placeholder="Race Name Optional"></div>
503
504 <div><label for="passwordInput">Password (optional)</label>
505 <input type="text" id="passwordInput" placeholder="Race Password Optional"></div>
506
507 <div><label for="betAmountInput">(Max 10M, Optional<br>Bet Amount for Race)</label>
508 <input type="number" id="betAmountInput" value="0" min="0" max="10000000"></div>
509
510 <div class="time-config">
511 <label>Race Start Time (TCT 24hr):</label>
512 <div>
513 <select id="hourSelect"></select> :
514 <select id="minuteSelect"></select>
515 <button id="setNowButton" class="gui-button" style="padding: 5px 10px; font-size: 0.8em; margin-left: 5px; margin-right: 0px; vertical-align: baseline;">NOW</button>
516 <span style="font-size: 0.8em; color: #ccc; margin-left: 5px;">(TCT, 1 min interval)</span>
517 </div>
518 </div>
519 </div>
520
521
522 <div class="car-select-section config-section">
523 <h4>Car Selection</h4>
524 <div>
525 <label for="carIdInput">Car ID:</label>
526 <div style="display: flex; align-items: center;">
527 <input type="text" id="carIdInput" placeholder="Enter Car ID or use dropdown below" style="margin-right: 5px;">
528 <button id="changeCarButton" class="gui-button" style="padding: 8px 10px; font-size: 0.8em; margin-top: 0px; margin-right: 0px; vertical-align: baseline; display: none;">Change Car</button> </div>
529 </div>
530
531 <div>
532 <label for="carDropdown">Car:</label>
533 <select id="carDropdown">
534 <option value="">Select a car...</option>
535 </select>
536 </div>
537 <div style="text-align: center; margin-top: 10px;">
538 <button id="updateCarsButton" class="gui-button" style="width: 80%; max-width: 200px; display: block; margin: 0 auto;">Update Cars</button>
539 <div id="carStatusMessage" style="font-size: 0.8em; color: #aaa; margin-top: 5px;"></div>
540 </div>
541 </div>
542
543
544 <div class="presets-section config-section">
545 <h4>Presets</h4>
546 <div id="presetButtonsContainer" class="preset-buttons-container">
547 </div>
548
549 <div>
550 <button id="savePresetButton" class="gui-button" style="width: 80%; max-width: 200px; display: block; margin: 10px auto 5px auto;">Save Preset</button>
551 <button id="clearPresetsButton" class="gui-button" style="width: 80%; max-width: 200px; display: block; margin: 5px auto 10px auto;">Clear Presets</button>
552 </div>
553 <div id="statusMessageBox" style="display:none;">Status Message</div>
554 </div>
555
556
557 <div style="text-align: center; margin-top: 20px; color: #888; font-size: 0.8em;">
558 Script created by GNSC4 (<a href="https://www.torn.com/profiles.php?XID=268863" target="_blank" style="color: #888; text-decoration: none;">268863</a>)-v3.0.13<br>
559 <a href="https://github.com/GNSC4/torn-race-config-gui" target="_blank" style="color: #888; text-decoration: none;">v3.0.16 - No GM Functions</a>
560 </div>
561 <button type="button" id="closeGUIButton" class="close-button" title="Close GUI">×</button>
562 `;
563 return gui;
564 }
565
566
567 function initializeGUI(gui) {
568 loadApiKey();
569 populateTimeDropdowns();
570 updateCarDropdown();
571 loadPresets();
572
573 // --- Initialize GUI Elements AFTER GUI is in DOM and perform null checks - v3.0.24 ---
574 const apiKeyInput = document.getElementById('apiKeyInput');
575 const saveApiKeyButton = document.getElementById('saveApiKeyButton');
576 const trackSelect = document.getElementById('trackSelect');
577 const lapsInput = document.getElementById('lapsInput');
578 const minDriversInput = document.getElementById('minDriversInput');
579 const maxDriversInput = document.getElementById('maxDriversInput');
580 const raceNameInput = document.getElementById('raceNameInput');
581 const passwordInput = document.getElementById('passwordInput');
582 const betAmountInput = document.getElementById('betAmountInput');
583 const hourSelect = document.getElementById('hourSelect');
584 const minuteSelect = document.getElementById('minuteSelect');
585 const setNowButton = document.getElementById('setNowButton');
586 const carIdInput = document.getElementById('carIdInput');
587 const changeCarButton = document.getElementById('changeCarButton');
588 const carDropdown = document.getElementById('carDropdown');
589 const updateCarsButton = document.getElementById('updateCarsButton');
590 const carStatusMessage = document.getElementById('carStatusMessage');
591 const savePresetButton = document.getElementById('savePresetButton');
592 const clearPresetsButton = document.getElementById('clearPresetsButton');
593 const presetButtonsContainer = document.getElementById('presetButtonsContainer');
594 const statusMessageBox = document.getElementById('statusMessageBox');
595 const createRaceButton = document.getElementById('createRaceButton');
596 const closeGUIButton = document.getElementById('closeGUIButton');
597 // --- End of GUI Element Initialization ---
598
599
600 if (saveApiKeyButton) { // --- Null check before adding listener - v3.0.24 ---
601 saveApiKeyButton.addEventListener('click', () => {
602 saveApiKey();
603 });
604 } else {
605 console.error("Error: saveApiKeyButton element not found in initializeGUI"); // --- Error Log - v3.0.24 ---
606 }
607
608
609 if (setNowButton) { // --- Null check before adding listener - v3.0.24 ---
610 setNowButton.addEventListener('click', () => {
611 setTimeToNow();
612 });
613 } else {
614 console.error("Error: setNowButton element not found in initializeGUI"); // --- Error Log - v3.0.24 ---
615 }
616
617
618 if (updateCarsButton) { // --- Null check before adding listener - v3.0.24 ---
619 updateCarsButton.addEventListener('click', () => {
620 updateCarList();
621 });
622 } else {
623 console.error("Error: updateCarsButton element not found in initializeGUI"); // --- Error Log - v3.0.24 ---
624 }
625
626
627 if (carDropdown) { // --- Null check before adding listener - v3.0.24 ---
628 carDropdown.addEventListener('change', () => {
629 carIdInput.value = carDropdown.value;
630 });
631 } else {
632 console.error("Error: carDropdown element not found in initializeGUI"); // --- Error Log - v3.0.24 ---
633 }
634
635
636 if (savePresetButton) { // --- Null check before adding listener - v3.0.24 ---
637 savePresetButton.addEventListener('click', () => {
638 savePreset();
639 });
640 } else {
641 console.error("Error: savePresetButton element not found in initializeGUI"); // --- Error Log - v3.0.24 ---
642 }
643
644
645 if (clearPresetsButton) { // --- Null check before adding listener - v3.0.24 ---
646 clearPresetsButton.addEventListener('click', () => {
647 clearPresets();
648 });
649 } else {
650 console.error("Error: clearPresetsButton element not found in initializeGUI"); // --- Error Log - v3.0.24 ---
651 }
652
653
654 if (createRaceButton) { // --- Null check before adding listener - v3.0.24 ---
655 createRaceButton.addEventListener('click', () => {
656 createRace();
657 });
658 } else {
659 console.error("Error: createRaceButton element not found in initializeGUI"); // --- Error Log - v3.0.24 ---
660 }
661
662
663 if (closeGUIButton) { // --- Null check before adding listener - v3.0.24 ---
664 closeGUIButton.addEventListener('click', () => {
665 toggleRaceGUI();
666 });
667 } else {
668 console.error("Error: closeGUIButton element not found in initializeGUI"); // --- Error Log - v3.0.24 ---
669 }
670
671
672 dragElement(gui);
673
674 displayPresets();
675 updateQuickPresetsDisplay();
676
677 displayStatusMessage('GUI Loaded', 'success');
678 setTimeout(() => displayStatusMessage('', ''), 3000);
679
680 }
681
682 function createToggleButton() {
683 const button = document.createElement('button');
684 button.id = 'toggleRaceGUIButton';
685 button.className = 'gui-button';
686 button.textContent = 'Race Config PDA';
687 button.style.position = 'relative';
688 button.style.zIndex = '999';
689
690 button.addEventListener('click', toggleRaceGUI);
691
692 // --- Robustly find the title element - v3.0.24 ---
693 let titleElement = document.querySelector('div.content-title > h4');
694 if (!titleElement) {
695 titleElement = document.querySelector('div.body > div.content-title > h4'); // --- Alternative selector - v3.0.24 ---
696 }
697
698
699 if (titleElement) {
700 titleElement.appendChild(button);
701 } else {
702 console.error("Error: Could not find title element to append toggle button."); // --- Error Log - v3.0.24 ---
703 document.body.appendChild(button); // Fallback: append to body - v3.0.24
704 }
705 return button;
706 }
707
708 function toggleRaceGUI() {
709 const gui = document.getElementById('raceConfigGUI');
710 if (gui) {
711 gui.style.display = gui.style.display === 'none' ? '' : 'none';
712 } else {
713 initializeGUI(raceConfigGUI); // Re-initialize if somehow removed from DOM - v3.0.23 - Important for correct setup
714 raceConfigGUI.style.display = ''; // Ensure it's visible after re-initialization - v3.0.23
715 }
716 }
717
718 function dragElement(elmnt) {
719 var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
720 if (document.getElementById(elmnt.id + "Header")) {
721 document.getElementById(elmnt.id + "Header").onmousedown = dragMouseDown;
722 } else {
723 elmnt.onmousedown = dragMouseDown;
724 }
725
726 function dragMouseDown(e) {
727 e = e || window.event;
728 e.preventDefault();
729 pos3 = e.clientX;
730 pos4 = e.clientY;
731 document.onmouseup = closeDragElement;
732 document.onmousemove = elementDrag;
733 }
734
735 function elementDrag(e) {
736 e = e || window.event;
737 e.preventDefault();
738 pos1 = pos3 - e.clientX;
739 pos2 = pos4 - e.clientY;
740 pos3 = e.clientX;
741 pos4 = e.clientY;
742 elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
743 elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
744 }
745
746 function closeDragElement() {
747 document.onmouseup = null;
748 document.onmousemove = null;
749 }
750 }
751
752 function loadApiKey() {
753 const apiKeyInput = document.getElementById('apiKeyInput');
754 if (apiKeyInput) {
755 apiKeyInput.value = get_value('torn_api_key') || '';
756 }
757 }
758
759 function saveApiKey() {
760 const apiKey = document.getElementById('apiKeyInput').value;
761 set_value('torn_api_key', apiKey);
762 displayStatusMessage('API Key Saved', 'success');
763 setTimeout(() => displayStatusMessage('', ''), 3000);
764 }
765
766 function displayStatusMessage(message, type = '') {
767 const statusMessageBox = document.getElementById('statusMessageBox');
768 if (!statusMessageBox) return;
769
770 statusMessageBox.textContent = message;
771 statusMessageBox.style.display = message ? 'block' : 'none';
772
773 statusMessageBox.className = ' ';
774 if (type === 'error' || type === 'success') {
775 statusMessageBox.classList.add(type);
776 }
777 }
778
779 function savePreset() {
780 const presetName = prompt("Enter a name for this preset:");
781 if (!presetName) {
782 displayStatusMessage('Preset name cannot be empty.', 'error');
783 setTimeout(() => displayStatusMessage('', ''), 3000);
784 return;
785 }
786
787 const presetData = {
788 track: document.getElementById('trackSelect').value,
789 laps: document.getElementById('lapsInput').value,
790 minDrivers: document.getElementById('minDriversInput').value,
791 maxDrivers: document.getElementById('maxDriversInput').value,
792 raceName: document.getElementById('raceNameInput').value,
793 password: document.getElementById('passwordInput').value,
794 betAmount: document.getElementById('betAmountInput').value,
795 hour: document.getElementById('hourSelect').value,
796 minute: document.getElementById('minuteSelect').value,
797 carId: document.getElementById('carIdInput').value
798 };
799 let presets = loadPresets();
800 presets[presetName] = presetData;
801 set_value('race_presets', presets);
802 displayPresets();
803 updateQuickPresetsDisplay();
804 displayStatusMessage(`Preset "${presetName}" saved.`, 'success');
805 setTimeout(() => displayStatusMessage('', ''), 3000);
806 }
807
808 function loadPresets() {
809 return get_value('race_presets') || {};
810 }
811
812 function loadAllPresets() {
813 return loadPresets() || {};
814 }
815
816 function displayPresets() {
817 const presets = loadPresets();
818 const container = document.getElementById('presetButtonsContainer');
819 if (!container) return;
820
821 container.innerHTML = '';
822
823 if (Object.keys(presets).length === 0) {
824 container.textContent = 'No presets saved yet.';
825 return;
826 }
827
828 Object.keys(presets).forEach(presetName => {
829 const presetButtonContainer = document.createElement('div');
830 presetButtonContainer.className = 'preset-button-container';
831 container.appendChild(presetButtonContainer);
832
833 const presetButton = document.createElement('button');
834 presetButton.className = 'preset-button';
835 presetButton.textContent = presetName;
836 presetButton.title = `Apply preset: ${presetName}`;
837 presetButton.addEventListener('click', () => applyPreset(presetName));
838 presetButtonContainer.appendChild(presetButton);
839
840 const removeButton = document.createElement('a');
841 removeButton.className = 'remove-preset';
842 removeButton.href = '#';
843 removeButton.textContent = '×';
844 removeButton.title = `Remove preset: ${presetName}`;
845 removeButton.addEventListener('click', (event) => {
846 event.preventDefault();
847 removePreset(presetName);
848 });
849 presetButtonContainer.appendChild(removeButton);
850
851 });
852 }
853
854 function applyPreset(presetName) {
855 const presets = loadPresets();
856 const preset = presets[presetName];
857 if (preset) {
858 document.getElementById('trackSelect').value = preset.track;
859 document.getElementById('lapsInput').value = preset.laps;
860 document.getElementById('minDriversInput').value = preset.minDrivers;
861 document.getElementById('maxDriversInput').value = preset.maxDrivers;
862 document.getElementById('raceNameInput').value = preset.raceName;
863 document.getElementById('passwordInput').value = preset.password;
864 document.getElementById('betAmountInput').value = preset.betAmount;
865 document.getElementById('hourSelect').value = preset.hour;
866 document.getElementById('minuteSelect').value = preset.minute;
867 document.getElementById('carIdInput').value = preset.carId;
868
869 displayStatusMessage(`Preset "${presetName}" applied.`, 'success');
870 setTimeout(() => displayStatusMessage('', ''), 3000);
871
872 } else {
873 displayStatusMessage(`Preset "${presetName}" not found.`, 'error');
874 setTimeout(() => displayStatusMessage('', ''), 3000);
875 }
876 }
877
878 function removePreset(presetName) {
879 if (!confirm(`Are you sure you want to remove preset "${presetName}"?`)) {
880 return;
881 }
882 let presets = loadPresets();
883 delete presets[presetName];
884 set_value('race_presets', presets);
885 displayPresets();
886 updateQuickPresetsDisplay();
887 displayStatusMessage(`Preset "${presetName}" removed.`, 'success');
888 setTimeout(() => displayStatusMessage('', ''), 3000);
889 }
890
891 function clearPresets() {
892 if (confirm("Are you sure you want to clear ALL saved presets?")) {
893 set_value('race_presets', {});
894 displayPresets();
895 updateQuickPresetsDisplay();
896 displayStatusMessage('All presets cleared.', 'success');
897 setTimeout(() => displayStatusMessage('', ''), 3000);
898 }
899 }
900
901 function updateQuickPresetsDisplay() {
902 const presets = loadAllPresets();
903 const quickPresetsContainer = document.getElementById('quickPresetButtonsContainer');
904
905 if (!quickPresetsContainer) return;
906
907 quickPresetsContainer.innerHTML = '';
908
909 const quickPresets = [
910 { name: "Quick Uptown 10 Laps", config: { track: '6', laps: '10' } },
911 { name: "Quick Speedway 50 Laps", config: { track: '5', laps: '50' } },
912 ];
913
914 if (quickPresets.length > 0) {
915 quickPresets.forEach(quickPreset => {
916 const button = document.createElement('button');
917 button.className = 'gui-button quick-race-button';
918 button.textContent = quickPreset.name;
919 button.title = `Quick Race: ${quickPreset.name}`;
920 button.addEventListener('click', () => applyQuickPreset(quickPreset.config));
921 quickPresetsContainer.appendChild(button);
922 });
923 } else {
924 quickPresetsContainer.textContent = 'No quick presets defined.';
925 }
926 }
927
928 function applyQuickPreset(config) {
929 if (config) {
930 document.getElementById('trackSelect').value = config.track || document.getElementById('trackSelect').options[0].value;
931 document.getElementById('lapsInput').value = config.laps || 100;
932
933 displayStatusMessage('Quick preset applied.', 'success');
934 setTimeout(() => displayStatusMessage('', ''), 3000);
935 } else {
936 displayStatusMessage('Quick preset config error.', 'error');
937 setTimeout(() => displayStatusMessage('', ''), 3000);
938 }
939 }
940
941 function populateTimeDropdowns() {
942 const hourSelect = document.getElementById('hourSelect');
943 const minuteSelect = document.getElementById('minuteSelect');
944
945 if (!hourSelect || !minuteSelect) return;
946
947 for (let i = 0; i <= 23; i++) {
948 const option = document.createElement('option');
949 option.value = String(i).padStart(2, '0');
950 option.textContent = String(i).padStart(2, '0');
951 hourSelect.appendChild(option);
952 }
953
954 for (let i = 0; i <= 59; i++) {
955 if (i % 1 === 0) {
956 const option = document.createElement('option');
957 option.value = String(i).padStart(2, '0');
958 option.textContent = String(i).padStart(2, '0');
959 minuteSelect.appendChild(option);
960 }
961 }
962 }
963
964 function setTimeToNow() {
965 const hourSelect = document.getElementById('hourSelect');
966 const minuteSelect = document.getElementById('minuteSelect');
967
968 if (!hourSelect || !minuteSelect) return;
969
970 const now = moment.utc();
971
972 hourSelect.value = String(now.hour()).padStart(2, '0');
973 minuteSelect.value = String(now.minute()).padStart(2, '0');
974 }
975
976 async function updateCarList() {
977 const apiKey = get_value('torn_api_key');
978 const carDropdown = document.getElementById('carDropdown');
979 const carStatusMessage = document.getElementById('carStatusMessage');
980
981
982 if (!apiKey) {
983 carStatusMessage.textContent = 'API Key Required';
984 carStatusMessage.style.color = 'red';
985 return;
986 }
987
988 carStatusMessage.textContent = 'Updating Cars...';
989 carStatusMessage.style.color = '#aaa';
990 carDropdown.disabled = true;
991 updateCarsButton.disabled = true;
992
993
994 try {
995 const response = await GM.xmlHttpRequest({
996 url: `https://api.torn.com/torn/?selections=vehicles&key=${apiKey}&v=5`,
997 method: 'GET',
998 onload: function (response) {
999 if (response.status === 200) {
1000 const data = JSON.parse(response.responseText);
1001 if (data.error) {
1002 carStatusMessage.textContent = `API Error: ${data.error.error}`;
1003 carStatusMessage.style.color = 'red';
1004 } else if (data.vehicles) {
1005 populateCarDropdown(data.vehicles);
1006 carStatusMessage.textContent = 'Cars Updated';
1007 carStatusMessage.style.color = '#efe';
1008 } else {
1009 carStatusMessage.textContent = 'No car data received.';
1010 carStatusMessage.style.color = 'orange';
1011 }
1012 } else {
1013 carStatusMessage.textContent = `HTTP Error: ${response.status}`;
1014 carStatusMessage.style.color = 'red';
1015 }
1016 carDropdown.disabled = false;
1017 updateCarsButton.disabled = false;
1018 setTimeout(() => { carStatusMessage.textContent = ''; }, 3000);
1019 },
1020 onerror: function (error) {
1021 carStatusMessage.textContent = `Request failed: ${error.statusText}`;
1022 carStatusMessage.style.color = 'red';
1023 carDropdown.disabled = false;
1024 updateCarsButton.disabled = false;
1025 setTimeout(() => { carStatusMessage.textContent = ''; }, 5000);
1026 }
1027 });
1028
1029
1030 } catch (error) {
1031 carStatusMessage.textContent = `Error updating cars: ${error.message}`;
1032 carStatusMessage.style.color = 'red';
1033 carDropdown.disabled = false;
1034 updateCarsButton.disabled = false;
1035 setTimeout(() => { carStatusMessage.textContent = ''; }, 5000);
1036 }
1037 }
1038
1039 function populateCarDropdown(vehicles) {
1040 const carDropdown = document.getElementById('carDropdown');
1041 if (!carDropdown) return;
1042
1043 carDropdown.innerHTML = '<option value="">Select a car...</option>';
1044 const sortedVehicles = Object.values(vehicles).sort((a, b) => a.name.localeCompare(b.name));
1045
1046 sortedVehicles.forEach(car => {
1047 if (car.leased !== '1') {
1048 const option = document.createElement('option');
1049 option.value = car.ID;
1050 option.textContent = `${car.name} (ID: ${car.ID})`;
1051 carDropdown.appendChild(option);
1052 }
1053 });
1054 }
1055
1056 function updateCarDropdown() {
1057 updateCarList();
1058 }
1059
1060
1061 async function createRace() {
1062 const apiKey = get_value('torn_api_key');
1063 if (!apiKey) {
1064 displayStatusMessage('API Key is required to create race.', 'error');
1065 setTimeout(() => displayStatusMessage('', ''), 3000);
1066 return;
1067 }
1068
1069 const trackId = document.getElementById('trackSelect').value;
1070 const laps = document.getElementById('lapsInput').value;
1071 const minDrivers = document.getElementById('minDriversInput').value;
1072 const maxDrivers = document.getElementById('maxDriversInput').value;
1073 const raceName = document.getElementById('raceNameInput').value;
1074 const password = document.getElementById('passwordInput').value;
1075 const betAmount = document.getElementById('betAmountInput').value;
1076 const raceHour = document.getElementById('hourSelect').value;
1077 const raceMinute = document.getElementById('minuteSelect').value;
1078 const carId = document.getElementById('carIdInput').value;
1079
1080 let startTime = '';
1081 if (raceHour && raceMinute) {
1082 startTime = `${raceHour}:${raceMinute}`;
1083 }
1084
1085 const params = new URLSearchParams();
1086 params.append('trackID', trackId);
1087 params.append('laps', laps);
1088 params.append('min_driver', minDrivers);
1089 params.append('max_driver', maxDrivers);
1090 if (raceName) params.append('name', raceName);
1091 if (password) params.append('password', password);
1092 if (betAmount > 0) params.append('bet_amount', betAmount);
1093 if (startTime) params.append('start_time', startTime);
1094 if (carId) params.append('vehicleID', carId);
1095 params.append('key', apiKey);
1096 params.append('v', 5);
1097
1098 displayStatusMessage('Creating Race...', 'info');
1099
1100 try {
1101 const response = await GM.xmlHttpRequest({
1102 url: 'https://api.torn.com/torn/racing/races',
1103 method: 'POST',
1104 headers: {
1105 'Content-Type': 'application/x-www-form-urlencoded',
1106 },
1107 data: params.toString(),
1108 onload: function (response) {
1109 if (response.status === 200) {
1110 const data = JSON.parse(response.responseText);
1111 if (data.error) {
1112 displayStatusMessage(`API Error: ${data.error.error}`, 'error');
1113 } else if (data.race_id) {
1114 const raceLink = `https://www.torn.com/racing.php#/view/${data.race_id}`;
1115 displayStatusMessage(`Race Created! <a href="${raceLink}" target="_blank">View Race</a>`, 'success');
1116 } else {
1117 displayStatusMessage('Race creation response error.', 'error');
1118 }
1119 } else {
1120 displayStatusMessage(`HTTP Error: ${response.status}`, 'error');
1121 }
1122 setTimeout(() => displayStatusMessage('', ''), 5000);
1123 },
1124 onerror: function (error) {
1125 displayStatusMessage(`Request failed: ${error.statusText}`, 'error');
1126 setTimeout(() => displayStatusMessage('', ''), 5000);
1127 }
1128 });
1129
1130
1131 } catch (error) {
1132 displayStatusMessage(`Error creating race: ${error.message}`, 'error');
1133 setTimeout(() => displayStatusMessage('', ''), 5000);
1134 }
1135 }
1136
1137 function set_value(key, value) {
1138 try {
1139 localStorage.setItem(key, JSON.stringify(value));
1140 } catch (e) {
1141 console.error('Error saving value to localStorage:', e);
1142 }
1143 }
1144
1145 function get_value(key, defaultValue) {
1146 try {
1147 const storedValue = localStorage.getItem(key);
1148 if (storedValue === null) {
1149 return defaultValue;
1150 }
1151 return JSON.parse(storedValue);
1152 } catch (e) {
1153 console.error('Error reading value from localStorage:', e);
1154 return defaultValue;
1155 }
1156 }
1157
1158 (() => {
1159 'use strict';
1160
1161 if (guiInitialized) {
1162 console.warn('GUI Initialization skipped - already initialized.');
1163 return;
1164 }
1165 guiInitialized = true;
1166
1167 let raceConfigGUI = createRaceConfigGUI();
1168 raceConfigGUI.style.display = 'none';
1169 document.body.appendChild(raceConfigGUI);
1170
1171
1172 function checkDomReady() { // --- DOM Readiness Polling Function - v3.0.26 ---
1173 domCheckAttempts++;
1174 if (domCheckAttempts > MAX_DOM_CHECK_ATTEMPTS) {
1175 console.error("Fatal error: createRaceButton element not found after multiple attempts. GUI initialization failed."); // --- Fatal Error Log - v3.0.26 ---
1176 return; // Stop polling after max attempts
1177 }
1178
1179 const createRaceButton = document.getElementById('createRaceButton');
1180 if (createRaceButton) {
1181 initializeGUI(raceConfigGUI); // Initialize GUI when element is found - v3.0.26
1182 createToggleButton(); // Create toggle button AFTER GUI is initialized - v3.0.24, v3.0.26
1183 } else {
1184 setTimeout(checkDomReady, 50); // Poll again after 50ms if not found - v3.0.26
1185 }
1186 }
1187
1188 checkDomReady(); // Start polling for DOM readiness - v3.0.26
1189
1190
1191 })();
1192
1193})();