NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name UNIT3D BON Giveaway // @description Enables the functionality to become poor // @version 1.1.5 // @updateURL https://openuserjs.org/meta/greenbeanyum/UNIT3D_BON_Giveaway.meta.js // @downloadURL https://openuserjs.org/install/greenbeanyum/UNIT3D_BON_Giveaway.user.js // @license GPL-3.0-or-later // @match https://oldtoons.world/ // @match https://aither.cc/ // @match https://reelflix.xyz/ // @match https://fearnopeer.com/ // @grant none // ==/UserScript== // ==OpenUserJS== // @author jacksaw // ==/OpenUserJS== // UNIT3D support dantayy //Split entries table on scroll? Maybe just use tabulation for form and entries. Tabulation would allow for more detailed form //2nd and 3rd? //Add option to extend the time with gifts //Add option to remove a user from the giveaway (not ban, just delete entry). //remove panel-body padding for div that contains the form so the input layout will be condensed // Optional setting (checkbox) "Lower rank priority" > when two users tie, the lower rank wins. // Detect ongoing giveaways // Possibly send all duplicate entries via chatpm instead of in the general chat to avoid spam. This could also allow to send messages to notify that the entry was successful. // Add more ! commands (!abort, !removeBon, !expandEntryRange, !addTime, !pauseTimer, !resumeTimer) // Handle decimal places for initial BON value // BUGS //Switching tabs reloads ALL messages in the cb, duplicating all BON gifts and spamming the chat. This can be achieved with the timestamps now included in the chatbox messages //giveaway amount incorrect when BON donated after reset // When doing a giveaway for 123 minutes, it will trigger a reminder right at the start (6 reminders, triggered at 2 hours, 2 minutes, 58 seconds) //Modify timer so that timestamp is used in order to keep accuracy // Validation of the entry range only sets one field red const GENERAL_SETTINGS = { default_mins_per_reminder: 5, mins_per_reminder_limit: 3 } // These settings can be used to test different portions of the script. By default, all should be set to false. const DEBUG_SETTINGS = { log_chat_messages: false, disable_chat_output: false } // DOM Selectors here for easier compatibility let messageSelector; let authorSelector; let botSelector; let fancySelector; let chatboxID; let chatboxSelector; const currentUrl = window.location.href let newUnited = false var processedGiftMessages = [] let autoSponsor = null let giveawayStartTime = null let aither = currentUrl.includes("aither") let fear = currentUrl.includes("fear") // Sites that don't broadcast gift messages in the main chatbox if (aither || fear) { autoSponsor = false } else { autoSponsor = true } let chatbox = null let observer, giveawayData let numberEntries = new Map() let fancyNames = new Map() const regNum = /^-?\d+$/ // const regGift = /([^ \n]+)\shas\sgifted\s([0-9.]+)\sBON\sto\s([^ \n]+)/ const regAith = /">([^ \n]+)<\/a.*\(taxed\s([0-9.]+)\)\sBON\sto.*">([^ \n]+)<\/a/ // messages through the api are different <div><div><a href="https://fearnopeer.com/users/gifter">gifter</a> has gifted 13 BON to <a href="https://fearnopeer.com/users/recipient">recipient</a></div></div> const regApi = /">([^ \n]+)<\/a.\shas\sgifted\s([0-9.]+)\sBON\sto.*">([^ \n]+)<\/a/ const whitespace = document.createTextNode(" ") const sponsorMessages = { "aither": "Note: [color=#999999][b]Bon gifted to the host during the duration of the Giveaway will be added to the Pot! Adjusted with Aither tax.[/b][/color]", "fear": "Note: [color=#999999][b]Bon gifted to the host during the duration of the Giveaway will be added to the Pot![/b][/color]", "default": "Note: [color=#999999][b]Any BON gifted to the host during the duration of the Giveaway is automatically added to the Pot![/b][/color]" , } // Setup giveaway menu let coinsIcon = document.createElement("i") coinsIcon.setAttribute("class", "fas fa-coins") let goldCoins = document.createElement("i") goldCoins.setAttribute("class", "fas fa-coins") goldCoins.style.color = "#ffc00a" goldCoins.style.padding = "5px" let giveawayBTN = document.createElement("a") giveawayBTN.setAttribute("class", "form__button form__button--text") giveawayBTN.textContent = "Giveaway" giveawayBTN.prepend(coinsIcon.cloneNode(false)) giveawayBTN.onclick = toggleMenu let frameHTML = ` <section id="giveawayFrame" class="panelV2" style="width: 450px; height: 90%; position: fixed; z-index: 9999; inset: 50px 150px auto auto; overflow: auto; border-style: solid; border-width: 1px; border-color: black" hidden="true"> <header class="panel__heading"> <div class="button-holder no-space"> <div class="button-left"> <h4 class="panel__heading"> <i class="fas fa-coins" style="padding: 5px;"></i> Giveaway Menu </h4> </div> <div class="button-right"> <button id="resetButton" class="form__button form__button--text"> Reset </button> <button id="closeButton" class="form__button form__button--text"> Close </button> </div> </div> </header> <div class="panel__body"> <h1 id="coinHeader" class="panel__heading--centered"> </h1> <form class="form" id="giveawayForm" style="display: flex; flex-flow: column; align-items: center;"> <p class="form__group" style="max-width: 35%;"> <input class="form__text" required="" id="giveawayAmount" pattern="[0-9]*" value="" inputmode="numeric" type="text"> <label class="form__label form__label--floating" for="giveawayAmount"> Giveaway Amount </label> </p> <div class="panel__body" style="display: flex; justify-content: center; gap: 20px"> <p class="form__group" style="width: 20%;"> <input class="form__text" required="" id="startNum" pattern="[0-9]*" value="1" inputmode="numeric" type="text" maxlength="6"> <label class="form__label form__label--floating" for="startNum"> Start # </label> </p> <p class="form__group" style="width: 20%;"> <input class="form__text" required="" id="endNum" pattern="[0-9]*" value="50" inputmode="numeric" type="text" maxlength="6"> <label class="form__label form__label--floating" for="endNum"> End # </label> </p> </div> <div class="panel__body" style="display: flex; justify-content: center; gap: 20px"> <p class="form__group" style="width: 35%;"> <input class="form__text" required="" id="timerNum" pattern="[0-9]*" value="15" inputmode="numeric" type="text"> <label class="form__label form__label--floating" for="timerNum"> Time (minutes) </label> </p> <p class="form__group" style="width: 35%;"> <input class="form__text" required="" id="reminderNum" pattern="[0-9]*" value="2" inputmode="numeric" type="text"> <label class="form__label form__label--floating" for="reminderNum"> # of Reminders </label> </p> </div> <p class="form__group" style="text-align: center;"> <button id="startButton" class="form__button form__button--filled"> Start </button> </p> </form> <h2 id="countdownHeader" class="panel__heading--centered" hidden=""> </h2> <div id="entriesWrapper" class="data-table-wrapper" hidden=""> <table id="entriesTable" class="data-table"> <thead> <tr> <th> User </th> <th> Entry # </th> </tr> </thead> <tbody> </tbody> </table> </div> </div> </section> ` // instantiate UI variables let giveawayFrame, resetButton, closeButton, coinHeader, coinInput, startInput, endInput, timerInput, reminderInput, startButton, countdownHeader, entriesWrapper, giveawayForm injectMenu() function reminderAutoScaling() { let reminders = Math.floor(parseInt(timerInput.value) / GENERAL_SETTINGS.default_mins_per_reminder) - 1 if (reminders < 0) { reminderInput.value = 0 } else { reminderInput.value = reminders } reminderInput.setCustomValidity("") } // This could be improved function entryRangeValidation() { if (parseInt(startInput.value) > parseInt(endInput.value)) { startInput.setCustomValidity("Start # should be lower than End #") endInput.setCustomValidity("Start # should be lower than End #") } else { startInput.setCustomValidity("") endInput.setCustomValidity("") } } function remindersValidation() { if (timerInput.value / reminderInput.value < GENERAL_SETTINGS.mins_per_reminder_limit) { reminderInput.setCustomValidity(`There cannot be more than 1 reminder every: ${parseTime(GENERAL_SETTINGS.mins_per_reminder_limit * 60000)}.`) reminderInput.reportValidity() } else { reminderInput.setCustomValidity("") } } function checkUnit3d() { const newUnit3d = document.querySelector("#chatbox_header div") const oldUnit3d = document.querySelector(".panel__heading#frameHeader .button-right") if (newUnit3d == null && oldUnit3d == null) { setTimeout(function () { checkUnit3d() }, 100) } else if (newUnit3d) { newUnited = true messageSelector = ".chatbox-message__content" authorSelector = ".user-tag" botSelector = ".chatbox-message__content" fancySelector = ".user-tag" chatboxSelector = "#chatbox_header div" chatboxID = "#chatbox__messages-create" } else if (oldUnit3d) { messageSelector = ".sent .text-bright div" authorSelector = ".list-group-item-heading span a" botSelector = ".sent div.system.bot" fancySelector = ".badge-user.text-bold" chatboxSelector = ".panel__heading#frameHeader .button-right" chatboxID = "#chat-message" } else { console.log("Something went wrong") } } function injectMenu() { checkUnit3d() var chatbox_header = document.querySelector(`${chatboxSelector}`); document.body.insertAdjacentHTML("beforeend", frameHTML) // New panel name chatbox_header.prepend(giveawayBTN) giveawayBTN.parentNode.insertBefore(whitespace, giveawayBTN.nextSibling) giveawayFrame = document.getElementById("giveawayFrame") resetButton = document.getElementById("resetButton") resetButton.onclick = resetGiveaway closeButton = document.getElementById("closeButton") closeButton.onclick = toggleMenu coinHeader = document.getElementById("coinHeader") coinHeader.textContent = document.getElementsByClassName("ratio-bar__points")[0].firstElementChild.textContent.trim() coinHeader.prepend(goldCoins.cloneNode(false)) coinInput = document.getElementById("giveawayAmount") startInput = document.getElementById("startNum") endInput = document.getElementById("endNum") timerInput = document.getElementById("timerNum") reminderInput = document.getElementById("reminderNum") startButton = document.getElementById("startButton") startButton.onclick = startGiveaway countdownHeader = document.getElementById("countdownHeader") entriesWrapper = document.getElementById("entriesWrapper") giveawayForm = document.getElementById("giveawayForm") document.body.appendChild(giveawayFrame) // Attach event listener to scale the number of reminders automatically timerInput.addEventListener("input", function () { reminderAutoScaling() }) // Add validation of the reminders to ensure that the frequency is not too high reminderInput.addEventListener("input", function () { remindersValidation() }) // Add entry range validation to ensure endInput > startInput startInput.addEventListener("input", function () { entryRangeValidation() }) endInput.addEventListener("input", function () { entryRangeValidation() }) } function toggleMenu() { giveawayFrame.hidden = !giveawayFrame.hidden } function startGiveaway() { if (!giveawayForm[0].checkValidity() || !giveawayForm[1].checkValidity() || !giveawayForm[2].checkValidity() || !giveawayForm[3].checkValidity() || !giveawayForm[4].checkValidity() || !giveawayForm[5].checkValidity()) { return; } // Chatbox isnt caught at the beginning when the script loads, so I moved it here for now if (chatbox == null) { chatbox = document.querySelector(chatboxID) } giveawayStartTime = null; giveawayStartTime = new Date() startButton.disabled = true coinInput.disabled = true startInput.disabled = true endInput.disabled = true timerInput.disabled = true reminderInput.disabled = true let sponsorMessage if (aither) sponsorMessage = sponsorMessages.aither else if (fear) sponsorMessage = sponsorMessages.fear else sponsorMessage = sponsorMessages.default startButton.parentElement.hidden = true entriesWrapper.hidden = false var totalTimeMs = timerInput.value * 60000 var reminderNum = parseInt(reminderInput.value) // Using this to pass by reference giveawayData = { host: document.getElementsByClassName("top-nav__username")[0].children[0].textContent.trim(), amount: parseInt(coinInput.value), startNum: parseInt(startInput.value), endNum: parseInt(endInput.value), totalEntries: parseInt(endInput.value) - parseInt(startInput.value) + 1, winningNumber: null, totalSeconds: totalTimeMs / 1000, timeLeft: totalTimeMs / 1000, reminderNum: reminderNum, reminderFreqSec: (totalTimeMs / 1000 / (reminderNum + 1)).toFixed(0), sponsors: [], // hack probably crappy, but it should work for now. winnerSent: false, } var currentBon = parseInt(document.getElementsByClassName("ratio-bar__points")[0].textContent.trim().replace(/\s/g, ''), 10) if (currentBon < giveawayData.amount) { window.alert(`GIVEAWAY ERROR: The amount entered (${giveawayData.amount}), is above your current BON (${currentBon}). You may need to refresh the page to update your BON amount.`) resetGiveaway(giveawayData) } else { giveawayData.winningNumber = getRandomInt(giveawayData.startNum, giveawayData.endNum) // Setup an alert when trying to exit the tab during a giveaway window.onbeforeunload = function () { return "Giveaway in progress" } var introMessage = "I am hosting a giveaway for [b][color=#ffc00a]" + `${giveawayData.amount} BON[/color][/b]. Entries will be open for [b][color=green]` + `${parseTime(totalTimeMs)}[/color][/b]. You may enter by submitting a whole number [b]between [color=red]` + `${giveawayData.startNum} and ${giveawayData.endNum}[/color] inclusive[/b]. ${sponsorMessage} [img]https://cdn.7tv.app/emote/635764cc11b745c4f0230c96/1x.webp[/img]` sendMessage(introMessage) if (observer) { startObserver() } else { addObserver(giveawayData) } giveawayData.countdownTimerID = countdownTimer(countdownHeader, giveawayData) if (!autoSponsor) { sponsorsInterval = setInterval(async () => { await getSponsors() }, 15000) } //MAYBE UNNEEDED RETURN? return false; } } function addObserver(giveawayData) { observer = new MutationObserver((mutations) => { mutations.forEach((mutation) => { for (var i = 0; i < mutation.addedNodes.length; i++) { var tablist = document.querySelectorAll('[role="tablist"]'); var general = tablist[0].childNodes[0] //console.log(general) //if(general.className != "panel__tab panel__tab--active") { // console.log("General chat is not active") //} // !!! Here it is really important not to pass the giveawayData object to the parseMessage function. If done so, then it will for some reason always retain the pointer of the first // reference it was given, therefore when more than one giveaway is done in a row, the following giveaways messages will be parsed against the old giveawayData information. parseMessage(mutation.addedNodes[i]) } }) }) startObserver() } function startObserver() { let messageList if (newUnited) { messageList = document.getElementsByClassName("chatroom__messages")[0] } else { messageList = document.querySelectorAll(".messages .list-group")[0] } observer.observe(messageList, { childList: true }) } function parseMessage(messageNode) { let messageContent let isBot if (newUnited) { isBot = messageNode.querySelector(botSelector) == null } else { isBot = messageNode.querySelector(botSelector) !== null } if (isBot) { if (newUnited) { messageContent = messageNode.querySelector(botSelector).querySelector("div").textContent } else { messageContent = messageNode.querySelector(botSelector).querySelector("div div").textContent } } else { messageContent = messageNode.querySelector(messageSelector).textContent var author = messageNode.querySelector(authorSelector).textContent.trim() var fancyName = messageNode.querySelector(fancySelector).outerHTML } let isValid = regNum.test(messageContent) if (isValid) { handleEntryMessage(parseInt(messageContent, 10), author, fancyName, giveawayData) } else if (messageContent[0] == "!") { handleGiveawayCommands(author, messageContent, fancyName, giveawayData) } else if (isBot && autoSponsor) { handleGiftMessage(messageContent, giveawayData) } } function handleGiveawayCommands(author, messageContent, fancyName, giveawayData) { let arguments = messageContent.substring(1).trim().split(" ") let command = arguments[0].toLowerCase() let userNumber = numberEntries.get(author) let message const validCommands = ['time', 'random', 'number', 'lucky', 'addbon', 'commands'] if (!validCommands.includes(command)) { return } switch (command) { case 'time': message = `Time left in the giveaway: [b][color=green]${parseTime(giveawayData.timeLeft * 1000)}[/color][/b].` sendMessage(message) break case 'random': if (userNumber) { message = `Sorry [color=#d85e27]${author}[/color], but [color=#32cd53]you[/color] already entered with number [color=red][b]${userNumber}[/b][/color]!` } else { let randomNum = 0 let currentNumbers = Array.from(numberEntries.values()) do { randomNum = Math.floor(Math.random() * (giveawayData.endNum - giveawayData.startNum + 1)) + giveawayData.startNum } while (currentNumbers.includes(randomNum)) addNewEntry(author, fancyName, randomNum) message = `[color=#d85e27]${author}[/color] entered with number [color=green][b]${randomNum}[/b][/color]` } sendMessage(message) break case 'number': if (userNumber) { message = `[color=#d85e27]${author}[/color] your number is [color=red][b]${userNumber}[/b][/color]` } else { message = `[color=#d85e27]${author}[/color] you are not currently in the giveaway.` } sendMessage(message) break case 'lucky': message = `The current giveaway lucky number is: [b][color=green]${getLuckyNumber(giveawayData)}[/color][/b].` sendMessage(message) break case 'addbon': if (author == giveawayData.host) { let amount = parseFloat(arguments[1]) if (!isNaN(amount) && amount > 0) { giveawayData.amount += amount message = `The host is adding [color=red][b]${amount}[/b][/color] BON to the pot! The total is now: [b][color=#ffc00a]${cleanPotString(giveawayData.amount)} BON[/color][/b]` } } else { message = "Only the host can use the !addbon command" } sendMessage(message) break case 'commands': message = "Valid commands are !random !number !lucky !time & for hosts !addbon" sendMessage(message) break default: break } } function handleEntryMessage(number, author, fancyName, giveawayData) { var repeatMessage // Check if number is legal if (number < giveawayData.startNum || number > giveawayData.endNum) { const outOfBoundsMessage = `Sorry [color=#d85e27]${author}[/color], but the number [color=red][b]${number}[/b][/color] is outside of the given range! Please try another number that is [b]between ${giveawayData.startNum} and ${giveawayData.endNum} inclusive[/b]!` sendMessage(outOfBoundsMessage) return; } for (let [msgAuthor, msgValue] of numberEntries.entries()) { if (msgAuthor == author) { repeatMessage = `Sorry [color=#d85e27]${author}[/color], but [color=#32cd53]you[/color] already entered with number [color=red][b]${msgValue}[/b][/color]!` sendMessage(repeatMessage) return; } else if (msgValue == number) { repeatMessage = `Sorry [color=#d85e27]${author}[/color], but [color=#32cd53]${msgAuthor}[/color] already entered with number [color=red][b]${number}[/b][/color]! Please try another number!` sendMessage(repeatMessage) return; } } if (!numberEntries.has(author)) { addNewEntry(author, fancyName, number) } } function addNewEntry(author, fancyName, number) { numberEntries.set(author, number) fancyNames.set(author, fancyName) updateEntries() } function handleGiftMessage(messageContent, giveawayData) { let gift; if (fear) { gift = regApi.exec(messageContent) } else if (aither) { gift = regAith.exec(messageContent) } else { gift = regGift.exec(messageContent) } var addAmount = parseFloat(gift[2]) var gifter = gift[1] var recpt = gift[3] if (recpt == giveawayData.host) { giveawayData.amount += addAmount var giftMessage = `[color=green][b]${gifter}[/b][/color] is sponsoring [color=red][b]${addAmount}[/b][/color] additional BON! The total pot is now: [b][color=#ffc00a]${cleanPotString(giveawayData.amount)} BON[/color][/b]` sendMessage(giftMessage) // If not yet included, add gifter to the list of giveaway sponsors if (!giveawayData.sponsors.includes(gifter)) { giveawayData.sponsors.push(gifter) } } } const getSponsors = async () => { let chatroom = aither ? '4' : '2'; const api = `${currentUrl}api/chat/messages/${chatroom}`; const startTimeTimestamp = giveawayStartTime.getTime(); try { const response = await fetch(api, { method: 'GET', headers: { 'Cookie': document.cookie // Include cookies from the main page } }); if (!response.ok) { throw new Error('Network response was not ok'); } const data = await response.json(); const systemMessages = data.data; const filteredMessages = systemMessages.filter(message => { const messageTime = new Date(message.created_at).getTime(); const giftMessage = message.message.includes("gifted"); return giftMessage && messageTime > startTimeTimestamp; }); for (const msg of filteredMessages) { // prevent duplicate sponsorship messages if (!processedGiftMessages.includes(msg.id)) { const messageContent = msg.message; processedGiftMessages.push(msg.id) handleGiftMessage(messageContent, giveawayData); } } } catch (error) { console.error('There was a problem with the fetch operation:', error); } }; function updateEntries() { let tableStart = "<thead><tr><th>User</th><th>Entry #</th></tr></thead><tbody>" let tableEntries = "" let tableEnd = "</tbody>" numberEntries.forEach((entry, author) => { let fancyName = fancyNames.get(author) tableEntries += `<tr><td>${fancyName}</td><td>${entry}</td></tr>`; //need ; to fix syntax highligthing }) document.getElementById("entriesTable").innerHTML = tableStart + tableEntries + tableEnd } async function endGiveaway(giveawayData) { observer.disconnect() clearInterval(sponsorsInterval) // If the site doesn't have a gift messages broadcast, we need to get the sponsors from the api if (!autoSponsor) { await getSponsors() } if (numberEntries.size == 0) { var emptyMessage = `Unfortunately, no one has entered the giveaway so no one wins!` sendMessage(emptyMessage) } else { if (giveawayData.sponsors.length > 0) { var sponsorsMessage = `Thank you to all the additional sponsors! ` sponsorsMessage += `[color=green][b]${giveawayData.sponsors[0]}[/b][/color]` var i = 1 while (i < giveawayData.sponsors.length) { sponsorsMessage += `, [color=green][b]${giveawayData.sponsors[i]}[/b][/color]` i++ } sendMessage(sponsorsMessage) } var bestGuess = Number.MAX_VALUE var tie = false var gapToWinningNumber, currentBestEntryGap var entryAuthor, tieAuthor, tieGuess numberEntries.forEach((entry, author) => { currentBestEntryGap = Math.abs(giveawayData.winningNumber - bestGuess) gapToWinningNumber = Math.abs(giveawayData.winningNumber - entry) if (currentBestEntryGap > gapToWinningNumber) { tie = false bestGuess = entry entryAuthor = author } else if (gapToWinningNumber == currentBestEntryGap) { tie = true tieAuthor = author tieGuess = entry } }) if (bestGuess == giveawayData.winningNumber) { var winMessage = `With a guess of [color=green][b]${bestGuess}[/b][/color] hitting the winning number exactly, [color=red][b]${entryAuthor}[/b][/color] has won [color=#ffc00a][b]${cleanPotString(giveawayData.amount)} BON[/b][/color]!` sendMessage(winMessage) } else if (!tie) { var winMessage = `With a guess of [color=green][b]${bestGuess}[/b][/color] only [color=green][b]${Math.abs(giveawayData.winningNumber - bestGuess)}[/b][/color] away from the winning number [color=green][b]${giveawayData.winningNumber}[/b][/color], [color=red][b]${entryAuthor}[/b][/color] has won [color=#ffc00a][b]${cleanPotString(giveawayData.amount)} BON[/b][/color]!` sendMessage(winMessage) } else if (tie) { var tieMessage = `With a tie between [color=#d85e27][b]${entryAuthor}[/b][/color] ([b]${bestGuess}[/b]) and [color=#d85e27][b]${tieAuthor}[/b][/color] ([b]${tieGuess}[/b]), both being only [color=green][b]${Math.abs(giveawayData.winningNumber - bestGuess)}[/b][/color] away from the winning number [color=green][b]${giveawayData.winningNumber}[/b][/color], [color=red][b]${entryAuthor}[/b][/color] has won [color=#ffc00a][b]${cleanPotString(giveawayData.amount)} BON[/b][/color] as their entry was submitted first!` sendMessage(tieMessage) } else { console.log("Something went wrong while ending the giveaway") } if (!giveawayData.winnerSent) { var giftMessage = `/gift ${entryAuthor} ${giveawayData.amount} Congratulations! You won the giveaway!` sendMessage(giftMessage) // hopefully stops repeat gift bug giveawayData.winnerSent = true; } } // Clear onbeforeunload alert window.onbeforeunload = null clearInterval(giveawayData.countdownTimerID) observer.disconnect() delete giveawayData } function getRandomInt(min, max) { min = Math.ceil(min); max = Math.floor(max); return Math.floor(Math.random() * (max - min + 1)) + min; } // Unified all time related events in the countdown (endGiveaway and reminders) to avoid any kind of drifting between them. function countdownTimer(display, giveawayData) { display.hidden = false var minutes, seconds var timerID = setInterval(function () { giveawayData.timeLeft-- minutes = parseInt(giveawayData.timeLeft / 60, 10) seconds = parseInt(giveawayData.timeLeft % 60, 10) minutes = minutes < 0 ? "0" + minutes : minutes seconds = seconds < 10 ? "0" + seconds : seconds display.textContent = minutes + ":" + seconds if (giveawayData.timeLeft <= 0) { endGiveaway(giveawayData) return } else if (giveawayData.totalEntries == numberEntries.size) { // Color scheme of this message could be improved var earlyFinishMessage = "All [b][color=#ffc00a]" + `${giveawayData.totalEntries}[/color][/b] slot(s) filled up! Therefore, the giveaway is ending with [b][color=green]` + `${parseTime(giveawayData.timeLeft * 1000)}[/color][/b] remaining!` sendMessage(earlyFinishMessage) endGiveaway(giveawayData) return } else if ((giveawayData.timeLeft) % giveawayData.reminderFreqSec == 0) { let reminderMessage; let reminderSponsor; if (aither) reminderSponsor = sponsorMessages.aither; else if (fear) reminderSponsor = sponsorMessages.fear; else reminderSponsor = sponsorMessages.default; reminderMessage = `There is an ongoing giveaway for [b][color=#ffc00a]` + `${giveawayData.amount} BON[/color][/b]. Time left: [b][color=green]` + `${parseTime(giveawayData.timeLeft * 1000)}[/color][/b]. You may enter by submitting a whole number [b]between [color=red]` + `${giveawayData.startNum} and ${giveawayData.endNum}[/color] inclusive[/b]. ${reminderSponsor}` sendMessage(reminderMessage) } }, 1000) return timerID } function sendMessage(messageStr) { if (!DEBUG_SETTINGS.disable_chat_output) { chatbox.value = messageStr chatbox.dispatchEvent(new KeyboardEvent("keydown", { keyCode: 13 })) } if (DEBUG_SETTINGS.log_chat_messages) { console.log(messageStr) } } function getLuckyNumber(giveawayData) { var rangeStart = giveawayData.startNum var rangeEnd = giveawayData.endNum var numbers = Array.from(numberEntries.values()).sort((a, b) => { if (a < b) { return -1 } else { return 1 } }) numbers.push(rangeEnd + 1) var bestGap = 0 var lucky = 0 var pastNum = rangeStart - 1 var currentNum, gap for (var i = 0; i < numbers.length; i++) { currentNum = numbers[i] gap = currentNum - pastNum if (gap > bestGap) { lucky = Math.floor(gap / 2) + pastNum bestGap = gap } pastNum = currentNum } return lucky } function cleanPotString(giveawayPotAmount) { if (giveawayPotAmount % 1 == 0) { return giveawayPotAmount } else { return giveawayPotAmount.toFixed(2) } } function parseTime(timeInMs) { var hours = Math.floor((timeInMs / 3600000) % 60) var minutes = Math.floor((timeInMs / 60000) % 60) var seconds = Math.floor((timeInMs / 1000) % 60) var timeString = `` if (hours > 0) { timeString += `${hours} hour` if (hours > 1) { timeString += `s` } } if (minutes > 0) { if (timeString != ``) { timeString += `, ` } timeString += `${minutes} minute` if (minutes > 1) { timeString += `s` } } if (seconds > 0) { if (timeString != ``) { timeString += `, ` } timeString += `${seconds} second` if (seconds > 1) { timeString += `s` } } return timeString } function resetGiveaway() { clearInterval(giveawayData.countdownTimerID) // Clear onbeforeunload alert window.onbeforeunload = null numberEntries = new Map() fancyNames = new Map() entriesWrapper.hidden = true countdownHeader.hidden = true startButton.parentElement.hidden = false startButton.disabled = false coinInput.disabled = false startInput.disabled = false endInput.disabled = false timerInput.disabled = false reminderInput.disabled = false observer.disconnect() delete observer giveawayForm.reset() updateEntries() }