Top Cut Calculator - Limitless TCG

Calculate tournament top cuts on Limitless TCG

  1. // ==UserScript==
  2. // @name Top Cut Calculator - Limitless TCG
  3. // @name:pt-BR Calculadora de Top Cut - Limitless TCG
  4. // @namespace http://tampermonkey.net/
  5. // @version 1.0.1
  6. // @description Calculate tournament top cuts on Limitless TCG
  7. // @description:pt-BR Calculadora de top cut para torneios no Limitless TCG
  8. // @author Marcsx (https://github.com/Marcsx)
  9. // @match https://play.limitlesstcg.com/tournament/*
  10. // @include https://play.limitlesstcg.com/tournament/*
  11. // @include https://play.limitlesstcg.com/tournaments
  12. // @exclude https://play.limitlesstcg.com/tournament/*/match/*
  13. // @exclude https://play.limitlesstcg.com/tournament/*/round/*
  14. // @grant none
  15. // @source https://github.com/Marcsx/limitless-topcut-calculator
  16. // @supportURL https://github.com/Marcsx/limitless-topcut-calculator/issues
  17. // @license MIT
  18. // ==/UserScript==
  19.  
  20. /*
  21. This is a Tampermonkey adaptation of the Chrome extension created by Marcsx
  22. Original repository: https://github.com/Marcsx/limitless-topcut-calculator
  23. */
  24.  
  25. (function() {
  26. 'use strict';
  27.  
  28. const translations = {
  29. en: {
  30. title: "Top Cut Calculator",
  31. players: "Players",
  32. rounds: "Rounds",
  33. topCut: "Top Cut",
  34. records: "Records",
  35. calculate: "Calculate",
  36. noTopCut: "No Top Cut"
  37. },
  38. pt: {
  39. title: "Calculadora de Top Cut",
  40. players: "Jogadores",
  41. rounds: "Rodadas",
  42. topCut: "Top Cut",
  43. records: "Recordes",
  44. calculate: "Calcular",
  45. noTopCut: "Sem Top Cut"
  46. }
  47. };
  48.  
  49. class I18n {
  50. constructor() {
  51. this.currentLocale = navigator.language.startsWith('pt') ? 'pt' : 'en';
  52. }
  53.  
  54. t(key) {
  55. return translations[this.currentLocale][key] || translations['en'][key];
  56. }
  57. }
  58.  
  59. class TopCutCalculator {
  60. constructor() {
  61. this.i18n = new I18n();
  62. this.defaultTopCutRules = [
  63. { maxPlayers: 8, rounds: 3, topCut: 0 },
  64. { maxPlayers: 16, rounds: 4, topCut: 4 },
  65. { maxPlayers: 32, rounds: 6, topCut: 8 },
  66. { maxPlayers: 64, rounds: 7, topCut: 8 },
  67. { maxPlayers: 128, rounds: 6, topCut: 16 },
  68. { maxPlayers: 256, rounds: 7, topCut: 16 },
  69. { maxPlayers: 512, rounds: 8, topCut: 16 },
  70. { maxPlayers: 1024, rounds: 9, topCut: 32 },
  71. { maxPlayers: 2048, rounds: 10, topCut: 32 },
  72. { maxPlayers: Infinity, rounds: 10, topCut: 64 }
  73. ];
  74. this.createStyles();
  75. this.createElements();
  76. this.attachEventListeners();
  77. }
  78.  
  79. createStyles() {
  80. const style = document.createElement('style');
  81. style.textContent = `
  82. :root {
  83. --primary-color: #121212;
  84. --text-color: #ffffff;
  85. --surface-color: #1e1e1e;
  86. --accent-color: #bb86fc;
  87. }
  88.  
  89. #topcut-fab {
  90. position: fixed;
  91. bottom: 20px;
  92. right: 20px;
  93. width: 56px;
  94. height: 56px;
  95. border-radius: 50%;
  96. background-color: var(--accent-color);
  97. box-shadow: 0 3px 5px -1px rgba(0,0,0,.2),0 6px 10px 0 rgba(0,0,0,.14),0 1px 18px 0 rgba(0,0,0,.12);
  98. cursor: pointer;
  99. display: flex;
  100. align-items: center;
  101. justify-content: center;
  102. z-index: 1000;
  103. border: none;
  104. }
  105.  
  106. #topcut-fab:hover {
  107. background-color: #9965db;
  108. }
  109.  
  110. #topcut-modal {
  111. position: fixed;
  112. bottom: 90px;
  113. right: 20px;
  114. width: 320px;
  115. max-width: 90vw;
  116. background-color: var(--surface-color);
  117. border-radius: 8px;
  118. box-shadow: 0 5px 5px -3px rgba(0,0,0,.2),0 8px 10px 1px rgba(0,0,0,.14),0 3px 14px 2px rgba(0,0,0,.12);
  119. padding: 16px;
  120. color: var(--text-color);
  121. z-index: 999;
  122. display: none;
  123. }
  124.  
  125. .modal-header {
  126. font-size: 18px;
  127. font-weight: 500;
  128. margin-bottom: 16px;
  129. }
  130.  
  131. .input-group {
  132. margin-bottom: 16px;
  133. }
  134.  
  135. .input-group label {
  136. display: block;
  137. margin-bottom: 8px;
  138. color: rgba(255, 255, 255, 0.87);
  139. }
  140.  
  141. .input-group input, .input-group select {
  142. width: 100%;
  143. padding: 8px;
  144. border-radius: 4px;
  145. border: 1px solid rgba(255,255,255,0.12);
  146. background-color: var(--primary-color);
  147. color: var(--text-color);
  148. }
  149.  
  150. .button {
  151. background-color: var(--accent-color);
  152. color: var(--primary-color);
  153. padding: 8px 16px;
  154. border-radius: 4px;
  155. border: none;
  156. cursor: pointer;
  157. width: 100%;
  158. }
  159.  
  160. .button:hover {
  161. background-color: #9965db;
  162. }
  163.  
  164. .results {
  165. margin-top: 16px;
  166. padding-top: 16px;
  167. border-top: 1px solid rgba(255,255,255,0.12);
  168. }
  169. `;
  170. document.head.appendChild(style);
  171. }
  172.  
  173. createElements() {
  174. const fab = document.createElement('button');
  175. fab.id = 'topcut-fab';
  176. fab.innerHTML = '📊';
  177. document.body.appendChild(fab);
  178.  
  179. const modal = document.createElement('div');
  180. modal.id = 'topcut-modal';
  181. modal.innerHTML = `
  182. <div class="modal-header">
  183. ${this.i18n.t('title')}
  184. </div>
  185. <div class="input-group">
  186. <label>${this.i18n.t('players')}</label>
  187. <input type="number" id="players-input" placeholder="Auto-detect">
  188. </div>
  189. <div class="input-group">
  190. <label>${this.i18n.t('rounds')}</label>
  191. <input type="number" id="rounds-input" placeholder="Auto-detect">
  192. </div>
  193. <div class="input-group">
  194. <label>${this.i18n.t('topCut')}</label>
  195. <select id="topcut-input">
  196. <option value="auto">Auto</option>
  197. <option value="0">${this.i18n.t('noTopCut')}</option>
  198. <option value="4">Top 4</option>
  199. <option value="8">Top 8</option>
  200. <option value="16">Top 16</option>
  201. <option value="32">Top 32</option>
  202. <option value="64">Top 64</option>
  203. <option value="128">Top 128</option>
  204. </select>
  205. </div>
  206. <button class="button" id="calculate-button">${this.i18n.t('calculate')}</button>
  207. <div class="results" id="results"></div>
  208. `;
  209. document.body.appendChild(modal);
  210. }
  211.  
  212. attachEventListeners() {
  213. const fab = document.getElementById('topcut-fab');
  214. const modal = document.getElementById('topcut-modal');
  215. const calculateButton = document.getElementById('calculate-button');
  216. const playersInput = document.getElementById('players-input');
  217.  
  218. fab.addEventListener('click', () => {
  219. const isVisible = modal.style.display === 'block';
  220. modal.style.display = isVisible ? 'none' : 'block';
  221.  
  222. if (!isVisible) {
  223. this.autoDetectValues();
  224. }
  225. });
  226.  
  227. calculateButton.addEventListener('click', () => {
  228. this.calculateTopCut();
  229. });
  230.  
  231. playersInput.addEventListener('change', () => {
  232. const topCutSelect = document.getElementById('topcut-input');
  233. if (topCutSelect.value === 'auto') {
  234. const players = parseInt(playersInput.value);
  235. const suggestedTopCut = this.determineTopCutSize(players);
  236. this.updateTopCutSuggestion(suggestedTopCut);
  237. }
  238. });
  239. }
  240.  
  241. async autoDetectValues() {
  242. const url = window.location.href;
  243. const tournamentId = url.match(/tournament\/(.*?)(\/|$)/)?.[1];
  244.  
  245. if (!tournamentId) return;
  246.  
  247. try {
  248. const standingsResponse = await fetch(`https://play.limitlesstcg.com/tournament/${tournamentId}/standings`);
  249. const standingsText = await standingsResponse.text();
  250. const playersCount = this.extractPlayersCount(standingsText);
  251.  
  252. if (playersCount) {
  253. document.getElementById('players-input').value = playersCount;
  254. const rule = this.defaultTopCutRules.find(r => playersCount <= r.maxPlayers);
  255.  
  256. if (rule) {
  257. document.getElementById('rounds-input').value = rule.rounds;
  258. document.getElementById('topcut-input').value = rule.topCut;
  259. this.calculateTopCut();
  260. }
  261. }
  262. } catch (error) {
  263. console.error('Error auto-detecting values:', error);
  264. }
  265. }
  266.  
  267. extractPlayersCount(html) {
  268. const parser = new DOMParser();
  269. const doc = parser.parseFromString(html, 'text/html');
  270. const rows = doc.querySelectorAll('tbody tr');
  271. return rows.length > 0 ? rows.length - 1 : 0;
  272. }
  273.  
  274. determineTopCutSize(players) {
  275. const rule = this.defaultTopCutRules.find(r => players <= r.maxPlayers);
  276. return rule ? rule.topCut : 64;
  277. }
  278.  
  279. calculateTopCut() {
  280. const playersCount = parseInt(document.getElementById('players-input').value);
  281. const roundsCount = parseInt(document.getElementById('rounds-input').value);
  282. const topCutSelect = document.getElementById('topcut-input');
  283.  
  284. if (!playersCount || !roundsCount) {
  285. document.getElementById('results').innerHTML =
  286. '<span style="color: rgba(255, 255, 255, 0.87)">Please enter all values.</span>';
  287. return;
  288. }
  289.  
  290. let topCutSize = topCutSelect.value === 'auto'
  291. ? this.determineTopCutSize(playersCount)
  292. : parseInt(topCutSelect.value);
  293.  
  294. const results = this.calculatePossibleRecords(roundsCount, topCutSize);
  295. const topCutPercentage = topCutSize > 0
  296. ? Math.round((topCutSize/playersCount) * 100)
  297. : 0;
  298.  
  299. document.getElementById('results').innerHTML = `
  300. <div style="color: rgba(255, 255, 255, 0.87)">
  301. <div style="margin-bottom: 2px">${this.i18n.t('topCut')}: ${topCutSize === 0 ? this.i18n.t('noTopCut') : `Top ${topCutSize} (${topCutPercentage}%)`}</div>
  302. <div style="white-space: nowrap">${this.i18n.t('records')}: ${results}</div>
  303. </div>
  304. `;
  305. }
  306.  
  307. calculatePossibleRecords(rounds, topCutSize) {
  308. const possibleRecords = [];
  309. const totalPlayers = parseInt(document.getElementById('players-input').value);
  310. let remainingSpots = topCutSize;
  311.  
  312. for (let wins = rounds; wins >= 0; wins--) {
  313. const losses = rounds - wins;
  314.  
  315. const playersWithThisRecord = Math.round(
  316. totalPlayers *
  317. this.binomialCoefficient(rounds, wins) *
  318. Math.pow(0.5, rounds)
  319. );
  320.  
  321. if (remainingSpots > 0) {
  322. const playersAdvancing = Math.min(remainingSpots, playersWithThisRecord);
  323. const percentageAdvancing = Math.round((playersAdvancing / playersWithThisRecord) * 100);
  324.  
  325. if (percentageAdvancing > 0) {
  326. possibleRecords.push({
  327. record: `${wins}-${losses}`,
  328. percentage: percentageAdvancing
  329. });
  330.  
  331. remainingSpots -= playersAdvancing;
  332. }
  333. }
  334. }
  335.  
  336. return possibleRecords
  337. .map(r => `${r.record} (${r.percentage}%)`)
  338. .join(' | ');
  339. }
  340.  
  341. binomialCoefficient(n, k) {
  342. let result = 1;
  343. for (let i = 1; i <= k; i++) {
  344. result *= (n + 1 - i);
  345. result /= i;
  346. }
  347. return result;
  348. }
  349.  
  350. updateTopCutSuggestion(topCut) {
  351. const results = document.getElementById('results');
  352. results.innerHTML = topCut === 0
  353. ? this.i18n.t('noTopCut')
  354. : `Top ${topCut}`;
  355. }
  356. }
  357.  
  358. // Initialize the calculator
  359. new TopCutCalculator();
  360. })();