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})();