// ==UserScript== // @name Pokechill精灵获取图鉴 // @namespace http://tampermonkey.net/ // @version 1.1 // @description 在游戏界面显示所有未捕获但可获得的宝可梦列表,包含精确获取方式(进化、商店、野生区域等),点击行可查看详情。 // @author 人民当家做主 // @match https://play-pokechill.github.io/* // @match https://g1tyx.github.io/play-pokechill/* // @grant none // ==/UserScript== (function() { 'use strict'; // ---------- 配置 ---------- const EXCLUDE_IDS = new Set([ 'arceus', 'burmySandy', 'burmyTrash', 'galarianMrmime', 'kyuremWhite', 'megaAbsol', 'megaAltaria', 'megaAmpharos', 'megaAudino', 'megaBanette', 'megaGardevoir', 'megaHoundoom', 'megaKangaskhan', 'megaLopunny', 'megaMedicham', 'megaRayquaza', 'megaSableye', 'megaSalamence', 'megaSharpedo', 'megaSlowbro', 'wormadamSandy', 'wormadamTrash' ]); // 辅助翻译:优先从汉化字典获取,否则返回原词(先格式化) function t(english) { if (!english) return english; const formatted = typeof window.format === 'function' ? window.format(english) : english; return (window.EN_CN_DICT && window.EN_CN_DICT[formatted]) || formatted; } // 获取中文名称 function getChineseName(key) { return t(key); } // 判断区域是否允许捕获野生宝可梦 function isAreaCatchable(area) { if (area.uncatchable === true) return false; if (area.type === 'dungeon') return false; if (area.trainer === true) return false; return true; } // ---------- 获取详细获取方式 ---------- function getAcquisitionMethod(id) { const p = pkmn[id]; if (!p) return '未知'; const sources = []; // 1. 进化获得(反向查找) const preEvoList = []; for (const key in pkmn) { const candidate = pkmn[key]; if (candidate.evolve) { const evoObj = candidate.evolve(); for (const slot in evoObj) { if (evoObj[slot].pkmn === p) { if (!candidate.hidden) { let condition = ''; if (evoObj[slot].level) { condition = `在等级 ${evoObj[slot].level} `; } if (evoObj[slot].item) { const itemId = evoObj[slot].item.id; const itemName = getChineseName(itemId); condition += `使用 ${itemName} `; } condition += '进化获得'; preEvoList.push(`由 ${getChineseName(candidate.id)} ${condition}`); } } } } } if (preEvoList.length > 0) { sources.push(...preEvoList); } // 2. 商店购买 if (typeof shop !== 'undefined') { for (const key in shop) { if (shop[key].pkmn === id) { sources.push(t('Poke-Mart') + '购买'); break; } } } // 3. 遍历所有区域(野生、事件、开拓区奖励等) if (typeof areas !== 'undefined') { for (const areaId in areas) { const area = areas[areaId]; let areaName = area.name || areaId; const areaNameCN = getChineseName(areaName); let prefix = ''; if (area.type === 'wild') prefix = t('Wild Area') + '--'; else if (area.type === 'dungeon') prefix = t('Dungeons') + '--'; else if (area.type === 'event') prefix = t('Events') + '--'; else if (area.type === 'frontier') prefix = t('Battle Frontier') + '--'; else if (area.type === 'dimension') prefix = t('Mega Dimension') + '--'; else if (area.type === 'season') prefix = '季节活动--'; else if (area.type === 'vs' && area.name) prefix = t('VS') + ' ' + t('Trainer') + ' '; let rotationSuffix = ''; if (area.rotation !== undefined) { rotationSuffix = ` (${t('Rotation')}${area.rotation})`; } // 3a. spawns if (area.spawns && isAreaCatchable(area)) { for (const rarity in area.spawns) { const list = area.spawns[rarity]; if (Array.isArray(list) && list.some(p => p.id === id)) { const rarityCN = { common: t('common'), uncommon: t('uncommon'), rare: t('rare') }[rarity] || rarity; sources.push(`${prefix}${areaNameCN}${rotationSuffix} (${rarityCN})`); } } } // 3b. reward if (area.reward) { const rewardList = area.reward; if (Array.isArray(rewardList) && rewardList.some(p => p.id === id)) { sources.push(`${prefix}${area.name ? areaNameCN + ' ' + t('Reward') : areaNameCN + ' ' + t('Reward')}`); } } // 3c. itemReward if (area.itemReward) { for (const key in area.itemReward) { const reward = area.itemReward[key]; if (reward.item === id) { sources.push(`${prefix}${area.name ? areaNameCN + ' ' + t('Reward') : areaNameCN + ' ' + t('Reward')}`); } } } } } // 4. 开拓区专属列表 if (typeof exclusiveFrontierPkmn !== 'undefined' && exclusiveFrontierPkmn.includes(p)) { sources.push(t('Battle Frontier') + '随机奖励'); } // 5. 野生公园 if (typeof areas !== 'undefined' && areas.wildlifePark) { const park = areas.wildlifePark; if (park.spawns) { for (const rarity in park.spawns) { const list = park.spawns[rarity]; if (Array.isArray(list) && list.some(p => p.id === id)) { const rarityCN = { common: t('common'), uncommon: t('uncommon'), rare: t('rare') }[rarity] || rarity; sources.push(`野生公园 (${rarityCN})`); } } } } // 6. 去重返回 if (sources.length > 0) { const unique = [...new Set(sources)]; return unique.join(';'); } // 7. 回退到基础标签 const tag = p.tagObtainedIn; const map = { 'wild': t('Wild Area'), 'event': t('Events'), 'park': '野生公园', 'frontier': t('Battle Frontier'), 'mart': t('Poke-Mart'), 'unobtainable': '不可获取', 'arceus': '阿尔宙斯解锁' }; return map[tag] || '其他'; } // ---------- 收集未捕获数据 ---------- function fetchMissing() { const list = []; for (const id in pkmn) { const p = pkmn[id]; if (EXCLUDE_IDS.has(id)) continue; if (p.hidden) continue; if (p.caught === 0 && p.tagObtainedIn !== 'unobtainable') { list.push({ id, name: getChineseName(id), method: getAcquisitionMethod(id) }); } } list.sort((a, b) => a.id.localeCompare(b.id)); return list; } function filterData(data, keyword) { if (!keyword) return data; const lower = keyword.toLowerCase(); return data.filter(item => item.id.toLowerCase().includes(lower) || item.name.toLowerCase().includes(lower) || item.method.toLowerCase().includes(lower) ); } // ---------- 面板更新 ---------- let currentData = []; let filterText = ''; let panel = null; let tbodyRef = null; let statsCountRef = null; let panelCollapsed = false; function updateDisplay() { if (!tbodyRef || !statsCountRef) return; const filtered = filterData(currentData, filterText); statsCountRef.textContent = filtered.length; const rowsHtml = filtered.map(item => ` ${item.id} ${item.name} ${item.method} `).join('') || '🎉 没有匹配的宝可梦'; tbodyRef.innerHTML = rowsHtml; // 点击行显示详情(带延迟关闭) tbodyRef.querySelectorAll('tr[data-pkmn]').forEach(row => { row.addEventListener('click', (e) => { const id = row.dataset.pkmn; if (typeof tooltipData === 'function') { if (typeof closeTooltip === 'function') { closeTooltip(); setTimeout(() => tooltipData('pkmn', id), 200); } else { tooltipData('pkmn', id); } } else { alert(`宝可梦: ${id}\n获取方式: ${getAcquisitionMethod(id)}`); } }); }); } function toggleCollapse() { panelCollapsed = !panelCollapsed; const content = panel.querySelector('.panel-content'); const footer = panel.querySelector('.footer'); const toggleBtn = panel.querySelector('.collapse-btn'); if (panelCollapsed) { content.style.display = 'none'; footer.style.display = 'none'; toggleBtn.innerHTML = '▶'; } else { content.style.display = 'block'; footer.style.display = 'block'; toggleBtn.innerHTML = '▼'; } } // ---------- 样式 ---------- const style = document.createElement('style'); style.textContent = ` #pkmn-missing-panel { position: fixed; bottom: 20px; right: 20px; width: 500px; max-width: calc(100vw - 40px); background: rgba(28, 28, 35, 0.96); backdrop-filter: blur(8px); border: 1px solid rgba(120, 120, 150, 0.3); border-radius: 16px; box-shadow: 0 12px 28px rgba(0, 0, 0, 0.6); color: #f0f0f0; font-family: 'Segoe UI', Roboto, system-ui, sans-serif; font-size: 13px; z-index: 9999; resize: both; overflow: hidden; user-select: none; transition: opacity 0.2s, box-shadow 0.2s; letter-spacing: 0.3px; } @media (max-width: 600px) { #pkmn-missing-panel { left: 10px !important; right: 10px !important; bottom: 10px !important; width: auto !important; max-width: none; font-size: 12px; } .id-col { width: 70px; } .name-col { width: 90px; } .method-col { max-width: 180px; } .panel-header { font-size: 14px; padding: 8px 12px; } .panel-content { padding: 12px; max-height: 400px; } .stats { padding: 6px 12px; font-size: 13px; } .stats .count { font-size: 22px; } .search-box { padding: 8px 12px; font-size: 13px; } th, td { padding: 6px 4px; } } #pkmn-missing-panel.dragging { opacity: 0.85; box-shadow: 0 18px 32px rgba(0, 0, 0, 0.7); transition: none; } .panel-header { padding: 10px 16px; background: linear-gradient(145deg, #2f2f3c, #23232e); border-radius: 16px 16px 0 0; cursor: move; display: flex; justify-content: space-between; align-items: center; font-weight: 600; border-bottom: 1px solid #45455a; color: #f0e6d0; font-size: 15px; } .panel-header-left { display: flex; align-items: center; gap: 8px; } .panel-header-left span { display: flex; align-items: center; gap: 6px; } .panel-header-left span::before { content: "📋"; font-size: 18px; filter: drop-shadow(0 2px 3px #00000055); } .collapse-btn { background: #3a3a4c; border: none; color: #ddd; font-size: 16px; cursor: pointer; padding: 4px 8px; border-radius: 20px; transition: background 0.2s, color 0.2s; line-height: 1; box-shadow: 0 2px 4px #00000033; width: 32px; text-align: center; } .collapse-btn:hover { background: #5a5a78; color: white; } .panel-header button:not(.collapse-btn) { background: #3a3a4c; border: none; color: #ddd; font-size: 16px; cursor: pointer; padding: 4px 8px; border-radius: 20px; transition: background 0.2s, color 0.2s; line-height: 1; margin-left: 6px; box-shadow: 0 2px 4px #00000033; } .panel-header button:not(.collapse-btn):hover { background: #5a5a78; color: white; } .panel-content { padding: 16px; max-height: 520px; overflow-y: auto; scrollbar-width: thin; scrollbar-color: #6a6a8a #2d2d3a; transition: max-height 0.2s ease; } .panel-content::-webkit-scrollbar { width: 8px; } .panel-content::-webkit-scrollbar-track { background: #2d2d3a; border-radius: 8px; } .panel-content::-webkit-scrollbar-thumb { background: #6a6a8a; border-radius: 8px; border: 2px solid #2d2d3a; } .stats { background: #1d1d28; border-radius: 28px; padding: 10px 16px; margin-bottom: 16px; text-align: center; font-size: 15px; border: 1px solid #3e3e52; box-shadow: inset 0 2px 5px #00000033; display: flex; justify-content: center; align-items: center; gap: 6px; } .stats .count { color: #ffd966; font-weight: 700; font-size: 26px; line-height: 1; text-shadow: 0 0 10px #ffb34755; } .search-box { width: 100%; padding: 10px 16px; margin-bottom: 16px; background: #21212e; border: 1px solid #4b4b64; border-radius: 40px; color: #f0f0f0; font-size: 14px; outline: none; box-sizing: border-box; transition: border-color 0.2s, box-shadow 0.2s; box-shadow: 0 2px 6px #00000033; } .search-box::placeholder { color: #8888a0; font-style: italic; } .search-box:focus { border-color: #9a9aff; box-shadow: 0 0 12px #5a5aff66; } .refresh-btn { background: #3f4d7a; border: none; color: white; padding: 5px 12px; border-radius: 30px; cursor: pointer; font-size: 13px; font-weight: 500; transition: background 0.2s, transform 0.1s; box-shadow: 0 3px 6px #00000044; margin-left: 8px; } .refresh-btn:hover { background: #5f73b0; transform: scale(1.02); } table { width: 100%; border-collapse: collapse; background: #1a1a24; border-radius: 16px; overflow: hidden; box-shadow: 0 4px 12px #00000055; } th { background: #2c2c3c; padding: 10px 8px; text-align: left; font-weight: 600; color: #c0c0e0; font-size: 13px; border-bottom: 1px solid #4a4a60; } td { padding: 8px 8px; border-top: 1px solid #333342; cursor: pointer; transition: background 0.15s; } tr:hover td { background: #2f2f42; } .id-col { color: #b0d0ff; font-family: 'JetBrains Mono', 'Fira Code', monospace; font-size: 12px; width: 85px; } .name-col { color: #f0f0f0; font-weight: 500; width: 110px; } .method-col { color: #c0e6b0; max-width: 280px; white-space: normal; word-break: break-word; } .footer { padding: 10px; text-align: center; font-size: 11px; color: #7a7a90; border-top: 1px solid #2e2e3e; background: #1b1b26; } `; document.head.appendChild(style); // ---------- 创建面板 ---------- if (document.getElementById('pkmn-missing-panel')) { document.getElementById('pkmn-missing-panel').remove(); } panel = document.createElement('div'); panel.id = 'pkmn-missing-panel'; panel.innerHTML = `
可获取未捕捉宝可梦
0 只(已排除隐藏宝可梦)(首次加载请点击右侧刷新按钮)
编号名称获取方式
`; document.body.appendChild(panel); // 获取 DOM 引用 tbodyRef = panel.querySelector('tbody'); statsCountRef = panel.querySelector('.stats .count'); const searchInput = panel.querySelector('.search-box'); const refreshBtn = panel.querySelector('.refresh-btn'); const closeBtn = panel.querySelector('.close-btn'); const collapseBtn = panel.querySelector('.collapse-btn'); const header = panel.querySelector('.panel-header'); // 加载初始数据 currentData = fetchMissing(); filterText = ''; updateDisplay(); // 事件绑定 collapseBtn.addEventListener('click', (e) => { e.stopPropagation(); toggleCollapse(); }); let debounceTimer; searchInput.addEventListener('input', (e) => { clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { filterText = e.target.value; updateDisplay(); }, 200); }); refreshBtn.addEventListener('click', () => { currentData = fetchMissing(); filterText = ''; searchInput.value = ''; updateDisplay(); }); closeBtn.addEventListener('click', () => { panel.remove(); }); // ---------- 拖拽功能(支持触摸)---------- let isDragging = false; let offsetX, offsetY; header.addEventListener('mousedown', (e) => { if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT') return; isDragging = true; panel.classList.add('dragging'); const rect = panel.getBoundingClientRect(); offsetX = e.clientX - rect.left; offsetY = e.clientY - rect.top; }); header.addEventListener('touchstart', (e) => { if (e.target.tagName === 'BUTTON' || e.target.tagName === 'INPUT') return; isDragging = true; panel.classList.add('dragging'); const rect = panel.getBoundingClientRect(); const touch = e.touches[0]; offsetX = touch.clientX - rect.left; offsetY = touch.clientY - rect.top; e.preventDefault(); }, { passive: false }); document.addEventListener('mousemove', (e) => { if (!isDragging) return; e.preventDefault(); const left = e.clientX - offsetX; const top = e.clientY - offsetY; panel.style.left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, left)) + 'px'; panel.style.top = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, top)) + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }); document.addEventListener('touchmove', (e) => { if (!isDragging) return; e.preventDefault(); const touch = e.touches[0]; const left = touch.clientX - offsetX; const top = touch.clientY - offsetY; panel.style.left = Math.max(0, Math.min(window.innerWidth - panel.offsetWidth, left)) + 'px'; panel.style.top = Math.max(0, Math.min(window.innerHeight - panel.offsetHeight, top)) + 'px'; panel.style.right = 'auto'; panel.style.bottom = 'auto'; }, { passive: false }); document.addEventListener('mouseup', () => { isDragging = false; panel.classList.remove('dragging'); }); document.addEventListener('touchend', () => { isDragging = false; panel.classList.remove('dragging'); }); })();