// ==UserScript==
// @name MSU 寵物技能快快出
// @namespace http://tampermonkey.net/
// @version 0.85
// @author Alex from MyGOTW
// @description 擷取 MSU.io 寵物技能
// @match https://msu.io/marketplace/*categories=1000400000*
// @grant none
// @run-at document-end
// @license MIT
// ==/UserScript==
(function() {
'use strict';
// 追蹤已處理過的 tokenID
const processedTokens = new Set();
// Pet skill img
const petImg = {
'Item Pouch': 'https://cdn.wikimg.net/en/strategywiki/images/8/87/MS_Pet_Item_only.png',
'NESO Magnet': 'https://msu.io/marketplace/images/neso.png',
'Auto HP Potion Pouch': 'https://cdn.wikimg.net/en/strategywiki/images/2/22/MS_Pet_Autopot.png',
'Auto MP Potion Pouch': 'https://cdn.wikimg.net/en/strategywiki/images/f/f9/MS_Pet_MP_Recharge.png',
'Auto Move': 'https://cdn.wikimg.net/en/strategywiki/images/2/27/MS_Pet_Autoloot.png',
'Expanded Auto Move': 'https://cdn.wikimg.net/en/strategywiki/images/2/2a/MS_Pet_Range.png',
'Fatten Up': 'https://github.com/aliceric27/picx-images-hosting/raw/master/hexo-blog/image.8vmyjueg5g.webp',
'Auto Buff': 'https://github.com/aliceric27/picx-images-hosting/raw/master/hexo-blog/image.1hs9b2tx0k.webp',
'Pet Training Skill': 'https://cdn.wikimg.net/en/strategywiki/images/3/34/MS_Pet_Unlootable_Item.png',
'Magnet Effect': '🧲'
}
// 定義要篩選的技能
const skillTranslations = {
'Item Pouch': `撿取道具`,
'NESO Magnet': `撿取NESO`,
'Auto HP Potion Pouch': `自動HP藥水`,
'Auto MP Potion Pouch': `自動MP藥水`,
'Auto Move': `自動移動`,
'Expanded Auto Move': `擴大自動移動範圍`,
'Fatten Up': `寵物巨大化`,
'Auto Buff': `自動上Buff`,
'Pet Training Skill': `親密度提升`,
'Magnet Effect': `磁力效果(P寵)`
};
function getskillImg(skill) {
if (skill === 'Magnet Effect') {
return '🧲';
}
return `<img src="${petImg[skill]}" alt="${skill}" width="24">` || skill;
}
// 新增翻譯函數
function translateSkill(skill) {
return skillTranslations[skill] || skill;
}
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const [resource, config] = args;
// 檢查是否是目標 API 請求
if (resource.includes('/marketplace/api/marketplace/explore/items')) {
console.log('監聽到 API 請求:', {
url: resource,
body: config ? JSON.parse(config.body) : null
});
try {
const response = await originalFetch(resource, config);
const clone = response.clone();
clone.json().then(async data => {
console.log('物品資料:', data);
if (data.items) {
let AllToken = [];
let allData = [];
const storedData = getFromStorage() || {};
// 先處理已存儲的資料
for (const item of data.items) {
const tokenId = item.tokenId;
if (storedData[tokenId]) {
const storedItem = storedData[tokenId];
if (storedItem.item && storedItem.item.pet) {
const petSkills = storedItem.item.pet.petSkills || [];
const mintingNo = storedItem.tokenInfo?.mintingNo;
const fullPetName = `${storedItem.item.name} #${mintingNo}`;
await tryFindAndInsertSkills(fullPetName, petSkills);
}
processedTokens.add(tokenId);
}
}
// 篩選出未處理過且未存儲的 tokenID
AllToken = data.items
.map(item => item.tokenId)
.filter(tokenId => !processedTokens.has(tokenId) && !storedData[tokenId]);
console.log('未處理的 Token:', AllToken);
// 對每個未處理的 tokenID 發送 API 請求
const fetchPromises = AllToken.map(async (tokenId) => {
try {
console.log('開始抓資料啦');
await delay(50);
const response = await originalFetch(`https://msu.io/marketplace/api/marketplace/items/${tokenId}`);
const itemData = await response.json();
allData.push(itemData);
storedData[tokenId] = itemData;
if (itemData.item && itemData.item.pet) {
const petSkills = itemData.item.pet.petSkills || [];
const mintingNo = itemData.tokenInfo?.mintingNo;
const fullPetName = `${itemData.item.name} #${mintingNo}`;
await tryFindAndInsertSkills(fullPetName, petSkills);
}
processedTokens.add(tokenId);
} catch (error) {
console.error(`無法獲取 tokenID ${tokenId} 的資料:`, error);
}
});
// 等待所有請求完成
await Promise.all(fetchPromises);
// 儲存更新後的資料
saveToStorage(storedData);
console.log('allData', allData);
// 在所有資料處理完後,創建或更新過濾面板
await delay(500); // 等待DOM更新
if (!document.querySelector('.skill-filter')) {
createFilterPanel();
}
// 更新頁面上的技能資訊
updateSkillsOnPage();
}
});
return response;
} catch (error) {
console.error('請求錯誤:', error);
throw error;
}
}
return originalFetch(resource, config);
};
// 新增延遲函數
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
// 新增重試函數
async function tryFindAndInsertSkills(fullPetName, petSkills, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
// 使用 msuui-tr 選擇器
const allRows = document.querySelectorAll('.msuui-tr');
let found = false;
for (const row of allRows) {
// 在每個 row 中尋找包含寵物名稱的 span
const nameSpan = row.querySelector('span');
if (nameSpan && nameSpan.textContent.includes(fullPetName)) {
// 找到包含寵物資訊的第二個 msuui-td
const infoCell = row.querySelector('.msuui-td:nth-child(2)');
if (infoCell) {
// 找到包含寵物名稱的 div 的下一個 div
const targetDiv = infoCell.querySelector('div > div:nth-child(2)');
const existingSkills = targetDiv?.querySelector('.pet-skills-info');
if (!existingSkills && targetDiv) {
const skillsContainer = document.createElement('div');
skillsContainer.className = 'pet-skills-info';
skillsContainer.style.cssText = `
border-radius: 5px;
padding: 5px;
margin-top: 5px;
z-index: 1;
`;
const { basic, special } = categorizeSkills(petSkills);
// 基礎技能行
const basicRow = document.createElement('div');
basicRow.style.marginBottom = special.length ? '4px' : '0';
basic.forEach(skill => basicRow.appendChild(createSkillElement(skill)));
skillsContainer.appendChild(basicRow);
// 特殊技能行
if (special.length > 0) {
const specialRow = document.createElement('div');
special.forEach(skill => specialRow.appendChild(createSkillElement(skill, true)));
skillsContainer.appendChild(specialRow);
}
targetDiv.appendChild(skillsContainer);
found = true;
break;
}
}
}
}
if (found) break;
await delay(1000);
}
}
function categorizeSkills(petSkills) {
const basicSkills = [
'Item Pouch',
'NESO Magnet',
'Auto HP Potion Pouch',
'Auto MP Potion Pouch'
];
return {
basic: petSkills.filter(skill => basicSkills.includes(skill)),
special: petSkills.filter(skill => !basicSkills.includes(skill))
};
}
function createSkillElement(skill, isSpecial = false) {
const skillDiv = document.createElement('div');
skillDiv.style.cssText = `
display: inline-block;
margin: 2px 4px;
padding: 2px 6px;
border-radius: 4px;
background-color: black;
color: ${isSpecial ? '#ffd700' : '#ffffff'};
font-size: 11px;
`;
skillDiv.innerHTML = `${translateSkill(skill)}`;
return skillDiv;
}
// 在 skillTranslations 後面加入
const filterStyles = `
.skill-filter {
position: fixed;
left: -180px;
top: 50%;
transform: translateY(-50%);
background: rgba(0, 0, 0, 0.8);
padding: 15px;
border-radius: 0 8px 8px 0;
z-index: 1000;
color: white;
width: 200px;
transition: left 0.3s ease;
}
.skill-filter:hover {
left: 0;
}
.skill-filter h3 {
margin: 0 0 10px 0;
color: white;
padding-left: 10px;
border-left: 3px solid #fff;
}
.skill-filter label {
display: block;
margin: 5px 0;
cursor: pointer;
padding: 5px;
transition: background-color 0.2s;
}
.skill-filter label:hover {
background-color: rgba(255, 255, 255, 0.1);
}
.skill-filter input[type="checkbox"] {
margin-right: 8px;
}
.skill-filter::after {
content: "▶";
position: absolute;
right: 10px;
top: 50%;
transform: translateY(-50%);
opacity: 0.7;
font-size: 12px;
}
.skill-filter:hover::after {
opacity: 0;
}
`;
// 在 window.fetch 之前加入
function createFilterPanel() {
// 添加樣式
const styleSheet = document.createElement('style');
styleSheet.textContent = filterStyles;
document.head.appendChild(styleSheet);
// 創建過濾面板
const filterDiv = document.createElement('div');
filterDiv.className = 'skill-filter';
filterDiv.innerHTML = `
<h3>技能過濾</h3>
${Object.entries(skillTranslations)
.map(([eng, chi]) => `
<label style="display: flex; align-items: center;">
<input type="checkbox" value="${eng}"/> <span style="display: inline-flex; align-items: center;">${getskillImg(eng)} ${chi}</span>
</label>
`).join('')}
`;
// 添加事件監聽
filterDiv.addEventListener('change', (e) => {
if (e.target.type === 'checkbox') {
filterPetsBySkills();
}
});
document.body.appendChild(filterDiv);
// 初始化時執行一次過濾
filterPetsBySkills();
// 在過濾面板創建完成後立即創建更新按鈕
createUpdateButton();
}
function filterPetsBySkills() {
const selectedSkills = Array.from(document.querySelectorAll('.skill-filter input:checked'))
.map(checkbox => checkbox.value);
// 修改選擇器為 msuui-tr
const petRows = document.querySelectorAll('.msuui-tr');
petRows.forEach(row => {
const skillsInfo = row.querySelector('.pet-skills-info');
if (!skillsInfo) {
row.style.display = 'none';
return;
}
// 獲取所有技能 div 的文字內容
const petSkills = Array.from(skillsInfo.querySelectorAll('div > div'))
.map(skillDiv => {
// 從中文技能名稱反查英文名稱
const chineseSkill = skillDiv.textContent.trim();
const entry = Object.entries(skillTranslations)
.find(([_, value]) => value === chineseSkill);
return entry ? entry[0] : chineseSkill;
});
// 修改邏輯:必須完全符合所有勾選的技能才顯示
const hasAllSelectedSkills = selectedSkills.length === 0 ||
selectedSkills.every(skill => petSkills.includes(skill));
row.style.display = hasAllSelectedSkills ? '' : 'none';
});
}
// 在 skillTranslations 後面加入新的常數
const STORAGE_KEY = 'msu_pet_data';
const DATA_EXPIRE_TIME = 24 * 60 * 60 * 1000; // 24小時的毫秒數
// 新增用於處理 localStorage 的函數
function saveToStorage(data) {
const storageData = {
timestamp: Date.now(),
items: data
};
localStorage.setItem(STORAGE_KEY, JSON.stringify(storageData));
}
function getFromStorage() {
const storageData = localStorage.getItem(STORAGE_KEY);
if (!storageData) return null;
const { timestamp, items } = JSON.parse(storageData);
// 檢查資料是否過期
if (Date.now() - timestamp > DATA_EXPIRE_TIME) {
localStorage.removeItem(STORAGE_KEY);
return null;
}
return items;
}
// 新增更新頁面技能資訊的函數
function updateSkillsOnPage() {
const storedData = getFromStorage() || {};
for (const tokenId in storedData) {
const itemData = storedData[tokenId];
if (itemData.item && itemData.item.pet) {
const petSkills = itemData.item.pet.petSkills || [];
const mintingNo = itemData.tokenInfo?.mintingNo;
const fullPetName = `${itemData.item.name} #${mintingNo}`;
tryFindAndInsertSkills(fullPetName, petSkills);
}
}
}
// 新增 Toast 樣式
const toastStyles = `
.toast {
position: fixed;
bottom: 80px;
left: 20px;
background-color: rgba(0, 0, 0, 0.8);
color: white;
padding: 12px 24px;
border-radius: 4px;
z-index: 1001;
animation: fadeInOut 2s ease;
}
@keyframes fadeInOut {
0% { opacity: 0; transform: translateY(20px); }
10% { opacity: 1; transform: translateY(0); }
90% { opacity: 1; transform: translateY(0); }
100% { opacity: 0; transform: translateY(-20px); }
}
`;
// 新增 Toast 函數
function showToast(message) {
// 添加樣式(如果還沒添加)
if (!document.querySelector('#toastStyles')) {
const style = document.createElement('style');
style.id = 'toastStyles';
style.textContent = toastStyles;
document.head.appendChild(style);
}
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = message;
document.body.appendChild(toast);
// 2秒後移除 toast
setTimeout(() => {
toast.remove();
}, 2000);
}
// 新增按鈕創建函式
function createUpdateButton() {
const style = document.createElement('style');
style.textContent = `
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.loading-button {
position: relative;
color: transparent !important;
}
.loading-button::after {
content: '';
position: absolute;
width: 16px;
height: 16px;
top: 50%;
left: 50%;
margin-left: -8px;
margin-top: -8px;
border: 2px solid #ffffff;
border-radius: 50%;
border-top-color: transparent;
animation: spin 1s linear infinite;
}
`;
document.head.appendChild(style);
const button = document.createElement('button');
button.textContent = '更新頁面';
button.style.cssText = `
position: fixed;
left: 20px;
bottom: 20px;
padding: 10px 20px;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
z-index: 1000;
min-width: 120px;
min-height: 40px;
`;
button.addEventListener('mouseover', () => {
if (!button.classList.contains('loading-button')) {
button.style.backgroundColor = '#45a049';
}
});
button.addEventListener('mouseout', () => {
if (!button.classList.contains('loading-button')) {
button.style.backgroundColor = '#4CAF50';
}
});
button.addEventListener('click', () => {
if (button.classList.contains('loading-button')) return;
button.classList.add('loading-button');
button.disabled = true;
// 顯示提示訊息
showToast('正在重新載入頁面...');
// 延遲一小段時間後重新載入頁面,讓使用者能看到提示訊息
setTimeout(() => {
window.location.reload();
}, 1000);
});
document.body.appendChild(button);
return button;
}
})();