MaaCopilotPlus

增强MAA作业站的筛选功能

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         MaaCopilotPlus
// @namespace    https://github.com/HauKuen
// @license MIT
// @version      1.7
// @description  增强MAA作业站的筛选功能
// @author       haukuen
// @match        https://prts.plus/*
// @match        https://zoot.plus/*
// @icon         https://zoot.plus/favicon-32x32.png?v=1
// @grant        GM_setValue
// @grant        GM_getValue
// ==/UserScript==

(function () {
  "use strict";

  // 初始化角色列表
  let myOperators = GM_getValue("myOperators", []);
  // 筛选开关状态
  let filterEnabled = GM_getValue("filterEnabled", true);
  // 允许缺少一个干员
  let allowOneMissing = GM_getValue("allowOneMissing", false);

  // 添加一个安全的查询函数
  function safeQuerySelector(selector, parent = document) {
    try {
      return parent.querySelector(selector);
    } catch (e) {
      console.warn(`查询选择器失败: ${selector}`, e);
      return null;
    }
  }

  function safeQuerySelectorAll(selector, parent = document) {
    try {
      return parent.querySelectorAll(selector);
    } catch (e) {
      console.warn(`查询选择器失败: ${selector}`, e);
      return [];
    }
  }

  // 创建UI
  function createUI() {
    // 创建插件控制面板
    const controlPanel = document.createElement("div");
    controlPanel.id = "maa-copilot-plus";
    controlPanel.style.position = "fixed";
    controlPanel.style.top = "10px";
    controlPanel.style.right = "10px";
    controlPanel.style.zIndex = "9999";
    controlPanel.style.backgroundColor = "#f0f0f0";
    controlPanel.style.padding = "10px";
    controlPanel.style.borderRadius = "5px";
    controlPanel.style.boxShadow = "0 0 10px rgba(0,0,0,0.2)";
    controlPanel.style.cursor = "move";

    // 添加拖拽功能
    let isDragging = false;
    let currentX;
    let currentY;
    let initialX;
    let initialY;

    controlPanel.addEventListener("mousedown", (e) => {
      isDragging = true;

      // 获取鼠标相对于面板的初始位置
      initialX = e.clientX - controlPanel.offsetLeft;
      initialY = e.clientY - controlPanel.offsetTop;

      controlPanel.style.opacity = "0.8";
      controlPanel.style.transition = "none";
    });

    document.addEventListener("mousemove", (e) => {
      if (isDragging) {
        e.preventDefault();

        // 计算新位置
        currentX = e.clientX - initialX;
        currentY = e.clientY - initialY;

        // 限制在窗口内
        const maxX = window.innerWidth - controlPanel.offsetWidth;
        const maxY = window.innerHeight - controlPanel.offsetHeight;

        currentX = Math.max(0, Math.min(currentX, maxX));
        currentY = Math.max(0, Math.min(currentY, maxY));

        // 更新位置
        controlPanel.style.left = currentX + "px";
        controlPanel.style.top = currentY + "px";
        controlPanel.style.right = "auto"; // 清除right属性以避免冲突
      }
    });

    document.addEventListener("mouseup", () => {
      if (isDragging) {
        isDragging = false;
        // 恢复正常样式
        controlPanel.style.opacity = "1";
        controlPanel.style.transition = "opacity 0.2s";
      }
    });

    // 保存面板位置到本地存储
    window.addEventListener("beforeunload", () => {
      if (controlPanel.style.left) {
        // 只有在面板被移动过时才保存
        GM_setValue("panelPosition", {
          left: controlPanel.style.left,
          top: controlPanel.style.top,
        });
      }
    });

    // 恢复上次保存的位置
    const savedPosition = GM_getValue("panelPosition", null);
    if (savedPosition) {
      controlPanel.style.left = savedPosition.left;
      controlPanel.style.top = savedPosition.top;
      controlPanel.style.right = "auto";
    }

    // 创建标题
    const title = document.createElement("h3");
    title.textContent = "MAA Copilot Plus";
    title.style.margin = "0 0 10px 0";
    title.style.cursor = "move"; // 标题也可以用来拖动

    const buttonContainer = document.createElement("div");
    buttonContainer.style.display = "flex";
    buttonContainer.style.marginBottom = "10px";

    const importButton = document.createElement("button");
    importButton.textContent = "导入角色列表";
    importButton.onclick = openImportDialog;

    buttonContainer.appendChild(importButton);

    // 创建开关容器
    const toggleContainer = document.createElement("div");
    toggleContainer.style.display = "flex";
    toggleContainer.style.alignItems = "center";
    toggleContainer.style.marginBottom = "10px";

    // 创建开关
    const toggleLabel = document.createElement("label");
    toggleLabel.style.display = "flex";
    toggleLabel.style.alignItems = "center";
    toggleLabel.style.cursor = "pointer";

    const toggleInput = document.createElement("input");
    toggleInput.type = "checkbox";
    toggleInput.checked = filterEnabled;
    toggleInput.style.margin = "0 5px 0 0";
    toggleInput.onchange = function () {
      filterEnabled = this.checked;
      GM_setValue("filterEnabled", filterEnabled);
      updateStatus();
      if (filterEnabled) {
        filterGuides();
      } else {
        resetFilter();
      }
    };

    const toggleText = document.createElement("span");
    toggleText.textContent = "启用筛选";

    toggleLabel.appendChild(toggleInput);
    toggleLabel.appendChild(toggleText);
    toggleContainer.appendChild(toggleLabel);

    // 创建允许缺少一个干员的设置
    const missingContainer = document.createElement("div");
    missingContainer.style.display = "flex";
    missingContainer.style.alignItems = "center";
    missingContainer.style.marginBottom = "10px";

    const missingLabel = document.createElement("label");
    missingLabel.style.display = "flex";
    missingLabel.style.alignItems = "center";
    missingLabel.style.cursor = "pointer";

    const missingInput = document.createElement("input");
    missingInput.type = "checkbox";
    missingInput.checked = allowOneMissing;
    missingInput.style.margin = "0 5px 0 0";
    missingInput.onchange = function () {
      allowOneMissing = this.checked;
      GM_setValue("allowOneMissing", allowOneMissing);
      if (filterEnabled) {
        filterGuides();
      }
    };

    const missingText = document.createElement("span");
    missingText.textContent = "允许缺少一个干员";

    missingLabel.appendChild(missingInput);
    missingLabel.appendChild(missingText);
    missingContainer.appendChild(missingLabel);



    // 创建状态显示
    const status = document.createElement("div");
    status.id = "maa-status";
    status.style.fontSize = "12px";

    // 组装控制面板
    controlPanel.appendChild(title);
    controlPanel.appendChild(buttonContainer);
    controlPanel.appendChild(toggleContainer);
    controlPanel.appendChild(missingContainer);
    controlPanel.appendChild(status);

    document.body.appendChild(controlPanel);

    // 初始化状态显示
    updateStatus();
  }

  // 更新状态显示
  function updateStatus(filteredCount) {
    try {
      const status = document.getElementById("maa-status");
      if (status) {
        let statusText = `已导入 ${myOperators.length} 个角色`;
        if (filterEnabled) {
          statusText += filteredCount !== undefined
            ? `, 筛选掉 ${filteredCount} 个不符合条件的攻略`
            : " (筛选已启用)";
        } else {
          statusText += " (筛选已禁用)";
        }
        status.textContent = statusText;
        status.style.color = filterEnabled ? "green" : "gray";
      }
    } catch (e) {
      console.warn("更新状态显示失败:", e);
    }
  }

  // 导入角色对话框
  function openImportDialog() {
    // 创建模态对话框
    const modal = document.createElement("div");
    modal.style.position = "fixed";
    modal.style.top = "0";
    modal.style.left = "0";
    modal.style.width = "100%";
    modal.style.height = "100%";
    modal.style.backgroundColor = "rgba(0,0,0,0.5)";
    modal.style.display = "flex";
    modal.style.justifyContent = "center";
    modal.style.alignItems = "center";
    modal.style.zIndex = "10000";

    // 创建对话框内容
    const dialog = document.createElement("div");
    dialog.style.backgroundColor = "white";
    dialog.style.padding = "20px";
    dialog.style.borderRadius = "5px";
    dialog.style.width = "80%";
    dialog.style.maxWidth = "600px";
    dialog.style.maxHeight = "80%";
    dialog.style.overflow = "auto";

    // 创建标题
    const title = document.createElement("h3");
    title.textContent = "导入角色列表";
    title.style.marginTop = "0";

    // 创建文本区域
    const textarea = document.createElement("textarea");
    textarea.style.width = "100%";
    textarea.style.height = "200px";
    textarea.style.marginBottom = "10px";
    textarea.placeholder = "粘贴角色列表 JSON 数据...";

    // 创建按钮
    const buttonContainer = document.createElement("div");
    buttonContainer.style.display = "flex";
    buttonContainer.style.justifyContent = "flex-end";

    const cancelButton = document.createElement("button");
    cancelButton.textContent = "取消";
    cancelButton.style.marginRight = "10px";
    cancelButton.onclick = () => document.body.removeChild(modal);

    const importButton = document.createElement("button");
    importButton.textContent = "导入";
    importButton.onclick = () => {
      try {
        const data = JSON.parse(textarea.value);

        if (Array.isArray(data)) {
          myOperators = data
            .filter((op) => op.own)
            .map((op) => ({
              name: op.name,
              elite: op.elite,
              level: op.level,
              rarity: op.rarity,
              // 根据精英化等级计算已解锁的最大技能
              maxSkill: op.elite === 0 ? 1 : op.elite === 1 ? 2 : 3
            }));

          GM_setValue("myOperators", myOperators);
          updateStatus();

          document.body.removeChild(modal);

          // 导入成功后自动筛选(如果筛选功能已启用)
          if (filterEnabled) {
            filterGuides();
          }
        } else {
          alert("无效的数据格式,请确保是有效的 JSON 数组");
        }
      } catch (e) {
        alert("解析失败: " + e.message);
      }
    };

    buttonContainer.appendChild(cancelButton);
    buttonContainer.appendChild(importButton);

    // 组装对话框
    dialog.appendChild(title);
    dialog.appendChild(textarea);
    dialog.appendChild(buttonContainer);
    modal.appendChild(dialog);

    document.body.appendChild(modal);
  }

  // 过滤出真正的干员标签(排除关卡标签等)
  function filterOperatorTags(allTags) {
    return Array.from(allTags).filter(tag => {
      const text = tag.textContent?.trim();
      return text &&
        !text.includes('|') &&
        !text.includes('活动关卡') &&
        !text.includes('主线') &&
        !text.includes('资源收集') &&
        !text.includes('普通') &&
        !text.includes('突袭') &&
        !text.match(/^\[.*\]$/) &&
        text.match(/^[\u4e00-\u9fa5\w\s]+\s+[123]$|^[\u4e00-\u9fa5\w\s]+$/);
    });
  }

  // 筛选攻略
  function filterGuides() {
    try {
      if (myOperators.length === 0) {
        console.warn("未导入角色列表");
        return;
      }

      if (!filterEnabled) {
        return;
      }

      // 更新选择器以匹配新的卡片结构
      const guideCards = safeQuerySelectorAll(
        'div[class*="bp4-card"]'
      );

      if (!guideCards.length) {
        console.warn("未找到攻略卡片");
        return;
      }

      let filteredCount = 0;

      guideCards.forEach((card) => {
        try {
          const listItem = card.closest('li');
          if (!listItem) {
            return;
          }

          const allTags = safeQuerySelectorAll('.bp4-tag', card);

          const operatorTags = filterOperatorTags(allTags)
            .map(tag => tag.querySelector('span.bp4-fill') || tag);

          if (!operatorTags.length) return;

          // 重置高亮样式
          operatorTags.forEach(tag => {
            const tagElement = tag.closest('.bp4-tag');
            if (!tagElement) return;

            tagElement.style.color = "";
            tagElement.style.backgroundColor = "";
            tagElement.style.border = "";
            tagElement.style.transition = "";
            tagElement.onmouseover = null;
            tagElement.onmouseout = null;
            tagElement.title = "";
          });

          let missingOperators = 0;
          let lastMissingTag = null;

          operatorTags.forEach((tag) => {
            const operatorText = tag.textContent.trim();
            if (operatorText.match(/^\[.*\]$/)) return;

            const [operatorName, skillText] = operatorText.split(" ");
            const skillNumber = skillText ? parseInt(skillText.replace(/[^0-9]/g, "")) : 1;

            const operator = myOperators.find(op => op.name === operatorName);
            if (!operator || skillNumber > operator.maxSkill) {
              missingOperators++;
              if (missingOperators === 1) {
                lastMissingTag = tag;
              }
            }
          });

          // 高亮缺失的一个干员
          if (allowOneMissing && missingOperators === 1 && lastMissingTag) {
            const tagElement = lastMissingTag.closest('.bp4-tag');
            if (tagElement) {
              tagElement.style.backgroundColor = "rgba(59, 130, 246, 0.1)";
              tagElement.style.color = "#2563eb";
              tagElement.style.border = "1px solid rgba(59, 130, 246, 0.5)";
              tagElement.style.transition = "all 0.2s ease";
              tagElement.title = "缺少此干员/精英化等级不足";
            }
          }

          // 判断是否隐藏卡片
          const shouldHide = (!allowOneMissing && missingOperators > 0) ||
            (allowOneMissing && missingOperators > 1);

          // 隐藏整个网格项(LI元素),避免留下空白
          if (shouldHide) {
            listItem.style.display = "none";
            filteredCount++;
          } else {
            listItem.style.display = "";
          }
        } catch (e) {
          console.warn("处理卡片时出错:", e);
        }
      });

      updateStatus(filteredCount);
    } catch (e) {
      console.error("筛选功能出错:", e);
    }
  }

  // 重置筛选
  function resetFilter() {
    try {
      // 获取所有攻略卡片
      const guideCards = safeQuerySelectorAll(
        'div[class*="bp4-card"]'
      );

      guideCards.forEach((card) => {
        const listItem = card.closest('li');
        if (!listItem) {
          return;
        }

        listItem.style.display = "";

        const allTags = safeQuerySelectorAll('.bp4-tag', card);

        const operatorTags = filterOperatorTags(allTags);

        operatorTags.forEach(tag => {
          tag.style.color = "";
          tag.style.backgroundColor = "";
          tag.style.border = "";
          tag.style.transition = "";
          tag.style.boxShadow = "";
          tag.onmouseover = null;
          tag.onmouseout = null;
          tag.title = "";
        });
      });

      updateStatus();
    } catch (e) {
      console.warn("重置筛选时出错:", e);
    }
  }

  let lastUrl = location.href;

  // MutationObserver 监视DOM变化
  const observer = new MutationObserver((mutations) => {
    try {
      // URL 变化检查
      if (lastUrl !== location.href) {
        lastUrl = location.href;
        setTimeout(() => {
          try {
            if (filterEnabled && myOperators.length > 0) {
              filterGuides();
            }
          } catch (e) {
            console.warn("URL变化后筛选失败:", e);
          }
        }, 1000);
        return; // URL 变化时直接返回,避免重复处理
      }

      // 检查攻略列表变化
      for (const mutation of mutations) {
        // 只处理节点添加的情况
        if (mutation.type === "childList" && mutation.addedNodes.length > 0) {
          // 检查是否有新的攻略卡片添加
          const hasNewCards = Array.from(mutation.addedNodes).some(node => {
            return node.nodeType === 1 && // 元素节点
              (node.classList.contains('bp4-card') || // 直接是卡片
                node.querySelector('div[class*="bp4-card"]')); // 包含卡片
          });

          if (hasNewCards) {
            // 给一点延时确保 DOM 完全更新
            setTimeout(() => {
              try {
                if (filterEnabled && myOperators.length > 0) {
                  console.log("检测到新的攻略卡片,重新应用筛选");
                  filterGuides();
                }
              } catch (e) {
                console.warn("处理新卡片时出错:", e);
              }
            }, 200);
            break; // 找到新卡片后退出循环
          }
        }
      }

      // 移除广告
      try {
        removeAds();
      } catch (e) {
        console.warn("移除广告失败:", e);
      }
    } catch (e) {
      console.error("Observer错误:", e);
    }
  });

  // 修改观察配置,增加更多细节监听
  observer.observe(document.body, {
    childList: true,
    subtree: true,
    attributes: true,
    attributeFilter: ['class', 'style'] // 只监听类名和样式变化
  });

  const removeAds = () => {
    // 移除侧边广告
    const sideAd = safeQuerySelector(
      "body > main > div > div:nth-child(2) > div > div:nth-child(2) > div > a"
    );
    if (sideAd) {
      sideAd.style.display = "none";
    }

    // 移除所有广告链接
    const adSelectors = [
      'a[href*="gad.netease.com"]',
      'a[href*="ldmnq.com"]',
      'a[href*="ldy/ldymuban"]',
      'a[class*="block relative"]'
    ];

    adSelectors.forEach(selector => {
      safeQuerySelectorAll(selector).forEach(ad => {
        ad.style.display = "none";
      });
    });
  };

  // 等待页面加载完成
  window.addEventListener("load", () => {
    createUI();

    removeAds();

    // 观察DOM变化
    observer.observe(document.body, { childList: true, subtree: true });

    // 如果已有角色列表且筛选功能已启用,自动筛选
    if (filterEnabled && myOperators.length > 0) {
      setTimeout(filterGuides, 1000);
    }
  });
})();