您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
아이템 가격 모니터링
当前为
// ==UserScript== // @name 아이템 가격 모니터링 툴 // @namespace http://tampermonkey.net/ // @version 1.4 // @description 아이템 가격 모니터링 // @match https://*.milkywayidle.com/* // @license MIT // @grant none // ==/UserScript== (function () { 'use strict'; 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'); // 패널 생성 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 pipBtn = document.createElement('button'); pipBtn.textContent = 'PiP'; Object.assign(pipBtn.style, { marginLeft: '10px', padding: '2px 6px', fontSize: '11px', cursor: 'pointer', backgroundColor: 'rgba(255,255,255,0.2)', border: 'none', borderRadius: '4px', color: 'white' }); header.appendChild(pipBtn); 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, columnHeader, contentDiv, info); document.body.appendChild(panel); const toggleButton = document.createElement('button'); toggleButton.textContent = 'Hide'; Object.assign(toggleButton.style, { marginLeft: '10px', padding: '2px 6px', fontSize: '11px', cursor: 'pointer', backgroundColor: 'rgba(255,255,255,0.2)', border: 'none', borderRadius: '4px', color: 'white' }); header.appendChild(toggleButton); toggleButton.addEventListener('click', () => { if (contentDiv.style.display === 'none') { contentDiv.style.display = ''; info.style.display = ''; columnHeader.style.display = 'flex'; header.style.borderBottom = '1px solid rgba(255,255,255,0.2)'; toggleButton.textContent = 'Hide'; } else { contentDiv.style.display = 'none'; info.style.display = 'none'; columnHeader.style.display = 'none'; header.style.borderBottom = ''; toggleButton.textContent = 'Show'; } }); // 2. 드래그 앤 드롭 기능 추가 (헤더를 드래그) 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; // sprite SVG fetch 및 파싱 fetch(spriteUrl).then(res => res.text()).then(svgText => { iconSpriteText = svgText; const parser = new DOMParser(); iconSpriteDoc = parser.parseFromString(svgText, "image/svg+xml"); }); pipBtn.addEventListener('click', async () => { await video.play(); if (document.pictureInPictureElement) { await document.exitPictureInPicture(); } else { await video.requestPictureInPicture(); } }); 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; 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; const prev = prevPrices[key] || { ask: null, bid: null, askColor: 'white', bidColor: 'white' }; let askColor = 'white', bidColor = 'white'; // 효과음 재생 플래그 let playUp = false, playDown = false, playAppear = false, playDisappear = false; if (updateCount >= 2) { // ASK if (price && typeof price.a === 'number' && price.a > 0) { if (prev.ask == null || prev.ask <= 0) { askColor = 'green'; playAppear = true; } else if (price.a > prev.ask) { askColor = 'red'; playUp = true; } else if (price.a < prev.ask) { askColor = 'blue'; playDown = true; } else { askColor = prev.askColor || 'white'; } } else if (prev.ask != null && prev.ask > 0 && (!price || price.a <= 0)) { askColor = 'yellow'; playDisappear = true; } else { askColor = 'white'; } // BID if (price && typeof price.b === 'number' && price.b > 0) { if (prev.bid == null || prev.bid <= 0) { bidColor = 'green'; playAppear = true; } else if (price.b > prev.bid) { bidColor = 'red'; playUp = true; } else if (price.b < prev.bid) { bidColor = 'blue'; playDown = true; } else { bidColor = prev.bidColor || 'white'; } } else if (prev.bid != null && prev.bid > 0 && (!price || price.b <= 0)) { bidColor = 'yellow'; playDisappear = true; } else { bidColor = 'white'; } } // 효과음 재생 (한 번만) if (playUp) soundUp.play(); if (playDown) soundDown.play(); if (playAppear) soundAppear.play(); if (playDisappear) soundDisappear.play(); prevPrices[key] = { ask: price && typeof price.a === 'number' ? price.a : null, bid: price && typeof price.b === 'number' ? price.b : null, askColor, bidColor }; return { hrid, name, enhancement, ask: price ? formatPrice(price.a) : '❌', bid: price ? formatPrice(price.b) : '❌', askColor, bidColor }; }); updateCount++; updatePanel(lines); updatePiPCanvas(lines); } function updatePanel(lines) { contentDiv.innerHTML = ''; lines.forEach(({ hrid, name, enhancement, ask, bid, askColor, bidColor }) => { 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}">${ask}</div> <div style="flex:1; text-align:right; color:${bidColor}">${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); }); } 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'; ctx.fillStyle = line.askColor || 'white'; ctx.fillText(line.ask, 250, y); // 구매가 (bid) ctx.textAlign = 'right'; 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); }; } }); } const iconCache = {}; // 아이콘 개별 SVG 추출 및 변환 function getIconImage(iconName) { if (iconCache[iconName]) return iconCache[iconName]; if (!iconSpriteDoc) return new Image(); // 아직 로드 안됨 // symbol 추출 const symbol = iconSpriteDoc.getElementById(iconName); if (!symbol) return new Image(); // 새 SVG 문서 생성 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"); // viewBox 복사 (symbol에 있으면) if (symbol.hasAttribute("viewBox")) { svgElem.setAttribute("viewBox", symbol.getAttribute("viewBox")); } else { svgElem.setAttribute("viewBox", "0 0 15 15"); } // symbol의 child 복사 for (const child of symbol.children) { svgElem.appendChild(child.cloneNode(true)); } // SVG 문자열로 변환 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; } 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); } 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; } } })();