NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @namespace https://openuserjs.org/users/jacksaw // @name Blutopia BON Giveaway // @description Enables the functionality to become poor // @version 1.6.5 // @updateURL https://openuserjs.org/meta/jacksaw/Blutopia_BON_Giveaway.meta.js // @downloadURL https://openuserjs.org/install/jacksaw/Blutopia_BON_Giveaway.user.js // @license GPL-3.0-or-later // @match https://blutopia.cc/ // @match https://aither.cc/ // @grant none // ==/UserScript== // ==OpenUserJS== // @author jacksaw // @collaborator Coasty // ==/OpenUserJS== // Additional credits // @TheEther - Integration with Aither + some additional features // Ideas //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 //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, !expandEntryRange, !pauseTimer, !resumeTimer) // TODO // Add version to menu // Add command for !pot, !help, !addTime, !removeBon, !kick, and !amilucky (tells your your current chances given the spread) // 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 var GENERAL_SETTINGS = { default_mins_per_reminder: 5, mins_per_reminder_limit: 3, disable_random: false, disable_lucky: false, } // These settings can be used to test different portions of the script. By default, all should be set to false. var DEBUG_SETTINGS = { log_chat_messages: false, disable_chat_output: false } const currentUrl = window.location.href const chatboxID = "chatbox__messages-create" var chatbox = null var observer var giveawayData var numberEntries = new Map() var fancyNames = new Map() var regNum = /^-?\d+$/ var regGift = /([^ \n]+)\shas\sgifted\s([0-9.]+)\sBON\sto\s([^ \n]+)/ var whitespace = document.createTextNode(" ") var coinsIcon = document.createElement("i") coinsIcon.setAttribute("class", "fas fa-coins") var goldCoins = document.createElement("i") goldCoins.setAttribute("class", "fas fa-coins") goldCoins.style.color = "#ffc00a" goldCoins.style.padding = "5px" var giveawayBTN = document.createElement("a") giveawayBTN.setAttribute("class", "form__button form__button--text") giveawayBTN.textContent = "Giveaway" giveawayBTN.prepend(coinsIcon.cloneNode(false)) giveawayBTN.onclick = toggleMenu var 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> <button id="disableRandomLucky" class="form__button form__button--text" style="color: #44D71C"> Random & Lucky </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,1}[0-9]*" value="1" inputmode="numeric" type="text" maxlength="9"> <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,1}[0-9]*" value="50" inputmode="numeric" type="text" maxlength="9"> <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="10" 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="1" 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> ` var giveawayFrame, resetButton, closeButton, coinHeader, coinInput, startInput, endInput, timerInput, reminderInput, startButton, randomLuckyButton, countdownHeader, entriesWrapper, giveawayFrom, disableRandomLucky injectMenu() function reminderAutoScaling() { var 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 injectMenu() { var chatbox_header = document.querySelector(`#chatbox_header div`) if(chatbox_header == null) { // Chatbox hasnt loaded, so wait another 100ms before checking again setTimeout(function() {injectMenu()}, 100) } else { 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 randomLuckyButton = document.getElementById("disableRandomLucky") randomLuckyButton.onclick = toggleRandomLucky 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}`) } startButton.disabled = true coinInput.disabled = true startInput.disabled = true endInput.disabled = true timerInput.disabled = true reminderInput.disabled = true 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: [] } 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=#1DDC5D]${parseTime(totalTimeMs)}[/color][/b]. You may enter by submitting a whole number [b]between [color=#DC3D1D]${giveawayData.startNum} and ${giveawayData.endNum}[/color] inclusive[/b]. ` + (currentUrl.includes("blutopia") ? ` . Note: [color=#999999][b]Any BON gifted to the host during the duration of the Giveaway is automatically added to the Pot![/b][/color]` : "" ) sendMessage(introMessage) if (observer) { startObserver() } else { addObserver(giveawayData) } giveawayData.countdownTimerID = countdownTimer(countdownHeader, giveawayData) } } 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() { var messageList = document.getElementsByClassName("chatroom__messages")[0] observer.observe(messageList, { childList: true }) } function parseMessage(messageNode) { var isBot = messageNode.querySelector(".chatbox-message__content") == null if (isBot) { var messageContent = messageNode.querySelector(".chatbox-message__header").querySelector("div").textContent.trim() } else { var author = messageNode.querySelector(".user-tag").textContent.trim() var messageContent = messageNode.querySelector(".chatbox-message__content").textContent.trim() var fancyName = messageNode.querySelector(".user-tag").outerHTML } if (regNum.test(messageContent)) { handleEntryMessage(parseInt(messageContent, 10), author, fancyName, giveawayData) } else if (isBot) { handleGiftMessage(messageContent, giveawayData) } else if (messageContent[0] == "!") { handleGiveawayCommands(author, messageContent, fancyName, giveawayData) } } function handleGiveawayCommands(author, messageContent, fancyName, giveawayData) { var arguments = messageContent.substring(1).trim().split(" ") var command = arguments[0].toLowerCase() if(command == "time") { var remainingTime = `Time left in the giveaway: [b][color=#1DDC5D]${parseTime(giveawayData.timeLeft*1000)}[/color][/b].` sendMessage(remainingTime) } else if (command == "help" || command == "commands") { var help = `Commands are ![color=#E50E68][b]random[/b][/color] - ![color=#E50E68][b]time[/b][/color] - ![color=#E50E68][b]free[/b][/color] - ![color=#E50E68][b]number[/b][/color] - ![color=#E50E68][b]lucky[/b][/color] - ![color=#E50E68][b]bon[/b][/color] - ![color=#E50E68][b]range[/b][/color] - ![color=#E50E68][b]help[/b][/color] - ![color=#E50E68][b]commands[/b][/color].` sendMessage(help) } else if (command == "bon") { var message = `Giveaway Amount: [b][color=#FFB700]${giveawayData.amount}[/color][/b]` sendMessage(message) } else if (command == "range") { var message = `Numbers between [color=#DC3D1D]${giveawayData.startNum} and ${giveawayData.endNum}[/color] inclusive are valid.` sendMessage(message) } else if (command == "random") { if(GENERAL_SETTINGS.disable_random) { var message = `Sorry [color=#d85e27]${author}[/color], but [color=#999999]!random[/color] has been disabled for this giveaway.` sendMessage(message) return } userNumber = numberEntries.get(author) if(userNumber != undefined) { var message = `Sorry [color=#d85e27]${author}[/color], but [color=#32cd53]you[/color] already entered with number [color=#DC3D1D][b]${userNumber}[/b][/color]!` } else { var randomNum = 0 var 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) var message = `[color=#d85e27]${author}[/color] has entered with the number [color=#DC3D1D][b]${randomNum}[/b][/color]!` } sendMessage(message) } else if (command == "number") { var userNumber = numberEntries.get(author) if(userNumber != undefined) { var message = `[color=#d85e27]${author}[/color] your number is [color=#DC3D1D][b]${userNumber}[/b][/color]` } else { var message = `[color=#d85e27]${author}[/color] you are not currently in the giveaway.` } sendMessage(message) } else if (command == "free") { var free = new Array() var numOfFreeToPrint = 5 for (var k = giveawayData.startNum; k <= giveawayData.endNum; k++) { if (![...numberEntries.values()].includes(k)) { free.push(k) } } var randomValues = free.sort(() => Math.random() - 0.5).slice(0, numOfFreeToPrint) let freeNum = "Some Free Numbers: " + randomValues.join(", ") + "." sendMessage(freeNum) } else if (command == "lucky") { if(GENERAL_SETTINGS.disable_lucky) { var disable_msg = `Sorry [color=#d85e27]${author}[/color], but [color=#999999]!lucky[/color] has been disabled for this giveaway.` sendMessage(disable_msg) return } var luckyMessage = `The current giveaway lucky number is: [b][color=#1DDC5D]${getLuckyNumber(giveawayData)}[/color][/b].` sendMessage(luckyMessage) } else if (author == giveawayData.host) { // Here are the commands only the host can execute if(command == "addbon" || command == "add" || command == "addpot") { // .toFixed converts it into a string, therefore it is needed to parse it once again. var amount = parseFloat(arguments[1]) if(!isNaN(amount) && amount > 0) { giveawayData.amount += amount var addMsg = `The host is adding [color=#DC3D1D][b]${amount}[/b][/color] BON to the pot! The total is now: [b][color=#ffc00a]${cleanPotString(giveawayData.amount)} BON[/color][/b]` sendMessage(addMsg) } } else if(command == "rig") { var msg = `[color=#DC3D1D][b]Giveaway is now rigged[/b][/color]` sendMessage(msg) } else if(command == "unrig") { var msg = `[color=#DC3D1D][b]No, the giveaway will be rigged[/b][/color]` sendMessage(msg) } else if(command == "reminder") { var reminderMessage = `There is an ongoing giveaway for [b][color=#ffc00a]${cleanPotString(giveawayData.amount)} BON[/color][/b]. Time left: [b][color=#1DDC5D]${parseTime(giveawayData.timeLeft*1000)}[/color][/b]. You may enter by submitting a whole number [b]between [color=#DC3D1D]${giveawayData.startNum} and ${giveawayData.endNum}[/color] inclusive[/b] ` + (currentUrl.includes("blutopia") ? ` . Note: [color=#999999][b]Any BON gifted to the host during the duration of the Giveaway is automatically added to the Pot![/b][/color]` : "" ) sendMessage(reminderMessage) } else if (command == "end") { endGiveaway(giveawayData) } } } function handleEntryMessage(number, author, fancyName, giveawayData) { var repeatMessage 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=#DC3D1D][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=#DC3D1D][b]${number}[/b][/color]! Please try another number!` sendMessage(repeatMessage) return; } } if (number < giveawayData.startNum || number > giveawayData.endNum) { var outOfBoundsMessage = `Sorry [color=#d85e27]${author}[/color], but the number [color=#DC3D1D][b]${number}[/b][/color] is outside of the given range! Enter a number between [color=#DC3D1D][b]${giveawayData.startNum}[/b] and [b]${giveawayData.endNum}[/b][/color]!` sendMessage(outOfBoundsMessage) 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) { // This throws a TypeError when there is no gift var 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=#1DDC5D][b]${gifter}[/b][/color] is sponsoring [color=#DC3D1D][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) } } } function updateEntries() { let tableStart = "<thead><tr><th>User</th><th>Entry #</th></tr></thead><tbody>" let tableEntries = "" let tableEnd = "</tbody>" numberEntries.forEach((entry, author) => { 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 } function endGiveaway(giveawayData) { observer.disconnect() 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=#1DDC5D][b]${giveawayData.sponsors[0]}[/b][/color]` var i = 1 while (i < giveawayData.sponsors.length) { sponsorsMessage += `, [color=#1DDC5D][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=#1DDC5D][b]${bestGuess}[/b][/color] hitting the winning number exactly, [color=#DC3D1D][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=#1DDC5D][b]${bestGuess}[/b][/color] only [color=#1DDC5D][b]${Math.abs(giveawayData.winningNumber - bestGuess)}[/b][/color] away from the winning number [color=#1DDC5D][b]${giveawayData.winningNumber}[/b][/color], [color=#DC3D1D][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=#1DDC5D][b]${Math.abs(giveawayData.winningNumber - bestGuess)}[/b][/color] away from the winning number [color=#1DDC5D][b]${giveawayData.winningNumber}[/b][/color], [color=#DC3D1D][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") } var giftMessage = `/gift ${entryAuthor} ${giveawayData.amount} Congratulations! You won the giveaway!` sendMessage(giftMessage) } // 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) } 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=#1DDC5D]${parseTime(giveawayData.timeLeft*1000)}[/color][/b] remaining!` sendMessage(earlyFinishMessage) endGiveaway(giveawayData) } else if ((giveawayData.timeLeft) % giveawayData.reminderFreqSec == 0) { var reminderMessage = `There is an ongoing giveaway for [b][color=#ffc00a]${cleanPotString(giveawayData.amount)} BON[/color][/b]. Time left: [b][color=#1DDC5D]${parseTime(giveawayData.timeLeft*1000)}[/color][/b]. You may enter by submitting a whole number [b]between [color=#DC3D1D]${giveawayData.startNum} and ${giveawayData.endNum}[/color] inclusive[/b]. ` + (currentUrl.includes("blutopia") ? ` . Note: [color=#999999][b]Any BON gifted to the host during the duration of the Giveaway is automatically added to the Pot![/b][/color]` : "" ) 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() } function toggleRandomLucky() { GENERAL_SETTINGS.disable_random = !GENERAL_SETTINGS.disable_random GENERAL_SETTINGS.disable_lucky = GENERAL_SETTINGS.disable_random if (!GENERAL_SETTINGS.disable_random) { randomLuckyButton.style.color="#44D71C" } else { randomLuckyButton.style.color="#D71C1C" } }