您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
아이템 가격 모니터링
当前为
// ==UserScript== // @name 아이템 가격 모니터링 툴 // @namespace http://tampermonkey.net/ // @version 1.8 // @description 아이템 가격 모니터링 // @match https://*.milkywayidle.com/* // @license MIT // @grant none // ==/UserScript== (function () { 'use strict'; // mwi 객체가 로드될 때까지 대기 function waitForMwi() { return new Promise(resolve => { const interval = setInterval(() => { if (window.mwi !== undefined) { clearInterval(interval); resolve(); } }, 100); }); } waitForMwi().then(init); function init() { const refreshTime = 5000; let hoveredItem = null; // PiP 관련 캔버스/비디오 const canvas = document.createElement('canvas'); const ctx = canvas.getContext('2d'); const stream = canvas.captureStream(); const video = document.createElement('video'); video.srcObject = stream; video.muted = true; // 효과음 const soundUp = new Audio('https://cdn.pixabay.com/download/audio/2022/03/19/audio_b1e725b098.mp3?filename=beep-6-96243.mp3'); const soundDown = new Audio('https://cdn.pixabay.com/download/audio/2024/11/29/audio_04f3a2096b.mp3?filename=ui-sound-off-270300.mp3'); const soundAppear = new Audio('https://cdn.pixabay.com/download/audio/2022/03/15/audio_17cba0354b.mp3?filename=ping-82822.mp3'); const soundDisappear = new Audio('https://cdn.pixabay.com/download/audio/2022/03/10/audio_dbb9bd8504.mp3?filename=pop-39222.mp3'); // 각 효과음별 볼륨 비율 (0~1 사이 값, 필요에 따라 조절) const VOLUME_UP = 0.3; // 상승음 const VOLUME_DOWN = 0.9; // 하락음 const VOLUME_APPEAR = 0.5; // 등장음 const VOLUME_DISAPPEAR = 0.4; // 사라짐음 // 볼륨 조절 함수 function setVolume(masterVol) { soundUp.volume = masterVol * VOLUME_UP; soundDown.volume = masterVol * VOLUME_DOWN; soundAppear.volume = masterVol * VOLUME_APPEAR; soundDisappear.volume = masterVol * VOLUME_DISAPPEAR; } setVolume(1); // 패널 생성 const panel = document.createElement('div'); Object.assign(panel.style, { position: 'fixed', top: '10px', right: '10px', width: '350px', maxHeight: '1000px', overflowY: 'auto', backgroundColor: 'rgba(0,0,0,0.8)', color: 'white', fontSize: '12px', zIndex: 9999, borderRadius: '8px', boxShadow: '0 0 10px rgba(0,0,0,0.5)', display: 'flex', flexDirection: 'column', userSelect: 'none' }); // 헤더 const header = document.createElement('div'); header.textContent = '📈 Market Watch List (refresh ' + refreshTime / 1000 + 's)'; Object.assign(header.style, { backgroundColor: 'rgba(255,255,255,0.1)', padding: '6px 10px', fontWeight: 'bold', fontSize: '13px', textAlign: 'center', borderTopLeftRadius: '8px', borderTopRightRadius: '8px', borderBottom: '1px solid rgba(255,255,255,0.2)' }); // 버튼 바 const buttonBar = document.createElement('div'); Object.assign(buttonBar.style, { display: 'flex', alignItems: 'center', justifyContent: 'flex-start', // 좌측 정렬 gap: '8px', padding: '6px 10px 2px 10px', background: 'rgba(0,0,0,0.8)', border: 'none', boxShadow: 'none', marginLeft: '40px' // 좌측에서 살짝 띄워 중앙 쪽으로 이동 }); // 버튼 스타일 공통 const buttonStyle = { padding: '2px 10px', fontSize: '12px', cursor: 'pointer', backgroundColor: 'rgba(255,255,255,0.08)', border: 'none', borderRadius: '4px', color: 'white', boxShadow: 'none', transition: 'background 0.15s' }; // PiP 버튼 const pipBtn = document.createElement('button'); pipBtn.textContent = 'PiP'; Object.assign(pipBtn.style, buttonStyle); pipBtn.addEventListener('mouseenter', () => { pipBtn.style.backgroundColor = 'rgba(255,255,255,0.18)'; }); pipBtn.addEventListener('mouseleave', () => { pipBtn.style.backgroundColor = 'rgba(255,255,255,0.08)'; }); buttonBar.appendChild(pipBtn); // 숨기기/보이기 버튼 const toggleButton = document.createElement('button'); toggleButton.textContent = 'Hide'; Object.assign(toggleButton.style, buttonStyle); toggleButton.addEventListener('mouseenter', () => { toggleButton.style.backgroundColor = 'rgba(255,255,255,0.18)'; }); toggleButton.addEventListener('mouseleave', () => { toggleButton.style.backgroundColor = 'rgba(255,255,255,0.08)'; }); buttonBar.appendChild(toggleButton); // Reset 버튼 const resetBtn = document.createElement('button'); resetBtn.textContent = '⟳'; Object.assign(resetBtn.style, buttonStyle, { backgroundColor: '#b22222', // 진한 빨강 color: 'white' }); resetBtn.addEventListener('mouseenter', () => { resetBtn.style.backgroundColor = '#d9534f'; // hover시 밝은 빨강 }); resetBtn.addEventListener('mouseleave', () => { resetBtn.style.backgroundColor = '#b22222'; }); buttonBar.appendChild(resetBtn); // 소리 온/오프 버튼 let soundEnabled = true; const soundToggleBtn = document.createElement('button'); soundToggleBtn.textContent = '🔊'; Object.assign(soundToggleBtn.style, buttonStyle); soundToggleBtn.addEventListener('mouseenter', () => { soundToggleBtn.style.backgroundColor = 'rgba(255,255,255,0.18)'; }); soundToggleBtn.addEventListener('mouseleave', () => { soundToggleBtn.style.backgroundColor = 'rgba(255,255,255,0.08)'; }); buttonBar.appendChild(soundToggleBtn); // 볼륨 슬라이더 const volumeSlider = document.createElement('input'); volumeSlider.type = 'range'; volumeSlider.min = 0; volumeSlider.max = 1; volumeSlider.step = 0.01; volumeSlider.value = 1; Object.assign(volumeSlider.style, { width: '60px', verticalAlign: 'middle', marginLeft: '4px', background: 'rgba(255,255,255,0.08)', border: 'none' }); buttonBar.appendChild(volumeSlider); // Reset 버튼 기능: 이전 가격/색상 데이터 초기화 resetBtn.addEventListener('click', () => { for (const key in prevPrices) { delete prevPrices[key]; } updateCount = 0; printMarketPrices(); }); // 컬럼 헤더 const columnHeader = document.createElement('div'); columnHeader.innerHTML = ` <div style="flex: 2; text-align: center;">아이템</div> <div style="flex: 1; text-align: center;">판매</div> <div style="flex: 1; text-align: center;">구매</div> `; Object.assign(columnHeader.style, { display: 'flex', padding: '4px 10px', fontWeight: 'bold', borderBottom: '1px solid rgba(255,255,255,0.2)' }); const contentDiv = document.createElement('div'); contentDiv.style.padding = '4px 10px'; const info = document.createElement('div'); info.textContent = '* 원하는 아이템 hover 후 등록(z), 제거(c)'; Object.assign(info.style, { textAlign: 'right', fontSize: '11px', color: '#ccc', marginTop: '4px' }); panel.append(header, buttonBar, columnHeader, contentDiv, info); document.body.appendChild(panel); toggleButton.addEventListener('click', () => { if (contentDiv.style.display === 'none') { contentDiv.style.display = ''; info.style.display = ''; columnHeader.style.display = 'flex'; toggleButton.textContent = 'Hide'; } else { contentDiv.style.display = 'none'; info.style.display = 'none'; columnHeader.style.display = 'none'; toggleButton.textContent = 'Show'; } }); // 드래그 앤 드롭 기능 (헤더를 드래그) let isDragging = false, offsetX = 0, offsetY = 0; header.style.cursor = 'move'; header.addEventListener('mousedown', e => { isDragging = true; offsetX = e.clientX - panel.getBoundingClientRect().left; offsetY = e.clientY - panel.getBoundingClientRect().top; document.body.style.userSelect = 'none'; }); document.addEventListener('mousemove', e => { if (isDragging) { panel.style.left = `${e.clientX - offsetX}px`; panel.style.top = `${e.clientY - offsetY}px`; panel.style.right = 'auto'; } }); document.addEventListener('mouseup', () => { isDragging = false; document.body.style.userSelect = ''; }); // 아이템 아이콘 스프라이트 관련 let iconSprite = null; const spriteUrl = "/static/media/items_sprite.6d12eb9d.svg"; let iconSpriteText = null; let iconSpriteDoc = null; // 스프라이트 SVG fetch 및 파싱 fetch(spriteUrl).then(res => res.text()).then(svgText => { iconSpriteText = svgText; const parser = new DOMParser(); iconSpriteDoc = parser.parseFromString(svgText, "image/svg+xml"); }); // PiP 버튼 이벤트 pipBtn.addEventListener('click', async () => { await video.play(); if (document.pictureInPictureElement) { await document.exitPictureInPicture(); } else { await video.requestPictureInPicture(); } }); // Watch List 로드/저장 function loadWatchList() { const raw = localStorage.getItem("marketWatchList"); if (!raw) return []; try { return JSON.parse(raw).map(parseNameWithEnhancement); } catch (e) { console.error("marketWatchList 파싱 오류:", e); return []; } } function saveWatchList(list) { list.sort((a, b) => { if (a.name === b.name) { return a.enhancement - b.enhancement; } return a.name.localeCompare(b.name); }); localStorage.setItem("marketWatchList", JSON.stringify( list.map(({ name, enhancement }) => enhancement === 0 ? name : `${name} +${enhancement}`) )); } function parseNameWithEnhancement(str) { const match = str.match(/(.+?)\s*\+\s*(\d+)/); return match ? { name: match[1].trim(), enhancement: +match[2] } : { name: str.trim(), enhancement: 0 }; } // 가격 표시 포맷 function formatPrice(value) { if (value <= 0) return "-"; if (value >= 1e12) return (value / 1e12).toFixed(2) + "T"; if (value >= 1e9) return (value / 1e9).toFixed(2) + "B"; if (value >= 1e6) return (value / 1e6).toFixed(2) + "M"; if (value >= 1e3) return (value / 1e3).toFixed(2) + "K"; return value.toString(); } // 이전 가격/색상 저장 const prevPrices = {}; let updateCount = 0; // 마켓 가격 출력 및 PiP/패널 갱신 function printMarketPrices() { const items = loadWatchList(); if (items.length === 0) { contentDiv.innerHTML = '<div style="text-align:center;">(등록된 아이템 없음)</div>'; canvas.height = 50; drawEmptyPiP(); return; } const lines = items.map(({ name, enhancement }) => { const hrid = window.mwi?.itemNameToHridDict?.[name]; const price = hrid ? window.mwi?.marketJson?.marketData?.[hrid]?.[enhancement] : null; const key = hrid ? `${hrid}|${enhancement}` : name; // prev에 사라짐 상태와 이전값도 저장 const prev = prevPrices[key] || { ask: null, bid: null, askColor: 'white', bidColor: 'white', askGone: false, bidGone: false, askPrevValue: null, bidPrevValue: null }; let askColor = 'white', bidColor = 'white'; let askGone = prev.askGone, bidGone = prev.bidGone; let askPrevValue = prev.askPrevValue, bidPrevValue = prev.bidPrevValue; // 효과음 재생 플래그 let playUp = false, playDown = false, playAppear = false, playDisappear = false; // 처음 추가된 경우: 이전 가격 정보가 없으면 색상만 흰색, 소리 없음 if (updateCount < 2 || (prev.ask == null && prev.bid == null)) { askColor = 'white'; bidColor = 'white'; askGone = false; bidGone = false; askPrevValue = null; bidPrevValue = null; // 소리 플래그는 모두 false } else { // 기존의 가격 변화 감지 로직 (updateCount >= 2) // ASK if (price && typeof price.a === 'number' && price.a > 0) { if (prev.ask == null || prev.ask <= 0) { askColor = '#00ff00'; playAppear = true; } else if (price.a > prev.ask) { askColor = '#d5443d'; playUp = true; } else if (price.a < prev.ask) { askColor = '#0ff'; playDown = true; } else { askColor = prev.askColor || 'white'; } askGone = false; askPrevValue = null; } else if (prev.ask != null && prev.ask > 0 && (!price || price.a <= 0)) { askColor = '#ffb300'; askGone = true; askPrevValue = prev.ask; playDisappear = true; } else if (prev.askGone && prev.askPrevValue) { askColor = '#ffb300'; askGone = true; askPrevValue = prev.askPrevValue; } else { askColor = 'white'; askGone = false; askPrevValue = null; } // BID if (price && typeof price.b === 'number' && price.b > 0) { if (prev.bid == null || prev.bid <= 0) { bidColor = '#00ff00'; playAppear = true; } else if (price.b > prev.bid) { bidColor = '#d5443d'; playUp = true; } else if (price.b < prev.bid) { bidColor = '#0ff'; playDown = true; } else { bidColor = prev.bidColor || 'white'; } bidGone = false; bidPrevValue = null; } else if (prev.bid != null && prev.bid > 0 && (!price || price.b <= 0)) { bidColor = '#ffb300'; bidGone = true; bidPrevValue = prev.bid; playDisappear = true; } else if (prev.bidGone && prev.bidPrevValue) { bidColor = '#ffb300'; bidGone = true; bidPrevValue = prev.bidPrevValue; } else { bidColor = 'white'; bidGone = false; bidPrevValue = null; } } // 효과음 재생 (한 번만) if (soundEnabled) { if (playUp) soundUp.play(); if (playDown) soundDown.play(); if (playAppear) soundAppear.play(); if (playDisappear) soundDisappear.play(); } // 사라진 상태와 이전값을 prevPrices에 저장 prevPrices[key] = { ask: price && typeof price.a === 'number' ? price.a : null, bid: price && typeof price.b === 'number' ? price.b : null, askColor, bidColor, askGone, bidGone, askPrevValue, bidPrevValue }; return { hrid, name, enhancement, ask: askGone ? `<span style="color:#ffb300;text-decoration:line-through">${formatPrice(askPrevValue)}</span>` : (price ? formatPrice(price.a) : '❌'), bid: bidGone ? `<span style="color:#ffb300;text-decoration:line-through">${formatPrice(bidPrevValue)}</span>` : (price ? formatPrice(price.b) : '❌'), askColor, bidColor, askGone, bidGone }; }); updateCount++; updatePanel(lines); updatePiPCanvas(lines); } // 패널 UI 갱신 function updatePanel(lines) { contentDiv.innerHTML = ''; lines.forEach(({ hrid, name, enhancement, ask, bid, askColor, bidColor, askGone, bidGone }) => { const row = document.createElement('div'); row.style.display = 'flex'; row.style.padding = '2px 0'; row.style.cursor = 'pointer'; let iconName = hrid ? hrid.split("/")[2] : ''; row.innerHTML = ` <svg width="15px" height="15px" style="display:inline-block; margin-right: 2px"> <use href="/static/media/items_sprite.6d12eb9d.svg#${iconName}"></use></svg> <div style="flex:2;">${name}${enhancement > 0 ? ' +' + enhancement : ''}</div> <div style="flex:1; text-align:right; color:${askColor}">${askGone ? ask : ask}</div> <div style="flex:1; text-align:right; color:${bidColor}">${bidGone ? bid : bid}</div>`; row.addEventListener('mouseenter', () => { row.style.backgroundColor = 'rgba(255,255,255,0.1)'; hoveredItem = { name, enhancement }; }); row.addEventListener('mouseleave', () => { row.style.backgroundColor = 'transparent'; hoveredItem = null; }); row.addEventListener('click', () => { window.mwi?.game?.handleGoToMarketplace(hrid, enhancement); }); contentDiv.appendChild(row); }); } // PiP 캔버스 갱신 function updatePiPCanvas(lines) { const rowHeight = 20; canvas.width = 350; canvas.height = rowHeight * lines.length + 10; ctx.fillStyle = 'black'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.font = '14px sans-serif'; lines.forEach((line, index) => { const y = 20 + index * rowHeight; // 아이템명 + 강화 ctx.textAlign = 'left'; ctx.fillStyle = 'white'; ctx.fillText(`${line.name}${line.enhancement > 0 ? ' +' + line.enhancement : ''}`, 30, y); // 판매가 (ask) ctx.textAlign = 'center'; if (line.askGone) { ctx.fillStyle = '#ffb300'; const priceText = formatPrice(prevPrices[`${line.hrid}|${line.enhancement}`]?.askPrevValue); ctx.fillText(priceText, 250, y); // 취소선 그리기 const textWidth = ctx.measureText(priceText).width; ctx.save(); ctx.strokeStyle = '#ffb300'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(250 - textWidth / 2, y - 6); ctx.lineTo(250 + textWidth / 2, y - 6); ctx.stroke(); ctx.restore(); } else { ctx.fillStyle = line.askColor || 'white'; ctx.fillText(line.ask, 250, y); } // 구매가 (bid) ctx.textAlign = 'right'; if (line.bidGone) { ctx.fillStyle = '#ffb300'; const priceText = formatPrice(prevPrices[`${line.hrid}|${line.enhancement}`]?.bidPrevValue); ctx.fillText(priceText, canvas.width - 10, y); // 취소선 그리기 const textWidth = ctx.measureText(priceText).width; ctx.save(); ctx.strokeStyle = '#ffb300'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(canvas.width - 10 - textWidth, y - 6); ctx.lineTo(canvas.width - 10, y - 6); ctx.stroke(); ctx.restore(); } else { ctx.fillStyle = line.bidColor || 'white'; ctx.fillText(line.bid, canvas.width - 10, y); } // 아이콘 ctx.fillStyle = 'white'; const iconName = line.hrid ? line.hrid.split("/")[2] : ''; const img = getIconImage(iconName); if (img.complete) { ctx.drawImage(img, 7, y - 12, 14, 14); } else { img.onload = () => { ctx.drawImage(img, 7, y - 12, 14, 14); }; } }); } // 아이콘 SVG 추출 및 변환 const iconCache = {}; function getIconImage(iconName) { if (iconCache[iconName]) return iconCache[iconName]; if (!iconSpriteDoc) return new Image(); // 아직 로드 안됨 const symbol = iconSpriteDoc.getElementById(iconName); if (!symbol) return new Image(); const svgElem = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svgElem.setAttribute("xmlns", "http://www.w3.org/2000/svg"); svgElem.setAttribute("width", "15"); svgElem.setAttribute("height", "15"); if (symbol.hasAttribute("viewBox")) { svgElem.setAttribute("viewBox", symbol.getAttribute("viewBox")); } else { svgElem.setAttribute("viewBox", "0 0 15 15"); } for (const child of symbol.children) { svgElem.appendChild(child.cloneNode(true)); } const svgString = new XMLSerializer().serializeToString(svgElem); const blob = new Blob([svgString], { type: 'image/svg+xml' }); const url = URL.createObjectURL(blob); const img = new Image(); img.src = url; iconCache[iconName] = img; return img; } // PiP에 아이템 없음 표시 function drawEmptyPiP() { ctx.fillStyle = 'black'; ctx.fillRect(0, 0, canvas.width, canvas.height); ctx.fillStyle = 'white'; ctx.font = '14px sans-serif'; ctx.fillText('(등록된 아이템 없음)', 10, 25); } // 온오프 버튼 이벤트 soundToggleBtn.addEventListener('click', () => { soundEnabled = !soundEnabled; soundToggleBtn.textContent = soundEnabled ? '🔊' : '🔇'; volumeSlider.style.display = soundEnabled ? 'inline-block' : 'none'; }); // 볼륨 슬라이더 이벤트 volumeSlider.addEventListener('input', () => { setVolume(Number(volumeSlider.value)); }); // 슬라이더는 소리 꺼짐일 때 숨김 if (!soundEnabled) volumeSlider.style.display = 'none'; // 단축키: z(등록), c(제거) document.addEventListener('keydown', e => { const itemKey = getItemKey(); const list = loadWatchList(); const keySet = list.map(i => i.enhancement === 0 ? i.name : `${i.name} +${i.enhancement}`); if (e.key === 'z' && itemKey) { if (!keySet.includes(itemKey)) { const parsed = parseNameWithEnhancement(itemKey); list.push(parsed); saveWatchList(list); printMarketPrices(); } } if (e.key === 'c') { let targetKey = itemKey; if (!targetKey && hoveredItem) { targetKey = hoveredItem.enhancement === 0 ? hoveredItem.name : `${hoveredItem.name} +${hoveredItem.enhancement}`; } if (!targetKey) return; if (keySet.includes(targetKey)) { const updated = list.filter(i => (i.enhancement === 0 ? i.name : `${i.name} +${i.enhancement}`) !== targetKey); saveWatchList(updated); printMarketPrices(); } } }); // 주기적 갱신 setInterval(printMarketPrices, refreshTime); } // 아이템 키 추출 function getItemKey() { const modal = document.querySelector('.MuiPopper-root'); if (!modal) return null; const nameEl = modal.querySelector('.ItemTooltipText_name__2JAHA'); const detail = modal.querySelector('.ItemTooltipText_equipmentDetail__3sIHT'); const name = nameEl ? nameEl.textContent.trim() : null; if (!name) return null; if (detail) { const enhance = [...detail.querySelectorAll('span')] .map(e => e.textContent.trim()) .find(t => /^\+\d+$/.test(t)); const enhancement = enhance ? +enhance.replace('+', '') : 0; return enhancement === 0 ? name : `${name} +${enhancement}`; } else { return name; } } })();