// ==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 = `
`;
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');
});
})();