// ==UserScript== // @name 눈사람 구출 작전 자동화 스크립트 // @namespace https://www.inven.co.kr/board/lostark/4821/108845 // @version 1.0.0 // @description 눈사람 구출 작전 자동화 // @author 킹암살, TellurideX // @match https://lostark.game.onstove.com/Promotion/Mission/251210* // @icon data:image/svg+xml;utf8,%3Csvg%20xmlns%3D'http%3A//www.w3.org/2000/svg'%20viewBox%3D'0%200%2048%2048'%3E%3Ccircle%20cx%3D'24'%20cy%3D'26'%20r%3D'19'%20fill%3D'%23ffffff'%20stroke%3D'%230b0b0b'%20stroke-width%3D'1.5'/%3E%3Cpath%20d%3D'M10%2018c3-8%2025-8%2028%200v6H10z'%20fill%3D'%2314161a'%20stroke%3D'%230b0b0b'%20stroke-width%3D'1.2'%20stroke-linejoin%3D'round'/%3E%3Cpath%20d%3D'M9%2023c4-4%2026-4%2030%200'%20fill%3D'%230f1114'%20stroke%3D'%230b0b0b'%20stroke-width%3D'1.2'%20stroke-linecap%3D'round'/%3E%3Ctext%20x%3D'24'%20y%3D'21.1'%20font-family%3D'Arial%2C%20Helvetica%2C%20sans-serif'%20font-size%3D'7.2'%20text-anchor%3D'middle'%20fill%3D'%23ffffff'%20letter-spacing%3D'.8'%20font-weight%3D'700'%3ELOA%3C/text%3E%3Crect%20x%3D'13.0'%20y%3D'25.2'%20width%3D'9.0'%20height%3D'5.0'%20rx%3D'1.6'%20fill%3D'%23111418'%20stroke%3D'%230b0b0b'%20stroke-width%3D'1.1'/%3E%3Crect%20x%3D'26.0'%20y%3D'25.2'%20width%3D'9.0'%20height%3D'5.0'%20rx%3D'1.6'%20fill%3D'%23111418'%20stroke%3D'%230b0b0b'%20stroke-width%3D'1.1'/%3E%3Cpath%20d%3D'M22.2%2027.7h3.6'%20stroke%3D'%230b0b0b'%20stroke-width%3D'1.1'%20stroke-linecap%3D'round'/%3E%3Cpath%20d%3D'M13.0%2027.6h-1.3'%20stroke%3D'%230b0b0b'%20stroke-width%3D'1.4'%20stroke-linecap%3D'round'/%3E%3Cpath%20d%3D'M35.0%2027.6h1.3'%20stroke%3D'%230b0b0b'%20stroke-width%3D'1.4'%20stroke-linecap%3D'round'/%3E%3Ccircle%20cx%3D'24'%20cy%3D'32.1'%20r%3D'2.2'%20fill%3D'%23ff7a2f'%20stroke%3D'%23c14d18'%20stroke-width%3D'1'/%3E%3Cpath%20d%3D'M18.5%2036.4c2.2%202.1%208.8%202.1%2011%200'%20fill%3D'none'%20stroke%3D'%230b0b0b'%20stroke-width%3D'1.4'%20stroke-linecap%3D'round'/%3E%3C/svg%3E // @homepageURL https://github.com/TellurideX/Snowman-Rescue-Operation-Automation-Script-Tampermonkey // @updateURL https://raw.githubusercontent.com/TellurideX/Snowman-Rescue-Operation-Automation-Script-Tampermonkey/main/sro-automation.user.js // @downloadURL https://raw.githubusercontent.com/TellurideX/Snowman-Rescue-Operation-Automation-Script-Tampermonkey/main/sro-automation.user.js // @grant GM_setValue // @grant GM_getValue // @run-at document-start // ==/UserScript== (function () { 'use strict'; // ================================ // DEBUG 설정 // ================================ const DEBUG = true; const LOG_PREFIX = '[SnowmanDBG]'; function log(...args) { if (!DEBUG) return; const t = new Date().toLocaleTimeString(); console.log(`${LOG_PREFIX} ${t}`, ...args); } // 팝업 처리 속도 const DELAY_POPUP_CONFIRM_MS = 1300; // 팝업 감지 후 "확인" 누르기까지 딜레이 const DELAY_AFTER_LOAD_START_MS = 2000; // 리로드 후 DOM 준비된 다음 코어 시작까지 딜레이 const POLL_INTERVAL_MS = 300; // ================================ // GM helpers // ================================ const KEY_RUNNING = '__snowman_running__'; // true/false const KEY_PHASE = '__snowman_phase__'; // 'click_play' | 'await_token' | 'start_core' | 'await_reward' const KEY_LAST_TS = '__snowman_last_ts__'; // 디버그용 function gmGet(key, def) { try { if (typeof GM_getValue === 'function') return GM_getValue(key, def); } catch (e) { log('[GM] GM_getValue error', e); } return def; } function gmSet(key, val) { try { if (typeof GM_setValue === 'function') GM_setValue(key, val); } catch (e) { log('[GM] GM_setValue error', e); } } function setPhase(phase) { gmSet(KEY_PHASE, phase); gmSet(KEY_LAST_TS, Date.now()); log('[PHASE] ->', phase); } function getPhase() { return gmGet(KEY_PHASE, ''); } function isRunning() { return !!gmGet(KEY_RUNNING, false); } // ================================ // UI // ================================ function createButtonWithTooltipUI(options) { const { id, label, bottom, background, tooltip } = options; if (document.getElementById(id)) return document.getElementById(id); 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', () => { tooltipBox.style.opacity = '1'; }); icon.addEventListener('mouseleave', () => { 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', () => { btn.style.opacity = '1'; }); btn.addEventListener('mouseleave', () => { btn.style.opacity = '0.9'; }); container.appendChild(icon); container.appendChild(btn); container.appendChild(tooltipBox); document.body.appendChild(container); return btn; } // ================================ // DOM helpers (팝업/버튼) // ================================ function findPlayButton() { return document.querySelector('button.button.button--play'); } function findTokenConfirmButton() { const body = document.querySelector('.lui-modal__body'); if (!body) return null; const textEl = body.querySelector('.popup_text'); const text = (textEl ? (textEl.textContent || '') : ''); if (!text.includes('토큰을 사용하여 게임을 플레이')) return null; return body.querySelector('button.lui-modal__confirm'); } function findRewardConfirmButton() { const body = document.querySelector('.lui-modal__body'); if (!body) return null; const titleEl = body.querySelector('.lui-modal__title'); const title = (titleEl ? (titleEl.textContent || '') : '').trim(); if (title !== 'STAGE CLEAR') return null; return body.querySelector('button.lui-modal__confirm'); } function hasGameDom() { const ok = !!document.querySelector('#event1') && !!document.querySelector('section.stage'); return ok; } // ================================ // 코어 (로그/워치독 추가) // ================================ function ensureSnowmanCoreLoadedOnce() { if (window.__snowmanCoreLoadedOnce) return; window.__snowmanCoreLoadedOnce = true; (function () { 'use strict'; if (window.__snowmanAutoLight) { log('[CORE] already __snowmanAutoLight true -> skip'); return; } window.__snowmanAutoLight = true; log('[CORE] loaded'); const sleep = ms => new Promise(r => setTimeout(r, ms)); function getCurrentFloor() { const floors = document.querySelectorAll('[class^="floor"]'); for (const el of floors) { if (el.querySelector('.obj_character')) { const name = el.className; const num = Number(name.slice(5)); if (!isNaN(num)) return num; } } return null; } function getDoors(floor) { const el = document.querySelector('.floor' + floor); if (!el) return []; return Array.from(el.querySelectorAll('button.door')); } const nextDoorIndex = {}; let lastClicked = null; let stopFlag = true; let loopRunning = false; let lastDoorClickAt = 0; let watchdogTimer = null; function clickNextDoor(floor) { const doors = getDoors(floor); if (!doors.length) { log('[CORE] no doors on floor', floor); return; } if (nextDoorIndex[floor] == null) nextDoorIndex[floor] = 0; const idx = nextDoorIndex[floor]; const btn = doors[idx]; if (!btn) { log('[CORE] btn missing floor', floor, 'idx', idx, 'doorsLen', doors.length); return; } if (btn.disabled) { log('[CORE] btn disabled floor', floor, 'idx', idx); return; } lastClicked = { floor, idx }; lastDoorClickAt = Date.now(); log('[CORE] CLICK door floor', floor, 'idx', idx); try { btn.click(); } catch (e) { log('[CORE] click error', e); } } (function hookXHR() { // XMLHttpRequest.prototype을 직접 패치 if (window.__snowmanXHRHooked) return; window.__snowmanXHRHooked = true; const XHRProto = window.XMLHttpRequest && window.XMLHttpRequest.prototype; if (!XHRProto) { console.log('[SnowmanDBG][XHR] XMLHttpRequest.prototype not found'); return; } if (XHRProto.__snowmanPatched) return; XHRProto.__snowmanPatched = true; console.log('[SnowmanDBG][XHR] prototype patch installed'); const originalOpen = XHRProto.open; const originalSend = XHRProto.send; XHRProto.open = function (method, url) { try { this.__snowmanUrl = url; } catch {} return originalOpen.apply(this, arguments); }; XHRProto.send = function () { try { if (!this.__snowmanListenerAttached) { this.__snowmanListenerAttached = true; this.addEventListener('load', () => { try { const url = this.__snowmanUrl || ''; if (!url.includes('SetDoor')) return; if (!lastClicked) return; const res = JSON.parse(this.responseText); const { floor, idx } = lastClicked; console.log('[SnowmanDBG][XHR] SetDoor load', 'floor=', floor, 'idx=', idx, 'isCorrect=', res && res.isCorrect); if (res && res.isCorrect) { nextDoorIndex[floor + 1] = 0; } else { nextDoorIndex[floor] = (nextDoorIndex[floor] ?? 0) + 1; } } catch (e) { console.log('[SnowmanDBG][XHR] handler error', e); } }); } } catch (e) { console.log('[SnowmanDBG][XHR] send hook error', e); } return originalSend.apply(this, arguments); }; })(); async function loop() { if (loopRunning) { log('[CORE] loop already running'); return; } loopRunning = true; log('[CORE] loop start'); while (!stopFlag) { const floor = getCurrentFloor(); if (floor != null) { clickNextDoor(floor); } else { log('[CORE] current floor = null (character not found?)'); } await sleep(150); } loopRunning = false; log('[CORE] loop stop'); } function startWatchdog() { if (watchdogTimer) return; watchdogTimer = window.setInterval(() => { if (stopFlag) return; const now = Date.now(); if (lastDoorClickAt && (now - lastDoorClickAt) > 2500) { log('[CORE][WATCHDOG] no door click for', (now - lastDoorClickAt), 'ms (maybe disabled/DOM not ready?)'); } }, 1000); } window.addEventListener('keydown', e => { if (e.key === 'Escape') { stopFlag = true; log('[CORE] ESC -> stopFlag=true'); } }); window.__snowmanStartNow = function () { stopFlag = false; log('[CORE] __snowmanStartNow() called'); startWatchdog(); loop(); }; window.__snowmanStopNow = function () { stopFlag = true; log('[CORE] __snowmanStopNow() called'); }; })(); } function startCore() { ensureSnowmanCoreLoadedOnce(); if (typeof window.__snowmanStartNow === 'function') window.__snowmanStartNow(); else log('[CORE] StartNow missing'); } function stopCore() { if (typeof window.__snowmanStopNow === 'function') window.__snowmanStopNow(); else log('[CORE] StopNow missing'); } // ================================ // 엔진 // ================================ let pollTimer = null; let mo = null; let lastActionAt = 0; function throttle(ms) { const now = Date.now(); if (now - lastActionAt < ms) return false; lastActionAt = now; return true; } function stopEngineAll() { log('[ENGINE] STOP ALL'); gmSet(KEY_RUNNING, false); setPhase(''); try { if (pollTimer) clearTimeout(pollTimer); } catch {} pollTimer = null; try { if (mo) mo.disconnect(); } catch {} mo = null; stopCore(); } const RECOVER_AWAIT_TOKEN_MS = 6000; function tokenPopupExistsNow() { return !!findTokenConfirmButton(); } function getLastTs() { return gmGet(KEY_LAST_TS, 0) || 0; } function ensureEngineStarted() { if (mo) return; log('[ENGINE] start (running=', isRunning(), 'phase=', getPhase(), ')'); if (window.MutationObserver) { mo = new MutationObserver(() => { if (!isRunning()) return; const phase = getPhase(); // 토큰 팝업 if (phase === 'await_token') { const tokenBtn = findTokenConfirmButton(); if (tokenBtn && throttle(700)) { log('[POPUP] token detected -> will confirm in', DELAY_POPUP_CONFIRM_MS, 'ms'); setPhase('start_core'); setTimeout(() => { try { tokenBtn.click(); log('[POPUP] token confirm clicked'); } catch (e) { log('[POPUP] token click err', e); } }, DELAY_POPUP_CONFIRM_MS); } } // 보상 팝업 if (phase === 'await_reward') { const rewardBtn = findRewardConfirmButton(); if (rewardBtn && throttle(700)) { log('[POPUP] reward detected -> stop core and confirm in', DELAY_POPUP_CONFIRM_MS, 'ms'); stopCore(); setPhase('click_play'); setTimeout(() => { try { rewardBtn.click(); log('[POPUP] reward confirm clicked'); } catch (e) { log('[POPUP] reward click err', e); } }, DELAY_POPUP_CONFIRM_MS); } } }); mo.observe(document.body, { childList: true, subtree: true }); } else { log('[ENGINE] MutationObserver not available'); } const tick = () => { if (!isRunning()) { log('[ENGINE] tick stop (not running)'); return; } const phase = getPhase(); if (phase === 'click_play') { if (!hasGameDom()) { log('[ENGINE] click_play: waiting game DOM...'); pollTimer = setTimeout(tick, POLL_INTERVAL_MS); return; } const playBtn = findPlayButton(); if (!playBtn) { log('[ENGINE] click_play: play button not found'); pollTimer = setTimeout(tick, POLL_INTERVAL_MS); return; } if (throttle(1200)) { log('[ENGINE] click_play: clicking play button'); try { playBtn.click(); } catch (e) { log('[ENGINE] play click err', e); } setPhase('await_token'); } pollTimer = setTimeout(tick, POLL_INTERVAL_MS); return; } if (phase === 'start_core') { if (!hasGameDom()) { log('[ENGINE] start_core: waiting game DOM...'); pollTimer = setTimeout(tick, POLL_INTERVAL_MS); return; } if (throttle(1500)) { log('[ENGINE] start_core: game DOM OK -> start core in', DELAY_AFTER_LOAD_START_MS, 'ms'); setTimeout(() => { if (!isRunning()) return; log('[ENGINE] start_core: starting original core now'); startCore(); setPhase('await_reward'); }, DELAY_AFTER_LOAD_START_MS); } pollTimer = setTimeout(tick, POLL_INTERVAL_MS); return; } if (phase === 'await_token') { const elapsed = Date.now() - getLastTs(); if (!tokenPopupExistsNow() && elapsed > RECOVER_AWAIT_TOKEN_MS) { const playBtn = findPlayButton(); log('[RECOVER] await_token stuck for', elapsed, 'ms. token popup not found. playBtn=', !!playBtn, '-> phase=click_play'); setPhase('click_play'); pollTimer = setTimeout(tick, POLL_INTERVAL_MS); return; } const tokenBtn = findTokenConfirmButton(); if (tokenBtn && throttle(700)) { log('[POLL][POPUP] token detected -> confirm in', DELAY_POPUP_CONFIRM_MS, 'ms'); setPhase('start_core'); setTimeout(() => { try { tokenBtn.click(); log('[POLL][POPUP] token confirm clicked'); } catch (e) { log('[POLL][POPUP] token click err', e); } }, DELAY_POPUP_CONFIRM_MS); } else { log('[ENGINE] await_token: waiting token popup...'); } pollTimer = setTimeout(tick, POLL_INTERVAL_MS); return; } if (phase === 'await_reward') { const rewardBtn = findRewardConfirmButton(); if (rewardBtn && throttle(700)) { log('[POLL][POPUP] reward detected -> stop core + confirm in', DELAY_POPUP_CONFIRM_MS, 'ms'); stopCore(); setPhase('click_play'); setTimeout(() => { try { rewardBtn.click(); log('[POLL][POPUP] reward confirm clicked'); } catch (e) { log('[POLL][POPUP] reward click err', e); } }, DELAY_POPUP_CONFIRM_MS); } else { log('[ENGINE] await_reward: core running... (waiting reward popup)'); } pollTimer = setTimeout(tick, 600); return; } if (!phase) { log('[ENGINE] phase empty -> recover to click_play'); setPhase('click_play'); pollTimer = setTimeout(tick, POLL_INTERVAL_MS); return; } pollTimer = setTimeout(tick, POLL_INTERVAL_MS); }; tick(); } function isGameInProgressNow() { const floors = document.querySelectorAll('[class^="floor"]'); for (const el of floors) { if (el && el.querySelector('.obj_character')) return true; } const anyEnabledDoor = document.querySelector('button.door:not([disabled])'); if (anyEnabledDoor) return true; return false; } // ================================ // UI 초기화 // ================================ function initUI() { log('[INIT] readyState=', document.readyState, 'running=', isRunning(), 'phase=', getPhase()); const startBtn = createButtonWithTooltipUI({ id: 'ui-auto-100', label: '눈사람 구출 작전 자동화 시작', bottom: 80, background: '#ff6b00', tooltip: '버튼을 클릭하면 자동화가 시작됩니다.' }); const stopBtn = createButtonWithTooltipUI({ id: 'ui-auto-stop', label: '중단하기', bottom: 40, background: '#d62828', tooltip: '진행 중인 자동화를 중단합니다.' }); startBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); log('[UI] START clicked'); gmSet(KEY_RUNNING, true); const rewardBtn = findRewardConfirmButton(); const tokenBtn = findTokenConfirmButton(); const inGame = isGameInProgressNow(); if (rewardBtn) { log('[UI] START: reward popup already present -> await_reward'); setPhase('await_reward'); } else if (tokenBtn) { log('[UI] START: token popup already present -> await_token'); setPhase('await_token'); } else if (inGame) { log('[UI] START: game already in progress -> start_core'); setPhase('start_core'); } else { log('[UI] START: normal -> click_play'); setPhase('click_play'); } ensureEngineStarted(); }); stopBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); log('[UI] STOP clicked'); stopEngineAll(); }); if (isRunning()) { const phase = getPhase(); log('[INIT] resume mode, phase=', phase); if (!phase) setPhase('click_play'); if (phase === 'await_token' && !findTokenConfirmButton()) { log('[INIT][RECOVER] phase=await_token but token popup not present -> phase=click_play'); setPhase('click_play'); } ensureEngineStarted(); } } if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initUI); } else { initUI(); } })();