// ==UserScript== // @name Stove Flake Automation Script // @namespace https://github.com/TellurideX/Stove-Flake-Automation-Script-Tampermonkey // @version 1.2.3 // @description 스토브 플레이크 샵 뽑기 자동화 스크립트 // @author TellurideX // @match https://reward.onstove.com/ko/event* // @icon data:image/x-icon;base64,AAABAAEAICAAAAEAIACoEAAAFgAAACgAAAAgAAAAQAAAAAEAIAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAzb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wRO8P8GT/D/CFHw/wZP8P8CTPD/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/GV3x/4+w+P/o7/7///////////////////////7+///E1fz/TYH1/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/0p/9P/v9P7/////////////////////////////////////////////////mbf5/wJM8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9ikPb/+vv/////////////////////////////////////////////////////////////g6f4/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/Z5P2/+ju/v//////////////////////1uL9/4yu+P98o/f/n7v5//P3/v/////////////////6+///CVHx/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v90nff//////////////////////4Gm+P8BTPD/AEvw/wBL8P8AS/D/MG3z//7+//////////////////8hYvL/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/3qh9//8/f/////////////V4v3/AUzw/wBL8P8AS/D/AEvw/wBL8P8AS/D/7PL+/////////////////yxq8/8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2uW9v+Lrfj/r8f6/5u5+f8zb/P/AEvw/wBL8P8AS/D/AUzw/3ae9///////////////////////Jmby/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2+Z9/9umfb/fKP3/6nC+v/l7f3///////////////////////////8RV/H/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/32j9//N3Pz//f7/////////////////////////////////////////////tcv7/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v+Ssvn/+vv//////////////////////////////////////////////////8nZ/P8TWPH/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/eKD3//j6/////////////////////////////////////////////+Pr/f92nvf/Bk/w/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v+5zfv///////////////////////7+///e6P3/wdP7/6W/+v9ZivX/Akzw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/9zm/f/////////////////q8P7/eKD3/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/4er9/////////////////7bL+/9mk/b/ZpP2/2aT9v9mk/b/ZpP2/8jY/P+Ytvn/OHPz/wNN8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v/P3fz/////////////////4ur9/2yX9v9mk/b/ZpP2/2aT9v+gvPn/////////////////7vP+/wdQ8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/5y5+f//////////////////////6e/+/7bM+/+wyPr/1OH8///////////////////////c5v3/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/Z5T2/93n/f///////////////////////////////////////////////////////////5Oz+f8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/wBL8P9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/c5z3/9/p/f////////////////////////////////////////////7+//+qw/r/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/AEvw/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/aZX2/6rD+v/r8f7///////////////////////3+///U4fz/iaz4/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P8AS/D/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9olPb/apb2/2qW9v9plfb/Z5P2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/wBL8P9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/AEvw/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P8AS/D/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/wBL8P9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AEvw/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v9mk/b/ZpP2/2aT9v8zb/P/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA= // @homepageURL https://github.com/TellurideX/Stove-Flake-Automation-Script-Tampermonkey // @updateURL https://raw.githubusercontent.com/TellurideX/Stove-Flake-Automation-Script-Tampermonkey/main/stove-flake-automation.user.js // @downloadURL https://raw.githubusercontent.com/TellurideX/Stove-Flake-Automation-Script-Tampermonkey/main/stove-flake-automation.user.js // @grant GM_setValue // @grant GM_getValue // ==/UserScript== (function() { 'use strict'; // ================================ // 스크립트 버전 & 패치노트 관리 // ================================ // ⚠ @version 메타데이터와 반드시 동일하게 유지하세요. const SCRIPT_VERSION = '1.2.3'; // 패치내역은 여기만 수정하면 됩니다. // 새 버전 배포 시, 맨 위에 항목을 하나 더 추가하세요. const PATCH_HISTORY = [ { version: '1.2.3', title: 'v1.2.3', lines: [ '- 버그 픽스' ] }, { version: '1.2.2', title: 'v1.2.2', lines: [ '- 버그 픽스' ] }, { version: '1.2.1', title: 'v1.2.1', lines: [ '- 100 뽑기 / 1,000 뽑기 횟수 통계 표시 기능 추가', '- 플레이크 순이익(실제 획득) 추가', ] }, { version: '1.0.0', title: 'v1.0.0', lines: [ '- Stove Flake Automation Script 최초 공개 버전' ] } ]; const VERSION_STORAGE_KEY = 'stove_flake_last_version_seen'; function gmGet(key, defaultValue) { try { if (typeof GM_getValue === 'function') { return GM_getValue(key, defaultValue); } } catch (e) { console.log('[GM] GM_getValue 오류', e); } return defaultValue; } function gmSet(key, value) { try { if (typeof GM_setValue === 'function') { GM_setValue(key, value); } } catch (e) { console.log('[GM] GM_setValue 오류', e); } } function getCurrentPatchEntry() { for (let i = 0; i < PATCH_HISTORY.length; i += 1) { if (PATCH_HISTORY[i].version === SCRIPT_VERSION) { return PATCH_HISTORY[i]; } } return null; } // ================================ // 자동화 상태 // ================================ let isAuto100Running = false; let isAuto1000Running = false; let auto100Timer = null; let auto1000Timer = null; let remainingDraws = null; // 오늘 남은 뽑기 횟수 (자동 실행용) // ================================ // 계정별 보상 기록 상태 (하루 단위) // ================================ const BASE_STORAGE_KEY_REWARD = 'stove_flake_reward_v1'; let rewardState = null; let currentAccountId = null; function getRewardStorageKey() { if (!currentAccountId) { currentAccountId = 'default'; } return BASE_STORAGE_KEY_REWARD + '::' + currentAccountId; } // ================================ // 계정 ID 해석 // ================================ function extractNicknameFromUserInfoLayer() { const layer = document.getElementById('gnb-userinfo-layer'); if (!layer) return null; const nickSpan = layer.querySelector( 'span.stds-text.text-2xl.leading-xl.font-bold' ); if (!nickSpan) return null; const nick = (nickSpan.textContent || '').trim(); return nick || null; } function closeUserInfoLayer(triggerBtn) { const layer = document.getElementById('gnb-userinfo-layer'); if (!layer) { try { triggerBtn.click(); } catch (e) { // ignore } return; } const closeBtn = layer.querySelector('button.stds-button-ghost'); if (closeBtn) { try { closeBtn.click(); console.log('[계정] 팝업 닫기 버튼 클릭'); return; } catch (e) { console.log('[계정] 팝업 닫기 버튼 클릭 실패', e); } } try { triggerBtn.click(); console.log('[계정] gnb-user-menu-button 재클릭으로 팝업 닫기 시도'); } catch (e) { console.log('[계정] gnb-user-menu-button 재클릭 실패', e); } } // 렌더링을 기다리면서 계정 ID를 결정 function resolveAccountId(callback) { if (currentAccountId) { if (callback) callback(); return; } let tries = 0; const maxTries = 40; // 40 * 250ms ≒ 10초 const interval = 250; let menuClicked = false; const timer = window.setInterval(function() { tries += 1; if (currentAccountId) { window.clearInterval(timer); if (callback) callback(); return; } const nick = extractNicknameFromUserInfoLayer(); if (nick) { currentAccountId = nick; console.log('[계정] 메뉴에서 닉네임 읽음 →', currentAccountId); const menuRootNow = document.getElementById('gnb-user-menu-button'); const triggerBtnNow = menuRootNow ? menuRootNow.querySelector('button') : null; if (triggerBtnNow) { closeUserInfoLayer(triggerBtnNow); } window.clearInterval(timer); if (callback) callback(); return; } const menuRoot = document.getElementById('gnb-user-menu-button'); if (!menuRoot) { if (tries >= maxTries) { window.clearInterval(timer); console.log('[계정] gnb-user-menu-button 미등장 → default 사용'); currentAccountId = 'default'; if (callback) callback(); } return; } const triggerBtn = menuRoot.querySelector('button'); if (!triggerBtn) { if (tries >= maxTries) { window.clearInterval(timer); console.log('[계정] gnb-user-menu-button 안에 button 없음 → default 사용'); currentAccountId = 'default'; if (callback) callback(); } return; } if (!menuClicked) { try { triggerBtn.click(); menuClicked = true; console.log('[계정] 계정 메뉴 자동 오픈 시도'); } catch (e) { console.log('[계정] 계정 메뉴 자동 오픈 실패 → default 사용', e); window.clearInterval(timer); currentAccountId = 'default'; if (callback) callback(); } return; } if (tries >= maxTries) { window.clearInterval(timer); console.log('[계정] 닉네임을 찾지 못함 → default 사용'); currentAccountId = 'default'; closeUserInfoLayer(triggerBtn); if (callback) callback(); } }, interval); } // ================================ // 날짜/보상 상태 관리 // ================================ function getTodayDateString() { const d = new Date(); const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, '0'); const day = String(d.getDate()).padStart(2, '0'); return y + '-' + m + '-' + day; } function initRewardState() { const today = getTodayDateString(); const key = getRewardStorageKey(); const stored = gmGet(key, null); if (stored && stored.date === today && Array.isArray(stored.logs)) { rewardState = { date: stored.date, logs: stored.logs || [], flakeTotal: stored.flakeTotal || 0, otherCounts: stored.otherCounts || {}, lastIndex: stored.lastIndex || 0, lastLabel: stored.lastLabel || null, // 뽑기 종류별 횟수 drawCount100: stored.drawCount100 || 0, drawCount1000: stored.drawCount1000 || 0 }; } else { rewardState = { date: today, logs: [], flakeTotal: 0, otherCounts: {}, lastIndex: 0, lastLabel: null, drawCount100: 0, drawCount1000: 0 }; gmSet(key, rewardState); } console.log('[보상 상태] init, account =', currentAccountId, ', logs =', rewardState.logs.length); } function ensureRewardStateForToday() { const today = getTodayDateString(); const key = getRewardStorageKey(); if (!rewardState || rewardState.date !== today) { rewardState = { date: today, logs: [], flakeTotal: 0, otherCounts: {}, lastIndex: 0, lastLabel: null, drawCount100: 0, drawCount1000: 0 }; gmSet(key, rewardState); } } function saveRewardState() { if (!rewardState) return; const key = getRewardStorageKey(); gmSet(key, rewardState); } // ================================ // UI: 보상 기록 패널 // ================================ function escapeHtml(str) { if (!str) return ''; return str.replace(/[&<>"']/g, function(ch) { switch (ch) { case '&': return '&'; case '<': return '<'; case '>': return '>'; case '"': return '"'; case '\'': return '''; default: return ch; } }); } function renderRewardPanel() { if (!rewardState) return; let panel = document.getElementById('stove-reward-panel'); if (!panel) { panel = document.createElement('div'); panel.id = 'stove-reward-panel'; panel.style.position = 'fixed'; panel.style.right = '20px'; panel.style.bottom = '180px'; panel.style.width = '260px'; panel.style.background = 'rgba(0, 0, 0, 0.7)'; panel.style.color = '#fff'; panel.style.borderRadius = '8px'; panel.style.padding = '8px 10px'; panel.style.fontSize = '12px'; panel.style.zIndex = '999999'; panel.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.4)'; panel.style.backdropFilter = 'blur(4px)'; document.body.appendChild(panel); } const accountLabel = currentAccountId || 'default'; let html = ''; // 제목 + 날짜 html += '
보상 기록 (계정: ' + escapeHtml(accountLabel) + ')
'; html += '
날짜: ' + escapeHtml(rewardState.date) + '
'; // 로그 영역 (스크롤 자동 하단 이동용 id) html += '
'; if (!rewardState.logs || rewardState.logs.length === 0) { html += '
기록된 보상이 없습니다.
'; } else { rewardState.logs.forEach(function(log) { const idx = String(log.index).padStart(2, '0'); html += '
[' + idx + '] "' + escapeHtml(log.label) + '" 보상 획득
'; }); } html += '
'; // 로그 박스 닫기 // 종합 보상 계산 (플레이크 순이익 포함) const totalRewardFlake = rewardState.flakeTotal || 0; const drawCount100 = rewardState.drawCount100 || 0; const drawCount1000 = rewardState.drawCount1000 || 0; const consumedFlake = drawCount100 * 100 + drawCount1000 * 1000; const netFlake = totalRewardFlake - consumedFlake; // 종합 보상 제목에 뽑기 횟수 표시 html += '
' + '종합 보상 (100 뽑기 : ' + drawCount100 + '회 // 1,000 뽑기 : ' + drawCount1000 + '회)' + '
'; html += '
'; let hasSummary = false; // 플레이크 : 총 보상 + 실제 획득 if (totalRewardFlake > 0 || consumedFlake !== 0) { const totalFormatted = totalRewardFlake.toLocaleString('ko-KR'); const netFormatted = netFlake.toLocaleString('ko-KR'); html += '
플레이크 : ' + totalFormatted + ' (실제 획득 : ' + netFormatted + ')
'; hasSummary = true; } // 기타 보상 if (rewardState.otherCounts) { const labels = Object.keys(rewardState.otherCounts); labels.forEach(function(label) { const count = rewardState.otherCounts[label]; html += '
' + escapeHtml(label) + ' : ' + count + '
'; hasSummary = true; }); } if (!hasSummary) { html += '
집계된 보상이 없습니다.
'; } html += '
'; // 종합 보상 박스 닫기 // 실제 DOM에 반영 panel.innerHTML = html; // 로그 스크롤을 항상 맨 아래로 이동 const logBox = document.getElementById('stove-reward-log'); if (logBox) { logBox.scrollTop = logBox.scrollHeight; } } // ================================ // 오늘 뽑기 횟수 파싱 // ================================ function getTodayDrawInfo() { const boxes = document.querySelectorAll('.stds-box'); for (let i = 0; i < boxes.length; i += 1) { const box = boxes[i]; if (!box) continue; const raw = (box.textContent || '').replace(/\s+/g, ''); if (raw.indexOf('오늘뽑기') === -1) continue; const match = raw.match(/(\d+)\s*\/\s*(\d+)회?/); if (!match) continue; const used = parseInt(match[1], 10); const total = parseInt(match[2], 10); if (isNaN(used) || isNaN(total)) continue; return { used: used, total: total }; } return null; } function getRemainingDrawsFromPage() { const info = getTodayDrawInfo(); if (!info) return null; let remaining = info.total - info.used; if (remaining < 0) remaining = 0; if (remaining > info.total) remaining = info.total; return remaining; } // ================================ // 버튼 찾기 헬퍼 // ================================ function findButtonByText(keyword) { const buttons = document.querySelectorAll('button'); for (let i = 0; i < buttons.length; i += 1) { const btn = buttons[i]; if (!btn) continue; const text = (btn.innerText || btn.textContent || '').trim(); if (text.indexOf(keyword) !== -1) { return btn; } } return null; } function findButtonByTextCommaInsensitive(keyword) { const normalizedKeyword = keyword.replace(/,/g, ''); const buttons = document.querySelectorAll('button'); for (let i = 0; i < buttons.length; i += 1) { const btn = buttons[i]; if (!btn) continue; const rawText = (btn.innerText || btn.textContent || '').trim(); const normalizedText = rawText.replace(/,/g, ''); if (normalizedText.indexOf(normalizedKeyword) !== -1) { return btn; } } return null; } // ================================ // 메인/팝업 버튼 찾기 // ================================ function findMain100Button() { const buttons = document.querySelectorAll('button'); for (let i = 0; i < buttons.length; i += 1) { const btn = buttons[i]; if (!btn) continue; const text = (btn.innerText || btn.textContent || '').trim(); if (text.indexOf('100 뽑기') !== -1 && text.indexOf('100 뽑기 한번 더!') === -1) { return btn; } } return null; } function findPopup100MoreButton() { return findButtonByText('100 뽑기 한번 더!'); } function findMain1000Button() { return findButtonByTextCommaInsensitive('1000 뽑기'); } function findPopup1000MoreButton() { return findButtonByTextCommaInsensitive('1000 뽑기 한번 더!'); } function findPopupCloseButton() { return findButtonByText('닫기'); } // ================================ // 남은 횟수 사용 / 체크 // ================================ function ensureRemainingDraws() { if (remainingDraws === null) { console.log('[공통] remainingDraws가 null 입니다.'); return false; } if (remainingDraws <= 0) { console.log('[공통] 남은 뽑기 횟수가 0회입니다. 자동 종료.'); stopAutomation(); return false; } return true; } function consumeOneDraw(tag) { if (remainingDraws === null) return; remainingDraws -= 1; console.log('[' + tag + '] 1회 사용 → 남은 횟수: ' + remainingDraws); if (remainingDraws <= 0) { console.log('[' + tag + '] 설정된 남은 횟수를 모두 사용했습니다. 자동 종료.'); stopAutomation(); } } // ================================ // 보상 기록 로직 // ================================ // 팝업에서 100/1000 뽑기 종류 추출 function detectDrawTypeFromPanel(panelEl) { if (!panelEl) return null; const buttons = panelEl.querySelectorAll('button'); for (let i = 0; i < buttons.length; i += 1) { const btn = buttons[i]; const text = (btn.innerText || btn.textContent || '').replace(/\s+/g, ''); if (text.includes('100뽑기한번더!')) { return '100'; } if (text.includes('1000뽑기한번더!')) { return '1000'; } } return null; } function recordReward(rewardLabel, drawType) { // drawType: '100' | '1000' | null ensureRewardStateForToday(); const info = getTodayDrawInfo(); let index; if (info && typeof info.used === 'number') { index = info.used; } else if (rewardState.logs.length > 0) { index = rewardState.logs[rewardState.logs.length - 1].index + 1; } else { index = 1; } if (rewardState.lastIndex === index && rewardState.lastLabel === rewardLabel) { console.log('[보상 기록] 중복 감지 (index=' + index + ', label=' + rewardLabel + '), 기록 생략'); return; } rewardState.logs.push({ index: index, label: rewardLabel }); // 플레이크 보상 합계 if (rewardLabel.indexOf('플레이크') !== -1) { const match = rewardLabel.replace(/,/g, '').match(/(\d+)/); if (match) { const amount = parseInt(match[1], 10); if (!isNaN(amount)) { rewardState.flakeTotal = (rewardState.flakeTotal || 0) + amount; } } } else { // 기타 보상 개수 if (!rewardState.otherCounts[rewardLabel]) { rewardState.otherCounts[rewardLabel] = 0; } rewardState.otherCounts[rewardLabel] += 1; } // 뽑기 종류별 사용 횟수 기록 if (drawType === '100') { rewardState.drawCount100 = (rewardState.drawCount100 || 0) + 1; } else if (drawType === '1000') { rewardState.drawCount1000 = (rewardState.drawCount1000 || 0) + 1; } rewardState.lastIndex = index; rewardState.lastLabel = rewardLabel; saveRewardState(); renderRewardPanel(); } function handleRewardPanel(panelEl) { if (!panelEl) return; const rewardSpan = panelEl.querySelector('.l1l2-flakehub-popup-common-received_reward'); if (!rewardSpan) return; const rewardText = (rewardSpan.textContent || '').trim(); if (!rewardText) return; const drawType = detectDrawTypeFromPanel(panelEl); // '100' | '1000' | null console.log('[보상 기록] 팝업 감지: ' + rewardText + ', drawType=' + drawType); recordReward(rewardText, drawType); } function scanAllRewardPanels() { const panels = document.querySelectorAll('.stds-dialog-panel'); panels.forEach(function(panel) { handleRewardPanel(panel); }); } function setupPopupObserver() { if (!window.MutationObserver) { console.log('[보상 기록] MutationObserver 미지원'); return; } const observer = new MutationObserver(function() { scanAllRewardPanels(); }); observer.observe(document.body, { childList: true, subtree: true }); } // ================================ // 100 뽑기 자동 루프 // ================================ function loop100() { if (!isAuto100Running) return; if (!ensureRemainingDraws()) { return; } const popupBtn = findPopup100MoreButton(); if (popupBtn) { if (popupBtn.disabled) { console.log('[100] 팝업 버튼 비활성화 → 0.9초 후 재시도'); auto100Timer = window.setTimeout(loop100, 900); return; } try { popupBtn.click(); console.log('[100] 팝업 "100 뽑기 한번 더!" 클릭'); consumeOneDraw('100'); if (!isAuto100Running) return; } catch (e) { console.log('[100] "100 뽑기 한번 더!" 클릭 중 오류, 자동 종료', e); stopAutomation(); return; } auto100Timer = window.setTimeout(loop100, 900); return; } const mainBtn = findMain100Button(); if (mainBtn) { if (mainBtn.disabled) { console.log('[100] 메인 "100 뽑기" 버튼 비활성화 → 자동 종료'); stopAutomation(); return; } try { mainBtn.click(); console.log('[100] 메인 "100 뽑기" 클릭'); consumeOneDraw('100'); if (!isAuto100Running) return; } catch (e) { console.log('[100] 메인 "100 뽑기" 클릭 중 오류, 자동 종료', e); stopAutomation(); return; } auto100Timer = window.setTimeout(loop100, 4000); return; } console.log('[100] 관련 버튼을 찾지 못했습니다. 자동 종료'); stopAutomation(); } // ================================ // 1,000 뽑기 자동 루프 // ================================ function loop1000() { if (!isAuto1000Running) return; if (!ensureRemainingDraws()) { return; } const popupBtn = findPopup1000MoreButton(); if (popupBtn) { if (popupBtn.disabled) { console.log('[1000] 팝업 버튼 비활성화 → 0.9초 후 재시도'); auto1000Timer = window.setTimeout(loop1000, 900); return; } try { popupBtn.click(); console.log('[1000] 팝업 "1000 뽑기 한번 더!" 클릭'); consumeOneDraw('1000'); if (!isAuto1000Running) return; } catch (e) { console.log('[1000] "1000 뽑기 한번 더!" 클릭 중 오류, 자동 종료', e); stopAutomation(); return; } auto1000Timer = window.setTimeout(loop1000, 900); return; } const mainBtn = findMain1000Button(); if (mainBtn) { if (mainBtn.disabled) { console.log('[1000] 메인 "1,000 뽑기" 버튼 비활성화 → 자동 종료'); stopAutomation(); return; } try { mainBtn.click(); console.log('[1000] 메인 "1,000 뽑기" 클릭'); consumeOneDraw('1000'); if (!isAuto1000Running) return; } catch (e) { console.log('[1000] 메인 "1,000 뽑기" 클릭 중 오류, 자동 종료', e); stopAutomation(); return; } auto1000Timer = window.setTimeout(loop1000, 4000); return; } console.log('[1000] 관련 버튼을 찾지 못했습니다. 자동 종료'); stopAutomation(); } // ================================ // 패치노트 모달 UI // ================================ function openPatchNotesModal() { if (document.getElementById('stove-flake-patch-overlay')) { return; } const overlay = document.createElement('div'); overlay.id = 'stove-flake-patch-overlay'; overlay.style.position = 'fixed'; overlay.style.left = '0'; overlay.style.top = '0'; overlay.style.right = '0'; overlay.style.bottom = '0'; overlay.style.background = 'rgba(0, 0, 0, 0.55)'; overlay.style.backdropFilter = 'blur(2px)'; overlay.style.zIndex = '1000000'; overlay.style.display = 'flex'; overlay.style.alignItems = 'center'; overlay.style.justifyContent = 'center'; const modal = document.createElement('div'); modal.style.background = 'rgba(15, 15, 15, 0.95)'; modal.style.color = '#ffffff'; modal.style.borderRadius = '10px'; modal.style.padding = '14px 16px 12px 16px'; modal.style.width = '480px'; modal.style.maxHeight = '70vh'; modal.style.boxShadow = '0 4px 16px rgba(0, 0, 0, 0.6)'; modal.style.display = 'flex'; modal.style.flexDirection = 'column'; modal.style.fontSize = '13px'; const header = document.createElement('div'); header.textContent = 'Stove Flake Automation 패치 노트 (v' + SCRIPT_VERSION + ')'; header.style.fontWeight = 'bold'; header.style.marginBottom = '8px'; header.style.fontSize = '14px'; const content = document.createElement('div'); content.style.flex = '1'; content.style.overflowY = 'auto'; content.style.paddingRight = '4px'; // 패치내역 전체 렌더링 PATCH_HISTORY.forEach(function(entry) { const section = document.createElement('div'); section.style.marginBottom = '10px'; const title = document.createElement('div'); title.textContent = entry.title || ('v' + entry.version); title.style.fontWeight = 'bold'; title.style.marginBottom = '4px'; title.style.fontSize = '13px'; section.appendChild(title); if (entry.lines && entry.lines.length > 0) { const ul = document.createElement('ul'); ul.style.paddingLeft = '18px'; ul.style.margin = '0'; entry.lines.forEach(function(line) { const li = document.createElement('li'); li.textContent = line; li.style.marginBottom = '2px'; ul.appendChild(li); }); section.appendChild(ul); } content.appendChild(section); }); const footer = document.createElement('div'); footer.style.display = 'flex'; footer.style.justifyContent = 'flex-end'; footer.style.marginTop = '8px'; const closeBtn = document.createElement('button'); closeBtn.textContent = '닫기'; closeBtn.style.padding = '6px 12px'; closeBtn.style.borderRadius = '6px'; closeBtn.style.border = 'none'; closeBtn.style.cursor = 'pointer'; closeBtn.style.fontSize = '12px'; closeBtn.style.background = '#555'; closeBtn.style.color = '#fff'; closeBtn.addEventListener('mouseenter', function() { closeBtn.style.background = '#666'; }); closeBtn.addEventListener('mouseleave', function() { closeBtn.style.background = '#555'; }); closeBtn.addEventListener('click', function(e) { e.stopPropagation(); closePatchNotesModal(); }); footer.appendChild(closeBtn); modal.appendChild(header); modal.appendChild(content); modal.appendChild(footer); // 오버레이 클릭 시 모달 닫기 (모달 내부 클릭은 전파 차단) modal.addEventListener('click', function(e) { e.stopPropagation(); }); overlay.addEventListener('click', function() { closePatchNotesModal(); }); overlay.appendChild(modal); document.body.appendChild(overlay); } function closePatchNotesModal() { const overlay = document.getElementById('stove-flake-patch-overlay'); if (overlay && overlay.parentNode) { overlay.parentNode.removeChild(overlay); } } // ================================ // 버전 최초 1회만 보여주는 작은 팝업 // ================================ function showVersionBannerIfNeeded() { const lastSeen = gmGet(VERSION_STORAGE_KEY, null); if (lastSeen === SCRIPT_VERSION) { return; } // 현재 버전으로 갱신 (이후 재접속 시에는 다시 뜨지 않음) gmSet(VERSION_STORAGE_KEY, SCRIPT_VERSION); const entry = getCurrentPatchEntry(); const banner = document.createElement('div'); banner.id = 'stove-flake-version-banner'; banner.style.position = 'fixed'; banner.style.right = '20px'; banner.style.bottom = '420px'; banner.style.width = '260px'; banner.style.background = 'rgba(0, 0, 0, 0.82)'; banner.style.color = '#fff'; banner.style.borderRadius = '8px'; banner.style.padding = '8px 10px'; banner.style.fontSize = '12px'; banner.style.zIndex = '999999'; banner.style.boxShadow = '0 2px 8px rgba(0, 0, 0, 0.5)'; banner.style.cursor = 'pointer'; banner.style.backdropFilter = 'blur(4px)'; let html = ''; html += '
Stove Flake 패치 안내 (v' + SCRIPT_VERSION + ')
'; if (entry && entry.lines && entry.lines.length > 0) { html += '
'; entry.lines.forEach(function(line) { html += escapeHtml(line) + '
'; }); html += '
'; } else { html += '
' + '새 버전이 적용되었습니다.' + '
'; } html += '
(창을 클릭하면 닫힙니다.)
'; banner.innerHTML = html; // 아무 곳이나 클릭하면 종료 banner.addEventListener('click', function(e) { e.stopPropagation(); const el = document.getElementById('stove-flake-version-banner'); if (el && el.parentNode) { el.parentNode.removeChild(el); } }); document.body.appendChild(banner); } // ================================ // 자동화 시작 / 중단 // ================================ function startAuto100() { if (isAuto100Running) { alert('이미 100 뽑기 자동화가 진행중입니다.'); return; } if (isAuto1000Running) { alert('이미 1,000 뽑기 자동화가 진행중입니다. 100 뽑기 자동화를 실행하려면 먼저 중단하기 버튼을 눌러주세요.'); return; } const remain = getRemainingDrawsFromPage(); if (remain === null) { alert('오늘 뽑기 횟수를 확인하지 못했습니다. 페이지 구조가 변경되었을 수 있습니다.'); return; } if (remain <= 0) { alert('오늘은 이미 모두 참여했습니다.\n00시 이후에 다시 시도해주시길 바랍니다.'); return; } remainingDraws = remain; console.log('[100] 자동화 시작, 남은 횟수: ' + remainingDraws); isAuto100Running = true; loop100(); } function startAuto1000() { if (isAuto1000Running) { alert('이미 1,000 뽑기 자동화가 진행중입니다.'); return; } if (isAuto100Running) { alert('이미 100 뽑기 자동화가 진행중입니다. 1,000 뽑기로 변경하려면 중단하기 버튼을 누르고 다시 시도해주세요.'); return; } const remain = getRemainingDrawsFromPage(); if (remain === null) { alert('오늘 뽑기 횟수를 확인하지 못했습니다. 페이지 구조가 변경되었을 수 있습니다.'); return; } if (remain <= 0) { alert('오늘은 이미 모두 참여했습니다.\n00시 이후에 다시 시도해주시길 바랍니다.'); return; } remainingDraws = remain; console.log('[1000] 자동화 시작, 남은 횟수: ' + remainingDraws); isAuto1000Running = true; loop1000(); } function stopAutomation() { if (!isAuto100Running && !isAuto1000Running) { alert('현재 스크립트가 작동중이지 않습니다.'); } console.log('[중단] 자동화 종료 요청'); isAuto100Running = false; isAuto1000Running = false; if (auto100Timer !== null) { window.clearTimeout(auto100Timer); auto100Timer = null; } if (auto1000Timer !== null) { window.clearTimeout(auto1000Timer); auto1000Timer = null; } remainingDraws = null; // 팝업 닫기 딜레이 1초 window.setTimeout(function() { const closeBtn = findPopupCloseButton(); if (!closeBtn) { console.log('[중단] 팝업 닫기 버튼을 찾지 못했습니다.'); return; } try { closeBtn.click(); console.log('[중단] 팝업 닫기 버튼 클릭'); } catch (e) { console.log('[중단] 팝업 닫기 버튼 클릭 중 오류', e); } }, 1000); } // ================================ // UI: 자동화 버튼 + 툴팁 // ================================ function createButtonWithTooltip(options) { const id = options.id; const label = options.label; const bottom = options.bottom; const onClick = options.onClick; const background = options.background; const tooltip = options.tooltip; if (document.getElementById(id)) { return; } const container = document.createElement('div'); container.style.position = 'fixed'; container.style.right = '20px'; container.style.bottom = bottom + 'px'; container.style.zIndex = '999999'; container.style.display = 'flex'; container.style.alignItems = 'center'; container.style.gap = '6px'; const icon = document.createElement('div'); icon.textContent = '?'; icon.style.width = '18px'; icon.style.height = '18px'; icon.style.borderRadius = '50%'; icon.style.background = '#555'; icon.style.color = '#fff'; icon.style.display = 'flex'; icon.style.alignItems = 'center'; icon.style.justifyContent = 'center'; icon.style.fontSize = '12px'; icon.style.cursor = 'default'; icon.style.opacity = '0.85'; const tooltipBox = document.createElement('div'); tooltipBox.textContent = tooltip; tooltipBox.style.position = 'absolute'; tooltipBox.style.right = '110%'; tooltipBox.style.top = '50%'; tooltipBox.style.transform = 'translateY(-50%)'; tooltipBox.style.background = '#333'; tooltipBox.style.color = '#fff'; tooltipBox.style.padding = '6px 10px'; tooltipBox.style.borderRadius = '6px'; tooltipBox.style.whiteSpace = 'nowrap'; tooltipBox.style.fontSize = '12px'; tooltipBox.style.opacity = '0'; tooltipBox.style.pointerEvents = 'none'; tooltipBox.style.transition = 'opacity 0.1s ease'; icon.addEventListener('mouseenter', function() { tooltipBox.style.opacity = '1'; }); icon.addEventListener('mouseleave', function() { tooltipBox.style.opacity = '0'; }); const btn = document.createElement('button'); btn.id = id; btn.textContent = label; btn.style.padding = '10px 12px'; btn.style.borderRadius = '6px'; btn.style.border = 'none'; btn.style.cursor = 'pointer'; btn.style.fontSize = '12px'; btn.style.fontFamily = 'inherit'; btn.style.color = '#ffffff'; btn.style.background = background || '#ff6b00'; btn.style.boxShadow = '0 2px 6px rgba(0, 0, 0, 0.3)'; btn.style.opacity = '0.9'; btn.style.width = '190px'; btn.style.textAlign = 'center'; btn.addEventListener('mouseenter', function() { btn.style.opacity = '1'; }); btn.addEventListener('mouseleave', function() { btn.style.opacity = '0.9'; }); btn.addEventListener('click', function(e) { e.preventDefault(); e.stopPropagation(); onClick(); }); container.appendChild(icon); container.appendChild(btn); container.appendChild(tooltipBox); document.body.appendChild(container); } function initUIButtons() { // 버튼 위치를 조금 위로 올려서 패치노트 버튼을 맨 아래에 배치 createButtonWithTooltip({ id: 'stove-auto-100', label: '100 뽑기 자동화 시작하기', bottom: 140, background: '#ff6b00', onClick: startAuto100, tooltip: '오늘 뽑기 남은 횟수만큼 100 뽑기를 자동으로 진행합니다.' }); createButtonWithTooltip({ id: 'stove-auto-1000', label: '1,000 뽑기 자동화 시작하기', bottom: 100, background: '#ff6b00', onClick: startAuto1000, tooltip: '오늘 뽑기 남은 횟수만큼 1,000 뽑기를 자동으로 진행합니다.' }); createButtonWithTooltip({ id: 'stove-auto-stop', label: '중단하기', bottom: 60, background: '#d62828', onClick: stopAutomation, tooltip: '진행 중인 자동화를 중단하고, 팝업이 있다면 닫기 버튼을 누릅니다.' }); // ✅ 패치노트 버튼 (초록색) createButtonWithTooltip({ id: 'stove-patch-notes', label: '패치노트', bottom: 20, background: '#2f9e44', onClick: openPatchNotesModal, tooltip: '버전별 패치 내역을 확인합니다.' }); } // ================================ // 초기화 // ================================ function onReady() { resolveAccountId(function() { initRewardState(); initUIButtons(); renderRewardPanel(); setupPopupObserver(); scanAllRewardPanels(); // 버전 최초 1회 팝업 showVersionBannerIfNeeded(); }); } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', onReady); } else { onReady(); } })();