Karuta Vote Automation

Automatically votes for Karuta on top.gg and tracks voting history

Size

15.9 KB

Version

1.0.1

Created

Nov 21, 2025

Updated

22 days ago

1// ==UserScript==
2// @name		Karuta Vote Automation
3// @description		Automatically votes for Karuta on top.gg and tracks voting history
4// @version		1.0.1
5// @match		https://*.top.gg/*
6// @icon		https://top.gg/favicon.png
7// @grant		GM.getValue
8// @grant		GM.setValue
9// @grant		GM.notification
10// ==/UserScript==
11(function() {
12    'use strict';
13
14    console.log('Karuta Vote Automation Extension loaded');
15
16    // Configuration
17    const CONFIG = {
18        AUTO_VOTE_ENABLED: 'karuta_auto_vote_enabled',
19        LAST_VOTE_TIME: 'karuta_last_vote_time',
20        VOTE_COUNT: 'karuta_vote_count',
21        VOTE_COOLDOWN: 12 * 60 * 60 * 1000, // 12 hours in milliseconds
22        CHECK_INTERVAL: 30000 // Check every 30 seconds
23    };
24
25    // Utility function to debounce
26    function debounce(func, wait) {
27        let timeout;
28        return function executedFunction(...args) {
29            const later = () => {
30                clearTimeout(timeout);
31                func(...args);
32            };
33            clearTimeout(timeout);
34            timeout = setTimeout(later, wait);
35        };
36    }
37
38    // Main automation class
39    class KarutaVoteAutomation {
40        constructor() {
41            this.isVoting = false;
42            this.checkTimer = null;
43            this.init();
44        }
45
46        async init() {
47            console.log('Initializing Karuta Vote Automation...');
48            
49            // Load settings
50            this.autoVoteEnabled = await GM.getValue(CONFIG.AUTO_VOTE_ENABLED, true);
51            this.lastVoteTime = await GM.getValue(CONFIG.LAST_VOTE_TIME, 0);
52            this.voteCount = await GM.getValue(CONFIG.VOTE_COUNT, 0);
53
54            // Create UI controls
55            this.createControlPanel();
56
57            // Start monitoring
58            this.startMonitoring();
59
60            console.log(`Auto-vote: ${this.autoVoteEnabled ? 'Enabled' : 'Disabled'}`);
61            console.log(`Total votes: ${this.voteCount}`);
62            console.log(`Last vote: ${this.lastVoteTime ? new Date(this.lastVoteTime).toLocaleString() : 'Never'}`);
63        }
64
65        createControlPanel() {
66            // Add styles
67            TM_addStyle(`
68                #karuta-vote-panel {
69                    position: fixed;
70                    top: 20px;
71                    right: 20px;
72                    background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
73                    color: white;
74                    padding: 16px;
75                    border-radius: 12px;
76                    box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
77                    z-index: 10000;
78                    font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
79                    min-width: 280px;
80                    backdrop-filter: blur(10px);
81                }
82                #karuta-vote-panel h3 {
83                    margin: 0 0 12px 0;
84                    font-size: 18px;
85                    font-weight: 600;
86                    display: flex;
87                    align-items: center;
88                    gap: 8px;
89                }
90                #karuta-vote-panel .status-row {
91                    display: flex;
92                    justify-content: space-between;
93                    align-items: center;
94                    margin: 8px 0;
95                    font-size: 14px;
96                }
97                #karuta-vote-panel .status-label {
98                    opacity: 0.9;
99                }
100                #karuta-vote-panel .status-value {
101                    font-weight: 600;
102                    background: rgba(255, 255, 255, 0.2);
103                    padding: 4px 8px;
104                    border-radius: 6px;
105                }
106                #karuta-vote-panel button {
107                    width: 100%;
108                    padding: 10px;
109                    margin-top: 12px;
110                    border: none;
111                    border-radius: 8px;
112                    font-size: 14px;
113                    font-weight: 600;
114                    cursor: pointer;
115                    transition: all 0.3s ease;
116                    background: rgba(255, 255, 255, 0.2);
117                    color: white;
118                }
119                #karuta-vote-panel button:hover {
120                    background: rgba(255, 255, 255, 0.3);
121                    transform: translateY(-2px);
122                }
123                #karuta-vote-panel button:active {
124                    transform: translateY(0);
125                }
126                #karuta-vote-panel .toggle-btn {
127                    background: rgba(255, 255, 255, 0.9);
128                    color: #667eea;
129                }
130                #karuta-vote-panel .toggle-btn.disabled {
131                    background: rgba(255, 255, 255, 0.3);
132                    color: rgba(255, 255, 255, 0.7);
133                }
134                #karuta-vote-panel .vote-now-btn {
135                    background: #10b981;
136                }
137                #karuta-vote-panel .vote-now-btn:hover {
138                    background: #059669;
139                }
140                #karuta-vote-panel .vote-now-btn:disabled {
141                    background: rgba(255, 255, 255, 0.2);
142                    cursor: not-allowed;
143                    opacity: 0.5;
144                }
145                #karuta-vote-panel .timer {
146                    font-size: 12px;
147                    text-align: center;
148                    margin-top: 8px;
149                    opacity: 0.8;
150                }
151                .karuta-icon {
152                    display: inline-block;
153                    width: 20px;
154                    height: 20px;
155                }
156            `);
157
158            // Create panel
159            const panel = document.createElement('div');
160            panel.id = 'karuta-vote-panel';
161            panel.innerHTML = `
162                <h3>
163                    <span class="karuta-icon">🎴</span>
164                    Karuta Vote Bot
165                </h3>
166                <div class="status-row">
167                    <span class="status-label">Total Votes:</span>
168                    <span class="status-value" id="karuta-vote-count">${this.voteCount}</span>
169                </div>
170                <div class="status-row">
171                    <span class="status-label">Status:</span>
172                    <span class="status-value" id="karuta-vote-status">Checking...</span>
173                </div>
174                <div class="status-row">
175                    <span class="status-label">Next Vote:</span>
176                    <span class="status-value" id="karuta-next-vote">Calculating...</span>
177                </div>
178                <button class="toggle-btn ${this.autoVoteEnabled ? '' : 'disabled'}" id="karuta-toggle-auto">
179                    Auto-Vote: ${this.autoVoteEnabled ? 'ON' : 'OFF'}
180                </button>
181                <button class="vote-now-btn" id="karuta-vote-now">
182                    Vote Now
183                </button>
184                <div class="timer" id="karuta-timer"></div>
185            `;
186
187            document.body.appendChild(panel);
188
189            // Add event listeners
190            document.getElementById('karuta-toggle-auto').addEventListener('click', () => this.toggleAutoVote());
191            document.getElementById('karuta-vote-now').addEventListener('click', () => this.manualVote());
192
193            // Update timer display
194            this.updateTimerDisplay();
195            setInterval(() => this.updateTimerDisplay(), 1000);
196        }
197
198        async toggleAutoVote() {
199            this.autoVoteEnabled = !this.autoVoteEnabled;
200            await GM.setValue(CONFIG.AUTO_VOTE_ENABLED, this.autoVoteEnabled);
201            
202            const toggleBtn = document.getElementById('karuta-toggle-auto');
203            toggleBtn.textContent = `Auto-Vote: ${this.autoVoteEnabled ? 'ON' : 'OFF'}`;
204            toggleBtn.classList.toggle('disabled', !this.autoVoteEnabled);
205
206            console.log(`Auto-vote ${this.autoVoteEnabled ? 'enabled' : 'disabled'}`);
207            
208            if (this.autoVoteEnabled) {
209                this.checkAndVote();
210            }
211        }
212
213        updateTimerDisplay() {
214            const now = Date.now();
215            const nextVoteTime = this.lastVoteTime + CONFIG.VOTE_COOLDOWN;
216            const timeRemaining = nextVoteTime - now;
217
218            const timerElement = document.getElementById('karuta-timer');
219            const nextVoteElement = document.getElementById('karuta-next-vote');
220
221            if (timeRemaining <= 0) {
222                nextVoteElement.textContent = 'Ready!';
223                timerElement.textContent = '✅ You can vote now!';
224            } else {
225                const hours = Math.floor(timeRemaining / (1000 * 60 * 60));
226                const minutes = Math.floor((timeRemaining % (1000 * 60 * 60)) / (1000 * 60));
227                const seconds = Math.floor((timeRemaining % (1000 * 60)) / 1000);
228                
229                nextVoteElement.textContent = `${hours}h ${minutes}m`;
230                timerElement.textContent = `⏱️ ${hours}h ${minutes}m ${seconds}s remaining`;
231            }
232        }
233
234        startMonitoring() {
235            console.log('Starting vote monitoring...');
236            
237            // Initial check
238            this.checkAndVote();
239
240            // Set up periodic checks
241            this.checkTimer = setInterval(() => {
242                this.checkAndVote();
243            }, CONFIG.CHECK_INTERVAL);
244
245            // Monitor DOM changes for vote button
246            const observer = new MutationObserver(debounce(() => {
247                this.checkAndVote();
248            }, 1000));
249
250            observer.observe(document.body, {
251                childList: true,
252                subtree: true
253            });
254        }
255
256        async checkAndVote() {
257            if (this.isVoting) {
258                console.log('Already voting, skipping check');
259                return;
260            }
261
262            const statusElement = document.getElementById('karuta-vote-status');
263            
264            // Check if we're on the vote page
265            if (!window.location.href.includes('/vote')) {
266                if (statusElement) statusElement.textContent = 'Not on vote page';
267                return;
268            }
269
270            // Check cooldown
271            const now = Date.now();
272            const timeSinceLastVote = now - this.lastVoteTime;
273            
274            if (timeSinceLastVote < CONFIG.VOTE_COOLDOWN) {
275                const timeRemaining = CONFIG.VOTE_COOLDOWN - timeSinceLastVote;
276                const hours = Math.floor(timeRemaining / (1000 * 60 * 60));
277                const minutes = Math.floor((timeRemaining % (1000 * 60 * 60)) / (1000 * 60));
278                
279                if (statusElement) statusElement.textContent = `Cooldown (${hours}h ${minutes}m)`;
280                console.log(`Vote cooldown active. ${hours}h ${minutes}m remaining`);
281                return;
282            }
283
284            // Look for the vote button
285            const voteButton = this.findVoteButton();
286            
287            if (voteButton) {
288                if (statusElement) statusElement.textContent = 'Vote Available!';
289                console.log('Vote button found!');
290                
291                if (this.autoVoteEnabled) {
292                    await this.performVote(voteButton);
293                }
294            } else {
295                // Check if already voted
296                const alreadyVotedText = document.body.textContent;
297                if (alreadyVotedText.includes('You have already voted') || alreadyVotedText.includes('already voted')) {
298                    if (statusElement) statusElement.textContent = 'Already Voted';
299                    console.log('Already voted, updating last vote time');
300                    
301                    // Update last vote time if not set recently
302                    if (timeSinceLastVote > CONFIG.VOTE_COOLDOWN) {
303                        await GM.setValue(CONFIG.LAST_VOTE_TIME, now);
304                        this.lastVoteTime = now;
305                    }
306                } else {
307                    if (statusElement) statusElement.textContent = 'Searching...';
308                }
309            }
310        }
311
312        findVoteButton() {
313            // Try multiple selectors to find the vote button
314            const selectors = [
315                'button[type="submit"]:not([disabled])',
316                'button:not([disabled])',
317                'a[href*="vote"]',
318                '.vote-button',
319                '[data-testid*="vote"]',
320                'button.chakra-button'
321            ];
322
323            for (const selector of selectors) {
324                const buttons = document.querySelectorAll(selector);
325                for (const button of buttons) {
326                    const text = button.textContent.toLowerCase();
327                    if (text.includes('vote') && !text.includes('already') && !button.disabled) {
328                        console.log('Found vote button:', button);
329                        return button;
330                    }
331                }
332            }
333
334            return null;
335        }
336
337        async performVote(button) {
338            if (this.isVoting) return;
339            
340            this.isVoting = true;
341            const statusElement = document.getElementById('karuta-vote-status');
342            
343            try {
344                console.log('Attempting to vote...');
345                if (statusElement) statusElement.textContent = 'Voting...';
346
347                // Click the vote button
348                button.click();
349
350                // Wait for vote to process
351                await new Promise(resolve => setTimeout(resolve, 3000));
352
353                // Update vote count and time
354                this.voteCount++;
355                this.lastVoteTime = Date.now();
356                
357                await GM.setValue(CONFIG.VOTE_COUNT, this.voteCount);
358                await GM.setValue(CONFIG.LAST_VOTE_TIME, this.lastVoteTime);
359
360                // Update UI
361                const countElement = document.getElementById('karuta-vote-count');
362                if (countElement) countElement.textContent = this.voteCount;
363                if (statusElement) statusElement.textContent = 'Vote Success!';
364
365                console.log(`Vote successful! Total votes: ${this.voteCount}`);
366
367                // Show notification
368                try {
369                    await GM.notification({
370                        title: 'Karuta Vote Automation',
371                        text: `Successfully voted! Total votes: ${this.voteCount}`,
372                        timeout: 5000
373                    });
374                } catch (e) {
375                    console.log('Notification not supported');
376                }
377
378            } catch (error) {
379                console.error('Error voting:', error);
380                if (statusElement) statusElement.textContent = 'Vote Failed';
381            } finally {
382                this.isVoting = false;
383            }
384        }
385
386        async manualVote() {
387            const button = document.getElementById('karuta-vote-now');
388            const originalText = button.textContent;
389            
390            // Check cooldown
391            const now = Date.now();
392            const timeSinceLastVote = now - this.lastVoteTime;
393            
394            if (timeSinceLastVote < CONFIG.VOTE_COOLDOWN) {
395                button.textContent = 'On Cooldown!';
396                setTimeout(() => {
397                    button.textContent = originalText;
398                }, 2000);
399                return;
400            }
401
402            button.disabled = true;
403            button.textContent = 'Searching...';
404
405            const voteButton = this.findVoteButton();
406            
407            if (voteButton) {
408                await this.performVote(voteButton);
409            } else {
410                button.textContent = 'No Vote Button Found';
411                setTimeout(() => {
412                    button.textContent = originalText;
413                }, 2000);
414            }
415
416            button.disabled = false;
417            button.textContent = originalText;
418        }
419    }
420
421    // Initialize when page is ready
422    TM_runBody(() => {
423        console.log('Page ready, initializing Karuta Vote Automation');
424        new KarutaVoteAutomation();
425    });
426
427})();
Karuta Vote Automation | Robomonkey