NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript==
// @name PTT BON Giveaway
// @description Giveaway BON dla PolishTorrent
// @version 2.0.37
// @author Aniouek32
// @match https://polishtorrent.top/*
// @grant none
// @license GPL-3.0-or-later
// @updateURL https://openuserjs.org/meta/Aniouek32/PTT_BON_Giveaway.meta.js
// @downloadURL https://openuserjs.org/install/Aniouek32/PTT_BON_Giveaway.user.js
// ==/UserScript==
// Tryby zabawy:
//
// Standard giveaway:
// - klasyczne zgłoszenia numerem na czacie
// - dostępne profile językowe
//
// Stawka większa niż pula BON (v2.0):
// - wejście tylko przez /gift HOST KWOTA numer
// - host nie bierze udziału w losowaniu
// - aktywny profil komunikatów: Sarkazm
//
// Komendy dla użytkowników:
//
// !time
// Pokazuje, ile czasu zostało do końca giveaway.
//
// !status
// Pokazuje skrócony status giveaway: pula, czas, zajęte/wolne numery i sponsorów.
//
// !random
// Losuje wolny numer i automatycznie zapisuje użytkownika do giveaway (tylko Standard).
//
// !number
// Pokazuje numer, z którym dany użytkownik już bierze udział.
//
// !lucky
// Pokazuje aktualnie „szczęśliwy” numer, wyliczony przez skrypt
// na podstawie największej luki między zajętymi numerami.
//
// !luckye
// Automatycznie zapisuje użytkownika na aktualny „szczęśliwy” numer (tylko Standard).
//
// !free
// Pokazuje kilka przykładowych wolnych numerów.
//
// !entries
// Pokazuje bieżące zgłoszenia: kto jaki numer zajął oraz ile numerów zostało wolnych.
//
// !sponsors
// Pokazuje sponsorów tego giveaway i ile BON dorzucił każdy z nich.
//
// !bon
// Pokazuje aktualną pulę BON w giveaway.
//
// !range
// Pokazuje zakres dozwolonych numerów, np. 1-50.
//
// !commands
// Wyświetla listę dostępnych komend.
//
// Komendy tylko dla hosta:
//
// !addbon 100
// Dodaje BON do puli giveaway.
//
// !removebon 100
// Usuwa BON z puli giveaway (tylko z BON dodanego później przez hosta).
//
// !winners 3
// Zmienia liczbę zwycięzców w trakcie trwania giveaway.
//
// !reminder
// Wysyła ręczne przypomnienie o giveaway na czat.
//
// !pause
// Wstrzymuje przyjmowanie nowych zgłoszeń (numery i autozapisy).
//
// !resume
// Wznawia przyjmowanie nowych zgłoszeń.
//
// !time add 5
// Dodaje 5 minut do czasu trwania giveaway.
//
// !time remove 5
// Odejmuje 5 minut od czasu trwania giveaway.
(function () {
'use strict';
const GENERAL_SETTINGS = {
defaultMinsPerReminder: 5,
minsPerReminderLimit: 3,
entryPollMs: 2000,
sponsorPollMs: 10000,
entriesRoomId: '1',
sponsorRoomId: '13',
verifyAttempts: 5,
verifyDelayMs: 5000,
};
const DEBUG_SETTINGS = {
logChatMessages: false,
disableChatOutput: false,
};
const COMMAND_WINDOW_MS = 10000;
const MAX_COMMANDS_PER_WINDOW = 3;
const BASE_PENALTY_SECONDS = 30;
const MIN_ACTION_GAP_MS = 900;
const ENTRY_FEEDBACK_COOLDOWN_MS = 8000;
const STRIKE_WINDOW_MS = 10 * 60 * 1000;
const MAX_STRIKE_MULTIPLIER = 8;
const REPEAT_COMMAND_COOLDOWNS_MS = Object.freeze({
time: 3000,
status: 5000,
entries: 5000,
free: 7000,
lucky: 7000,
luckye: 7000,
random: 7000,
range: 5000,
sponsors: 8000,
bon: 5000,
number: 5000,
commands: 10000
});
const SPONSOR_ANNOUNCE = {
mode: "digest",
digestMs: 60000,
immediateSingleMin: 500,
flushMinTotal: 250,
maxPendingEvents: 50,
showTopN: 5,
showMinPerUser: 0
};
function pickVariant(variants) {
if (!Array.isArray(variants) || variants.length === 0) return "";
return variants[Math.floor(Math.random() * variants.length)];
}
const CHAT_COPY_SARKAZM = Object.freeze({
intro: ({ amount, totalTimeMs, winnersText, startNum, endNum, sponsorMessage }) =>
pickVariant([
`Uwaga, uwaga: odpalam giveaway za [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b]. Macie [b][color=green]${parseTime(totalTimeMs)}[/color][/b], więc bez paniki. W puli czeka [b][color=#5DE2E7]${winnersText}[/color][/b]. Wrzucaj liczbę [b]od [color=red]${startNum} do ${endNum}[/color][/b]. `,
`Start zabawy: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b] na stole. Czas [b][color=green]${parseTime(totalTimeMs)}[/color][/b], zwycięzców [b][color=#5DE2E7]${winnersText}[/color][/b]. Celuj w zakres [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`No dobra, jedziemy: giveaway za [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b]. Zegar tyka [b][color=green]${parseTime(totalTimeMs)}[/color][/b], nagrodzonych bedzie [b][color=#5DE2E7]${winnersText}[/color][/b]. Numer od [b][color=red]${startNum} do ${endNum}[/color][/b]. `,
`Kto pierwszy ten lepszy: pula [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b], czas [b][color=green]${parseTime(totalTimeMs)}[/color][/b], liczba szczęśliwców [b][color=#5DE2E7]${winnersText}[/color][/b]. Gramy numerami [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Otwieram giveaway: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b] do zgarnięcia. Macie [b][color=green]${parseTime(totalTimeMs)}[/color][/b], zwycięzców [b][color=#5DE2E7]${winnersText}[/color][/b], zakres [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`To nie cwiczenia: w puli lezy [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b]. Czas [b][color=green]${parseTime(totalTimeMs)}[/color][/b], a na mecie [b][color=#5DE2E7]${winnersText}[/color][/b]. Strzelaj [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Ruszamy z loteria dla zdecydowanych: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b], limit czasu [b][color=green]${parseTime(totalTimeMs)}[/color][/b], zwyciezcow [b][color=#5DE2E7]${winnersText}[/color][/b]. Zakres [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Scena gotowa, RNG rozgrzane: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b] do podzialu. Masz [b][color=green]${parseTime(totalTimeMs)}[/color][/b], typuj od [b][color=red]${startNum} do ${endNum}[/color][/b]. `,
`Wchodzimy w tryb "kto trafi, ten bierze": [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b], czas [b][color=green]${parseTime(totalTimeMs)}[/color][/b], nagrodzonych [b][color=#5DE2E7]${winnersText}[/color][/b]. Numery [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Dobra, bez lania wody: pula [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b], licznik [b][color=green]${parseTime(totalTimeMs)}[/color][/b], cel [b][color=#5DE2E7]${winnersText}[/color][/b]. Gramy na [b][color=red]${startNum}-${endNum}[/color][/b]. `
]) + sponsorMessage,
reminder: ({ amount, timeLeftMs, winnersNum, startNum, endNum, sponsorMessage }) =>
pickVariant([
`Przypominajka dla spóźnialskich: giveaway trwa, pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b]. Zostało [b][color=green]${parseTime(timeLeftMs)}[/color][/b], zwycięzców [b][color=#5DE2E7]${winnersNum}[/color][/b], zakres [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Update dla tych z tylu: nadal gramy o [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b]. Czas do końca [b][color=green]${parseTime(timeLeftMs)}[/color][/b], liczba wygranych [b][color=#5DE2E7]${winnersNum}[/color][/b], numery [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Jeszcze żyje: giveaway dalej otwarty. Pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], zostało [b][color=green]${parseTime(timeLeftMs)}[/color][/b], wygrywa [b][color=#5DE2E7]${winnersNum}[/color][/b] osób. Zakres [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Alarm dla maruderów: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b] nadal czeka. Czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], zwycięzców [b][color=#5DE2E7]${winnersNum}[/color][/b], trafiasz liczbę z [b][color=red]${startNum} do ${endNum}[/color][/b]. `,
`Szybki ping: giveaway aktywny, pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b]. Do końca [b][color=green]${parseTime(timeLeftMs)}[/color][/b], miejsc [b][color=#5DE2E7]${winnersNum}[/color][/b], przedział [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Jeszcze jest o co bic sie: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], zwyciezcow [b][color=#5DE2E7]${winnersNum}[/color][/b], zakres [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Przeglad sytuacji: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], zegar [b][color=green]${parseTime(timeLeftMs)}[/color][/b], wolne sloty uciekaja. Zakres [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Krotki komunikat techniczny: giveaway nadal aktywny. W puli [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], do konca [b][color=green]${parseTime(timeLeftMs)}[/color][/b]. `,
`Jesli miales dolaczyc "za chwile", to ta chwila wlasnie trwa: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], zakres [b][color=red]${startNum}-${endNum}[/color][/b]. `,
`Przypomnienie kontrolne: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], zwyciezcow [b][color=#5DE2E7]${winnersNum}[/color][/b]. `
]) + sponsorMessage,
timeLeft: (timeLeftMs) => `Na zegarze zostało jeszcze [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
hostOnlyTime: () => "Ta komenda jest tylko dla hosta. Demokracja kończy się tutaj.",
usageTime: () => "Użycie: !time add 5 albo !time remove 5",
hostAddTime: (amount, timeLeftMs) => `Host dorzucił [b][color=green]${amount} min.[/color][/b]. Nowy czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
hostRemoveTime: (amount, timeLeftMs) => `Host skrócił o [b][color=red]${amount} min.[/color][/b]. Zostało: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
hostTimeEnded: () => "Host uciął czas do zera. Koniec imprezy.",
status: ({ amount, timeLeftMs, taken, total, freeCount, sponsorCount, entriesState }) =>
pickVariant([
`Status w pigułce: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], numery [b]${taken}/${total}[/b], wolne [b]${freeCount}[/b], sponsorzy [b]${sponsorCount}[/b], zgłoszenia [b]${entriesState}[/b].`,
`Raport bojowy: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b] w puli, timer [b][color=green]${parseTime(timeLeftMs)}[/color][/b], zajęte [b]${taken}/${total}[/b], wolne [b]${freeCount}[/b], sponsorzy [b]${sponsorCount}[/b], tryb zgłoszeń [b]${entriesState}[/b].`,
`Sytuacja na froncie: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], liczby [b]${taken}/${total}[/b], wolnych [b]${freeCount}[/b], sponsorów [b]${sponsorCount}[/b], zgłoszenia [b]${entriesState}[/b].`,
`Stan gry: kasa [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], do końca [b][color=green]${parseTime(timeLeftMs)}[/color][/b], sloty [b]${taken}/${total}[/b], wolne [b]${freeCount}[/b], sponsorzy [b]${sponsorCount}[/b], wpisy [b]${entriesState}[/b].`,
`Telemetry: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], obłożenie [b]${taken}/${total}[/b], wolne [b]${freeCount}[/b], sponsorzy [b]${sponsorCount}[/b], status zgłoszeń [b]${entriesState}[/b].`,
`Snapshot: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b] w puli, [b][color=green]${parseTime(timeLeftMs)}[/color][/b] do konca, obsada [b]${taken}/${total}[/b], wolne [b]${freeCount}[/b], sponsorzy [b]${sponsorCount}[/b], wpisy [b]${entriesState}[/b].`,
`Kontrola lotu: budzet [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], timer [b][color=green]${parseTime(timeLeftMs)}[/color][/b], zajete [b]${taken}/${total}[/b], wolne [b]${freeCount}[/b], sponsorzy [b]${sponsorCount}[/b].`,
`Suchy raport: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], numery [b]${taken}/${total}[/b], zapas [b]${freeCount}[/b], status wpisow [b]${entriesState}[/b].`,
`Panel stanu: kasa [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], odliczanie [b][color=green]${parseTime(timeLeftMs)}[/color][/b], sponsorzy [b]${sponsorCount}[/b], wolnych [b]${freeCount}[/b].`,
`Wersja dla konkretu: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], [b]${taken}/${total}[/b] zajete, [b]${freeCount}[/b] wolne, wpisy [b]${entriesState}[/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`
]),
alreadyJoined: (author, number) =>
pickVariant([
`Ej [color=#d85e27]${sanitizeNick(author)}[/color], już grasz numerem [color=red][b]${number}[/b][/color]. Drugi raz nie nalicza punktów za styl.`,
`[color=#d85e27]${sanitizeNick(author)}[/color], ten pociąg już odjechał: masz [color=red][b]${number}[/b][/color].`,
`Spokojnie [color=#d85e27]${sanitizeNick(author)}[/color], numer [color=red][b]${number}[/b][/color] już jest twój. Bonusu za dubel brak.`,
`[color=#d85e27]${sanitizeNick(author)}[/color], już widniejesz na liście z numerem [color=red][b]${number}[/b][/color].`,
`Nie przyspieszysz RNG, [color=#d85e27]${sanitizeNick(author)}[/color]. Aktualny numer to [color=red][b]${number}[/b][/color].`,
`Masz juz bilet, [color=#d85e27]${sanitizeNick(author)}[/color]: numer [color=red][b]${number}[/b][/color]. Kolejny nic nie zmieni.`,
`[color=#d85e27]${sanitizeNick(author)}[/color], system pamieta: grasz [color=red][b]${number}[/b][/color]. Nadpisywanie marzen wylaczone.`,
`Ten numer jest juz Twoj, [color=#d85e27]${sanitizeNick(author)}[/color]: [color=red][b]${number}[/b][/color]. Duble sa przereklamowane.`,
`[color=#d85e27]${sanitizeNick(author)}[/color], juz jestes na liscie z [color=red][b]${number}[/b][/color]. RNG nie lubi spamu.`,
`Bez nerwow, [color=#d85e27]${sanitizeNick(author)}[/color]. Aktywny numer to nadal [color=red][b]${number}[/b][/color].`
]),
entriesPaused: (author) => `Stop-klatka, [color=#d85e27]${sanitizeNick(author)}[/color]. Host chwilowo zamroził zgłoszenia.`,
noFreeForUser: (author) => `Przykro mi [color=#d85e27]${sanitizeNick(author)}[/color], ale wolnych numerów już brak. Magii też.`,
joinedRandom: (author, num, timeLeftMs) => `[color=#d85e27]${sanitizeNick(author)}[/color] wskakuje z numerem [color=green][b]${num}[/b][/color]. Czas tyka: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
numberInfo: (author, number) => `[color=#d85e27]${sanitizeNick(author)}[/color], Twój numer to [color=red][b]${number}[/b][/color]. Pilnuj go jak oka w głowie.`,
numberNone: (author) => `[color=#d85e27]${sanitizeNick(author)}[/color], na razie nie masz numeru w giveaway.`,
luckyNow: (num) => `Aktualny szczęśliwy numer to [b][color=green]${num}[/color][/b]. Totolotek mode: ON.`,
noLuckyCandidate: (author) => `Niestety [color=#d85e27]${sanitizeNick(author)}[/color], dziś algorytm nie ma sensownego "szczęśliwego" numeru.`,
joinedLuckye: (author, num, timeLeftMs) => `[color=#d85e27]${sanitizeNick(author)}[/color] wchodzi przez [b]!luckye[/b] z numerem [color=green][b]${num}[/b][/color]. Czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
noFreeLeft: () => "Wolnych numerów brak. Plansza wyczyszczona.",
freeCountOnly: (count) => `Wolnych numerów zostało: [b][color=green]${count}[/color][/b]. Jeszcze zdążysz.`,
freeSample: (count, sample) => `Wolne numery (${count}): [b][color=green]${sample.join(", ")}[/color][/b]. Bierz, póki są.`,
hostOnlyCmd: (cmd) => `Komenda ${cmd} jest tylko dla hosta. Nice try.`,
usage: (example) => `Użycie: ${example}`,
addBonBalanceError: (currentBon, newHostContribution) =>
`Spokojnie, milionerem jeszcze nie jesteś. Masz [b][color=#ffc00a]${formatBon(currentBon)} BON[/color][/b], ` +
`a wkład hosta skoczyłby do [b][color=red]${formatBon(newHostContribution)} BON[/color][/b].`,
hostAddedBon: (amount, total) => `Host dorzuca [color=red][b]${cleanPotString(amount)}[/b][/color] BON. Pula rośnie do [b][color=#ffc00a]${cleanPotString(total)} BON[/color][/b].`,
cannotRemoveNoExtra: () => "Nie ma czego odejmować. Możesz zdjąć tylko to, co dołożyłeś przez !addbon.",
cannotRemoveMax: (maxAmount) => `Możesz odjąć maksymalnie [b][color=red]${cleanPotString(maxAmount)}[/color][/b] BON. Matematyka jest nieugięta.`,
hostRemovedBon: (amount, total) => `Host zdejmuje [color=red][b]${cleanPotString(amount)}[/b][/color] BON z dodatkowego wkładu. Pula: [b][color=#ffc00a]${cleanPotString(total)} BON[/color][/b].`,
hostOnlyWinners: () => "Tylko host może zmienić liczbę zwycięzców.",
winnersSet: (count) => `Liczba zwycięzców ustawiona na [b][color=#5DE2E7]${count}[/color][/b]. Tak ma być, nie dyskutujemy.`,
hostOnlyReminder: () => "Przypomnienie ręczne? Tylko host.",
hostOnlyPause: () => "Wstrzymywać zgłoszenia może tylko host.",
pausedAlready: () => "Zgłoszenia już są wstrzymane. Bardziej się nie da.",
pausedNow: () => "Host zatrzymał nowe zgłoszenia. Chwila oddechu.",
hostOnlyResume: () => "Wznawiać zgłoszenia może tylko host.",
resumedAlready: () => "Zgłoszenia już działają. Wszystko pod kontrolą.",
resumedNow: () => "Host wznowił zgłoszenia. Wracamy do gry.",
commandsList: () => "Komendy: !time !status !random !number !lucky !luckye !free !entries !sponsors !bon !range !commands | host: !addbon !removebon !winners !reminder !pause !resume !time add/remove | Stawka większa niż pula BON: wejście [color=red][b]/gift HOST KWOTA numer[/b][/color].",
sponsorHint: (host) => pickVariant([
`[color=#999999][b]Każdy BON wysłany hostowi podczas giveaway automatycznie zwiększa pulę nagród.[/b][/color] [color=#ffc00a][b]Jak dorzucić BON:[/b][/color] [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color]`,
`Jeśli chcesz napompować pulę, wyślij hostowi [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color]. Tak, to działa automatycznie.`,
`Sponsor mode: ON. Każdy gift do [b]${sanitizeNick(host)}[/b] dokłada BON do puli. Format: [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color].`,
`Masz BON i gest? Rzuć [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color], a pula urośnie bez pytania o zgodę.`,
`Kto dokłada do puli, ten bohater dnia. Komenda: [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color].`,
`Szybka ścieżka do większej puli: [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color]. System sam to doliczy.`,
`Portfel swędzi? Użyj [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color] i patrz, jak pula puchnie.`,
`Wsparcie techniczne puli: [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color]. Resztę zrobi automat.`,
`Każdy BON wysłany do [b]${sanitizeNick(host)}[/b] podczas giveaway trafia do puli. Komenda: [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color].`,
`Ekonomia eventu w Twoich rękach: [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color] i już dokładamy do nagród.`
]),
entriesNone: () => "Na razie cisza, zero zgłoszeń.",
entriesList: (size, total, freeCount, list) => `Zgłoszenia (${size}/${total}, wolne: ${freeCount}): ${list}`,
sponsorsNone: () => "Sponsorów brak. Portfele dziś ostrożne.",
sponsorsList: (totalSponsored, list) => `Sponsorzy (łącznie ${cleanPotString(totalSponsored)} BON): ${list}`,
potNow: (amount) => `Aktualna pula: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b].`,
rangeNow: (start, end) => `Zakres numerów: [b][color=red]${start} - ${end}[/color][/b].`,
outOfRange: (author, number, start, end, freeSuggestion) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], numer [color=red][b]${number}[/b][/color] jest poza zakresem. ` +
`Celuj [b]od ${start} do ${end} (włącznie)[/b].` +
freeSuggestion,
numberTaken: (author, existingAuthor, number, freeSuggestion) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], numer [color=red][b]${number}[/b][/color] już zgarnął [color=#32cd53]${sanitizeNick(existingAuthor)}[/color]. ` +
`Spróbuj innego.` +
freeSuggestion,
entryJoined: (author, number, timeLeftMs) =>
pickVariant([
`[color=#d85e27]${sanitizeNick(author)}[/color] dołącza z numerem [color=red][b]${number}[/b][/color]. Do końca: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] wbija na [color=red][b]${number}[/b][/color]. Zegar pokazuje [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
`Mamy nowego gracza: [color=#d85e27]${sanitizeNick(author)}[/color] bierze [color=red][b]${number}[/b][/color]. Czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] wskakuje na numer [color=red][b]${number}[/b][/color]. Pozostało [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] zajmuje [color=red][b]${number}[/b][/color]. Timer: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] melduje numer [color=red][b]${number}[/b][/color]. Zegar: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
`Do listy dopisuje sie [color=#d85e27]${sanitizeNick(author)}[/color] z [color=red][b]${number}[/b][/color]. Do konca [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] lockuje slot [color=red][b]${number}[/b][/color]. Odliczanie pokazuje [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
`Nowy wpis: [color=#d85e27]${sanitizeNick(author)}[/color] -> [color=red][b]${number}[/b][/color]. Czas do finiszu [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] rezerwuje [color=red][b]${number}[/b][/color]. Licznik nadal tyka: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`
]),
noEntriesEnd: () => "Koniec giveaway i... nikt nie zagrał. Pula wraca do domu.",
hostAmongWinners: (names) => `Host znalazł się wśród zwycięzców: ${names}.`,
earlyFinish: (totalEntries, timeLeftMs) =>
"Wszystkie [b][color=#ffc00a]" +
`${totalEntries}[/color][/b] miejsca zajęte szybciej niż przewidywano. ` +
`Formalnie zostało jeszcze [b][color=green]${parseTime(timeLeftMs)}[/color][/b], ale kończymy wcześniej.`,
spamDetected: (author, penaltySec) =>
`[color=red][b]Spam wykryty. ${sanitizeNick(author)} ma blokadę komend na ${penaltySec} sek.[/b][/color]`,
sponsorDigest: ({ deltaTotalNum, sponsorCount, parts, othersCount, totalPot }) => {
let message = pickVariant([
`Sponsorzy dorzucili [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `,
`Wpadł świeży zrzut: [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `,
`Ekonomia czatu nie śpi: [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `,
`Portfele przemówiły: [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `,
`Nowy boost puli: [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `,
`Donacja wpadla: [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `,
`Kolejny zastrzyk puli: [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `,
`Rynek powiedzial "tak": [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `,
`Skarbiec dostal upgrade: [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `,
`Wplynal pakiet wsparcia: [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "graczy"}[/b]. `
]);
if (parts.length) {
message += parts.join(", ");
if (othersCount > 0) {
message += `, [i]+${othersCount} więcej[/i]`;
}
message += ". ";
}
message += pickVariant([
`Pula po zastrzyku wynosi [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`,
`Nowa suma na liczniku: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`,
`Bilans po tej akcji: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`,
`Aktualny stan skarbca: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`,
`Kasa po update: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`,
`Po tej rundzie licznik pokazuje [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`,
`Aktualizacja skarbca: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`,
`Nowy punkt odniesienia: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b] w puli.`,
`Stan po ksiegowaniu: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`,
`Pula po doliczeniu: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`
]);
return message;
},
finishSummary: ({ winningNumber, winnersSummary, hostContributionText, sponsorTotalText, sponsors, hasTie }) => {
let message = pickVariant([
`Koniec zabawy: trafiony numer to [b][color=green]${winningNumber}[/color][/b]. Podium zgarniają: ${winnersSummary}. Host dorzucił [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `,
`Kurtyna w dół: zwycięski numer [b][color=green]${winningNumber}[/color][/b]. Wygrani: ${winnersSummary}. Wkład hosta [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `,
`RNG zdecydowało: [b][color=green]${winningNumber}[/color][/b]. Na podium lądują: ${winnersSummary}. Host wrzucił [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `,
`Wynik końcowy: [b][color=green]${winningNumber}[/color][/b]. Topka tej rundy: ${winnersSummary}. Host dołożył [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `,
`Mamy finał: numer [b][color=green]${winningNumber}[/color][/b]. Wygrywają: ${winnersSummary}. Udział hosta: [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `,
`Final zamkniety: numer [b][color=green]${winningNumber}[/color][/b]. Na czele: ${winnersSummary}. Wklad hosta [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `,
`Wyrok RNG zapadl: [b][color=green]${winningNumber}[/color][/b]. Nagrody biora: ${winnersSummary}. Host dorzucil [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `,
`Domkniecie rundy: trafiony [b][color=green]${winningNumber}[/color][/b], podium: ${winnersSummary}. Udzial hosta [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `,
`Rachunek koncowy: [b][color=green]${winningNumber}[/color][/b]. Wygrani to ${winnersSummary}. Host wrzucil [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `,
`Zamykamy temat: numer [b][color=green]${winningNumber}[/color][/b], zwyciezcy ${winnersSummary}. Dorzut hosta [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `
]);
if (sponsors.length > 0) {
const sponsorDetails = sponsors
.map(item => `[color=green][b]${sanitizeNick(item.name)}[/b][/color] - ${cleanPotString(item.amount)} BON`)
.join(", ");
message += pickVariant([
`Sponsorzy dorzucili łącznie [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). Portfele zapłakały, chat zadowolony. `,
`Sponsorzy dołożyli razem [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). Ekipa finansowa dowiozła. `,
`Dodatkowy boost od sponsorów: [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). Szacunek dla bankierów. `,
`Sponsorzy wrzucili sumarycznie [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). Kultura finansowa na poziomie. `,
`Do puli od sponsorów weszło [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). To się nazywa wsparcie. `,
`Sponsorzy finalnie dolozyli [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). Finansowanie domkniete. `,
`Bilans sponsorow: [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). Robota zrobiona. `,
`Zaplecze finansowe dorzucilo [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). Szacunek. `,
`Kontrybucja sponsorow: [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). Pula mowi dziekuje. `,
`Zasilenie od sponsorow: [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] (${sponsorDetails}). Kasa dowieziona. `
]);
} else {
message += pickVariant([
`Dodatkowi sponsorzy odpalili klasyczne "może następnym razem". `,
`Sponsorzy tym razem zagrali w trybie oszczędnym. `,
`Sekcja sponsorów dzisiaj w trybie stealth. `,
`Nikt ekstra nie dorzucił do puli i żyjemy dalej. `,
`Bonus od sponsorów: 0 BON i solidne milczenie. `,
`Sponsorzy dzisiaj wybrali obserwacje zamiast transferow. `,
`Dodatkowy zrzut nie padl, portfele zostaly zamkniete. `,
`Sekcja wsparcia finansowego: online, ale bez ruchow. `,
`Nadprogramowego BON nie bylo, bo po co sobie ulatwiac. `,
`Bilans sponsorow na koncu: zero doplat i duzo spokoju. `
]);
}
if (hasTie) {
message += pickVariant([
`Przy remisie wyższe miejsce bierze osoba, która była szybsza na starcie.`,
`Przy remisie liczy się kolejność zgłoszeń, bez sentymentów.`,
`W remisie wygrywa szybszy wpis. Proste zasady, proste życie.`,
`Jeśli remis, pierwszeństwo ma ten kto zgłosił się wcześniej.`,
`Remis rozstrzygamy czasem zgłoszenia: szybszy bierze wyższą pozycję.`,
`Gdy wpada remis, priorytet ma ten, kto zareagowal szybciej.`,
`Remisy tniemy timestampem: pierwsze zgloszenie ma wyzsze miejsce.`,
`W przypadku remisu decyduje kolejnosc wejsc, nie glosnosc komentarzy.`,
`Jesli roznica jest ta sama, wygrywa szybszy wpis i temat zamkniety.`,
`Przy rownym dystansie decyduje czas zgloszenia: kto pierwszy, ten wyzej.`
]);
}
return message;
}
});
const CHAT_COPY_KLASYCZNY = Object.freeze({
intro: ({ amount, totalTimeMs, winnersText, startNum, endNum, sponsorMessage }) =>
`Hej! Startujemy giveaway o wartości [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b]. ` +
`Zapisy otwarte przez [b][color=green]${parseTime(totalTimeMs)}[/color][/b]. ` +
`Wyłonimy [b][color=#5DE2E7]${winnersText}[/color][/b]. ` +
`Wpisz liczbę całkowitą [b]od [color=red]${startNum} do ${endNum}[/color] włącznie[/b]. ` +
sponsorMessage,
reminder: ({ amount, timeLeftMs, winnersNum, startNum, endNum, sponsorMessage }) =>
`Trwa giveaway o wartości [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b]. ` +
`Pozostały czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b]. ` +
`Zwycięzców: [b][color=#5DE2E7]${winnersNum}[/color][/b]. ` +
`Wpisz liczbę całkowitą [b]od [color=red]${startNum} do ${endNum}[/color] włącznie[/b]. ` +
sponsorMessage,
timeLeft: (timeLeftMs) => `Pozostały czas giveaway: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
hostOnlyTime: () => "Tylko host może zmieniać czas giveaway.",
usageTime: () => "Użycie: !time add 5 lub !time remove 5",
hostAddTime: (amount, timeLeftMs) => `Host dodał [b][color=green]${amount} min.[/color][/b]. Nowy pozostały czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
hostRemoveTime: (amount, timeLeftMs) => `Host odjął [b][color=red]${amount} min.[/color][/b]. Nowy pozostały czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
hostTimeEnded: () => "Host skrócił czas giveaway do zera. Kończymy teraz.",
status: ({ amount, timeLeftMs, taken, total, freeCount, sponsorCount, entriesState }) =>
`Status giveaway: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], ` +
`czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], ` +
`numery [b]${taken}/${total}[/b] (wolne: ${freeCount}), ` +
`sponsorzy [b]${sponsorCount}[/b], zgłoszenia: [b]${entriesState}[/b].`,
alreadyJoined: (author, number) => `Przepraszam [color=#d85e27]${sanitizeNick(author)}[/color], ale [color=#32cd53]już[/color] bierzesz udział z numerem [color=red][b]${number}[/b][/color]!`,
entriesPaused: (author) => `Przepraszam [color=#d85e27]${sanitizeNick(author)}[/color], ale host chwilowo wstrzymał przyjmowanie nowych zgłoszeń.`,
noFreeForUser: (author) => `Przepraszam [color=#d85e27]${sanitizeNick(author)}[/color], ale nie ma już wolnych numerów.`,
joinedRandom: (author, num, timeLeftMs) => `[color=#d85e27]${sanitizeNick(author)}[/color] dołącza z numerem [color=green][b]${num}[/b][/color]! Pozostały czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
numberInfo: (author, number) => `[color=#d85e27]${sanitizeNick(author)}[/color], twój numer to [color=red][b]${number}[/b][/color].`,
numberNone: (author) => `[color=#d85e27]${sanitizeNick(author)}[/color], obecnie nie bierzesz udziału w giveaway.`,
luckyNow: (num) => `Aktualny szczęśliwy numer giveaway to: [b][color=green]${num}[/color][/b].`,
noLuckyCandidate: (author) => `Przepraszam [color=#d85e27]${sanitizeNick(author)}[/color], ale nie ma już sensownego wolnego numeru do automatycznego wyboru.`,
joinedLuckye: (author, num, timeLeftMs) => `[color=#d85e27]${sanitizeNick(author)}[/color] dołącza komendą [b]!luckye[/b] z numerem [color=green][b]${num}[/b][/color]! Pozostały czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
noFreeLeft: () => "Nie ma już wolnych numerów.",
freeCountOnly: (count) => `Pozostało wolnych numerów: [b][color=green]${count}[/color][/b].`,
freeSample: (count, sample) => `Wolne numery (${count}): [b][color=green]${sample.join(", ")}[/color][/b].`,
hostOnlyCmd: (cmd) => `Tylko host może użyć komendy ${cmd}.`,
usage: (example) => `Użycie: ${example}`,
addBonBalanceError: (currentBon, newHostContribution) =>
`Nie możesz dodać tyle BON. Według widocznego salda masz [b][color=#ffc00a]${formatBon(currentBon)} BON[/color][/b], ` +
`a wkład hosta wzrósłby do [b][color=red]${formatBon(newHostContribution)} BON[/color][/b].`,
hostAddedBon: (amount, total) => `Host dodaje [color=red][b]${cleanPotString(amount)}[/b][/color] BON do puli! Łączna pula wynosi teraz: [b][color=#ffc00a]${cleanPotString(total)} BON[/color][/b].`,
cannotRemoveNoExtra: () => "Nie możesz już odjąć BON — możesz usuwać tylko BON dodany później przez !addbon.",
cannotRemoveMax: (maxAmount) => `Możesz odjąć maksymalnie [b][color=red]${cleanPotString(maxAmount)}[/color][/b] BON.`,
hostRemovedBon: (amount, total) => `Host usuwa [color=red][b]${cleanPotString(amount)}[/b][/color] BON z dodatkowego wkładu. Łączna pula wynosi teraz: [b][color=#ffc00a]${cleanPotString(total)} BON[/color][/b].`,
hostOnlyWinners: () => "Tylko host może zmienić liczbę zwycięzców.",
winnersSet: (count) => `Liczba zwycięzców została zmieniona na [b][color=#5DE2E7]${count}[/color][/b].`,
hostOnlyReminder: () => "Tylko host może ręcznie wysłać przypomnienie.",
hostOnlyPause: () => "Tylko host może wstrzymać zgłoszenia.",
pausedAlready: () => "Zgłoszenia są już wstrzymane.",
pausedNow: () => "Host wstrzymał przyjmowanie nowych zgłoszeń.",
hostOnlyResume: () => "Tylko host może wznowić zgłoszenia.",
resumedAlready: () => "Zgłoszenia są już aktywne.",
resumedNow: () => "Host wznowił przyjmowanie nowych zgłoszeń.",
commandsList: () => "Dostępne komendy: !time !status !random !number !lucky !luckye !free !entries !sponsors !bon !range !commands oraz dla hosta !addbon !removebon !winners !reminder !pause !resume !time add/remove. W trybie Stawka większa niż pula BON wejście tylko przez [color=red][b]/gift HOST KWOTA numer[/b][/color].",
sponsorHint: (host) => `Każdy BON wysłany hostowi podczas giveaway automatycznie zwiększa pulę nagród. Jak dodać BON: [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color].`,
entriesNone: () => "Aktualnie nie ma jeszcze żadnych zgłoszeń.",
entriesList: (size, total, freeCount, list) => `Zgłoszenia (${size}/${total}, wolne: ${freeCount}): ${list}`,
sponsorsNone: () => "Aktualnie brak sponsorów w tym giveaway.",
sponsorsList: (totalSponsored, list) => `Sponsorzy tego giveaway (łącznie ${cleanPotString(totalSponsored)} BON): ${list}`,
potNow: (amount) => `Aktualna pula giveaway: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b].`,
rangeNow: (start, end) => `Zakres numerów dla tego giveaway: [b][color=red]${start} - ${end}[/color][/b].`,
outOfRange: (author, number, start, end, freeSuggestion) =>
`Przepraszam [color=#d85e27]${sanitizeNick(author)}[/color], ale numer [color=red][b]${number}[/b][/color] jest poza zakresem! ` +
`Spróbuj numeru [b]od ${start} do ${end} włącznie[/b]!` +
freeSuggestion,
numberTaken: (author, existingAuthor, number, freeSuggestion) =>
`Przepraszam [color=#d85e27]${sanitizeNick(author)}[/color], ale [color=#32cd53]${sanitizeNick(existingAuthor)}[/color] już wybrał numer [color=red][b]${number}[/b][/color]! ` +
`Spróbuj innego numeru!` +
freeSuggestion,
entryJoined: (author, number, timeLeftMs) =>
`[color=#d85e27]${sanitizeNick(author)}[/color] dołącza z numerem [color=red][b]${number}[/b][/color]! ` +
`Pozostały czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
noEntriesEnd: () => "Niestety nikt nie wziął udziału w giveaway, więc nikt nie wygrywa!",
hostAmongWinners: (names) => `Host znalazł się wśród zwycięzców: ${names}.`,
earlyFinish: (totalEntries, timeLeftMs) =>
"Wszystkie [b][color=#ffc00a]" +
`${totalEntries}[/color][/b] miejsca zostały zajęte! Giveaway kończy się wcześniej — pozostało jeszcze [b][color=green]` +
`${parseTime(timeLeftMs)}[/color][/b]!`,
spamDetected: (author, penaltySec) =>
`[color=red][b]Spam wykryty. ${sanitizeNick(author)} ma blokadę komend na ${penaltySec} sek.[/b][/color]`,
sponsorDigest: ({ deltaTotalNum, sponsorCount, parts, othersCount, totalPot }) => {
let message =
`Sponsorzy właśnie dorzucili [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] ` +
`od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "osób"}[/b]! `;
if (parts.length) {
message += parts.join(", ");
if (othersCount > 0) {
message += `, [i]+${othersCount} więcej[/i]`;
}
message += ". ";
}
message += `Łączna pula wynosi teraz [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`;
return message;
},
finishSummary: ({ winningNumber, winnersSummary, hostContributionText, sponsorTotalText, sponsors, hasTie }) => {
let message =
`Zwycięski numer to [b][color=green]${winningNumber}[/color][/b]. ` +
`Wygrywają: ${winnersSummary}. ` +
`Host wrzucił [b][color=#ffc00a]${hostContributionText} BON[/color][/b]. `;
if (sponsors.length > 0) {
const sponsorDetails = sponsors
.map(item => `[color=green][b]${sanitizeNick(item.name)}[/b][/color] - ${cleanPotString(item.amount)} BON`)
.join(", ");
message +=
`Sponsorzy dorzucili łącznie [b][color=#ffc00a]${sponsorTotalText} BON[/color][/b] ` +
`(${sponsorDetails}). `;
} else {
message += `Dodatkowi sponsorzy nie dorzucili nic do puli. `;
}
if (hasTie) {
message += `Przy remisie wyższe miejsce bierze osoba, która zgłosiła się wcześniej.`;
}
return message;
}
});
const CHAT_COPY_MEMICZNY = Object.freeze({
...CHAT_COPY_SARKAZM,
intro: ({ amount, totalTimeMs, winnersText, startNum, endNum, sponsorMessage }) =>
`[BOOT] Instancja giveaway wystartowa\u0142a z bud\u017cetem [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b]. ` +
`[TIMER] Okno eventu: [b][color=green]${parseTime(totalTimeMs)}[/color][/b]. ` +
`[QUEST] Sloty nagr\u00f3d: [b][color=#5DE2E7]${winnersText}[/color][/b]. ` +
`Wbijaj numer [b]od [color=red]${startNum} do ${endNum}[/color] (w\u0142\u0105cznie)[/b]. ` +
sponsorMessage,
reminder: ({ amount, timeLeftMs, winnersNum, startNum, endNum, sponsorMessage }) =>
`[EXPLORE] Event aktywny: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b]. ` +
`Pozosta\u0142o [b][color=green]${parseTime(timeLeftMs)}[/color][/b], zwyci\u0119zc\u00f3w: [b][color=#5DE2E7]${winnersNum}[/color][/b]. ` +
`Zakres wej\u015bcia: [b]${startNum}-${endNum}[/b]. ` +
sponsorMessage,
timeLeft: (timeLeftMs) => `[CLOCK] Do ko\u0144ca sesji: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
status: ({ amount, timeLeftMs, taken, total, freeCount, sponsorCount, entriesState }) =>
`[STATUS] Pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], ` +
`sloty [b]${taken}/${total}[/b], wolne [b]${freeCount}[/b], sponsorzy [b]${sponsorCount}[/b], tryb [b]${entriesState}[/b].`,
alreadyJoined: (author, number) => `[LOCK] [color=#d85e27]${sanitizeNick(author)}[/color] ma ju\u017c przypisany numer [color=red][b]${number}[/b][/color]. Duplikat odrzucony przez silnik.`,
entriesPaused: (author) => `[PAUSE] [color=#d85e27]${sanitizeNick(author)}[/color], rekrutacja wstrzymana przez admina eventu.`,
noFreeForUser: (author) => `[CAP] [color=#d85e27]${sanitizeNick(author)}[/color], wszystkie sloty zaj\u0119te.`,
joinedRandom: (author, num, timeLeftMs) => `[LOOT] [color=#d85e27]${sanitizeNick(author)}[/color] wylosowa\u0142 numer [color=green][b]${num}[/b][/color] (+RNG XP). Czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
numberInfo: (author, number) => `[ID] [color=#d85e27]${sanitizeNick(author)}[/color], tw\u00f3j token wej\u015bcia: [color=red][b]${number}[/b][/color].`,
luckyNow: (num) => `[SEED] Aktualny lucky seed: [b][color=green]${num}[/color][/b].`,
joinedLuckye: (author, num, timeLeftMs) => `[CRIT] [color=#d85e27]${sanitizeNick(author)}[/color] u\u017cy\u0142 [b]!luckye[/b] i wskoczy\u0142 na [color=green][b]${num}[/b][/color]. Timer: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
freeSample: (count, sample) => `[MAP] Wolne sloty (${count}): [b][color=green]${sample.join(", ")}[/color][/b].`,
hostAddedBon: (amount, total) => `[UPGRADE] Host dorzuci\u0142 [color=red][b]${cleanPotString(amount)}[/b][/color] BON. Nowa pula: [b][color=#ffc00a]${cleanPotString(total)} BON[/color][/b].`,
hostRemovedBon: (amount, total) => `[ROLLBACK] Host zdj\u0105\u0142 [color=red][b]${cleanPotString(amount)}[/b][/color] BON. Pula po rollbacku: [b][color=#ffc00a]${cleanPotString(total)} BON[/color][/b].`,
winnersSet: (count) => `[CONFIG] Liczba zwyci\u0119zc\u00f3w ustawiona na [b][color=#5DE2E7]${count}[/color][/b].`,
entriesNone: () => `[AFK] Brak zg\u0142osze\u0144. Serwer czeka na graczy.`,
sponsorsNone: () => `[ECONOMY] Sponsor\u00f3w brak. Rynek w trybie oszcz\u0119dnym.`,
sponsorsList: (totalSponsored, list) => `[GUILD] Sponsorzy (\u0142\u0105cznie ${cleanPotString(totalSponsored)} BON): ${list}`,
potNow: (amount) => `[BANK] Aktualna pula: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b].`,
rangeNow: (start, end) => `[ZONE] Dozwolony zakres numer\u00f3w: [b][color=red]${start}-${end}[/color][/b].`,
sponsorHint: (host) => `[ECONOMY] Każdy transfer BON do [b]${sanitizeNick(host)}[/b] buffuje pulę giveaway. Użyj: [color=red][b]/gift ${copyableNick(host)} KWOTA wiadomość[/b][/color].`,
outOfRange: (author, number, start, end, freeSuggestion) =>
`[BOUNDARY] [color=#d85e27]${sanitizeNick(author)}[/color], numer [color=red][b]${number}[/b][/color] poza map\u0105 (${start}-${end}).` +
freeSuggestion,
numberTaken: (author, existingAuthor, number, freeSuggestion) =>
`[COLLISION] [color=#d85e27]${sanitizeNick(author)}[/color], slot [color=red][b]${number}[/b][/color] jest ju\u017c zaj\u0119ty przez [color=#32cd53]${sanitizeNick(existingAuthor)}[/color].` +
freeSuggestion,
entryJoined: (author, number, timeLeftMs) =>
`[JOIN] [color=#d85e27]${sanitizeNick(author)}[/color] do\u0142\u0105czy\u0142 z numerem [color=red][b]${number}[/b][/color]. Pozosta\u0142y czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
noEntriesEnd: () => `[GAME OVER] Brak graczy, brak dropu, koniec sesji.`,
earlyFinish: (totalEntries, timeLeftMs) =>
`[VICTORY] Wszystkie [b][color=#ffc00a]${totalEntries}[/color][/b] sloty zaj\u0119te przed czasem. Na liczniku by\u0142o jeszcze [b][color=green]${parseTime(timeLeftMs)}[/color][/b].`,
sponsorDigest: ({ deltaTotalNum, sponsorCount, parts, othersCount, totalPot }) => {
let message =
`[LOOT] Sponsorzy wrzucili [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "os\u00f3b"}[/b]. `;
if (parts.length) {
message += parts.join(", ");
if (othersCount > 0) {
message += `, [i]+${othersCount} wi\u0119cej[/i]`;
}
message += ". ";
}
message += `[BANK++] Pula po dropie: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`;
return message;
}
});
const CHAT_COPY_SUPER_SARKAZM = Object.freeze({
...CHAT_COPY_SARKAZM,
intro: ({ amount, totalTimeMs, winnersText, startNum, endNum, sponsorMessage }) =>
`S\u0142uchamy uwa\u017cnie: giveaway za [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b] w\u0142a\u015bnie wystartowa\u0142. ` +
`Masz [b][color=green]${parseTime(totalTimeMs)}[/color][/b], wi\u0119c je\u015bli planowa\u0142e\u015b "zaraz wracam", to nie wracaj za p\u00f3\u017ano. ` +
`Liczba szcz\u0119\u015bliwc\u00f3w: [b][color=#5DE2E7]${winnersText}[/color][/b]. ` +
`Strzelaj liczb\u0105 [b]od [color=red]${startNum} do ${endNum}[/color] (w\u0142\u0105cznie)[/b]. ` +
sponsorMessage,
status: ({ amount, timeLeftMs, taken, total, freeCount, sponsorCount, entriesState }) =>
`Raport dla wytrwa\u0142ych: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], ` +
`zaj\u0119te [b]${taken}/${total}[/b], wolne [b]${freeCount}[/b], sponsorzy [b]${sponsorCount}[/b], zg\u0142oszenia [b]${entriesState}[/b].`,
alreadyJoined: (author, number) => `Spokojnie [color=#d85e27]${sanitizeNick(author)}[/color], ju\u017c grasz numerem [color=red][b]${number}[/b][/color]. Zach\u0142anno\u015b\u0107 nie zwi\u0119ksza szans.`,
noFreeForUser: (author) => `Niestety [color=#d85e27]${sanitizeNick(author)}[/color], wolne numery sko\u0144czy\u0142y si\u0119 szybciej ni\u017c wym\u00f3wki na czacie.`,
hostOnlyCmd: (cmd) => `Komenda ${cmd} jest tylko dla hosta. To nie casting na wsp\u00f3\u0142prowadz\u0105cego.`,
addBonBalanceError: (currentBon, newHostContribution) =>
`Pi\u0119kna pr\u00f3ba, ale saldo m\u00f3wi "nie". Masz [b][color=#ffc00a]${formatBon(currentBon)} BON[/color][/b], ` +
`a po ruchu host mia\u0142by [b][color=red]${formatBon(newHostContribution)} BON[/color][/b].`,
noEntriesEnd: () => `Koniec giveaway i... zero zg\u0142osze\u0144. Publiczno\u015b\u0107 najwyra\u017aniej by\u0142a dzi\u015b zaj\u0119ta oddychaniem.`,
sponsorsNone: () => "Sponsor\u00f3w brak. Portfele dzi\u015b postanowi\u0142y udawa\u0107 kamie\u0144.",
sponsorDigest: ({ deltaTotalNum, sponsorCount, parts, othersCount, totalPot }) => {
let message =
`Pad\u0142 zrzut [color=red][b]${cleanPotString(deltaTotalNum)} BON[/b][/color] od [b]${sponsorCount} ${sponsorCount === 1 ? "osoby" : "os\u00f3b"}[/b]. `;
if (parts.length) {
message += parts.join(", ");
if (othersCount > 0) {
message += `, [i]+${othersCount} wi\u0119cej[/i]`;
}
message += ". ";
}
message += `Pula po turbo-zastrzyku: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b].`;
return message;
}
});
function getV2DynamicMinStartNote(minEntryFee, dynamicMinEnabled) {
if (!dynamicMinEnabled) return "";
return " " + pickVariant([
`Tryb agresywny ON: minimalne wpisowe zawsze równa się aktualnej puli BON.`,
`Zasada rundy: minimum wejścia = bieżąca pula, bez stałych kroków.`,
`Dynamiczny próg agresywny: każda wpłata podnosi pulę, a tym samym kolejne minimum.`,
`W tym wariancie następny gracz musi wejść na poziomie aktualnej puli.`,
`Im większa pula po poprzednim wejściu, tym wyższe minimum dla kolejnej osoby.`,
`Mechanika agresywna: wpisowe nie rośnie liniowo, tylko podąża za aktualną pulą.`,
`Kto wrzuci ponad minimum, temu sam podbije próg dla następnych, bo minimum = pula.`,
`Tu nie ma taryfy ulgowej: aktualna pula wyznacza minimalne wejście.`,
`Szybki wzór: minimum teraz = aktualna pula BON na liczniku.`,
`Wersja hard: kolejny udział wymaga co najmniej tyle, ile wynosi bieżąca pula.`
]);
}
function getV2DynamicMinReminderNote(currentMinEntryFee, dynamicMinEnabled, dynamicMinStep) {
if (!dynamicMinEnabled) return "";
return " " + pickVariant([
`Przypomnienie: aktualne minimum to [b][color=#ffc00a]${cleanPotString(currentMinEntryFee)} BON[/color][/b] i jest równe bieżącej puli.`,
`Ten tryb działa prosto: minimum = aktualna pula, teraz [b][color=#ffc00a]${cleanPotString(currentMinEntryFee)} BON[/color][/b].`,
`Bieżący próg wejścia to [b][color=#ffc00a]${cleanPotString(currentMinEntryFee)} BON[/color][/b], bo tyle wynosi aktualna pula.`,
`Jeśli ktoś wpłaci więcej, od razu podbije pulę i kolejne minimum. Obecnie [b][color=#ffc00a]${cleanPotString(currentMinEntryFee)} BON[/color][/b]`,
`Tutaj próg nie ma stałego kroku, podąża za pulą na żywo. Obecnie [b][color=#ffc00a]${cleanPotString(currentMinEntryFee)} BON[/color][/b]`,
`Im większa pula po ostatnim wejściu, tym wyższe minimum dla następnych. [b][color=#ffc00a]${cleanPotString(currentMinEntryFee)} BON[/color][/b]`,
`Wariant hard: kolejny udział wymaga co najmniej bieżącej puli, teraz ${cleanPotString(currentMinEntryFee)} BON.`,
`Szybki komunikat: minimum wejścia = pula, obecnie ${cleanPotString(currentMinEntryFee)} BON.`,
`Agresywny scaling w toku: następny próg wynika bezpośrednio z aktualnej puli. [b][color=#ffc00a]${cleanPotString(currentMinEntryFee)} BON[/color][/b]`,
`Nie ma liniowego +X: minimum to po prostu aktualna pula BON. [b][color=#ffc00a]${cleanPotString(currentMinEntryFee)} BON[/color][/b]`
]);
}
const V2_SARKAZM_COPY = Object.freeze({
intro: ({ amount, hostContributionText, totalTimeMs, winnersText, startNum, endNum, minEntryFee, host, dynamicMinEnabled }) =>
pickVariant([
`Tryb [b]Stawka większa niż pula BON[/b] odpalony. Wejście kosztuje minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b]. Pula startowa: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`,
`Uruchamiam [b]Stawka większa niż pula BON[/b]: wpisowe od [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b]. Startujemy z [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`,
`Nowa runda [b]Stawka większa niż pula BON[/b]: tanio nie będzie. Minimalne wpisowe [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b], pula [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`,
`Tryb Stawka większa niż pula BON online: wejście przez [color=red][b]/gift ${copyableNick(host)} KWOTA numer[/b][/color], minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b]. Pula startowa: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`,
`Dobra, gramy na twardo: [b]Stawka większa niż pula BON[/b], wpisowe od [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b], bez darmowych wejść. Pula startowa: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`,
`Wersja [b]Stawka większa niż pula BON[/b] rusza: przynosisz BON i numer albo wracasz z kwitkiem. Minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)}[/color][/b]. Pula startowa: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`,
`Event [b]Stawka większa niż pula BON[/b] start. Portfel obowiązkowy: min. [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b]. Pula startowa: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`,
`Włączam [b]Stawka większa niż pula BON[/b]. Numer zgłaszasz w gifcie, a nie na słowo honoru. Wpisowe min. [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b]. Pula startowa: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`,
`Tryb [b]Stawka większa niż pula BON[/b]: host nie bierze udziału, gracze wchodzą tylko przez gift. Minimum wejścia: [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b]. Pula startowa: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`,
`Dzisiaj tryb [b]Stawka większa niż pula BON[/b]. Płać i graj: [color=red][b]/gift ${copyableNick(host)} KWOTA numer[/b][/color], min. [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b]. Pula startowa: [b][color=#ffc00a]${formatBon(amount)} BON[/color][/b].`
]) + (String(hostContributionText) === cleanPotString(amount) ? "" : ` Wkład hosta: [b][color=#ffc00a]${hostContributionText} BON[/color][/b].`) + ` Czas: [b][color=green]${parseTime(totalTimeMs)}[/color][/b], wygrywa: [b][color=#5DE2E7]${winnersText}[/color][/b], zakres [b][color=red]${startNum}-${endNum}[/color][/b]. Dołączasz przez [color=red][b]/gift ${copyableNick(host)} KWOTA numer[/b][/color]` + getV2DynamicMinStartNote(minEntryFee, dynamicMinEnabled),
reminder: ({ amount, timeLeftMs, winnersNum, startNum, endNum, minEntryFee, host, dynamicMinEnabled, dynamicMinStep }) =>
pickVariant([
`Przypomnienie Stawka większa niż pula BON: wejście jest płatne, a numer podajesz w treści gifta. Minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b].`,
`Tryb Stawka większa niż pula BON nadal aktywny. Jeśli chcesz grać, wysyłasz gift z numerem. Minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b].`,
`Szybki ping: numery z czatu nie działają w trybie Stawka większa niż pula BON. Liczy się tylko numer z treści gifta.`,
`Dla spóźnionych: w trybie Stawka większa niż pula BON wejście jest płatne. Minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b].`,
`Update rundy trybu Stawka większa niż pula BON: chcesz numer, wysyłasz gift z numerem. Inaczej system wzrusza ramionami.`,
`Runda trwa, zasady bez zmian: gift + numer, minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b].`,
`Check-in trybu Stawka większa niż pula BON: wpisowe od [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b], host poza losowaniem.`,
`Jeśli jeszcze nie dołączyłeś: wyślij gift z numerem i gotowe.`,
`Tryb Stawka większa niż pula BON przypomina o sobie: numer jest brany z treści gifta.`,
`Tryb płatny w toku: minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b], zgłoszenia przez gift.`
]) + ` Wejście: [color=red][b]/gift ${copyableNick(host)} KWOTA numer[/b][/color]. Pula: [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas: [b][color=green]${parseTime(timeLeftMs)}[/color][/b], zwycięzców: [b][color=#5DE2E7]${winnersNum}[/color][/b], zakres [b][color=red]${startNum}-${endNum}[/color][/b].` + getV2DynamicMinReminderNote(minEntryFee, dynamicMinEnabled, dynamicMinStep),
status: ({ amount, timeLeftMs, taken, total, freeCount, sponsorCount, entriesState, minEntryFee }) =>
`Status trybu Stawka większa niż pula BON: pula [b][color=#ffc00a]${cleanPotString(amount)} BON[/color][/b], czas [b][color=green]${parseTime(timeLeftMs)}[/color][/b], numery [b]${taken}/${total}[/b] (wolne: ${freeCount}), wpłacających [b]${sponsorCount}[/b], zgłoszenia [b]${entriesState}[/b], wpisowe min. [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b].`,
commandsList: (host, minEntryFee) =>
`Tryb Stawka większa niż pula BON: wejście tylko przez [color=red][b]/gift ${copyableNick(host)} KWOTA numer[/b][/color] (minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b]). Komendy: !time !status !number !lucky !free !entries !sponsors !bon !range !commands | host: !addbon !removebon !winners !reminder !pause !resume !time add/remove.`,
entryViaGiftOnly: (author, host, minEntryFee) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], w trybie Stawka większa niż pula BON numer podajesz tylko przez [color=red][b]/gift ${copyableNick(host)} KWOTA numer[/b][/color]. Minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b].`,
entryAccepted: ({ author, number, paid, timeLeftMs, totalPot, newMinEntryFee }) =>
pickVariant([
`[color=#d85e27]${sanitizeNick(author)}[/color] wszedł do trybu Stawka większa niż pula BON z numerem [color=red][b]${number}[/b][/color] i wrzucił [color=#ffc00a][b]${cleanPotString(paid)} BON[/b][/color].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] opłacił wejście: [color=#ffc00a][b]${cleanPotString(paid)} BON[/b][/color], numer [color=red][b]${number}[/b][/color].`,
`Mamy nową wpłatę od [color=#d85e27]${sanitizeNick(author)}[/color]: [color=#ffc00a][b]${cleanPotString(paid)} BON[/b][/color], numer [color=red][b]${number}[/b][/color].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] kupił los w trybie Stawka większa niż pula BON za [color=#ffc00a][b]${cleanPotString(paid)} BON[/b][/color], numer [color=red][b]${number}[/b][/color].`,
`Wpłata zaksięgowana: [color=#d85e27]${sanitizeNick(author)}[/color] gra numerem [color=red][b]${number}[/b][/color] po wpłacie [color=#ffc00a][b]${cleanPotString(paid)} BON[/b][/color].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] dorzucił wpisowe [color=#ffc00a][b]${cleanPotString(paid)} BON[/b][/color] i bierze numer [color=red][b]${number}[/b][/color].`,
`Rejestruję gracza [color=#d85e27]${sanitizeNick(author)}[/color]: numer [color=red][b]${number}[/b][/color], koszt wejścia [color=#ffc00a][b]${cleanPotString(paid)} BON[/b][/color].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] melduje gotowość: BON [color=#ffc00a][b]${cleanPotString(paid)}[/b][/color], numer [color=red][b]${number}[/b][/color].`,
`Tryb Stawka większa niż pula BON przyjął zgłoszenie: [color=#d85e27]${sanitizeNick(author)}[/color], numer [color=red][b]${number}[/b][/color], wpłata [color=#ffc00a][b]${cleanPotString(paid)} BON[/b][/color].`,
`[color=#d85e27]${sanitizeNick(author)}[/color] wszedł poprawnie: [color=#ffc00a][b]${cleanPotString(paid)} BON[/b][/color] + numer [color=red][b]${number}[/b][/color].`
]) + ` Pula teraz: [b][color=#ffc00a]${cleanPotString(totalPot)} BON[/color][/b]. Do końca: [b][color=green]${parseTime(timeLeftMs)}[/color][/b]. Nowe minimum wejścia: [b][color=#ffc00a]${cleanPotString(newMinEntryFee)} BON[/color][/b].`,
refundLow: (author, paid, minEntryFee) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], wpłata [b][color=#ffc00a]${cleanPotString(paid)} BON[/color][/b] jest poniżej minimum [b][color=#ffc00a]${cleanPotString(minEntryFee)} BON[/color][/b]. Robię refund.`,
refundNoNumber: (author) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], nie widzę numeru w treści gifta. Wysyłaj: [color=red][b]/gift HOST KWOTA numer[/b][/color]. Robię refund.`,
refundOutOfRange: (author, number, startNum, endNum) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], numer [color=red][b]${number}[/b][/color] jest poza zakresem [b]${startNum}-${endNum}[/b]. Refund poszedł.`,
refundTaken: (author, number, existingAuthor) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], numer [color=red][b]${number}[/b][/color] jest już zajęty przez [color=#32cd53]${sanitizeNick(existingAuthor)}[/color]. Refund.`,
refundPaused: (author) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], zgłoszenia są chwilowo wstrzymane. Wpłata wraca do nadawcy.`,
refundAlreadyJoined: (author, currentNumber) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], już grasz numerem [color=red][b]${currentNumber}[/b][/color]. Dubla nie przyjmuję, refund wykonany.`,
refundHost: (author) =>
`[color=#d85e27]${sanitizeNick(author)}[/color], host w trybie Stawka większa niż pula BON nie bierze udziału. Wpłata została zwrócona.`
});
const CHAT_PROFILE_STORAGE_KEY = "pttBonGiveaway.chatProfile";
const CHAT_PROFILE_LABELS = Object.freeze({
sarkazm: "Sarkazm",
memiczny: "Programistyczny RPG",
superSarkazm: "Super Extra Sarkazm",
klasyczny: "Klasyczny"
});
function sanitizeChatProfile(profile) {
const value = String(profile || "").trim().toLowerCase();
if (value === "memiczny") return "memiczny";
if (value === "supersarkazm") return "superSarkazm";
if (value === "supersark") return "superSarkazm";
if (value === "klasyczny") return "klasyczny";
return "sarkazm";
}
const DIRECT_EMOJI_BY_COPY_KEY = Object.freeze({
intro: "\uD83C\uDF89",
reminder: "\uD83D\uDCE2",
timeLeft: "\u23F3",
status: "\uD83D\uDCCA",
alreadyJoined: "\uD83D\uDD12",
entriesPaused: "\u23F8\uFE0F",
noFreeForUser: "\uD83D\uDEAB",
joinedRandom: "\uD83C\uDFB2",
numberInfo: "\uD83D\uDD22",
numberNone: "\uD83D\uDD22",
luckyNow: "\u2728",
noLuckyCandidate: "\u26A0\uFE0F",
joinedLuckye: "\u2728",
noFreeLeft: "\uD83D\uDEAB",
freeCountOnly: "\uD83C\uDFB2",
freeSample: "\uD83C\uDFB2",
hostOnlyCmd: "\u26A0\uFE0F",
usage: "\uD83D\uDCDC",
addBonBalanceError: "\u26A0\uFE0F",
hostAddedBon: "\uD83D\uDCB0\u2795",
cannotRemoveNoExtra: "\u26A0\uFE0F",
cannotRemoveMax: "\u26A0\uFE0F",
hostRemovedBon: "\uD83D\uDCB0\u2796",
hostOnlyWinners: "\u26A0\uFE0F",
winnersSet: "\uD83C\uDFC6",
hostOnlyReminder: "\u26A0\uFE0F",
hostOnlyPause: "\u26A0\uFE0F",
pausedAlready: "\u23F8\uFE0F",
pausedNow: "\u23F8\uFE0F",
hostOnlyResume: "\u26A0\uFE0F",
resumedAlready: "\u25B6\uFE0F",
resumedNow: "\u25B6\uFE0F",
commandsList: "\uD83D\uDCDC",
sponsorHint: "\uD83D\uDCB0",
entriesNone: "\uD83E\uDDFE",
entriesList: "\uD83E\uDDFE",
sponsorsNone: "\uD83E\uDD1D",
sponsorsList: "\uD83E\uDD1D",
potNow: "\uD83D\uDCB0",
rangeNow: "\uD83D\uDD22",
outOfRange: "\u26A0\uFE0F",
numberTaken: "\u26A0\uFE0F",
entryJoined: "\u2705",
noEntriesEnd: "\uD83D\uDE14",
hostAmongWinners: "\uD83D\uDC51",
earlyFinish: "\u2705",
spamDetected: "\u26A0\uFE0F",
sponsorDigest: "\uD83E\uDD1D",
finishSummary: "\uD83C\uDFC6",
entryViaGiftOnly: "\uD83D\uDCDC",
entryAccepted: "\u2705",
refundLow: "\u26A0\uFE0F",
refundNoNumber: "\u26A0\uFE0F",
refundOutOfRange: "\u26A0\uFE0F",
refundTaken: "\u26A0\uFE0F",
refundPaused: "\u23F8\uFE0F",
refundAlreadyJoined: "\u26A0\uFE0F",
refundHost: "\u26A0\uFE0F"
});
function prefixCopyEmoji(copyKey, messageValue) {
const text = String(messageValue || "");
const trimmed = text.trim();
if (!trimmed) return text;
if (trimmed.startsWith("/")) return text;
const emoji = DIRECT_EMOJI_BY_COPY_KEY[copyKey];
if (!emoji) return text;
if (trimmed.startsWith(`${emoji} `)) return text;
return `${emoji} ${text}`;
}
function withDirectEmoji(copyObject) {
const wrapped = {};
Object.keys(copyObject || {}).forEach((key) => {
const value = copyObject[key];
if (typeof value !== "function") {
wrapped[key] = value;
return;
}
wrapped[key] = (...args) => prefixCopyEmoji(key, value(...args));
});
return Object.freeze(wrapped);
}
const CHAT_COPY_SARKAZM_EMOJI = withDirectEmoji(CHAT_COPY_SARKAZM);
const CHAT_COPY_MEMICZNY_EMOJI = withDirectEmoji(CHAT_COPY_MEMICZNY);
const CHAT_COPY_SUPER_SARKAZM_EMOJI = withDirectEmoji(CHAT_COPY_SUPER_SARKAZM);
const CHAT_COPY_KLASYCZNY_EMOJI = withDirectEmoji(CHAT_COPY_KLASYCZNY);
const V2_SARKAZM_COPY_EMOJI = withDirectEmoji(V2_SARKAZM_COPY);
function resolveChatCopy(profile) {
const normalized = sanitizeChatProfile(profile);
if (normalized === "memiczny") return CHAT_COPY_MEMICZNY_EMOJI;
if (normalized === "superSarkazm") return CHAT_COPY_SUPER_SARKAZM_EMOJI;
if (normalized === "klasyczny") return CHAT_COPY_KLASYCZNY_EMOJI;
return CHAT_COPY_SARKAZM_EMOJI;
}
function readChatProfile() {
try {
return sanitizeChatProfile(localStorage.getItem(CHAT_PROFILE_STORAGE_KEY));
} catch (error) {
return "sarkazm";
}
}
function persistChatProfile(profile) {
try {
localStorage.setItem(CHAT_PROFILE_STORAGE_KEY, sanitizeChatProfile(profile));
} catch (error) {
}
}
const GAME_MODE_STORAGE_KEY = "pttBonGiveaway.gameMode";
const V2_DYNAMIC_MIN_STORAGE_KEY = "pttBonGiveaway.v2.dynamicMin";
function sanitizeGameMode(mode) {
const value = String(mode || "").trim().toLowerCase();
if (value === "v2" || value === "v2.0" || value === "v2_0") return "v2_0";
return "standard";
}
function readGameMode() {
try {
return sanitizeGameMode(localStorage.getItem(GAME_MODE_STORAGE_KEY));
} catch (error) {
return "standard";
}
}
function persistGameMode(mode) {
try {
localStorage.setItem(GAME_MODE_STORAGE_KEY, sanitizeGameMode(mode));
} catch (error) {
}
}
function readV2DynamicMinEnabled() {
try {
const raw = String(localStorage.getItem(V2_DYNAMIC_MIN_STORAGE_KEY) || "").trim().toLowerCase();
return raw === "1" || raw === "true";
} catch (error) {
return false;
}
}
function persistV2DynamicMinEnabled(enabled) {
try {
localStorage.setItem(V2_DYNAMIC_MIN_STORAGE_KEY, enabled ? "1" : "0");
} catch (error) {
}
}
function getEffectiveChatProfile(profile = activeChatProfile, gameMode = activeGameMode) {
const sanitizedProfile = sanitizeChatProfile(profile);
if (sanitizeGameMode(gameMode) === "v2_0") {
return "sarkazm";
}
return sanitizedProfile;
}
let activeGameMode = readGameMode();
let activeChatProfile = readChatProfile();
let activeV2DynamicMinEnabled = readV2DynamicMinEnabled();
let CHAT_COPY = resolveChatCopy(getEffectiveChatProfile(activeChatProfile, activeGameMode));
const apiHealth = {
entries: { ok: null, lastAt: 0, lastError: "" },
sponsors: { ok: null, lastAt: 0, lastError: "" }
};
const scriptMeta = getScriptMeta();
let processedGiftMessages = new Set();
let processedChatMessages = new Set();
let giveawayStartTime = null;
let sponsorsInterval = null;
let entriesInterval = null;
let chatbox = null;
let giveawayData = null;
let numberEntries = new Map();
let numberTakenBy = new Map();
let fancyNames = new Map();
let sponsorDigestBuffer = [];
let sponsorDigestWindowStartAt = 0;
let resolvedEntriesRoomId = String(GENERAL_SETTINGS.entriesRoomId || "1");
let resolvedSponsorRoomId = String(GENERAL_SETTINGS.sponsorRoomId || "13");
let roomIdResolvePromise = null;
let v2GiftHistoryCache = {
host: "",
fetchedAt: 0,
rows: []
};
let v2GiftHistoryFetchPromise = null;
let v2ConsumedGiftHistoryKeys = new Set();
let commandsButton;
let commandsMenu;
let roomsButton;
let roomsMenu;
let optionsButton;
let optionsCloseButton;
let optionsMenu;
let v2DynamicMinToggle;
let statsWrapper;
let statsPot;
let statsHostBase;
let statsHostExtra;
let statsSponsors;
let statsEntries;
let statsFree;
let restoreNotice;
let restoreNoticeTimer = null;
const userCooldown = new Map();
const userCommandLog = new Map();
const userLastActionAt = new Map();
const userLastCommandAt = new Map();
const userSpamStrikes = new Map();
const userFeedbackCooldown = new Map();
const regNum = /^-?\d+$/;
const whitespace = document.createTextNode(" ");
const coinsIcon = document.createElement("i");
coinsIcon.setAttribute("class", "fas fa-coins");
const goldCoins = document.createElement("i");
goldCoins.setAttribute("class", "fas fa-coins");
goldCoins.style.color = "#ffc00a";
goldCoins.style.padding = "5px";
const giveawayBTN = document.createElement("a");
giveawayBTN.setAttribute("class", "form__button form__button--text");
giveawayBTN.textContent = "Giveaway";
giveawayBTN.prepend(coinsIcon.cloneNode(false));
giveawayBTN.onclick = toggleMenu;
const frameHTML = `
<section id="giveawayFrame" class="panelV2" style="width: 470px; height: 90%; position: fixed; z-index: 9999; inset: 50px 150px auto auto; overflow: auto; border-style: solid; border-width: 1px; border-color: black" hidden>
<header class="panel__heading" style="padding:10px 12px 12px;">
<div class="button-holder no-space" style="display:flex; flex-direction:column; align-items:center; justify-content:center; gap:10px;">
<div class="button-left" style="display:flex; justify-content:center; width:100%;">
<h4 class="panel__heading" style="margin:0; text-align:center; display:flex; align-items:center; justify-content:center; gap:8px; font-size:1.34rem; font-weight:900; letter-spacing:0.45px; color:#ffe6a8; text-shadow:0 1px 0 rgba(0,0,0,0.6), 0 0 12px rgba(255,192,10,0.2);">
<i class="fas fa-coins" style="padding:0; color:#ffc00a; filter:drop-shadow(0 0 4px rgba(255,192,10,0.35));"></i>
<span style="display:inline-block;">PTT BON Giveaway</span>
</h4>
</div>
<div class="button-right" style="display:flex; justify-content:center; align-items:center; flex-wrap:wrap; gap:6px; width:100%;">
<button id="resetButton" type="button" class="form__button form__button--text">
Reset
</button>
<button id="commandsButton" type="button" class="form__button form__button--text">
Komendy
</button>
<button id="roomsButton" type="button" class="form__button form__button--text">
Status
</button>
<button id="optionsButton" type="button" class="form__button form__button--text">
Opcje
</button>
<button id="closeButton" type="button" class="form__button form__button--text">
X
</button>
</div>
</div>
</header>
<div id="giveawayCommandsMenu" style="
display: none;
position: absolute;
top: 55px;
right: 10px;
z-index: 10000;
background: linear-gradient(180deg, #172029 0%, #111920 100%);
color: #d8e3ea;
border: 1px solid #35566a;
border-radius: 10px;
padding: 12px;
min-width: 350px;
box-shadow: 0 12px 26px rgba(0,0,0,0.45), inset 0 1px 0 rgba(255,255,255,0.04);
font-size: 13px;
line-height: 1.35;
">
<div style="font-size:13px; font-weight:700; color:#9bb2bf; margin-bottom:8px; letter-spacing:0.2px; text-align:center;">Command Center</div>
<div style="border:1px solid #2f5a71; border-radius:8px; padding:8px 10px; background:rgba(22,44,56,0.55);">
<div style="font-size:12px; font-weight:700; color:#8fd8ff; margin-bottom:6px;">Komendy użytkowników</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!time</b> – pokazuje pozostały czas</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!status</b> – skrócony status giveaway</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!random</b> – losuje wolny numer (Standard)</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!number</b> – pokazuje twój numer</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!lucky</b> – pokazuje szczęśliwy numer</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!luckye</b> – zapisuje na szczęśliwy numer (Standard)</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!free</b> – pokazuje kilka wolnych numerów</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!entries</b> – pokazuje zgłoszenia</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!sponsors</b> – pokazuje sponsorów</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!bon</b> – pokazuje pulę BON</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!range</b> – pokazuje zakres numerów</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">!commands</b> – lista komend</div>
<div style="color:#c9d8e1; font-size:13px;"><b style="color:#ffe08f;">/gift HOST KWOTA numer</b> – wejście w trybie Stawka większa niż pula BON</div>
</div>
<div style="margin-top:10px; border:1px solid #5d5a2f; border-radius:8px; padding:8px 10px; background:rgba(56,46,22,0.45);">
<div style="font-size:12px; font-weight:700; color:#ffd27d; margin-bottom:6px;">Komendy hosta</div>
<div style="color:#e3d8c3; font-size:13px;"><b style="color:#ffe08f;">!addbon 100</b> – dodaje BON do puli</div>
<div style="color:#e3d8c3; font-size:13px;"><b style="color:#ffe08f;">!removebon 100</b> – usuwa BON z dodatkowego wkładu hosta</div>
<div style="color:#e3d8c3; font-size:13px;"><b style="color:#ffe08f;">!winners 3</b> – zmienia liczbę zwycięzców</div>
<div style="color:#e3d8c3; font-size:13px;"><b style="color:#ffe08f;">!reminder</b> – wysyła przypomnienie</div>
<div style="color:#e3d8c3; font-size:13px;"><b style="color:#ffe08f;">!pause</b> – wstrzymuje nowe zgłoszenia</div>
<div style="color:#e3d8c3; font-size:13px;"><b style="color:#ffe08f;">!resume</b> – wznawia nowe zgłoszenia</div>
<div style="color:#e3d8c3; font-size:13px;"><b style="color:#ffe08f;">!time add 5</b> – dodaje czas</div>
<div style="color:#e3d8c3; font-size:13px;"><b style="color:#ffe08f;">!time remove 5</b> – odejmuje czas</div>
</div>
</div>
<div id="giveawayRoomsMenu" style="
display: none;
position: absolute;
top: 55px;
right: 10px;
z-index: 10000;
background: #1f262b;
color: #cbd6dc;
border: 1px solid #3f4f58;
border-radius: 6px;
padding: 12px 14px;
min-width: 260px;
box-shadow: 0 4px 10px rgba(0,0,0,0.35);
">
<div style="font-weight:700; color:#9bb2bf; margin-bottom:8px;">Status skryptu</div>
<div style="font-size:13px;">Zgłoszenia: <b id="roomEntriesValue" style="color:#5DE2E7;">-</b></div>
<div style="font-size:13px; margin-top:4px;">Sponsorzy/system: <b id="roomSponsorsValue" style="color:#ffc00a;">-</b></div>
<div style="font-size:12px; color:#9bb2bf; margin:10px 0 6px;">Metadane skryptu</div>
<div style="font-size:13px;">Name: <b id="scriptNameValue" style="color:#cbd6dc;">-</b></div>
<div style="font-size:13px; margin-top:4px;">Description: <b id="scriptDescriptionValue" style="color:#cbd6dc;">-</b></div>
<div style="font-size:13px; margin-top:4px;">Version: <b id="scriptVersionValue" style="color:#5edb7f;">-</b></div>
<div style="font-size:13px; margin-top:4px;">Author: <b id="scriptAuthorValue" style="color:#9fd2ff;">-</b></div>
<div style="font-size:12px; color:#9bb2bf; margin:10px 0 6px;">Status API</div>
<div style="font-size:13px;">Entries API: <b id="apiEntriesStatusValue" style="color:#9bb2bf;">brak danych</b></div>
<div style="font-size:13px; margin-top:4px;">Sponsors API: <b id="apiSponsorsStatusValue" style="color:#9bb2bf;">brak danych</b></div>
</div>
<div id="giveawayOptionsMenu" style="
display: none;
position: absolute;
top: 55px;
right: 10px;
z-index: 10000;
background: #1c2328;
color: #d5dfe5;
border: 1px solid #3a4e5a;
border-radius: 8px;
padding: 10px 12px;
width: 360px;
min-width: 360px;
max-width: 360px;
box-sizing: border-box;
height: 332px;
min-height: 332px;
max-height: 332px;
overflow-y: auto;
box-shadow: 0 8px 18px rgba(0,0,0,0.4);
">
<button id="optionsCloseButton" type="button" style="float:right; width:20px; height:20px; border:none; border-radius:999px; background:#2a353d; color:#cfe0ea; font-size:12px; line-height:20px; text-align:center; cursor:pointer; padding:0; margin:-2px 0 6px 8px;">X</button>
<div style="font-weight:700; color:#9fd2ff; margin-bottom:8px;">Opcje</div>
<div style="font-size:12px; color:#9bb2bf; margin-bottom:6px;">Profile językowe</div>
<div id="chatProfileV2Hint" style="display:none; font-size:12px; color:#ffd27d; margin:4px 0 8px;">
W trybie Stawka większa niż pula BON aktywny jest profil: <b>Sarkazm</b>.
</div>
<label style="display:flex; align-items:center; gap:8px; font-size:13px; margin:4px 0; cursor:pointer;">
<input type="radio" name="chatProfile" value="sarkazm"> Sarkazm (obecny)
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:13px; margin:4px 0; cursor:pointer;">
<input type="radio" name="chatProfile" value="superSarkazm"> Super Extra Sarkazm
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:13px; margin:4px 0; cursor:pointer;">
<input type="radio" name="chatProfile" value="memiczny"> Programistyczny RPG vibe
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:13px; margin:4px 0; cursor:pointer;">
<input type="radio" name="chatProfile" value="klasyczny"> Klasyczny (oryginał)
</label>
<div style="height:1px; background:#34434d; margin:10px 0;"></div>
<div style="font-size:12px; color:#9bb2bf; margin-bottom:6px;">Wersja zabawy</div>
<label style="display:flex; align-items:center; gap:8px; font-size:13px; margin:4px 0; cursor:pointer;">
<input type="radio" name="gameMode" value="standard"> Standard giveaway
</label>
<label style="display:flex; align-items:center; gap:8px; font-size:13px; margin:4px 0; cursor:pointer;">
<input type="radio" name="gameMode" value="v2_0"> Stawka większa niż pula BON (wpisowe przez gift)
</label>
<div id="v2DynamicMinGroup" style="margin-top:8px;" hidden>
<label style="display:flex; align-items:center; gap:8px; font-size:13px; margin:4px 0; cursor:pointer; color:#ffdba6;">
<input id="v2DynamicMinToggle" type="checkbox"> Dynamiczne minimum wpisowego (x1, x2, x3...)
</label>
</div>
</div>
<div class="panel__body">
<h1 id="coinHeader" class="panel__heading--centered" style="font-size:1.28rem; margin:8px 0 6px;"></h1>
<div id="giveawayStats" hidden style="max-width: 430px; margin: 10px auto 14px;">
<div style="display:grid; grid-template-columns:1fr 1fr; gap:8px;">
<div style="border:1px solid #444; border-radius:6px; padding:8px;">
<div style="font-size:12px; color:#aaa;">Pula</div>
<div id="statPot" style="font-weight:700;"></div>
</div>
<div style="border:1px solid #444; border-radius:6px; padding:8px;">
<div style="font-size:12px; color:#aaa;">Host start</div>
<div id="statHostBase" style="font-weight:700;"></div>
</div>
<div style="border:1px solid #444; border-radius:6px; padding:8px;">
<div style="font-size:12px; color:#aaa;">Host extra</div>
<div id="statHostExtra" style="font-weight:700;"></div>
</div>
<div style="border:1px solid #444; border-radius:6px; padding:8px;">
<div style="font-size:12px; color:#aaa;">Sponsorzy</div>
<div id="statSponsors" style="font-weight:700;"></div>
</div>
<div style="border:1px solid #444; border-radius:6px; padding:8px;">
<div style="font-size:12px; color:#aaa;">Zajęte</div>
<div id="statEntries" style="font-weight:700;"></div>
</div>
<div style="border:1px solid #444; border-radius:6px; padding:8px;">
<div style="font-size:12px; color:#aaa;">Wolne</div>
<div id="statFree" style="font-weight:700;"></div>
</div>
</div>
</div>
<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" style="background:#23282e; color:#e7f2f8; border:1px solid #41586a; box-shadow:inset 0 1px 0 rgba(255,255,255,0.05);">
<label class="form__label form__label--floating" for="giveawayAmount">
Kwota Giveaway
</label>
</p>
<p id="entryFeeGroup" class="form__group" style="max-width: 35%;" hidden>
<input class="form__text" id="entryFeeMin" pattern="[0-9]*" value="50" inputmode="numeric" type="text" style="background:#23282e; color:#e7f2f8; border:1px solid #41586a; box-shadow:inset 0 1px 0 rgba(255,255,255,0.05);">
<label class="form__label form__label--floating" for="entryFeeMin">
Wpisowe min. (Stawka większa niż pula BON)
</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="7">
<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="7">
<label class="form__label form__label--floating" for="endNum">
Koniec #
</label>
</p>
</div>
<div class="panel__body" style="display: flex; justify-content: center; gap: 20px">
<p class="form__group" style="width: 28%;">
<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">
Czas (minuty)
</label>
</p>
<p class="form__group" style="width: 28%;">
<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">
Przypomnienia
</label>
</p>
<p class="form__group" style="width: 28%;">
<input class="form__text" required id="winnersNum" pattern="[0-9]*" value="1" inputmode="numeric" type="text">
<label class="form__label form__label--floating" for="winnersNum">
Zwycięzcy
</label>
</p>
</div>
<p class="form__group" style="text-align: center;">
<button id="startButton" type="button" class="form__button form__button--filled" style="height:44px; font-size:16px; font-weight:800;">
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>Uzytkownik</th>
<th>Numer</th>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
</section>
`;
let giveawayFrame;
let resetButton;
let closeButton;
let coinHeader;
let coinInput;
let entryFeeGroup;
let entryFeeInput;
let startInput;
let endInput;
let timerInput;
let reminderInput;
let winnersInput;
let startButton;
let countdownHeader;
let entriesWrapper;
let giveawayForm;
let roomEntriesValue;
let roomSponsorsValue;
let apiEntriesStatusValue;
let apiSponsorsStatusValue;
let scriptNameValue;
let scriptDescriptionValue;
let scriptVersionValue;
let scriptAuthorValue;
let isFrameDragging = false;
let frameDragStartX = 0;
let frameDragStartY = 0;
let frameDragStartLeft = 0;
let frameDragStartTop = 0;
injectMenu();
function getSponsorMessage(host) {
const safeHost = String(host || getLoggedInUsername() || "HOST").trim();
const effectiveProfile = getEffectiveChatProfile(activeChatProfile, activeGameMode);
const copy = resolveChatCopy(effectiveProfile);
if (copy && typeof copy.sponsorHint === "function") {
return copy.sponsorHint(safeHost);
}
return `Każdy BON wysłany hostowi podczas giveaway automatycznie zwiększa pulę nagród. ` +
`Jak dodać BON: [color=red][b]/gift ${copyableNick(safeHost)} KWOTA wiadomość[/b][/color].`;
}
function enableFrameDragging() {
if (!giveawayFrame) return;
const dragHandle = giveawayFrame.querySelector("header");
if (!dragHandle) return;
dragHandle.style.cursor = "move";
const stopFrameDragging = () => {
if (!isFrameDragging) return;
isFrameDragging = false;
document.body.style.userSelect = "";
};
const onFrameDragMove = (event) => {
if (!isFrameDragging) return;
const panelRect = giveawayFrame.getBoundingClientRect();
const maxLeft = Math.max(0, window.innerWidth - panelRect.width);
const maxTop = Math.max(0, window.innerHeight - 40);
const nextLeft = Math.min(maxLeft, Math.max(0, frameDragStartLeft + (event.clientX - frameDragStartX)));
const nextTop = Math.min(maxTop, Math.max(0, frameDragStartTop + (event.clientY - frameDragStartY)));
giveawayFrame.style.inset = "auto";
giveawayFrame.style.right = "auto";
giveawayFrame.style.bottom = "auto";
giveawayFrame.style.left = `${nextLeft}px`;
giveawayFrame.style.top = `${nextTop}px`;
};
dragHandle.addEventListener("mousedown", (event) => {
if (event.button !== 0) return;
const target = event.target;
if (target && target.closest("button, a, input, select, textarea, [role=button]")) {
return;
}
const rect = giveawayFrame.getBoundingClientRect();
frameDragStartX = event.clientX;
frameDragStartY = event.clientY;
frameDragStartLeft = rect.left;
frameDragStartTop = rect.top;
isFrameDragging = true;
document.body.style.userSelect = "none";
event.preventDefault();
});
document.addEventListener("mousemove", onFrameDragMove);
document.addEventListener("mouseup", stopFrameDragging);
document.addEventListener("mouseleave", stopFrameDragging);
}
function injectMenu() {
if (document.getElementById("giveawayFrame")) {
return;
}
const chatboxHeader = document.querySelector("#chatbox_header div");
if (!chatboxHeader) {
setTimeout(injectMenu, 200);
return;
}
document.body.insertAdjacentHTML("beforeend", frameHTML);
chatboxHeader.prepend(giveawayBTN);
giveawayBTN.parentNode.insertBefore(whitespace, giveawayBTN.nextSibling);
giveawayFrame = document.getElementById("giveawayFrame");
resetButton = document.getElementById("resetButton");
closeButton = document.getElementById("closeButton");
commandsButton = document.getElementById("commandsButton");
commandsMenu = document.getElementById("giveawayCommandsMenu");
roomsButton = document.getElementById("roomsButton");
roomsMenu = document.getElementById("giveawayRoomsMenu");
optionsButton = document.getElementById("optionsButton");
optionsMenu = document.getElementById("giveawayOptionsMenu");
optionsCloseButton = document.getElementById("optionsCloseButton");
v2DynamicMinToggle = document.getElementById("v2DynamicMinToggle");
coinHeader = document.getElementById("coinHeader");
coinInput = document.getElementById("giveawayAmount");
entryFeeGroup = document.getElementById("entryFeeGroup");
entryFeeInput = document.getElementById("entryFeeMin");
startInput = document.getElementById("startNum");
endInput = document.getElementById("endNum");
timerInput = document.getElementById("timerNum");
reminderInput = document.getElementById("reminderNum");
winnersInput = document.getElementById("winnersNum");
startButton = document.getElementById("startButton");
countdownHeader = document.getElementById("countdownHeader");
entriesWrapper = document.getElementById("entriesWrapper");
giveawayForm = document.getElementById("giveawayForm");
roomEntriesValue = document.getElementById("roomEntriesValue");
roomSponsorsValue = document.getElementById("roomSponsorsValue");
apiEntriesStatusValue = document.getElementById("apiEntriesStatusValue");
apiSponsorsStatusValue = document.getElementById("apiSponsorsStatusValue");
scriptNameValue = document.getElementById("scriptNameValue");
scriptDescriptionValue = document.getElementById("scriptDescriptionValue");
scriptVersionValue = document.getElementById("scriptVersionValue");
scriptAuthorValue = document.getElementById("scriptAuthorValue");
statsWrapper = document.getElementById("giveawayStats");
statsPot = document.getElementById("statPot");
statsHostBase = document.getElementById("statHostBase");
statsHostExtra = document.getElementById("statHostExtra");
statsSponsors = document.getElementById("statSponsors");
statsEntries = document.getElementById("statEntries");
statsFree = document.getElementById("statFree");
restoreNotice = document.createElement("div");
restoreNotice.id = "giveawayRestoreNotice";
restoreNotice.hidden = true;
restoreNotice.style.cssText = [
"display:none",
"max-width:430px",
"margin:10px auto 14px",
"padding:10px 12px",
"border:1px solid #3c763d",
"border-radius:6px",
"background:#1f3a24",
"color:#d7ffd9",
"font-weight:600",
"text-align:center"
].join(";");
const panelBody = giveawayFrame.querySelector(".panel__body");
if (panelBody) {
panelBody.insertBefore(restoreNotice, panelBody.firstChild);
}
enableFrameDragging();
applyHeaderButtonStyles();
styleStartButton(false);
resetButton.onclick = function () {
resetGiveaway(false);
};
closeButton.onclick = toggleMenu;
startButton.onclick = toggleStartStop;
commandsButton.onclick = function (e) {
e.stopPropagation();
toggleCommandsMenu();
};
roomsButton.onclick = function (e) {
e.stopPropagation();
toggleRoomsMenu();
};
optionsButton.onclick = function (e) {
e.stopPropagation();
toggleOptionsMenu();
};
if (optionsCloseButton) {
optionsCloseButton.onclick = function (e) {
e.stopPropagation();
closeOptionsMenu();
};
}
commandsMenu.onclick = function () {
closeCommandsMenu();
};
roomsMenu.onclick = function () {
closeRoomsMenu();
};
if (optionsMenu) {
optionsMenu.onclick = function (e) {
e.stopPropagation();
};
optionsMenu.addEventListener("change", function (e) {
const target = e.target;
if (!target) return;
if (target.name === "chatProfile") {
applyChatProfile(target.value, true);
return;
}
if (target.name === "gameMode") {
applyGameMode(target.value, true);
return;
}
if (target.id === "v2DynamicMinToggle") {
activeV2DynamicMinEnabled = Boolean(target.checked);
persistV2DynamicMinEnabled(activeV2DynamicMinEnabled);
updateOptionsMenuSelection();
}
});
}
timerInput.addEventListener("input", reminderAutoScaling);
if (entryFeeInput) {
entryFeeInput.addEventListener("input", () => {
if (parsePositiveInt(entryFeeInput.value) > 0) {
entryFeeInput.setCustomValidity("");
}
});
}
reminderInput.addEventListener("input", remindersValidation);
bindBonInputTheme(coinInput);
bindBonInputTheme(entryFeeInput);
startInput.addEventListener("input", entryRangeValidation);
endInput.addEventListener("input", entryRangeValidation);
winnersInput.addEventListener("input", winnersValidation);
updateCoinHeader(readHostBalance());
updateStatsPanel(null);
resetApiStatus();
applyChatProfile(activeChatProfile, false);
applyGameMode(activeGameMode, false, true);
document.addEventListener("click", function (e) {
if (!commandsMenu || !commandsButton || !roomsMenu || !roomsButton || !optionsMenu || !optionsButton) return;
const clickedInsideCommands = commandsMenu.contains(e.target);
const clickedCommandsButton = commandsButton.contains(e.target);
const clickedInsideRooms = roomsMenu.contains(e.target);
const clickedRoomsButton = roomsButton.contains(e.target);
const clickedInsideOptions = optionsMenu.contains(e.target);
const clickedOptionsButton = optionsButton.contains(e.target);
if (!clickedInsideCommands && !clickedCommandsButton) {
closeCommandsMenu();
}
if (!clickedInsideRooms && !clickedRoomsButton) {
closeRoomsMenu();
}
if (!clickedInsideOptions && !clickedOptionsButton) {
closeOptionsMenu();
}
});
restoreGiveawayFromStorage();
}
function toggleMenu() {
giveawayFrame.hidden = !giveawayFrame.hidden;
if (giveawayFrame.hidden) {
closeCommandsMenu();
closeRoomsMenu();
closeOptionsMenu();
} else {
updateStatsPanel(giveawayData);
updateRoomInfoPanel();
}
}
function toggleCommandsMenu() {
if (!commandsMenu) return;
const isOpen = commandsMenu.style.display === "block";
if (!isOpen) {
closeRoomsMenu();
closeOptionsMenu();
}
commandsMenu.style.display = isOpen ? "none" : "block";
}
function closeCommandsMenu() {
if (!commandsMenu) return;
commandsMenu.style.display = "none";
}
function toggleRoomsMenu() {
if (!roomsMenu) return;
const isOpen = roomsMenu.style.display === "block";
if (!isOpen) {
closeCommandsMenu();
closeOptionsMenu();
}
roomsMenu.style.display = isOpen ? "none" : "block";
}
function closeRoomsMenu() {
if (!roomsMenu) return;
roomsMenu.style.display = "none";
}
function toggleOptionsMenu() {
if (!optionsMenu) return;
const isOpen = optionsMenu.style.display === "block";
if (!isOpen) {
closeCommandsMenu();
closeRoomsMenu();
updateOptionsMenuSelection();
}
optionsMenu.style.display = isOpen ? "none" : "block";
}
function closeOptionsMenu() {
if (!optionsMenu) return;
optionsMenu.style.display = "none";
}
function updateOptionsMenuSelection() {
if (!optionsMenu) return;
const effectiveProfile = getEffectiveChatProfile(activeChatProfile, activeGameMode);
const isV2 = sanitizeGameMode(activeGameMode) === "v2_0";
const isRunning = Boolean(giveawayData && !giveawayData.ended);
const profileOptions = optionsMenu.querySelectorAll('input[name="chatProfile"]');
profileOptions.forEach(input => {
input.checked = input.value === effectiveProfile;
const disabledByMode = isV2 && input.value !== "sarkazm";
input.disabled = disabledByMode || isRunning;
const label = input.closest("label");
if (label) {
label.style.opacity = input.disabled ? "0.6" : "1";
label.style.cursor = input.disabled ? "default" : "pointer";
}
});
const gameModeOptions = optionsMenu.querySelectorAll('input[name="gameMode"]');
gameModeOptions.forEach(input => {
input.checked = input.value === sanitizeGameMode(activeGameMode);
input.disabled = isRunning;
const label = input.closest("label");
if (label) {
label.style.opacity = input.disabled ? "0.6" : "1";
label.style.cursor = input.disabled ? "default" : "pointer";
}
});
const v2Hint = optionsMenu.querySelector("#chatProfileV2Hint");
if (v2Hint) {
v2Hint.style.display = "block";
v2Hint.style.opacity = isV2 ? "1" : "0.45";
}
const dynamicGroup = optionsMenu.querySelector("#v2DynamicMinGroup");
if (dynamicGroup) {
dynamicGroup.hidden = false;
dynamicGroup.style.opacity = isV2 ? "1" : "0.45";
dynamicGroup.style.pointerEvents = isV2 ? "auto" : "none";
}
if (v2DynamicMinToggle) {
v2DynamicMinToggle.checked = Boolean(activeV2DynamicMinEnabled);
v2DynamicMinToggle.disabled = !isV2 || isRunning;
const toggleLabel = v2DynamicMinToggle.closest("label");
if (toggleLabel) {
toggleLabel.style.opacity = v2DynamicMinToggle.disabled ? "0.6" : "1";
toggleLabel.style.cursor = v2DynamicMinToggle.disabled ? "default" : "pointer";
}
}
}
function updateModeUiState() {
if (!entryFeeGroup || !entryFeeInput) return;
const isV2 = sanitizeGameMode(activeGameMode) === "v2_0";
const isRunning = Boolean(giveawayData && !giveawayData.ended);
entryFeeGroup.hidden = !isV2;
entryFeeInput.disabled = !isV2 || isRunning;
if (!isV2) {
entryFeeInput.setCustomValidity("");
}
refreshBonInputThemes();
updateOptionsMenuSelection();
}
function applyBonInputTheme(input) {
if (!input) return;
const fixedInputBg = "#23282e";
const fixedInputColor = "#e7f2f8";
const fixedInputBorder = "#41586a";
input.style.background = fixedInputBg;
input.style.backgroundColor = fixedInputBg;
input.style.color = fixedInputColor;
input.style.border = `1px solid ${fixedInputBorder}`;
input.style.opacity = "1";
// Force dark fill also when browser autocompletion/history styling kicks in.
input.style.setProperty("background-color", fixedInputBg, "important");
input.style.setProperty("-webkit-box-shadow", `0 0 0 1000px ${fixedInputBg} inset`, "important");
input.style.setProperty("box-shadow", `0 0 0 1000px ${fixedInputBg} inset`, "important");
input.style.setProperty("-webkit-text-fill-color", fixedInputColor, "important");
}
function refreshBonInputThemes() {
[coinInput, entryFeeInput].forEach((input) => {
applyBonInputTheme(input);
});
}
function bindBonInputTheme(input) {
if (!input) return;
["input", "change", "blur", "focus"].forEach((evt) => {
input.addEventListener(evt, () => {
applyBonInputTheme(input);
// Some browsers apply autofill styles asynchronously.
setTimeout(() => applyBonInputTheme(input), 0);
});
});
applyBonInputTheme(input);
}
function isV2Mode(activeGiveaway = giveawayData) {
const mode = activeGiveaway ? activeGiveaway.gameMode : activeGameMode;
return sanitizeGameMode(mode) === "v2_0";
}
function getCurrentV2MinEntryFee(activeGiveaway = giveawayData) {
if (!activeGiveaway || !isV2Mode(activeGiveaway)) {
return Math.max(1, Math.floor((activeGiveaway && activeGiveaway.minEntryFee) || 0));
}
const staticMin = Math.max(1, Math.floor(activeGiveaway.minEntryFee || 0));
if (!activeGiveaway.dynamicMinEnabled) {
return staticMin;
}
const baseMin = Math.max(1, Math.floor(activeGiveaway.dynamicMinBase || staticMin));
const potMin = Math.max(1, Math.floor(activeGiveaway.amount || 0));
return Math.max(baseMin, potMin);
}
function getV2Copy() {
return V2_SARKAZM_COPY_EMOJI;
}
function applyChatProfile(profile, persist = true) {
activeChatProfile = sanitizeChatProfile(profile);
CHAT_COPY = resolveChatCopy(getEffectiveChatProfile(activeChatProfile, activeGameMode));
if (persist) {
persistChatProfile(activeChatProfile);
}
updateOptionsMenuSelection();
}
function applyGameMode(mode, persist = true, force = false) {
const nextMode = sanitizeGameMode(mode);
if (!force && giveawayData && !giveawayData.ended) {
updateOptionsMenuSelection();
return;
}
activeGameMode = nextMode;
CHAT_COPY = resolveChatCopy(getEffectiveChatProfile(activeChatProfile, activeGameMode));
if (persist) {
persistGameMode(activeGameMode);
}
updateModeUiState();
}
function styleHeaderButton(button, palette = {}) {
if (!button) return;
const normalBg = palette.normalBg || "linear-gradient(180deg, #3a3a3a 0%, #222 100%)";
const hoverBg = palette.hoverBg || "linear-gradient(180deg, #474747 0%, #2a2a2a 100%)";
const normalBorder = palette.normalBorder || "#555";
const hoverBorder = palette.hoverBorder || "#7a7a7a";
button.style.minWidth = palette.minWidth || "78px";
button.style.height = palette.height || "32px";
button.style.padding = palette.padding || "0 10px";
button.style.display = "inline-flex";
button.style.alignItems = "center";
button.style.justifyContent = "center";
button.style.textAlign = "center";
button.style.lineHeight = "1";
button.style.whiteSpace = "nowrap";
button.style.border = `1px solid ${normalBorder}`;
button.style.borderRadius = "6px";
button.style.background = normalBg;
button.style.color = "#f1f1f1";
button.style.fontWeight = palette.fontWeight || "700";
button.style.fontSize = palette.fontSize || "13px";
button.style.letterSpacing = "0.1px";
button.style.cursor = "pointer";
button.style.boxShadow = "inset 0 1px 0 rgba(255,255,255,0.08)";
button.style.transition = "background 120ms ease, border-color 120ms ease, transform 80ms ease, box-shadow 120ms ease";
button.addEventListener("mouseenter", () => {
button.style.background = hoverBg;
button.style.borderColor = hoverBorder;
button.style.boxShadow = "0 2px 8px rgba(0,0,0,0.35)";
});
button.addEventListener("mouseleave", () => {
button.style.background = normalBg;
button.style.borderColor = normalBorder;
button.style.boxShadow = "inset 0 1px 0 rgba(255,255,255,0.08)";
button.style.transform = "";
});
button.addEventListener("mousedown", () => {
button.style.transform = "translateY(1px)";
});
button.addEventListener("mouseup", () => {
button.style.transform = "";
});
}
function applyHeaderButtonStyles() {
const holder = giveawayFrame ? giveawayFrame.querySelector(".button-right") : null;
if (holder) {
holder.style.display = "flex";
holder.style.alignItems = "center";
holder.style.justifyContent = "center";
holder.style.flexWrap = "wrap";
holder.style.gap = "6px";
}
styleHeaderButton(resetButton, {
normalBg: "linear-gradient(180deg, #7a2f2f 0%, #5d2020 100%)",
hoverBg: "linear-gradient(180deg, #944040 0%, #702929 100%)",
normalBorder: "#8f3e3e",
hoverBorder: "#bd6262"
});
styleHeaderButton(commandsButton, {
normalBg: "linear-gradient(180deg, #2b6174 0%, #1f4a58 100%)",
hoverBg: "linear-gradient(180deg, #34758b 0%, #265969 100%)",
normalBorder: "#2f6e83",
hoverBorder: "#57a6c2"
});
styleHeaderButton(roomsButton, {
normalBg: "linear-gradient(180deg, #846210 0%, #5f4508 100%)",
hoverBg: "linear-gradient(180deg, #9b7415 0%, #73560a 100%)",
normalBorder: "#9a7721",
hoverBorder: "#d2aa44"
});
styleHeaderButton(optionsButton, {
normalBg: "linear-gradient(180deg, #3b5f2e 0%, #2b461f 100%)",
hoverBg: "linear-gradient(180deg, #4a7639 0%, #355628 100%)",
normalBorder: "#547e44",
hoverBorder: "#7bb562"
});
styleHeaderButton(closeButton, {
normalBg: "linear-gradient(180deg, #3a3a3a 0%, #222 100%)",
hoverBg: "linear-gradient(180deg, #474747 0%, #2a2a2a 100%)",
normalBorder: "#555",
hoverBorder: "#7a7a7a",
minWidth: "34px",
padding: "0",
fontSize: "14px",
fontWeight: "800"
});
}
function styleStartButton(isRunning = false) {
if (!startButton) return;
const palette = isRunning
? {
normalBg: "linear-gradient(180deg, #8c1f1f 0%, #631515 100%)",
hoverBg: "linear-gradient(180deg, #a12626 0%, #751919 100%)",
normalBorder: "#b64040",
hoverBorder: "#d96969"
}
: {
normalBg: "linear-gradient(180deg, #2b8a3f 0%, #1f6a31 100%)",
hoverBg: "linear-gradient(180deg, #33a24a 0%, #257c39 100%)",
normalBorder: "#3fac59",
hoverBorder: "#6fd18a"
};
startButton.style.minWidth = "0";
startButton.style.height = "44px";
startButton.style.padding = "0 16px";
startButton.style.width = "auto";
startButton.style.display = "inline-flex";
startButton.style.alignItems = "center";
startButton.style.justifyContent = "center";
startButton.style.textAlign = "center";
startButton.style.lineHeight = "1";
startButton.style.whiteSpace = "nowrap";
startButton.style.border = `1px solid ${palette.normalBorder}`;
startButton.style.borderRadius = "999px";
startButton.style.background = palette.normalBg;
startButton.style.color = "#f6fff9";
startButton.style.fontSize = "16px";
startButton.style.fontWeight = "800";
startButton.style.letterSpacing = "0.2px";
startButton.style.cursor = "pointer";
startButton.style.boxShadow = "0 6px 16px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.16)";
startButton.style.transition = "background 140ms ease, border-color 140ms ease, transform 90ms ease, box-shadow 140ms ease";
startButton.dataset.normalBg = palette.normalBg;
startButton.dataset.hoverBg = palette.hoverBg;
startButton.dataset.normalBorder = palette.normalBorder;
startButton.dataset.hoverBorder = palette.hoverBorder;
if (!startButton.dataset.styleInit) {
startButton.onmouseenter = () => {
if (startButton.disabled) return;
startButton.style.background = startButton.dataset.hoverBg || palette.hoverBg;
startButton.style.borderColor = startButton.dataset.hoverBorder || palette.hoverBorder;
startButton.style.boxShadow = "0 8px 18px rgba(0,0,0,0.34), inset 0 1px 0 rgba(255,255,255,0.2)";
};
startButton.onmouseleave = () => {
startButton.style.background = startButton.dataset.normalBg || palette.normalBg;
startButton.style.borderColor = startButton.dataset.normalBorder || palette.normalBorder;
startButton.style.boxShadow = "0 6px 16px rgba(0,0,0,0.28), inset 0 1px 0 rgba(255,255,255,0.16)";
startButton.style.transform = "";
};
startButton.onmousedown = () => {
if (startButton.disabled) return;
startButton.style.transform = "translateY(1px)";
};
startButton.onmouseup = () => {
startButton.style.transform = "";
};
startButton.dataset.styleInit = "1";
}
}
function formatStatusClock(ts) {
if (!ts) return "--:--:--";
try {
return new Date(ts).toLocaleTimeString("pl-PL", { hour12: false });
} catch (error) {
return "--:--:--";
}
}
function getScriptMeta() {
const fallback = {
name: "PTT BON Giveaway",
description: "Giveaway BON dla PolishTorrent",
version: "2.0.19",
author: "Aniouek32"
};
try {
if (typeof GM_info !== "undefined" && GM_info && GM_info.script) {
const script = GM_info.script;
return {
name: String(script.name || fallback.name),
description: String(script.description || fallback.description),
version: String(script.version || fallback.version),
author: String(script.author || fallback.author)
};
}
} catch (error) {
}
return fallback;
}
function markApiStatus(bucket, ok, errorText = "") {
const target = apiHealth[bucket];
if (!target) return;
target.ok = ok;
target.lastAt = Date.now();
target.lastError = String(errorText || "").trim();
updateRoomInfoPanel();
}
function resetApiStatus() {
apiHealth.entries.ok = null;
apiHealth.entries.lastAt = 0;
apiHealth.entries.lastError = "";
apiHealth.sponsors.ok = null;
apiHealth.sponsors.lastAt = 0;
apiHealth.sponsors.lastError = "";
updateRoomInfoPanel();
}
function updateRoomInfoPanel() {
if (!roomEntriesValue || !roomSponsorsValue) return;
roomEntriesValue.textContent = getEntriesRoomId();
roomSponsorsValue.textContent = getSponsorRoomId();
if (scriptNameValue) {
scriptNameValue.textContent = scriptMeta.name;
}
if (scriptDescriptionValue) {
scriptDescriptionValue.textContent = scriptMeta.description;
}
if (scriptVersionValue) {
scriptVersionValue.textContent = scriptMeta.version;
}
if (scriptAuthorValue) {
scriptAuthorValue.textContent = scriptMeta.author;
}
if (!apiEntriesStatusValue || !apiSponsorsStatusValue) return;
const entries = apiHealth.entries;
if (entries.ok === true) {
apiEntriesStatusValue.textContent = `OK ${formatStatusClock(entries.lastAt)}`;
apiEntriesStatusValue.style.color = "#5edb7f";
} else if (entries.ok === false) {
apiEntriesStatusValue.textContent = `BŁĄD ${formatStatusClock(entries.lastAt)}`;
apiEntriesStatusValue.style.color = "#ff7979";
} else {
apiEntriesStatusValue.textContent = "brak danych";
apiEntriesStatusValue.style.color = "#9bb2bf";
}
const sponsors = apiHealth.sponsors;
if (sponsors.ok === true) {
apiSponsorsStatusValue.textContent = `OK ${formatStatusClock(sponsors.lastAt)}`;
apiSponsorsStatusValue.style.color = "#5edb7f";
} else if (sponsors.ok === false) {
apiSponsorsStatusValue.textContent = `BŁĄD ${formatStatusClock(sponsors.lastAt)}`;
apiSponsorsStatusValue.style.color = "#ff7979";
} else {
apiSponsorsStatusValue.textContent = "brak danych";
apiSponsorsStatusValue.style.color = "#9bb2bf";
}
}
function hideRestoreNotice() {
if (!restoreNotice) return;
if (restoreNoticeTimer) {
clearTimeout(restoreNoticeTimer);
restoreNoticeTimer = null;
}
restoreNotice.hidden = true;
restoreNotice.style.display = "none";
restoreNotice.textContent = "";
}
function showRestoreNotice(text, timeoutMs = 6000) {
if (!restoreNotice) return;
if (restoreNoticeTimer) {
clearTimeout(restoreNoticeTimer);
restoreNoticeTimer = null;
}
restoreNotice.textContent = text;
restoreNotice.hidden = false;
restoreNotice.style.display = "block";
if (timeoutMs > 0) {
restoreNoticeTimer = setTimeout(() => {
hideRestoreNotice();
}, timeoutMs);
}
}
function updateStatsPanel(activeGiveaway = giveawayData) {
if (!statsWrapper) return;
if (!activeGiveaway || activeGiveaway.ended) {
statsWrapper.hidden = true;
return;
}
const sponsorTotal = Object.values(activeGiveaway.sponsorContribs || {})
.reduce((sum, value) => sum + (Number(value) || 0), 0);
const hostBase = Math.floor(activeGiveaway.initialHostContribution || 0);
const hostCurrent = Math.floor(activeGiveaway.hostContribution || 0);
const hostExtra = Math.max(0, hostCurrent - hostBase);
const taken = numberEntries.size;
const free = Math.max(0, (activeGiveaway.totalEntries || 0) - taken);
statsPot.textContent = `${cleanPotString(activeGiveaway.amount)} BON`;
statsHostBase.textContent = `${cleanPotString(hostBase)} BON`;
statsHostExtra.textContent = `${cleanPotString(hostExtra)} BON`;
statsSponsors.textContent = `${cleanPotString(sponsorTotal)} BON`;
statsEntries.textContent = `${taken}/${activeGiveaway.totalEntries}`;
statsFree.textContent = String(free);
statsWrapper.hidden = false;
}
function getSerializableGiveawayData() {
if (!giveawayData) return null;
return {
host: giveawayData.host,
amount: giveawayData.amount,
hostContribution: giveawayData.hostContribution,
initialHostContribution: giveawayData.initialHostContribution,
gameMode: sanitizeGameMode(giveawayData.gameMode || activeGameMode),
minEntryFee: Math.max(0, Math.floor(giveawayData.minEntryFee || 0)),
dynamicMinEnabled: Boolean(giveawayData.dynamicMinEnabled),
dynamicMinBase: Math.max(0, Math.floor(giveawayData.dynamicMinBase || 0)),
startNum: giveawayData.startNum,
endNum: giveawayData.endNum,
totalEntries: giveawayData.totalEntries,
winningNumber: giveawayData.winningNumber,
totalSeconds: giveawayData.totalSeconds,
timeLeft: giveawayData.timeLeft,
endTs: giveawayData.endTs,
reminderNum: giveawayData.reminderNum,
reminderFreqSec: giveawayData.reminderFreqSec,
winnersNum: giveawayData.winnersNum,
sponsors: Array.isArray(giveawayData.sponsors) ? [...giveawayData.sponsors] : [],
sponsorContribs: { ...(giveawayData.sponsorContribs || {}) },
winnerSent: Boolean(giveawayData.winnerSent),
entriesPaused: Boolean(giveawayData.entriesPaused),
ended: Boolean(giveawayData.ended),
ending: Boolean(giveawayData.ending)
};
}
function saveState() {
try {
if (!giveawayData || giveawayData.ended) {
clearSavedState();
return;
}
const payload = {
version: 4,
giveawayStartTime: giveawayStartTime ? giveawayStartTime.getTime() : null,
giveawayData: getSerializableGiveawayData(),
numberEntries: Array.from(numberEntries.entries()),
fancyNames: Array.from(fancyNames.entries()),
processedGiftMessages: Array.from(processedGiftMessages),
processedChatMessages: Array.from(processedChatMessages),
sponsorDigestBuffer: Array.isArray(sponsorDigestBuffer) ? sponsorDigestBuffer : [],
sponsorDigestWindowStartAt: sponsorDigestWindowStartAt || 0
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(payload));
} catch (error) {
console.error("Nie udało się zapisać stanu giveaway:", error);
}
}
function clearSavedState() {
try {
localStorage.removeItem(STORAGE_KEY);
} catch (error) {
console.error("Nie udało się usunąć zapisanego stanu giveaway:", error);
}
}
function normalizeRoomId(value) {
const parsed = parseInt(String(value || "").trim(), 10);
if (!Number.isFinite(parsed) || parsed <= 0) {
return null;
}
return String(parsed);
}
function getEntriesRoomId() {
return normalizeRoomId(resolvedEntriesRoomId) ||
normalizeRoomId(GENERAL_SETTINGS.entriesRoomId) ||
"1";
}
function getSponsorRoomId() {
return normalizeRoomId(resolvedSponsorRoomId) ||
normalizeRoomId(GENERAL_SETTINGS.sponsorRoomId) ||
getEntriesRoomId();
}
function captureCurrentRoomId() {
const currentRoomInput = document.getElementById("currentChatroom");
const currentRoomId = normalizeRoomId(currentRoomInput ? currentRoomInput.value : null);
if (currentRoomId) {
resolvedEntriesRoomId = currentRoomId;
}
updateRoomInfoPanel();
}
async function resolveChatRoomIds() {
captureCurrentRoomId();
if (roomIdResolvePromise) {
await roomIdResolvePromise;
updateRoomInfoPanel();
return;
}
roomIdResolvePromise = (async () => {
try {
const [configResponse, roomsResponse] = await Promise.all([
fetch(`${window.location.origin}/api/chat/config`, {
method: "GET",
credentials: "same-origin"
}),
fetch(`${window.location.origin}/api/chat/rooms`, {
method: "GET",
credentials: "same-origin"
})
]);
if (!configResponse.ok || !roomsResponse.ok) {
throw new Error("Failed to resolve chat room IDs");
}
const configData = await configResponse.json();
const roomsData = await roomsResponse.json();
const rooms = Array.isArray(roomsData.data) ? roomsData.data : [];
const roomById = new Map();
const roomByName = new Map();
for (const room of rooms) {
const roomId = normalizeRoomId(room?.id);
if (!roomId) continue;
roomById.set(roomId, roomId);
const roomName = String(room?.name || "").trim().toLowerCase();
if (roomName) {
roomByName.set(roomName, roomId);
}
}
captureCurrentRoomId();
const fallbackEntries = normalizeRoomId(GENERAL_SETTINGS.entriesRoomId);
if (!normalizeRoomId(resolvedEntriesRoomId) && fallbackEntries && roomById.has(fallbackEntries)) {
resolvedEntriesRoomId = fallbackEntries;
}
const configRoom = configData ? configData.system_chatroom : null;
const configRoomId = normalizeRoomId(configRoom);
if (configRoomId && roomById.has(configRoomId)) {
resolvedSponsorRoomId = configRoomId;
} else {
const configRoomName = String(configRoom || "").trim().toLowerCase();
if (configRoomName && roomByName.has(configRoomName)) {
resolvedSponsorRoomId = roomByName.get(configRoomName);
}
}
const fallbackSponsor = normalizeRoomId(GENERAL_SETTINGS.sponsorRoomId);
if (!normalizeRoomId(resolvedSponsorRoomId) && fallbackSponsor && roomById.has(fallbackSponsor)) {
resolvedSponsorRoomId = fallbackSponsor;
}
} catch (error) {
if (DEBUG_SETTINGS.logChatMessages) {
console.warn("Nie udało się automatycznie rozpoznać room IDs czatu:", error);
}
} finally {
roomIdResolvePromise = null;
updateRoomInfoPanel();
}
})();
await roomIdResolvePromise;
updateRoomInfoPanel();
}
function restoreGiveawayFromStorage() {
try {
const raw = localStorage.getItem(STORAGE_KEY);
if (!raw) return;
const payload = JSON.parse(raw);
if (!payload || !payload.giveawayData) {
clearSavedState();
return;
}
const savedGiveaway = payload.giveawayData;
if (savedGiveaway.ended) {
clearSavedState();
return;
}
const currentUser = normalizeUser(getLoggedInUsername());
const savedHost = normalizeUser(savedGiveaway.host);
if (!currentUser || !savedHost || currentUser !== savedHost) {
clearSavedState();
return;
}
giveawayStartTime = payload.giveawayStartTime
? new Date(payload.giveawayStartTime)
: new Date();
giveawayData = {
...savedGiveaway,
gameMode: sanitizeGameMode(savedGiveaway.gameMode),
minEntryFee: Math.max(0, Math.floor(savedGiveaway.minEntryFee || 0)),
dynamicMinEnabled: Boolean(savedGiveaway.dynamicMinEnabled),
dynamicMinBase: Math.max(0, Math.floor(savedGiveaway.dynamicMinBase || 0)),
sponsors: Array.isArray(savedGiveaway.sponsors) ? [...savedGiveaway.sponsors] : [],
sponsorContribs: { ...(savedGiveaway.sponsorContribs || {}) },
countdownTimerID: null,
entriesPaused: Boolean(savedGiveaway.entriesPaused),
ended: false,
ending: false
};
activeV2DynamicMinEnabled = Boolean(giveawayData.dynamicMinEnabled);
coinInput.value = String(Math.floor(giveawayData.initialHostContribution || 0));
startInput.value = String(giveawayData.startNum || 1);
endInput.value = String(giveawayData.endNum || 50);
timerInput.value = String(Math.max(1, Math.round((giveawayData.totalSeconds || 0) / 60)));
reminderInput.value = String(Math.max(0, giveawayData.reminderNum || 0));
winnersInput.value = String(Math.max(1, giveawayData.winnersNum || 1));
if (entryFeeInput) {
const restoredBaseMin = Math.max(1, Math.floor(giveawayData.dynamicMinBase || giveawayData.minEntryFee || 50));
entryFeeInput.value = String(restoredBaseMin);
}
numberEntries = new Map(Array.isArray(payload.numberEntries) ? payload.numberEntries : []);
fancyNames = new Map(Array.isArray(payload.fancyNames) ? payload.fancyNames : []);
numberTakenBy = new Map();
numberEntries.forEach((number, author) => {
numberTakenBy.set(number, author);
});
processedGiftMessages = new Set(Array.isArray(payload.processedGiftMessages) ? payload.processedGiftMessages : []);
processedChatMessages = new Set(Array.isArray(payload.processedChatMessages) ? payload.processedChatMessages : []);
sponsorDigestBuffer = Array.isArray(payload.sponsorDigestBuffer) ? payload.sponsorDigestBuffer : [];
sponsorDigestWindowStartAt = Number(payload.sponsorDigestWindowStartAt) || 0;
chatbox = document.querySelector("#chatbox__messages-create");
updateEntries();
updateCoinHeader(giveawayData.amount);
updateStatsPanel(giveawayData);
showRestoreNotice("Przywrócono aktywny giveaway po odswiezeniu strony.");
entriesWrapper.hidden = false;
countdownHeader.hidden = false;
applyGameMode(giveawayData.gameMode || activeGameMode, false, true);
setRunningState(true);
void resolveChatRoomIds();
if (entriesInterval) {
clearInterval(entriesInterval);
}
if (sponsorsInterval) {
clearInterval(sponsorsInterval);
}
giveawayData.countdownTimerID = countdownTimer(countdownHeader, giveawayData);
entriesInterval = setInterval(() => { void getEntriesFromApi(); }, GENERAL_SETTINGS.entryPollMs);
sponsorsInterval = setInterval(() => { void getSponsors(); }, GENERAL_SETTINGS.sponsorPollMs);
void getEntriesFromApi();
void getSponsors();
window.onbeforeunload = function () {
return "Trwa giveaway";
};
} catch (error) {
console.error("Nie udało się przywrócić giveaway:", error);
clearSavedState();
}
}
function toggleStartStop() {
if (giveawayData && !giveawayData.ended) {
if (!window.confirm("Na pewno zakończyc giveaway?")) {
return;
}
endGiveaway(giveawayData);
} else {
void startGiveaway();
}
}
function reminderAutoScaling() {
const timerValue = parseInt(timerInput.value, 10);
if (Number.isNaN(timerValue)) {
reminderInput.value = 0;
reminderInput.setCustomValidity("");
return;
}
const reminders = Math.floor(timerValue / GENERAL_SETTINGS.defaultMinsPerReminder) - 1;
reminderInput.value = reminders < 0 ? 0 : reminders;
reminderInput.setCustomValidity("");
}
function entryRangeValidation() {
const startValue = parseInt(startInput.value, 10);
const endValue = parseInt(endInput.value, 10);
if (!Number.isNaN(startValue) && !Number.isNaN(endValue) && startValue > endValue) {
const msg = "Numer startowy powinien byc mniejszy lub równy numerowi koncowemu";
startInput.setCustomValidity(msg);
endInput.setCustomValidity(msg);
} else {
startInput.setCustomValidity("");
endInput.setCustomValidity("");
}
}
function remindersValidation() {
const timerValue = parseInt(timerInput.value, 10);
const reminderValue = parseInt(reminderInput.value, 10);
if (Number.isNaN(timerValue) || Number.isNaN(reminderValue) || reminderValue <= 0) {
reminderInput.setCustomValidity("");
return;
}
if (timerValue / reminderValue < GENERAL_SETTINGS.minsPerReminderLimit) {
reminderInput.setCustomValidity(`Nie może byc wiecej niz 1 przypomnienie co ${parseTime(GENERAL_SETTINGS.minsPerReminderLimit * 60000)}.`);
reminderInput.reportValidity();
} else {
reminderInput.setCustomValidity("");
}
}
function winnersValidation() {
const winnersValue = parseInt(winnersInput.value, 10);
if (Number.isNaN(winnersValue) || winnersValue < 1) {
winnersInput.setCustomValidity("Liczba zwycięzców musi byc co najmniej 1.");
} else {
winnersInput.setCustomValidity("");
}
}
async function startGiveaway() {
if (!giveawayForm.checkValidity()) {
giveawayForm.reportValidity();
return;
}
chatbox = document.querySelector("#chatbox__messages-create");
if (!chatbox) {
window.alert("Nie udało się znalezc pola czatu.");
return;
}
processedGiftMessages = new Set();
processedChatMessages = new Set();
giveawayStartTime = new Date();
resetV2GiftHistoryState();
numberEntries = new Map();
numberTakenBy = new Map();
fancyNames = new Map();
sponsorDigestBuffer = [];
sponsorDigestWindowStartAt = 0;
userCooldown.clear();
userCommandLog.clear();
userLastActionAt.clear();
userLastCommandAt.clear();
userSpamStrikes.clear();
userFeedbackCooldown.clear();
resetApiStatus();
updateEntries();
const totalTimeMs = parseInt(timerInput.value, 10) * 60000;
const reminderNum = Math.max(0, parseInt(reminderInput.value, 10) || 0);
const winnersNum = Math.max(1, parseInt(winnersInput.value, 10) || 1);
const amount = parsePositiveInt(coinInput.value);
const gameMode = sanitizeGameMode(activeGameMode);
const minEntryFee = gameMode === "v2_0" ? parsePositiveInt(entryFeeInput ? entryFeeInput.value : 0) : 0;
const dynamicMinEnabled = gameMode === "v2_0" ? Boolean(activeV2DynamicMinEnabled) : false;
if (amount <= 0) {
window.alert("Kwota giveaway musi byc wieksza niz 0 BON.");
return;
}
if (gameMode === "v2_0" && minEntryFee <= 0) {
window.alert("W trybie Stawka większa niż pula BON minimalne wpisowe musi być większe niż 0 BON.");
return;
}
giveawayData = {
host: getLoggedInUsername(),
amount,
hostContribution: amount,
initialHostContribution: amount,
gameMode,
minEntryFee,
dynamicMinEnabled,
dynamicMinBase: dynamicMinEnabled ? minEntryFee : 0,
startNum: parseInt(startInput.value, 10),
endNum: parseInt(endInput.value, 10),
totalEntries: parseInt(endInput.value, 10) - parseInt(startInput.value, 10) + 1,
winningNumber: null,
totalSeconds: totalTimeMs / 1000,
timeLeft: totalTimeMs / 1000,
endTs: Date.now() + totalTimeMs,
reminderNum,
reminderFreqSec: reminderNum > 0 ? Math.max(1, Math.round((totalTimeMs / 1000) / (reminderNum + 1))) : 0,
winnersNum,
sponsors: [],
sponsorContribs: {},
entriesPaused: false,
countdownTimerID: null,
winnerSent: false,
ended: false,
ending: false,
};
const sponsorMessage = getSponsorMessage(giveawayData.host);
const currentBon = readHostBalance();
if (currentBon < giveawayData.amount) {
window.alert(`BŁĄD GIVEAWAY: wpisana kwota (${giveawayData.amount}) jest wyższą niz twój aktualny BON (${currentBon}). Byc może musisz odswiezyc strone.`);
resetGiveaway(true);
return;
}
await resolveChatRoomIds();
setRunningState(true);
updateStatsPanel(giveawayData);
saveState();
const winnersText = giveawayData.winnersNum === 1
? "1 zwycięzca"
: `${giveawayData.winnersNum} zwycięzców`;
if (isV2Mode(giveawayData)) {
sendMessage(getV2Copy().intro({
amount: giveawayData.amount,
hostContributionText: cleanPotString(giveawayData.hostContribution),
totalTimeMs,
winnersText,
startNum: giveawayData.startNum,
endNum: giveawayData.endNum,
minEntryFee: getCurrentV2MinEntryFee(giveawayData),
host: giveawayData.host,
dynamicMinEnabled: Boolean(giveawayData.dynamicMinEnabled)
}));
} else {
sendMessage(CHAT_COPY.intro({
amount: giveawayData.amount,
totalTimeMs,
winnersText,
startNum: giveawayData.startNum,
endNum: giveawayData.endNum,
sponsorMessage
}));
}
entriesWrapper.hidden = false;
giveawayData.countdownTimerID = countdownTimer(countdownHeader, giveawayData);
entriesInterval = setInterval(() => { void getEntriesFromApi(); }, GENERAL_SETTINGS.entryPollMs);
sponsorsInterval = setInterval(() => { void getSponsors(); }, GENERAL_SETTINGS.sponsorPollMs);
void getEntriesFromApi();
void getSponsors();
window.onbeforeunload = function () {
return "Trwa giveaway";
};
}
function setRunningState(isRunning) {
startButton.textContent = isRunning ? "Zakończ" : "Start";
styleStartButton(isRunning);
coinInput.disabled = isRunning;
if (entryFeeInput) {
entryFeeInput.disabled = isRunning || sanitizeGameMode(activeGameMode) !== "v2_0";
}
startInput.disabled = isRunning;
endInput.disabled = isRunning;
timerInput.disabled = isRunning;
reminderInput.disabled = isRunning;
winnersInput.disabled = isRunning;
startButton.disabled = false;
refreshBonInputThemes();
updateModeUiState();
}
function handleGiveawayCommands(author, messageContent, fancyName, activeGiveaway) {
if (!activeGiveaway) return;
const args = messageContent.substring(1).trim().split(/\s+/);
const command = (args.shift() || "").toLowerCase();
const validCommands = [
'time', 'status', 'random', 'number', 'lucky', 'luckye', 'free',
'addbon', 'removebon', 'commands', 'entries', 'sponsors', 'bon', 'range',
'winners', 'reminder', 'pause', 'resume'
];
if (!validCommands.includes(command)) {
return;
}
const isHost = normalizeUser(author) === normalizeUser(activeGiveaway.host);
if (!isHost && applyCooldown(author, { command })) {
return;
}
const userNumber = numberEntries.get(author);
const isV2 = isV2Mode(activeGiveaway);
let message;
if (isV2 && (command === 'random' || command === 'luckye')) {
sendMessage(getV2Copy().entryViaGiftOnly(author, activeGiveaway.host, getCurrentV2MinEntryFee(activeGiveaway)));
return;
}
switch (command) {
case 'time': {
if (args.length === 0) {
sendMessage(CHAT_COPY.timeLeft(activeGiveaway.timeLeft * 1000));
return;
}
if (!isHost) {
sendMessage(CHAT_COPY.hostOnlyTime());
return;
}
const action = (args[0] || "").toLowerCase();
const amount = parseInt(args[1], 10);
if (!['add', 'remove'].includes(action) || !Number.isFinite(amount) || amount <= 0) {
sendMessage(CHAT_COPY.usageTime());
return;
}
const deltaMs = amount * 60000;
if (action === 'add') {
activeGiveaway.endTs += deltaMs;
activeGiveaway.timeLeft = Math.max(0, Math.ceil((activeGiveaway.endTs - Date.now()) / 1000));
recalculateReminderFrequency(activeGiveaway);
updateStatsPanel(activeGiveaway);
saveState();
sendMessage(CHAT_COPY.hostAddTime(amount, activeGiveaway.timeLeft * 1000));
} else {
activeGiveaway.endTs -= deltaMs;
activeGiveaway.timeLeft = Math.max(0, Math.ceil((activeGiveaway.endTs - Date.now()) / 1000));
if (activeGiveaway.timeLeft <= 0) {
sendMessage(CHAT_COPY.hostTimeEnded());
void endGiveaway(activeGiveaway);
return;
}
recalculateReminderFrequency(activeGiveaway);
updateStatsPanel(activeGiveaway);
saveState();
sendMessage(CHAT_COPY.hostRemoveTime(amount, activeGiveaway.timeLeft * 1000));
}
break;
}
case 'status': {
const freeCount = Math.max(0, activeGiveaway.totalEntries - numberEntries.size);
const sponsorCount = Object.keys(activeGiveaway.sponsorContribs || {}).length;
const entriesState = activeGiveaway.entriesPaused ? "wstrzymane" : "otwarte";
if (isV2) {
sendMessage(getV2Copy().status({
amount: activeGiveaway.amount,
timeLeftMs: activeGiveaway.timeLeft * 1000,
taken: numberEntries.size,
total: activeGiveaway.totalEntries,
freeCount,
sponsorCount,
entriesState,
minEntryFee: getCurrentV2MinEntryFee(activeGiveaway)
}));
} else {
sendMessage(CHAT_COPY.status({
amount: activeGiveaway.amount,
timeLeftMs: activeGiveaway.timeLeft * 1000,
taken: numberEntries.size,
total: activeGiveaway.totalEntries,
freeCount,
sponsorCount,
entriesState
}));
}
break;
}
case 'random': {
if (userNumber !== undefined) {
sendMessage(CHAT_COPY.alreadyJoined(author, userNumber));
return;
}
if (activeGiveaway.entriesPaused) {
if (canSendUserFeedback(author, "entry-paused")) {
sendMessage(CHAT_COPY.entriesPaused(author));
}
return;
}
const freeNumbers = getFreeNumbers(activeGiveaway);
if (freeNumbers.length === 0) {
sendMessage(CHAT_COPY.noFreeForUser(author));
return;
}
const randomNum = freeNumbers[Math.floor(Math.random() * freeNumbers.length)];
addNewEntry(author, fancyName || escapeHTML(author), randomNum);
sendMessage(CHAT_COPY.joinedRandom(author, randomNum, activeGiveaway.timeLeft * 1000));
break;
}
case 'number':
if (userNumber !== undefined) {
message = CHAT_COPY.numberInfo(author, userNumber);
} else {
message = CHAT_COPY.numberNone(author);
}
sendMessage(message);
break;
case 'lucky':
message = CHAT_COPY.luckyNow(getLuckyNumber(activeGiveaway));
sendMessage(message);
break;
case 'luckye': {
if (userNumber !== undefined) {
sendMessage(CHAT_COPY.alreadyJoined(author, userNumber));
return;
}
if (activeGiveaway.entriesPaused) {
if (canSendUserFeedback(author, "entry-paused")) {
sendMessage(CHAT_COPY.entriesPaused(author));
}
return;
}
const luckyNum = getLuckyNumber(activeGiveaway);
if (luckyNum === null || luckyNum === undefined || numberTakenBy.has(luckyNum)) {
sendMessage(CHAT_COPY.noLuckyCandidate(author));
return;
}
addNewEntry(author, fancyName || escapeHTML(author), luckyNum);
sendMessage(CHAT_COPY.joinedLuckye(author, luckyNum, activeGiveaway.timeLeft * 1000));
break;
}
case 'free': {
const sample = getFreeNumberSample(activeGiveaway, 5);
const freeCount = activeGiveaway.totalEntries - numberEntries.size;
if (freeCount <= 0) {
sendMessage(CHAT_COPY.noFreeLeft());
return;
}
if (sample.length === 0) {
sendMessage(CHAT_COPY.freeCountOnly(freeCount));
return;
}
sendMessage(CHAT_COPY.freeSample(freeCount, sample));
break;
}
case 'addbon': {
if (!isHost) {
sendMessage(CHAT_COPY.hostOnlyCmd("!addbon"));
return;
}
const amount = parsePositiveInt(args[0]);
if (!Number.isFinite(amount) || amount <= 0) {
sendMessage(CHAT_COPY.usage("!addbon 100"));
return;
}
const currentBon = readHostBalance();
const newHostContribution = Math.floor(activeGiveaway.hostContribution || 0) + amount;
if (currentBon < newHostContribution) {
sendMessage(CHAT_COPY.addBonBalanceError(currentBon, newHostContribution));
return;
}
activeGiveaway.amount += amount;
activeGiveaway.hostContribution += amount;
updateCoinHeader(activeGiveaway.amount);
updateStatsPanel(activeGiveaway);
saveState();
sendMessage(CHAT_COPY.hostAddedBon(amount, activeGiveaway.amount));
break;
}
case 'removebon': {
if (!isHost) {
sendMessage(CHAT_COPY.hostOnlyCmd("!removebon"));
return;
}
const amount = parsePositiveInt(args[0]);
if (!Number.isFinite(amount) || amount <= 0) {
sendMessage(CHAT_COPY.usage("!removebon 100"));
return;
}
const baseHostContribution = Math.floor(activeGiveaway.initialHostContribution || 0);
const currentHostContribution = Math.floor(activeGiveaway.hostContribution || 0);
const removableFromHost = Math.max(0, currentHostContribution - baseHostContribution);
if (removableFromHost <= 0) {
sendMessage(CHAT_COPY.cannotRemoveNoExtra());
return;
}
if (amount > removableFromHost) {
sendMessage(CHAT_COPY.cannotRemoveMax(removableFromHost));
return;
}
activeGiveaway.amount -= amount;
activeGiveaway.hostContribution -= amount;
updateCoinHeader(activeGiveaway.amount);
updateStatsPanel(activeGiveaway);
saveState();
sendMessage(CHAT_COPY.hostRemovedBon(amount, activeGiveaway.amount));
break;
}
case 'winners': {
if (!isHost) {
sendMessage(CHAT_COPY.hostOnlyWinners());
return;
}
const newWinners = parseInt(args[0], 10);
if (!Number.isFinite(newWinners) || newWinners < 1) {
sendMessage(CHAT_COPY.usage("!winners 3"));
return;
}
activeGiveaway.winnersNum = newWinners;
winnersInput.value = String(newWinners);
saveState();
sendMessage(CHAT_COPY.winnersSet(newWinners));
break;
}
case 'reminder':
if (!isHost) {
sendMessage(CHAT_COPY.hostOnlyReminder());
return;
}
sendReminderMessage(activeGiveaway);
break;
case 'pause':
if (!isHost) {
sendMessage(CHAT_COPY.hostOnlyPause());
return;
}
if (activeGiveaway.entriesPaused) {
sendMessage(CHAT_COPY.pausedAlready());
return;
}
activeGiveaway.entriesPaused = true;
saveState();
sendMessage(CHAT_COPY.pausedNow());
break;
case 'resume':
if (!isHost) {
sendMessage(CHAT_COPY.hostOnlyResume());
return;
}
if (!activeGiveaway.entriesPaused) {
sendMessage(CHAT_COPY.resumedAlready());
return;
}
activeGiveaway.entriesPaused = false;
saveState();
sendMessage(CHAT_COPY.resumedNow());
break;
case 'commands':
if (isV2) {
sendMessage(getV2Copy().commandsList(activeGiveaway.host, getCurrentV2MinEntryFee(activeGiveaway)));
} else {
sendMessage(CHAT_COPY.commandsList());
}
break;
case 'entries': {
if (numberEntries.size === 0) {
sendMessage(CHAT_COPY.entriesNone());
return;
}
const entriesList = Array.from(numberEntries.entries())
.sort((a, b) => a[1] - b[1])
.map(([entryAuthor, entryNumber]) => `[color=#d85e27]${sanitizeNick(entryAuthor)}[/color]: [b]${entryNumber}[/b]`)
.join(", ");
const freeCount = activeGiveaway.totalEntries - numberEntries.size;
sendMessage(CHAT_COPY.entriesList(numberEntries.size, activeGiveaway.totalEntries, freeCount, entriesList));
break;
}
case 'sponsors': {
const sponsors = getSortedSponsors(activeGiveaway);
if (sponsors.length === 0) {
sendMessage(CHAT_COPY.sponsorsNone());
return;
}
const totalSponsored = sponsors.reduce((sum, item) => sum + item.amount, 0);
const sponsorList = sponsors
.map(item => `[color=green][b]${sanitizeNick(item.name)}[/b][/color] ([color=#ffc00a]${cleanPotString(item.amount)} BON[/color])`)
.join(", ");
sendMessage(CHAT_COPY.sponsorsList(totalSponsored, sponsorList));
break;
}
case 'bon':
sendMessage(CHAT_COPY.potNow(activeGiveaway.amount));
break;
case 'range':
sendMessage(CHAT_COPY.rangeNow(activeGiveaway.startNum, activeGiveaway.endNum));
break;
default:
break;
}
}
function handleEntryMessage(number, author, fancyName, activeGiveaway) {
if (!activeGiveaway) return;
if (isV2Mode(activeGiveaway)) {
if (canSendUserFeedback(author, "entry-v2-gift")) {
sendMessage(getV2Copy().entryViaGiftOnly(author, activeGiveaway.host, getCurrentV2MinEntryFee(activeGiveaway)));
}
return;
}
if (activeGiveaway.entriesPaused) {
if (canSendUserFeedback(author, "entry-paused")) {
sendMessage(CHAT_COPY.entriesPaused(author));
}
return;
}
if (number < activeGiveaway.startNum || number > activeGiveaway.endNum) {
if (canSendUserFeedback(author, "entry-range")) {
const outOfBoundsMessage = CHAT_COPY.outOfRange(author, number, activeGiveaway.startNum, activeGiveaway.endNum, formatFreeNumberSuggestion(activeGiveaway));
sendMessage(outOfBoundsMessage);
}
return;
}
if (numberEntries.has(author)) {
const currentNumber = numberEntries.get(author);
if (canSendUserFeedback(author, "entry-repeat")) {
sendMessage(CHAT_COPY.alreadyJoined(author, currentNumber));
}
return;
}
addNewEntry(author, fancyName || escapeHTML(author), number);
const entryMessage = CHAT_COPY.entryJoined(author, number, activeGiveaway.timeLeft * 1000);
sendMessage(entryMessage);
}
function addNewEntry(author, fancyName, number) {
numberEntries.set(author, number);
numberTakenBy.set(number, author);
fancyNames.set(author, fancyName);
updateEntries();
updateStatsPanel(giveawayData);
}
function updateEntries() {
let tableStart = "<thead><tr><th>Uzytkownik</th><th>Numer</th></tr></thead><tbody>";
let tableEntries = "";
let tableEnd = "</tbody>";
numberEntries.forEach((entry, author) => {
const fancyName = fancyNames.get(author) || escapeHTML(author);
tableEntries += `<tr><td>${fancyName}</td><td>${entry}</td></tr>`;
});
document.getElementById("entriesTable").innerHTML = tableStart + tableEntries + tableEnd;
}
function collectGiftMetaText(metaMessage) {
if (!metaMessage || typeof metaMessage !== "object") {
return [];
}
const candidates = [];
const pushText = (value) => {
const text = String(value || "").replace(/\s+/g, " ").trim();
if (!text) return;
candidates.push(text);
};
const directKeys = [
"gift_message", "giftMessage", "gift_note", "giftNote",
"note", "message_text", "messageText", "comment"
];
directKeys.forEach((key) => {
pushText(metaMessage[key]);
});
const nestedKeys = ["data", "meta", "payload", "extra"];
nestedKeys.forEach((bucket) => {
const obj = metaMessage[bucket];
if (!obj || typeof obj !== "object") return;
directKeys.forEach((key) => {
pushText(obj[key]);
});
});
return Array.from(new Set(candidates));
}
function extractIntegerToken(textValue) {
const text = String(textValue || "").replace(/\s+/g, " ").trim();
if (!text) return null;
const matches = text.matchAll(/-?\d+/g);
for (const match of matches) {
const token = String(match[0] || "");
const idx = Number(match.index) || 0;
const end = idx + token.length;
const prev = text[idx - 1] || "";
const prevPrev = text[idx - 2] || "";
const next = text[end] || "";
const nextNext = text[end + 1] || "";
const decimalLeft = prev === "." && /\d/.test(prevPrev);
const decimalRight = next === "." && /\d/.test(nextNext);
if (decimalLeft || decimalRight) {
continue;
}
const parsed = parseInt(token, 10);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
function parseGiftHistoryRows(html) {
const doc = new DOMParser().parseFromString(String(html || ""), "text/html");
const rows = Array.from(doc.querySelectorAll("table.data-table tbody tr"));
return rows.map((row) => {
const cells = Array.from(row.querySelectorAll("td"));
if (cells.length < 5) {
return null;
}
const sender = normalizeMessageContent(cells[0].textContent || "");
const recipient = normalizeMessageContent(cells[1].textContent || "");
const bonText = normalizeMessageContent(cells[2].textContent || "");
const note = normalizeMessageContent(cells[3].textContent || "");
const dtValue = String(cells[4].querySelector("time")?.getAttribute("datetime") || "");
const createdAtMs = Date.parse(dtValue);
const amount = parseFloat(String(bonText).replace(/,/g, ".").replace(/[^0-9.]/g, ""));
const safeAmount = Number.isFinite(amount) ? amount : 0;
return {
sender,
recipient,
amount: safeAmount,
amountInt: Math.floor(safeAmount),
note,
createdAtMs: Number.isFinite(createdAtMs) ? createdAtMs : 0
};
}).filter(Boolean);
}
async function fetchGiftHistoryRows(hostName) {
const host = String(hostName || "").trim();
if (!host) {
return [];
}
const normalizedHost = normalizeUser(host);
const now = Date.now();
const cacheFresh = v2GiftHistoryCache.host === normalizedHost && (now - v2GiftHistoryCache.fetchedAt) < 12000;
if (cacheFresh) {
return v2GiftHistoryCache.rows;
}
if (v2GiftHistoryFetchPromise) {
return v2GiftHistoryFetchPromise;
}
const url = `${window.location.origin}/users/${encodeURIComponent(host)}/gifts`;
v2GiftHistoryFetchPromise = fetch(url, {
method: "GET",
credentials: "same-origin"
})
.then((response) => {
if (!response.ok) {
throw new Error(`Gift history fetch failed: ${response.status}`);
}
return response.text();
})
.then((html) => parseGiftHistoryRows(html))
.then((rows) => {
v2GiftHistoryCache = {
host: normalizedHost,
fetchedAt: Date.now(),
rows
};
return rows;
})
.catch((error) => {
console.warn("Nie udało się pobrać historii giftów Stawka większa niż pula BON:", error);
return [];
})
.finally(() => {
v2GiftHistoryFetchPromise = null;
});
return v2GiftHistoryFetchPromise;
}
function buildGiftHistoryKey(row) {
return [
normalizeUser(row?.sender || ""),
normalizeUser(row?.recipient || ""),
Math.floor(Number(row?.amount || 0)),
Number(row?.createdAtMs || 0),
normalizeMessageContent(row?.note || "")
].join("|");
}
async function resolveGiftEntryNumberFromHistory(gift, activeGiveaway) {
const rows = await fetchGiftHistoryRows(activeGiveaway?.host);
if (!rows.length) {
return null;
}
const expectedSender = normalizeUser(gift?.gifter || "");
const expectedRecipient = normalizeUser(activeGiveaway?.host || "");
const expectedAmount = Math.floor(Number(gift?.amount || 0));
const expectedCreatedAt = Date.parse(String(gift?.createdAt || ""));
const hasExpectedTime = Number.isFinite(expectedCreatedAt);
const candidates = rows
.filter((row) => {
if (normalizeUser(row.sender) !== expectedSender) return false;
if (normalizeUser(row.recipient) !== expectedRecipient) return false;
if (Math.floor(Number(row.amountInt || row.amount || 0)) !== expectedAmount) return false;
return true;
})
.sort((a, b) => {
if (hasExpectedTime) {
const deltaA = Math.abs((Number(a.createdAtMs) || 0) - expectedCreatedAt);
const deltaB = Math.abs((Number(b.createdAtMs) || 0) - expectedCreatedAt);
if (deltaA !== deltaB) {
return deltaA - deltaB;
}
}
return (Number(b.createdAtMs) || 0) - (Number(a.createdAtMs) || 0);
});
for (const row of candidates) {
const key = buildGiftHistoryKey(row);
if (v2ConsumedGiftHistoryKeys.has(key)) {
continue;
}
const parsed = extractIntegerToken(row.note);
if (!Number.isFinite(parsed)) {
continue;
}
v2ConsumedGiftHistoryKeys.add(key);
return parsed;
}
return null;
}
function resetV2GiftHistoryState() {
v2GiftHistoryCache = {
host: "",
fetchedAt: 0,
rows: []
};
v2GiftHistoryFetchPromise = null;
v2ConsumedGiftHistoryKeys = new Set();
}
function parseGiftMessage(html, metaMessage = null) {
try {
const doc = new DOMParser().parseFromString(String(html || ""), "text/html");
const links = Array.from(doc.querySelectorAll("a"));
const rawText = normalizeMessageContent(doc.body.textContent || "");
const match = rawText.match(/has gifted\s*([0-9.]+)\s*BON\s*to/i);
if (!match || links.length < 2) {
return null;
}
const gifter = (links[0].textContent || "").trim();
const recipient = (links[1].textContent || "").trim();
let messageText = "";
if (recipient) {
const idx = rawText.toLowerCase().indexOf(recipient.toLowerCase());
if (idx >= 0) {
messageText = rawText.slice(idx + recipient.length).trim();
}
}
messageText = messageText.replace(/^[\s:;,\-()]+/, "").trim();
let trailingText = "";
for (let node = links[1].nextSibling; node; node = node.nextSibling) {
trailingText += ` ${String(node.textContent || "").trim()}`;
}
trailingText = trailingText.replace(/\s+/g, " ").replace(/^[\s:;,\-()]+/, "").trim();
const attributeTexts = [];
const attrElements = doc.querySelectorAll("[title],[data-original-title],[data-bs-original-title],[aria-label]");
attrElements.forEach((el) => {
const values = [
el.getAttribute("title"),
el.getAttribute("data-original-title"),
el.getAttribute("data-bs-original-title"),
el.getAttribute("aria-label")
];
values.forEach((value) => {
const text = String(value || "").replace(/\s+/g, " ").trim();
if (!text) return;
attributeTexts.push(text);
});
});
const metaTexts = collectGiftMetaText(metaMessage);
return {
gifter,
recipient,
amount: parseFloat(match[1]),
rawText,
messageText,
trailingText,
attributeTexts,
metaTexts,
createdAt: String(metaMessage?.created_at || "")
};
} catch (error) {
return null;
}
}
function extractGiftEntryNumber(gift) {
const candidates = [
String(gift?.messageText || ""),
String(gift?.trailingText || ""),
...(Array.isArray(gift?.attributeTexts) ? gift.attributeTexts : []),
...(Array.isArray(gift?.metaTexts) ? gift.metaTexts : [])
];
for (const candidate of candidates) {
const text = String(candidate || "").replace(/\s+/g, " ").trim();
if (!text) continue;
const cleaned = text
.replace(/.*?has gifted\s*[0-9]+(?:\.[0-9]+)?\s*BON\s*to\s*/i, "")
.replace(/^[\s:;,\-()]+/, "")
.trim();
if (!cleaned) continue;
const parsed = extractIntegerToken(cleaned);
if (Number.isFinite(parsed)) {
return parsed;
}
}
return null;
}
async function handleV2GiftEntry(gift, activeGiveaway) {
const paidAmount = Math.floor(Number(gift.amount) || 0);
const gifter = String(gift.gifter || "").trim();
if (!gifter || paidAmount <= 0) {
return;
}
if (normalizeUser(gift.recipient) !== normalizeUser(activeGiveaway.host)) {
return;
}
const v2 = getV2Copy();
if (normalizeUser(gifter) === normalizeUser(activeGiveaway.host)) {
giftBon(gifter, paidAmount, "Stawka większa niż pula BON: host nie bierze udziału");
sendMessage(v2.refundHost(gifter));
return;
}
if (activeGiveaway.entriesPaused) {
giftBon(gifter, paidAmount, "Stawka większa niż pula BON: zgłoszenia są wstrzymane");
sendMessage(v2.refundPaused(gifter));
return;
}
const requiredMinFee = getCurrentV2MinEntryFee(activeGiveaway);
if (paidAmount < requiredMinFee) {
giftBon(gifter, paidAmount, `Stawka większa niż pula BON: minimum wpisowego to ${requiredMinFee} BON`);
sendMessage(v2.refundLow(gifter, paidAmount, requiredMinFee));
return;
}
let selectedNumber = extractGiftEntryNumber(gift);
if (!Number.isFinite(selectedNumber)) {
selectedNumber = await resolveGiftEntryNumberFromHistory(gift, activeGiveaway);
}
if (!Number.isFinite(selectedNumber)) {
giftBon(gifter, paidAmount, "Stawka większa niż pula BON: brak numeru w treści gifta");
sendMessage(v2.refundNoNumber(gifter));
return;
}
if (selectedNumber < activeGiveaway.startNum || selectedNumber > activeGiveaway.endNum) {
giftBon(gifter, paidAmount, `Stawka większa niż pula BON: numer poza zakresem ${activeGiveaway.startNum}-${activeGiveaway.endNum}`);
sendMessage(v2.refundOutOfRange(gifter, selectedNumber, activeGiveaway.startNum, activeGiveaway.endNum));
return;
}
if (numberEntries.has(gifter)) {
const current = numberEntries.get(gifter);
giftBon(gifter, paidAmount, `Stawka większa niż pula BON: już grasz numerem ${current}`);
sendMessage(v2.refundAlreadyJoined(gifter, current));
return;
}
if (numberTakenBy.has(selectedNumber)) {
const existingAuthor = numberTakenBy.get(selectedNumber);
giftBon(gifter, paidAmount, `Stawka większa niż pula BON: numer ${selectedNumber} jest zajęty`);
sendMessage(v2.refundTaken(gifter, selectedNumber, existingAuthor));
return;
}
addNewEntry(gifter, escapeHTML(gifter), selectedNumber);
activeGiveaway.amount += paidAmount;
activeGiveaway.sponsorContribs[gifter] = (activeGiveaway.sponsorContribs[gifter] || 0) + paidAmount;
if (!activeGiveaway.sponsors.includes(gifter)) {
activeGiveaway.sponsors.push(gifter);
}
updateCoinHeader(activeGiveaway.amount);
updateStatsPanel(activeGiveaway);
sendMessage(v2.entryAccepted({
author: gifter,
number: selectedNumber,
paid: paidAmount,
timeLeftMs: activeGiveaway.timeLeft * 1000,
totalPot: activeGiveaway.amount,
newMinEntryFee: getCurrentV2MinEntryFee(activeGiveaway)
}));
}
async function handleGiftMessage(messageContent, activeGiveaway, metaMessage = null) {
if (!activeGiveaway) return;
const gift = parseGiftMessage(messageContent, metaMessage);
if (!gift) {
return;
}
if (isV2Mode(activeGiveaway)) {
await handleV2GiftEntry(gift, activeGiveaway);
return;
}
const addAmount = parseFloat(gift.amount);
const gifter = gift.gifter;
const recipient = gift.recipient;
if (!gifter || !recipient || !Number.isFinite(addAmount) || addAmount <= 0) {
return;
}
if (normalizeUser(recipient) !== normalizeUser(activeGiveaway.host)) {
return;
}
activeGiveaway.amount += addAmount;
activeGiveaway.sponsorContribs[gifter] = (activeGiveaway.sponsorContribs[gifter] || 0) + addAmount;
if (!activeGiveaway.sponsors.includes(gifter)) {
activeGiveaway.sponsors.push(gifter);
}
updateCoinHeader(activeGiveaway.amount);
updateStatsPanel(activeGiveaway);
sponsorDigestBuffer.push({ gifter, amount: addAmount });
maybeFlushSponsorDigest(activeGiveaway);
}
async function getSponsors() {
if (!giveawayData || !giveawayStartTime) return;
const api = `${window.location.origin}/api/chat/messages/${getSponsorRoomId()}`;
const startTimeTimestamp = giveawayStartTime.getTime();
try {
const response = await fetch(api, {
method: 'GET',
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
markApiStatus("sponsors", true);
const systemMessages = Array.isArray(data.data) ? data.data : [];
let processedAny = false;
const filteredMessages = systemMessages
.filter(message => {
const messageTime = new Date(message.created_at).getTime();
const giftMessage = String(message.message || "").includes("has gifted");
return giftMessage && messageTime >= startTimeTimestamp;
})
.sort((a, b) => {
const idA = Number(a?.id) || 0;
const idB = Number(b?.id) || 0;
if (idA !== idB) {
return idA - idB;
}
const timeA = new Date(a?.created_at).getTime();
const timeB = new Date(b?.created_at).getTime();
const safeTimeA = Number.isFinite(timeA) ? timeA : 0;
const safeTimeB = Number.isFinite(timeB) ? timeB : 0;
return safeTimeA - safeTimeB;
});
for (const msg of filteredMessages) {
if (processedGiftMessages.has(msg.id)) {
continue;
}
processedGiftMessages.add(msg.id);
processedAny = true;
await handleGiftMessage(msg.message, giveawayData, msg);
}
if (sponsorDigestBuffer.length > 0) {
maybeFlushSponsorDigest(giveawayData);
}
if (processedAny) {
saveState();
}
} catch (error) {
markApiStatus("sponsors", false, error);
console.error('There was a problem with the sponsor fetch operation:', error);
}
}
async function getEntriesFromApi() {
if (!giveawayData || !giveawayStartTime) return;
const api = `${window.location.origin}/api/chat/messages/${getEntriesRoomId()}`;
const startTimeTimestamp = giveawayStartTime.getTime();
try {
const response = await fetch(api, {
method: 'GET',
credentials: 'same-origin'
});
if (!response.ok) {
throw new Error('Network response was not ok');
}
const data = await response.json();
markApiStatus("entries", true);
const messages = Array.isArray(data.data) ? data.data : [];
let processedAny = false;
const filteredMessages = messages
.filter(msg => {
const messageTime = new Date(msg.created_at).getTime();
return messageTime >= startTimeTimestamp;
})
.sort((a, b) => {
const idA = Number(a?.id) || 0;
const idB = Number(b?.id) || 0;
if (idA !== idB) {
return idA - idB;
}
const timeA = new Date(a?.created_at).getTime();
const timeB = new Date(b?.created_at).getTime();
const safeTimeA = Number.isFinite(timeA) ? timeA : 0;
const safeTimeB = Number.isFinite(timeB) ? timeB : 0;
return safeTimeA - safeTimeB;
});
for (const msg of filteredMessages) {
if (processedChatMessages.has(msg.id)) {
continue;
}
processedChatMessages.add(msg.id);
processedAny = true;
if (!msg.user || !msg.message) {
continue;
}
const author = String(msg.user.username || "").trim();
const fancyName = escapeHTML(author);
const messageContent = normalizeMessageContent(msg.message);
if (!author || !messageContent) {
continue;
}
if (regNum.test(messageContent)) {
handleEntryMessage(parseInt(messageContent, 10), author, fancyName, giveawayData);
} else if (messageContent.startsWith("!")) {
handleGiveawayCommands(author, messageContent, fancyName, giveawayData);
}
}
if (processedAny) {
saveState();
}
} catch (error) {
markApiStatus("entries", false, error);
console.error('There was a problem fetching giveaway entries:', error);
}
}
async function endGiveaway(activeGiveaway) {
if (!activeGiveaway || activeGiveaway.ending) {
return;
}
activeGiveaway.ending = true;
startButton.disabled = true;
if (activeGiveaway.countdownTimerID) {
clearInterval(activeGiveaway.countdownTimerID);
activeGiveaway.countdownTimerID = null;
}
if (entriesInterval) {
clearInterval(entriesInterval);
entriesInterval = null;
}
if (sponsorsInterval) {
clearInterval(sponsorsInterval);
sponsorsInterval = null;
}
await getSponsors();
flushSponsorDigest(activeGiveaway, true);
if (numberEntries.size === 0) {
sendMessage(CHAT_COPY.noEntriesEnd());
finishGiveawayCleanup(activeGiveaway);
return;
}
activeGiveaway.winningNumber = getRandomInt(activeGiveaway.startNum, activeGiveaway.endNum);
const sponsors = getSortedSponsors(activeGiveaway);
const isV2 = isV2Mode(activeGiveaway);
const rankedSource = Array.from(numberEntries.entries())
.filter(([author]) => !isV2 || normalizeUser(author) !== normalizeUser(activeGiveaway.host));
if (rankedSource.length === 0) {
sendMessage(CHAT_COPY.noEntriesEnd());
finishGiveawayCleanup(activeGiveaway);
return;
}
const rankedEntries = rankedSource
.map(([author, guess], index) => ({
author,
guess,
gap: Math.abs(activeGiveaway.winningNumber - guess),
order: index,
}))
.sort((a, b) => a.gap - b.gap || a.order - b.order);
const winnerCount = Math.min(activeGiveaway.winnersNum, rankedEntries.length);
const winners = rankedEntries.slice(0, winnerCount);
const payouts = calculatePayouts(activeGiveaway.amount, winnerCount);
const winnersSummary = winners.map((winner, index) => {
const payout = payouts[index] || 0;
return `[color=#5DE2E7][b]#${index + 1}[/b][/color] ` +
`[color=#d85e27][b]${sanitizeNick(winner.author)}[/b][/color] - ` +
`[color=green][b]${winner.guess}[/b][/color] ` +
`([b]różnica ${winner.gap}[/b]) - ` +
`[color=#ffc00a][b]${formatBon(payout)} BON[/b][/color]`;
}).join(", ");
const hasTie = winners.some((winner, index) => index > 0 && winner.gap === winners[index - 1].gap);
const sponsorTotal = Object.values(activeGiveaway.sponsorContribs || {})
.reduce((sum, value) => sum + value, 0);
const hostContributionText = cleanPotString(activeGiveaway.hostContribution || 0);
const sponsorTotalText = cleanPotString(sponsorTotal);
const summaryMessage = CHAT_COPY.finishSummary({
winningNumber: activeGiveaway.winningNumber,
winnersSummary,
hostContributionText,
sponsorTotalText,
sponsors,
hasTie
});
sendMessage(summaryMessage);
const winnersToVerify = [];
const payoutsToVerify = [];
for (let i = 0; i < winners.length; i++) {
const winner = winners[i];
const payout = payouts[i] || 0;
if (payout <= 0) {
continue;
}
if (normalizeUser(winner.author) === normalizeUser(activeGiveaway.host)) {
continue;
}
winnersToVerify.push(winner.author);
payoutsToVerify.push(payout);
giftBon(winner.author, payout, `Gratulacje! Zajales miejsce ${i + 1} w BON giveaway.`);
}
if (winnersToVerify.length > 0) {
void verifyWinnerGifts(winnersToVerify, payoutsToVerify, activeGiveaway.host);
}
finishGiveawayCleanup(activeGiveaway);
}
function finishGiveawayCleanup(activeGiveaway) {
if (activeGiveaway && activeGiveaway.countdownTimerID) {
clearInterval(activeGiveaway.countdownTimerID);
activeGiveaway.countdownTimerID = null;
}
if (entriesInterval) {
clearInterval(entriesInterval);
entriesInterval = null;
}
if (sponsorsInterval) {
clearInterval(sponsorsInterval);
sponsorsInterval = null;
}
window.onbeforeunload = null;
if (activeGiveaway) {
activeGiveaway.ended = true;
activeGiveaway.ending = false;
}
setRunningState(false);
startButton.disabled = false;
sponsorDigestBuffer = [];
sponsorDigestWindowStartAt = 0;
resetV2GiftHistoryState();
updateCoinHeader(readHostBalance());
entriesWrapper.hidden = true;
countdownHeader.hidden = true;
countdownHeader.textContent = "";
giveawayData = null;
clearSavedState();
hideRestoreNotice();
numberEntries = new Map();
numberTakenBy = new Map();
fancyNames = new Map();
updateEntries();
updateStatsPanel(null);
resetApiStatus();
}
function calculatePayouts(totalAmount, winnerCount) {
totalAmount = Math.floor(Number(totalAmount) || 0);
winnerCount = Math.floor(Number(winnerCount) || 0);
if (winnerCount <= 0) return [];
if (winnerCount === 1) return [totalAmount];
const weights = [];
for (let i = winnerCount; i >= 1; i--) {
weights.push(i);
}
const totalWeight = weights.reduce((sum, weight) => sum + weight, 0);
const payouts = weights.map(weight => Math.floor((totalAmount * weight) / totalWeight));
let distributed = payouts.reduce((sum, amount) => sum + amount, 0);
let remainder = totalAmount - distributed;
if (remainder > 0) {
payouts[0] += remainder;
}
return payouts;
}
async function verifyWinnerGifts(winners, payouts, hostName) {
const expected = new Map();
for (let i = 0; i < winners.length; i++) {
const name = winners[i];
const amount = payouts[i];
if (!name || !amount) continue;
expected.set(normalizeUser(name), { name, amount });
}
if (expected.size === 0) {
return;
}
for (let attempt = 0; attempt < GENERAL_SETTINGS.verifyAttempts; attempt++) {
try {
const response = await fetch(`${window.location.origin}/api/chat/messages/${getSponsorRoomId()}`, {
method: 'GET',
credentials: 'same-origin',
});
if (response.ok) {
const data = await response.json();
const messages = Array.isArray(data.data) ? data.data : [];
for (const msg of messages) {
const gift = parseGiftMessage(msg.message);
if (!gift) continue;
if (normalizeUser(gift.gifter) !== normalizeUser(hostName)) continue;
const expectedGift = expected.get(normalizeUser(gift.recipient));
if (!expectedGift) continue;
if (Math.round(gift.amount) === Math.round(expectedGift.amount)) {
expected.delete(normalizeUser(gift.recipient));
}
}
if (expected.size === 0) {
return;
}
}
} catch (error) {
console.error("Gift verification failed:", error);
}
await wait(GENERAL_SETTINGS.verifyDelayMs);
}
if (expected.size > 0 && DEBUG_SETTINGS.logChatMessages) {
const missing = Array.from(expected.values())
.map(item => `${item.name} (${formatBon(item.amount)} BON)`)
.join(", ");
console.warn("Nie udało się potwierdzic giftów po API dla:", missing);
}
}
function giftBon(recipient, amount, messageText) {
const safeRecipient = String(recipient || "").trim();
const safeAmount = Math.max(1, Math.floor(Number(amount) || 0));
const safeMessage = String(messageText || "").trim();
if (!safeRecipient || !safeAmount) {
return;
}
const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || '';
const giftUrl = `${window.location.origin}/users/${encodeURIComponent(safeRecipient)}/gifts`;
if (!csrfToken) {
sendMessage(`/gift ${safeRecipient} ${safeAmount} ${safeMessage}`);
return;
}
const formData = new FormData();
formData.append("_token", csrfToken);
formData.append("recipient_username", safeRecipient);
formData.append("bon", String(safeAmount));
formData.append("message", safeMessage);
fetch(giftUrl, {
method: "POST",
credentials: "same-origin",
body: formData
}).then(response => {
if (!response.ok) {
sendMessage(`/gift ${safeRecipient} ${safeAmount} ${safeMessage}`);
}
}).catch(() => {
sendMessage(`/gift ${safeRecipient} ${safeAmount} ${safeMessage}`);
});
}
function sendReminderMessage(activeGiveaway) {
if (isV2Mode(activeGiveaway)) {
sendMessage(getV2Copy().reminder({
amount: activeGiveaway.amount,
timeLeftMs: activeGiveaway.timeLeft * 1000,
winnersNum: activeGiveaway.winnersNum,
startNum: activeGiveaway.startNum,
endNum: activeGiveaway.endNum,
minEntryFee: getCurrentV2MinEntryFee(activeGiveaway),
host: activeGiveaway.host,
dynamicMinEnabled: Boolean(activeGiveaway.dynamicMinEnabled),
dynamicMinStep: Math.max(1, Math.floor(activeGiveaway.dynamicMinBase || activeGiveaway.minEntryFee || getCurrentV2MinEntryFee(activeGiveaway)))
}));
return;
}
sendMessage(CHAT_COPY.reminder({
amount: activeGiveaway.amount,
timeLeftMs: activeGiveaway.timeLeft * 1000,
winnersNum: activeGiveaway.winnersNum,
startNum: activeGiveaway.startNum,
endNum: activeGiveaway.endNum,
sponsorMessage: getSponsorMessage(activeGiveaway.host)
}));
}
function countdownTimer(display, activeGiveaway) {
display.hidden = false;
const timerID = setInterval(() => {
if (activeGiveaway.ending) {
return;
}
const secondsLeft = Math.max(0, Math.ceil((activeGiveaway.endTs - Date.now()) / 1000));
activeGiveaway.timeLeft = secondsLeft;
const minutes = Math.floor(secondsLeft / 60);
const seconds = secondsLeft % 60;
display.textContent =
String(minutes).padStart(2, "0") + ":" +
String(seconds).padStart(2, "0");
if (secondsLeft <= 0) {
void endGiveaway(activeGiveaway);
return;
}
if (activeGiveaway.totalEntries === numberEntries.size) {
sendMessage(CHAT_COPY.earlyFinish(activeGiveaway.totalEntries, activeGiveaway.timeLeft * 1000));
void endGiveaway(activeGiveaway);
return;
}
if (
activeGiveaway.reminderFreqSec > 0 &&
secondsLeft > 0 &&
secondsLeft % activeGiveaway.reminderFreqSec === 0
) {
sendReminderMessage(activeGiveaway);
}
}, 1000);
return timerID;
}
function normalizeForEmojiMatch(input) {
return String(input || "")
.toLowerCase()
.normalize("NFD")
.replace(/[\u0300-\u036f]/g, "")
.replace(/\s+/g, " ")
.trim();
}
function includesAnyKeyword(haystack, keywords) {
for (const keyword of keywords) {
if (haystack.includes(keyword)) {
return true;
}
}
return false;
}
function includesAllKeywords(haystack, keywords) {
for (const keyword of keywords) {
if (!haystack.includes(keyword)) {
return false;
}
}
return true;
}
const CHAT_EMOJI = Object.freeze({
party: "\uD83C\uDF89",
megaphone: "\uD83D\uDCE2",
hourglass: "\u23F3",
chart: "\uD83D\uDCCA",
trophy: "\uD83C\uDFC6",
money: "\uD83D\uDCB0",
moneyPlus: "\uD83D\uDCB0\u2795",
moneyMinus: "\uD83D\uDCB0\u2796",
handshake: "\uD83E\uDD1D",
receipt: "\uD83E\uDDFE",
numbers: "\uD83D\uDD22",
noEntry: "\uD83D\uDEAB",
warning: "\u26A0\uFE0F",
check: "\u2705",
exclaim: "\u2757",
dice: "\uD83C\uDFB2",
sparkles: "\u2728",
crown: "\uD83D\uDC51",
partyFace: "\uD83E\uDD73",
sad: "\uD83D\uDE14",
pause: "\u23F8\uFE0F",
play: "\u25B6\uFE0F",
scroll: "\uD83D\uDCDC",
package: "\uD83D\uDCE6",
compass: "\uD83E\uDDED",
rocket: "\uD83D\uDE80",
alarm: "\u23F0",
lock: "\uD83D\uDD12",
id: "\uD83C\uDD94",
seedling: "\uD83C\uDF31",
up: "\u2B06\uFE0F",
down: "\u2B07\uFE0F",
gear: "\u2699\uFE0F",
sleep: "\uD83D\uDCA4",
moneyWing: "\uD83D\uDCB8",
construction: "\uD83D\uDEA7",
skull: "\uD83D\uDC80"
});
function getTaggedEmoji(message) {
const match = String(message || "").match(/^\[([^\]]+)\]/i);
if (!match) return "";
const tag = (match[1] || "").trim().toUpperCase();
const map = {
"LOOT": CHAT_EMOJI.package,
"EXPLORE": CHAT_EMOJI.compass,
"VICTORY": CHAT_EMOJI.trophy,
"STATUS": CHAT_EMOJI.chart,
"BOOT": CHAT_EMOJI.rocket,
"TIMER": CHAT_EMOJI.hourglass,
"QUEST": CHAT_EMOJI.compass,
"CLOCK": CHAT_EMOJI.alarm,
"LOCK": CHAT_EMOJI.lock,
"PAUSE": CHAT_EMOJI.pause,
"CAP": CHAT_EMOJI.noEntry,
"ID": CHAT_EMOJI.id,
"SEED": CHAT_EMOJI.seedling,
"CRIT": CHAT_EMOJI.sparkles,
"MAP": CHAT_EMOJI.compass,
"UPGRADE": CHAT_EMOJI.up,
"ROLLBACK": CHAT_EMOJI.down,
"CONFIG": CHAT_EMOJI.gear,
"AFK": CHAT_EMOJI.sleep,
"ECONOMY": CHAT_EMOJI.moneyWing,
"GUILD": CHAT_EMOJI.handshake,
"BANK": CHAT_EMOJI.money,
"BANK++": CHAT_EMOJI.money,
"ZONE": CHAT_EMOJI.numbers,
"BOUNDARY": CHAT_EMOJI.construction,
"COLLISION": CHAT_EMOJI.warning,
"JOIN": CHAT_EMOJI.check,
"GAME OVER": CHAT_EMOJI.skull
};
return map[tag] || "";
}
function decorateChatMessage(messageStr) {
const raw = String(messageStr || "");
const trimmed = raw.trim();
if (!trimmed) return raw;
if (trimmed.startsWith("/")) return raw;
const knownPrefixes = [
CHAT_EMOJI.party,
CHAT_EMOJI.megaphone,
CHAT_EMOJI.hourglass,
CHAT_EMOJI.chart,
CHAT_EMOJI.trophy,
CHAT_EMOJI.money,
CHAT_EMOJI.moneyPlus,
CHAT_EMOJI.moneyMinus,
CHAT_EMOJI.handshake,
CHAT_EMOJI.receipt,
CHAT_EMOJI.numbers,
CHAT_EMOJI.noEntry,
CHAT_EMOJI.warning,
CHAT_EMOJI.check,
CHAT_EMOJI.exclaim,
CHAT_EMOJI.dice,
CHAT_EMOJI.sparkles,
CHAT_EMOJI.crown,
CHAT_EMOJI.partyFace,
CHAT_EMOJI.sad,
CHAT_EMOJI.pause,
CHAT_EMOJI.play,
CHAT_EMOJI.scroll,
CHAT_EMOJI.package,
CHAT_EMOJI.compass,
CHAT_EMOJI.rocket,
CHAT_EMOJI.alarm,
CHAT_EMOJI.lock,
CHAT_EMOJI.id,
CHAT_EMOJI.seedling,
CHAT_EMOJI.up,
CHAT_EMOJI.down,
CHAT_EMOJI.gear,
CHAT_EMOJI.sleep,
CHAT_EMOJI.moneyWing,
CHAT_EMOJI.construction,
CHAT_EMOJI.skull
];
const alreadyDecorated = knownPrefixes.some((emoji) => trimmed.startsWith(`${emoji} `));
if (alreadyDecorated) return raw;
const taggedEmoji = getTaggedEmoji(trimmed);
if (taggedEmoji) {
return `${taggedEmoji} ${raw}`;
}
const normalized = normalizeForEmojiMatch(trimmed.replace(/\[[^\]]*\]/g, " "));
const rules = [
{ emoji: CHAT_EMOJI.party, any: ["organizuje giveaway", "startujemy giveaway", "odpalam giveaway", "wystartowal", "wystartowala"] },
{ emoji: CHAT_EMOJI.megaphone, any: ["trwa giveaway", "giveaway trwa", "przypominajka dla spoznialskich", "przypomnienie", "event aktywny"] },
{ emoji: CHAT_EMOJI.chart, any: ["status giveaway", "status w pigulce", "status trybu", "raport dla wytrwalych", "[status]"] },
{ emoji: CHAT_EMOJI.hourglass, any: ["pozostaly czas giveaway", "pozostaly czas:", "na zegarze zostalo jeszcze", "do konca sesji"] },
{ emoji: CHAT_EMOJI.moneyPlus, any: ["host dodal", "host dodaje", "host dorzuca", "host dorzucil"] },
{ emoji: CHAT_EMOJI.moneyMinus, any: ["host odjal", "host usuwa", "host zdejmuje", "host skrocil o"] },
{ emoji: CHAT_EMOJI.pause, any: ["wstrzymal przyjmowanie nowych zgloszen", "zgloszenia sa juz wstrzymane", "zatrzymal nowe zgloszenia"] },
{ emoji: CHAT_EMOJI.play, any: ["wznowil przyjmowanie nowych zgloszen", "zgloszenia sa juz aktywne", "wznowil zgloszenia"] },
{ emoji: CHAT_EMOJI.scroll, any: ["dostepne komendy", "komendy:"] },
{ emoji: CHAT_EMOJI.money, any: ["aktualna pula giveaway", "laczna pula wynosi teraz", "aktualna pula:", "pula rosnie do", "jak dodac bon", "jak dorzucic bon", "pula rosnie do"] },
{ emoji: CHAT_EMOJI.numbers, any: ["zakres numerow"] },
{ emoji: CHAT_EMOJI.receipt, any: ["zgloszenia ("] },
{ emoji: CHAT_EMOJI.handshake, any: ["sponsorzy tego giveaway", "sponsorzy wlasnie dorzucili", "sponsorzy dorzucili", "sponsorzy (", "padl zrzut"] },
{ emoji: CHAT_EMOJI.dice, any: ["wolne numery", "szczesliwy numer giveaway", "aktualny szczesliwy numer"] },
{ emoji: CHAT_EMOJI.check, all: ["wszystkie", "miejsca zostaly zajete"] },
{ emoji: CHAT_EMOJI.trophy, any: ["zwycieski numer to"] },
{ emoji: CHAT_EMOJI.crown, any: ["host znalazl sie wsrod zwyciezcow"] },
{ emoji: CHAT_EMOJI.sad, any: ["niestety nikt nie wzial udzialu", "zero zgloszen", "brak graczy"] },
{ emoji: CHAT_EMOJI.warning, any: ["spam wykryty"] },
{ emoji: CHAT_EMOJI.noEntry, any: ["przepraszam", "nie ma juz wolnych numerow", "wolnych numerow juz brak"] }
];
for (const rule of rules) {
if (rule.all && includesAllKeywords(normalized, rule.all)) {
return `${rule.emoji} ${raw}`;
}
if (rule.any && includesAnyKeyword(normalized, rule.any)) {
return `${rule.emoji} ${raw}`;
}
}
return raw;
}
function applySarkazmFlavor(messageStr) {
const base = String(messageStr || "").trim();
if (!base) return String(messageStr || "");
return base;
}
function sendMessage(messageStr) {
const effectiveProfile = getEffectiveChatProfile(activeChatProfile, activeGameMode);
const sourceMessage = (effectiveProfile === "sarkazm") ? applySarkazmFlavor(messageStr) : messageStr;
const outgoingMessage = decorateChatMessage(sourceMessage);
if (!DEBUG_SETTINGS.disableChatOutput) {
if (!chatbox) {
chatbox = document.querySelector("#chatbox__messages-create");
}
if (!chatbox) {
console.error("Nie znaleziono pola czatu.");
return;
}
const nativeSetter =
Object.getOwnPropertyDescriptor(window.HTMLTextAreaElement.prototype, "value")?.set ||
Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, "value")?.set;
if (nativeSetter) {
nativeSetter.call(chatbox, outgoingMessage);
} else {
chatbox.value = outgoingMessage;
}
chatbox.dispatchEvent(new Event("input", { bubbles: true }));
chatbox.dispatchEvent(new Event("change", { bubbles: true }));
const enterOptions = {
key: "Enter",
code: "Enter",
keyCode: 13,
which: 13,
bubbles: true,
cancelable: true
};
chatbox.dispatchEvent(new KeyboardEvent("keydown", enterOptions));
chatbox.dispatchEvent(new KeyboardEvent("keypress", enterOptions));
chatbox.dispatchEvent(new KeyboardEvent("keyup", enterOptions));
}
if (DEBUG_SETTINGS.logChatMessages) {
console.log(outgoingMessage);
}
}
function getLuckyNumber(activeGiveaway) {
let rangeStart = activeGiveaway.startNum;
let rangeEnd = activeGiveaway.endNum;
let numbers = Array.from(numberEntries.values()).sort((a, b) => a - b);
numbers.push(rangeEnd + 1);
let bestGap = 0;
let lucky = rangeStart;
let pastNum = rangeStart - 1;
for (let i = 0; i < numbers.length; i++) {
const currentNum = numbers[i];
const gap = currentNum - pastNum;
if (gap > bestGap) {
lucky = Math.floor((gap - 1) / 2) + pastNum + 1;
bestGap = gap;
}
pastNum = currentNum;
}
return lucky;
}
function cleanPotString(giveawayPotAmount) {
if (giveawayPotAmount % 1 === 0) {
return formatBon(giveawayPotAmount);
}
return Number(giveawayPotAmount).toFixed(2);
}
function parseTime(timeInMs) {
const totalSeconds = Math.max(0, Math.floor(timeInMs / 1000));
const hours = Math.floor(totalSeconds / 3600);
const minutes = Math.floor((totalSeconds % 3600) / 60);
const seconds = Math.floor(totalSeconds % 60);
let timeString = ``;
if (hours > 0) {
timeString += `${hours} godz.`;
}
if (minutes > 0) {
if (timeString !== ``) {
timeString += `, `;
}
timeString += `${minutes} min.`;
}
if (seconds > 0 || timeString === ``) {
if (timeString !== ``) {
timeString += `, `;
}
timeString += `${seconds} sek.`;
}
return timeString;
}
function resetGiveaway(skipConfirm = false) {
const hasAnythingToReset =
!!giveawayData ||
numberEntries.size > 0 ||
sponsorDigestBuffer.length > 0 ||
(countdownHeader && countdownHeader.textContent) ||
(coinInput && String(coinInput.value || "").trim() !== "");
if (!skipConfirm && hasAnythingToReset) {
if (!window.confirm("Na pewno zresetowac giveaway?")) {
return;
}
}
if (giveawayData && giveawayData.countdownTimerID) {
clearInterval(giveawayData.countdownTimerID);
}
if (entriesInterval) {
clearInterval(entriesInterval);
entriesInterval = null;
}
if (sponsorsInterval) {
clearInterval(sponsorsInterval);
sponsorsInterval = null;
}
window.onbeforeunload = null;
closeCommandsMenu();
hideRestoreNotice();
clearSavedState();
processedGiftMessages = new Set();
processedChatMessages = new Set();
giveawayStartTime = null;
giveawayData = null;
sponsorDigestBuffer = [];
sponsorDigestWindowStartAt = 0;
resetV2GiftHistoryState();
numberEntries = new Map();
numberTakenBy = new Map();
fancyNames = new Map();
entriesWrapper.hidden = true;
countdownHeader.hidden = true;
countdownHeader.textContent = "";
setRunningState(false);
coinInput.disabled = false;
startInput.disabled = false;
endInput.disabled = false;
timerInput.disabled = false;
reminderInput.disabled = false;
winnersInput.disabled = false;
giveawayForm.reset();
timerInput.value = "15";
reminderInput.value = "2";
winnersInput.value = "1";
if (entryFeeInput) {
entryFeeInput.value = "50";
}
startInput.value = "1";
endInput.value = "50";
updateEntries();
updateCoinHeader(readHostBalance());
updateStatsPanel(null);
resetApiStatus();
applyChatProfile(activeChatProfile, false);
applyGameMode(activeGameMode, false, true);
userCooldown.clear();
userCommandLog.clear();
userLastActionAt.clear();
userLastCommandAt.clear();
userSpamStrikes.clear();
userFeedbackCooldown.clear();
}
function getSortedSponsors(activeGiveaway) {
return Object.entries(activeGiveaway.sponsorContribs || {})
.map(([name, amount]) => ({ name, amount }))
.sort((a, b) => b.amount - a.amount || a.name.localeCompare(b.name));
}
function getFreeNumbers(activeGiveaway) {
const freeNumbers = [];
for (let number = activeGiveaway.startNum; number <= activeGiveaway.endNum; number++) {
if (!numberTakenBy.has(number)) {
freeNumbers.push(number);
}
}
return freeNumbers;
}
function getFreeNumberSample(activeGiveaway, limit = 5) {
if (!activeGiveaway) return [];
const startNum = activeGiveaway.startNum;
const endNum = activeGiveaway.endNum;
const totalSlots = endNum - startNum + 1;
if (totalSlots <= 0) return [];
const taken = new Set(numberEntries.values());
if (totalSlots > 100000 && taken.size / totalSlots < 0.01) {
const sample = new Set();
let attempts = 0;
const maxAttempts = 1000;
while (sample.size < limit && attempts < maxAttempts) {
attempts++;
const candidate = Math.floor(Math.random() * totalSlots) + startNum;
if (!taken.has(candidate)) {
sample.add(candidate);
}
}
return [...sample].sort((a, b) => a - b);
}
const freeNumbers = [];
for (let number = startNum; number <= endNum; number++) {
if (!taken.has(number)) {
freeNumbers.push(number);
}
}
if (!freeNumbers.length) return [];
const actualSampleSize = Math.min(limit, freeNumbers.length);
for (let i = 0; i < actualSampleSize; i++) {
const j = i + Math.floor(Math.random() * (freeNumbers.length - i));
[freeNumbers[i], freeNumbers[j]] = [freeNumbers[j], freeNumbers[i]];
}
return freeNumbers.slice(0, actualSampleSize).sort((a, b) => a - b);
}
function formatFreeNumberSuggestion(activeGiveaway) {
const sample = getFreeNumberSample(activeGiveaway, 5);
if (sample.length === 0) {
return "";
}
return ` Wolne przyklady: [b][color=green]${sample.join(", ")}[/color][/b].`;
}
function updateCoinHeader(amount) {
if (!coinHeader) return;
coinHeader.textContent = `${formatBon(amount)} BON`;
coinHeader.prepend(goldCoins.cloneNode(false));
}
function readHostBalance() {
const points = document.getElementsByClassName("ratio-bar__points")[0];
if (!points) return 0;
const rawText = points.textContent || "";
const digits = rawText.replace(/[^\d]/g, '');
const parsed = parseInt(digits, 10);
return Number.isNaN(parsed) ? 0 : parsed;
}
function getLoggedInUsername() {
const navLink =
document.querySelector('.top-nav__username a[href*="/users/"]') ||
document.querySelector('.top-nav__username a');
if (navLink) {
const linkText = String(navLink.textContent || "")
.replace(/[\u200B\u200C\u200D\uFEFF]/g, "")
.trim();
if (linkText) {
return linkText;
}
const href = navLink.getAttribute("href") || navLink.href || "";
const match = href.match(/\/users\/([^/?#]+)/i);
if (match && match[1]) {
try {
return decodeURIComponent(match[1]);
} catch (error) {
return match[1];
}
}
}
const topNavText = String(document.querySelector('.top-nav__username')?.textContent || "")
.replace(/[\u200B\u200C\u200D\uFEFF]/g, "")
.trim();
return topNavText || "";
}
function normalizeMessageContent(value) {
return String(value || "")
.replace(/<[^>]*>/g, "")
.replace(/[\u200B\u200C\u200D\uFEFF]/g, "")
.trim();
}
function normalizeUser(name) {
return String(name || "")
.replace(/[\u200B\u200C\u200D\uFEFF]/g, "")
.trim()
.toLowerCase();
}
function copyableNick(nick) {
return String(nick || "")
.replace(/[\u200B\u200C\u200D\uFEFF]/g, "")
.trim();
}
function sanitizeNick(nick) {
if (typeof nick !== "string" || nick.length < 2) return nick;
return nick[0] + "\u200B" + nick.slice(1);
}
function escapeHTML(value) {
return String(value || "")
.replace(/&/g, "&")
.replace(/</g, "<")
.replace(/>/g, ">")
.replace(/"/g, """)
.replace(/'/g, "'");
}
function parsePositiveInt(value) {
const cleaned = String(value || "").replace(/[^\d]/g, "");
const parsed = parseInt(cleaned, 10);
return Number.isNaN(parsed) ? 0 : parsed;
}
function formatBon(value) {
const numeric = Math.floor(Number(value) || 0);
return numeric.toLocaleString('en-GB');
}
function getRandomInt(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function wait(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function canSendUserFeedback(author, bucket, cooldownMs = ENTRY_FEEDBACK_COOLDOWN_MS) {
const now = Date.now();
const authorKey = String(author || "").toLowerCase();
if (!authorKey) return true;
const key = `${authorKey}::${bucket}`;
const last = userFeedbackCooldown.get(key) || 0;
if (now - last < cooldownMs) {
return false;
}
userFeedbackCooldown.set(key, now);
return true;
}
function applyCooldown(author, opts = {}) {
const now = Date.now();
const rawAuthor = String(author || "");
const authorKey = rawAuthor.toLowerCase();
if (!authorKey) return false;
const lockoutExpires = userCooldown.get(authorKey) || 0;
if (now < lockoutExpires) {
return true;
}
const log = (userCommandLog.get(authorKey) || []).filter(ts => now - ts < COMMAND_WINDOW_MS);
log.push(now);
userCommandLog.set(authorKey, log);
const lastAny = userLastActionAt.get(authorKey) || 0;
const tooFast = (now - lastAny) < MIN_ACTION_GAP_MS;
userLastActionAt.set(authorKey, now);
let repeatBlocked = false;
const cmd = String(opts.command || "").trim().toLowerCase();
if (cmd) {
const cooldownMs = Number(REPEAT_COMMAND_COOLDOWNS_MS[cmd]) || 0;
if (cooldownMs > 0) {
const key = `${authorKey}::${cmd}`;
const lastCmd = userLastCommandAt.get(key) || 0;
repeatBlocked = (now - lastCmd) < cooldownMs;
userLastCommandAt.set(key, now);
}
}
if (log.length > MAX_COMMANDS_PER_WINDOW) {
const excess = log.length - MAX_COMMANDS_PER_WINDOW;
const prev = userSpamStrikes.get(authorKey) || { count: 0, lastAt: 0 };
if (now - prev.lastAt < STRIKE_WINDOW_MS) {
prev.count += 1;
} else {
prev.count = 1;
}
prev.lastAt = now;
userSpamStrikes.set(authorKey, prev);
const multiplier = Math.min(
MAX_STRIKE_MULTIPLIER,
Math.pow(2, Math.max(0, prev.count - 1))
);
const penaltySec = Math.max(1, Math.round(BASE_PENALTY_SECONDS * excess * multiplier));
userCooldown.set(authorKey, now + penaltySec * 1000);
userCommandLog.delete(authorKey);
if (canSendUserFeedback(rawAuthor, "spam-lockout", 60000)) {
sendMessage(CHAT_COPY.spamDetected(rawAuthor, penaltySec));
}
return true;
}
return tooFast || repeatBlocked;
}
function maybeFlushSponsorDigest(activeGiveaway, force = false) {
if (!activeGiveaway || !sponsorDigestBuffer.length) return;
if (SPONSOR_ANNOUNCE.mode === "off") {
sponsorDigestBuffer = [];
sponsorDigestWindowStartAt = 0;
return;
}
const now = Date.now();
if (!sponsorDigestWindowStartAt) {
sponsorDigestWindowStartAt = now;
}
if (SPONSOR_ANNOUNCE.mode === "immediate") {
flushSponsorDigest(activeGiveaway, true);
return;
}
const deltaTotalNum = sponsorDigestBuffer.reduce((sum, item) => sum + (Number(item.amount) || 0), 0);
const hasBigSingle = sponsorDigestBuffer.some(item => (Number(item.amount) || 0) >= SPONSOR_ANNOUNCE.immediateSingleMin);
const tooManyEvents = sponsorDigestBuffer.length >= SPONSOR_ANNOUNCE.maxPendingEvents;
const hitMinTotal = deltaTotalNum >= SPONSOR_ANNOUNCE.flushMinTotal;
const hitTime = (now - sponsorDigestWindowStartAt) >= SPONSOR_ANNOUNCE.digestMs;
if (force || hasBigSingle || tooManyEvents || hitMinTotal || hitTime) {
flushSponsorDigest(activeGiveaway, true);
}
}
function flushSponsorDigest(activeGiveaway, force = false) {
if (!activeGiveaway || !sponsorDigestBuffer.length) return;
if (!force && SPONSOR_ANNOUNCE.mode !== "immediate" && SPONSOR_ANNOUNCE.mode !== "digest") return;
const grouped = sponsorDigestBuffer.reduce((acc, item) => {
const name = String(item.gifter || "").trim();
const amount = Number(item.amount) || 0;
if (!name || amount <= 0) return acc;
acc[name] = (acc[name] || 0) + amount;
return acc;
}, {});
const entries = Object.entries(grouped)
.map(([name, amount]) => ({ name, amount }))
.filter(item => item.amount > 0)
.sort((a, b) => b.amount - a.amount);
const sponsorCount = entries.length;
const deltaTotalNum = entries.reduce((sum, item) => sum + item.amount, 0);
if (!sponsorCount || !deltaTotalNum) {
sponsorDigestBuffer = [];
sponsorDigestWindowStartAt = 0;
return;
}
const shown = [];
const topN = Math.max(0, Number(SPONSOR_ANNOUNCE.showTopN) || 0);
const minPerUser = Math.max(0, Number(SPONSOR_ANNOUNCE.showMinPerUser) || 0);
for (const item of entries) {
if (shown.length >= topN) break;
if (sponsorCount > 1 && item.amount < minPerUser) continue;
shown.push(item);
}
const parts = shown.map(item =>
`[color=green][b]${sanitizeNick(item.name)}[/b][/color] ` +
`([color=#ffc00a][b]${cleanPotString(item.amount)}[/b][/color])`
);
const othersCount = Math.max(0, sponsorCount - shown.length);
sendMessage(CHAT_COPY.sponsorDigest({
deltaTotalNum,
sponsorCount,
parts,
othersCount,
totalPot: activeGiveaway.amount
}));
sponsorDigestBuffer = [];
sponsorDigestWindowStartAt = 0;
}
function recalculateReminderFrequency(activeGiveaway) {
if (!activeGiveaway) return;
if (!activeGiveaway.reminderNum || activeGiveaway.reminderNum <= 0) {
activeGiveaway.reminderFreqSec = 0;
return;
}
activeGiveaway.reminderFreqSec = Math.max(
1,
Math.round(activeGiveaway.timeLeft / (activeGiveaway.reminderNum + 1))
);
}
})();